jskelet 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/AGENTS.md +19 -15
  2. package/CHANGELOG.md +165 -15
  3. package/README.md +16 -21
  4. package/bin/jskelet.mjs +23 -9
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +10 -4
  7. package/docs/03-routing.md +14 -7
  8. package/docs/04-render-ve-sablonlar.md +60 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/06-cache.md +18 -7
  11. package/docs/07-yapilandirma.md +69 -27
  12. package/docs/08-build.md +15 -9
  13. package/docs/09-dev-araclari.md +22 -8
  14. package/docs/10-dagitim.md +14 -13
  15. package/docs/11-tasima.md +51 -17
  16. package/docs/12-panel-ve-oturum.md +10 -4
  17. package/docs/README.md +10 -33
  18. package/docs/en/01-getting-started.md +4 -3
  19. package/docs/en/02-architecture.md +12 -6
  20. package/docs/en/03-routing.md +15 -8
  21. package/docs/en/04-rendering.md +71 -59
  22. package/docs/en/05-islands.md +13 -8
  23. package/docs/en/06-caching.md +21 -7
  24. package/docs/en/07-configuration.md +69 -29
  25. package/docs/en/08-build.md +16 -10
  26. package/docs/en/09-dev-tools.md +24 -8
  27. package/docs/en/10-deployment.md +14 -14
  28. package/docs/en/11-migration.md +51 -16
  29. package/docs/en/12-dashboards-and-sessions.md +9 -4
  30. package/docs/en/README.md +10 -35
  31. package/package.json +48 -13
  32. package/src/build/tasks/client.mjs +91 -10
  33. package/src/build/tasks/icons.mjs +11 -1
  34. package/src/client/index.js +2 -2
  35. package/src/compile/codegen.js +4 -0
  36. package/src/compile/compile-all.js +12 -21
  37. package/src/compile/expr.js +5 -0
  38. package/src/compile/parse.js +64 -8
  39. package/src/compile/resolve.js +3 -0
  40. package/src/config/defaults.js +48 -5
  41. package/src/config/index.js +138 -27
  42. package/src/dev-server.mjs +26 -3
  43. package/src/http/cookies-entry.js +1 -0
  44. package/src/http/cookies.js +18 -0
  45. package/src/logo.png +0 -0
  46. package/src/migrate/apply.mjs +262 -0
  47. package/src/migrate/babel.mjs +79 -0
  48. package/src/migrate/classify.mjs +155 -0
  49. package/src/migrate/config.mjs +126 -0
  50. package/src/migrate/fs-walk.mjs +191 -0
  51. package/src/migrate/parse.mjs +26 -0
  52. package/src/migrate/scan.mjs +177 -0
  53. package/src/migrate/transform/expr-source.mjs +168 -0
  54. package/src/migrate/transform/island.mjs +67 -0
  55. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  56. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  57. package/src/migrate/transform/page-split.mjs +435 -0
  58. package/src/migrate/write.mjs +81 -0
  59. package/src/migrate.mjs +171 -0
  60. package/src/server/auth/handoff.js +94 -11
  61. package/src/server/create-app.js +37 -10
  62. package/src/server/ejs-adapter.js +59 -0
  63. package/src/server/html-cache.js +178 -32
  64. package/src/server/image-optimizer.js +94 -26
  65. package/src/server/middleware/dev-gate.js +21 -8
  66. package/src/server/middleware/robots-txt.js +341 -0
  67. package/src/server/port-guard.js +255 -0
  68. package/src/server/prewarm.js +137 -51
  69. package/src/server/render.js +30 -10
  70. package/src/server/status-page.js +105 -4
  71. package/src/start.mjs +18 -3
  72. package/src/templates/layout.ejs +8 -28
  73. package/src/templates/layout.jsk +30 -0
  74. package/src/templates/layout.render.js +41 -0
  75. package/src/views/helpers/tags.js +86 -3
  76. package/types/build/resolve-peer.d.mts +13 -0
  77. package/types/client/dom.d.ts +55 -0
  78. package/types/client/form.d.ts +19 -0
  79. package/types/client/index.d.ts +20 -0
  80. package/types/client/registry.d.ts +53 -0
  81. package/types/client/safe-image.d.ts +19 -0
  82. package/types/client/shared-cookie.d.ts +82 -0
  83. package/types/client/store.d.ts +18 -0
  84. package/types/client/swap.d.ts +46 -0
  85. package/types/compile/codegen.d.ts +32 -0
  86. package/types/compile/compile-all.d.ts +42 -0
  87. package/types/compile/errors.d.ts +30 -0
  88. package/types/compile/expr.d.ts +67 -0
  89. package/types/compile/index.d.ts +10 -0
  90. package/types/compile/parse.d.ts +82 -0
  91. package/types/compile/resolve.d.ts +46 -0
  92. package/types/compile/scan-exports.d.ts +9 -0
  93. package/types/config/defaults.d.ts +477 -0
  94. package/types/config/index.d.ts +304 -0
  95. package/types/config/pattern.d.ts +38 -0
  96. package/types/http/control-flow.d.ts +45 -0
  97. package/types/http/cookies-entry.d.ts +5 -0
  98. package/types/http/cookies.d.ts +113 -0
  99. package/types/http/request-cache.d.ts +13 -0
  100. package/types/http/request-context.d.ts +67 -0
  101. package/types/http/shared-cookie.d.ts +73 -0
  102. package/types/index.d.ts +30 -0
  103. package/types/log.d.mts +153 -0
  104. package/types/server/admin/actions.d.ts +16 -0
  105. package/types/server/admin/auth.d.ts +52 -0
  106. package/types/server/admin/event-log.d.ts +38 -0
  107. package/types/server/admin/gate.d.ts +43 -0
  108. package/types/server/admin/inventory.d.ts +40 -0
  109. package/types/server/admin/mount.d.ts +6 -0
  110. package/types/server/admin/router.d.ts +6 -0
  111. package/types/server/admin/snapshot.d.ts +6 -0
  112. package/types/server/assets.d.ts +47 -0
  113. package/types/server/auth/handoff.d.ts +12 -0
  114. package/types/server/cache-deps.d.ts +16 -0
  115. package/types/server/cache-vary.d.ts +30 -0
  116. package/types/server/cloudflare.d.ts +163 -0
  117. package/types/server/create-app.d.ts +25 -0
  118. package/types/server/data-cache.d.ts +116 -0
  119. package/types/server/dev/devtools.d.ts +44 -0
  120. package/types/server/dev/report.d.ts +229 -0
  121. package/types/server/dev/socket.d.ts +17 -0
  122. package/types/server/dev/version-check.d.mts +15 -0
  123. package/types/server/ejs-adapter.d.ts +11 -0
  124. package/types/server/head-hints.d.ts +40 -0
  125. package/types/server/html-cache.d.ts +207 -0
  126. package/types/server/image-optimizer.d.ts +68 -0
  127. package/types/server/logs/access-middleware.d.ts +7 -0
  128. package/types/server/logs/file-sink.d.ts +17 -0
  129. package/types/server/logs/pipeline.d.ts +37 -0
  130. package/types/server/logs/s3-put.d.ts +85 -0
  131. package/types/server/logs/s3-sink.d.ts +26 -0
  132. package/types/server/metadata.d.ts +38 -0
  133. package/types/server/middleware/compression.d.ts +17 -0
  134. package/types/server/middleware/csrf.d.ts +4 -0
  135. package/types/server/middleware/dev-gate.d.ts +2 -0
  136. package/types/server/middleware/headers.d.ts +2 -0
  137. package/types/server/middleware/redirects.d.ts +2 -0
  138. package/types/server/middleware/robots-txt.d.ts +33 -0
  139. package/types/server/middleware/static-precompressed.d.ts +5 -0
  140. package/types/server/middleware/trailing-slash.d.ts +11 -0
  141. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  142. package/types/server/og-image.d.ts +149 -0
  143. package/types/server/port-guard.d.ts +50 -0
  144. package/types/server/prewarm.d.ts +131 -0
  145. package/types/server/redis.d.ts +163 -0
  146. package/types/server/render.d.ts +101 -0
  147. package/types/server/router.d.ts +5 -0
  148. package/types/server/status-page.d.ts +24 -0
  149. package/types/server/upstream-limiter.d.ts +123 -0
  150. package/types/server/upstream-tracking.d.ts +42 -0
  151. package/types/shared/cookie-domain.d.ts +29 -0
  152. package/types/templates/layout.render.d.ts +7 -0
  153. package/types/version.d.mts +10 -0
  154. package/types/views/components/loader.d.ts +5 -0
  155. package/types/views/helpers/html.d.ts +39 -0
  156. package/types/views/helpers/tags.d.ts +127 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Next App Router ağacında dosya keşfi ve path → URL eşlemesi.
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ const PAGE_RE = /^page\.(jsx?|tsx?|mjs|cjs)$/;
8
+ const LAYOUT_RE = /^layout\.(jsx?|tsx?|mjs|cjs)$/;
9
+ const SOURCE_RE = /\.(jsx?|tsx?|mjs|cjs)$/;
10
+
11
+ /**
12
+ * @param {string} root
13
+ * @returns {string | null}
14
+ */
15
+ export function findAppDir(root) {
16
+ for (const candidate of ["app", "src/app"]) {
17
+ const abs = path.join(root, candidate);
18
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return abs;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ /**
24
+ * @param {string} root
25
+ * @returns {string | null}
26
+ */
27
+ export function findNextConfig(root) {
28
+ for (const name of [
29
+ "next.config.mjs",
30
+ "next.config.js",
31
+ "next.config.cjs",
32
+ "next.config.ts",
33
+ ]) {
34
+ const abs = path.join(root, name);
35
+ if (fs.existsSync(abs)) return abs;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * @param {string} dir
42
+ * @param {(rel: string, abs: string) => void} visit
43
+ * @param {string} [base]
44
+ */
45
+ export function walkFiles(dir, visit, base = dir) {
46
+ if (!fs.existsSync(dir)) return;
47
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
48
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
49
+ const abs = path.join(dir, entry.name);
50
+ if (entry.isDirectory()) {
51
+ walkFiles(abs, visit, base);
52
+ continue;
53
+ }
54
+ visit(path.relative(base, abs).replace(/\\/g, "/"), abs);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * App Router segment yolunu Express path'ine çevirir.
60
+ * `news/[slug]` → `/news/:slug`, `docs/[...slug]` → `/docs/:slug*`
61
+ *
62
+ * @param {string} relativeDir app/ altındaki dizin (posix, trailing slash yok)
63
+ * @returns {string}
64
+ */
65
+ export function segmentToExpressPath(relativeDir) {
66
+ if (!relativeDir || relativeDir === ".") return "/";
67
+ const parts = relativeDir.split("/").filter(Boolean);
68
+ const out = [];
69
+ for (const part of parts) {
70
+ if (part.startsWith("(") && part.endsWith(")")) continue; // route groups
71
+ if (part.startsWith("@")) continue; // parallel routes
72
+ const catchAll = part.match(/^\[\.\.\.(.+)\]$/);
73
+ if (catchAll) {
74
+ out.push(`:${catchAll[1]}*`);
75
+ continue;
76
+ }
77
+ const optional = part.match(/^\[\[\.\.\.(.+)\]\]$/);
78
+ if (optional) {
79
+ out.push(`:${optional[1]}*`);
80
+ continue;
81
+ }
82
+ const dyn = part.match(/^\[(.+)\]$/);
83
+ if (dyn) {
84
+ out.push(`:${dyn[1]}`);
85
+ continue;
86
+ }
87
+ out.push(part);
88
+ }
89
+ return "/" + out.join("/");
90
+ }
91
+
92
+ /**
93
+ * Feature adı: ilk anlamlı segment veya `home`.
94
+ *
95
+ * @param {string} relativeDir
96
+ * @returns {string}
97
+ */
98
+ export function featureNameFromDir(relativeDir) {
99
+ const parts = relativeDir
100
+ .split("/")
101
+ .filter(Boolean)
102
+ .filter((p) => !(p.startsWith("(") && p.endsWith(")")))
103
+ .filter((p) => !p.startsWith("@"))
104
+ .filter((p) => !p.startsWith("["));
105
+ if (parts.length === 0) return "home";
106
+ return parts[0].replace(/[^a-zA-Z0-9_-]/g, "-") || "home";
107
+ }
108
+
109
+ /**
110
+ * Sayfa dosya gövde adı.
111
+ *
112
+ * @param {string} relativeDir
113
+ * @returns {string}
114
+ */
115
+ export function pageNameFromDir(relativeDir) {
116
+ const parts = relativeDir
117
+ .split("/")
118
+ .filter(Boolean)
119
+ .filter((p) => !(p.startsWith("(") && p.endsWith(")")))
120
+ .filter((p) => !p.startsWith("@"));
121
+ if (parts.length === 0) return "home";
122
+ const last = parts[parts.length - 1];
123
+ if (last.startsWith("[")) {
124
+ return parts.length > 1 ? parts[parts.length - 2] : "detail";
125
+ }
126
+ return last.replace(/[^a-zA-Z0-9_-]/g, "-") || "page";
127
+ }
128
+
129
+ /**
130
+ * @param {string} appDir
131
+ * @returns {{ pages: Array<{ abs: string, rel: string, dir: string, url: string, feature: string, page: string }>, layouts: Array<{ abs: string, rel: string, dir: string, depth: number }>, sources: Array<{ abs: string, rel: string }> }}
132
+ */
133
+ export function inventoryApp(appDir) {
134
+ /** @type {Array<{ abs: string, rel: string, dir: string, url: string, feature: string, page: string }>} */
135
+ const pages = [];
136
+ /** @type {Array<{ abs: string, rel: string, dir: string, depth: number }>} */
137
+ const layouts = [];
138
+ /** @type {Array<{ abs: string, rel: string }>} */
139
+ const sources = [];
140
+
141
+ walkFiles(appDir, (rel, abs) => {
142
+ const base = path.basename(rel);
143
+ const dir = path.posix.dirname(rel);
144
+ const dirKey = dir === "." ? "" : dir;
145
+
146
+ if (PAGE_RE.test(base)) {
147
+ pages.push({
148
+ abs,
149
+ rel,
150
+ dir: dirKey,
151
+ url: segmentToExpressPath(dirKey),
152
+ feature: featureNameFromDir(dirKey),
153
+ page: pageNameFromDir(dirKey),
154
+ });
155
+ return;
156
+ }
157
+ if (LAYOUT_RE.test(base)) {
158
+ const depth = dirKey ? dirKey.split("/").length : 0;
159
+ layouts.push({ abs, rel, dir: dirKey, depth });
160
+ return;
161
+ }
162
+ if (SOURCE_RE.test(base)) {
163
+ sources.push({ abs, rel });
164
+ }
165
+ });
166
+
167
+ layouts.sort((a, b) => a.depth - b.depth);
168
+ pages.sort((a, b) => a.url.localeCompare(b.url));
169
+ return { pages, layouts, sources };
170
+ }
171
+
172
+ /**
173
+ * @param {string} root
174
+ * @returns {string[]}
175
+ */
176
+ export function findClientEnvKeys(root) {
177
+ /** @type {Set<string>} */
178
+ const keys = new Set();
179
+ for (const name of [".env", ".env.local", ".env.production", ".env.development"]) {
180
+ const abs = path.join(root, name);
181
+ if (!fs.existsSync(abs)) continue;
182
+ const text = fs.readFileSync(abs, "utf8");
183
+ for (const line of text.split(/\r?\n/)) {
184
+ const m = line.match(/^\s*NEXT_PUBLIC_([A-Za-z0-9_]+)\s*=/);
185
+ if (m) keys.add(m[1]);
186
+ }
187
+ }
188
+ return [...keys].sort();
189
+ }
190
+
191
+ export { PAGE_RE, LAYOUT_RE, SOURCE_RE };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Next.js / React kaynak dosyalarını Babel ile parse eder (JSX + TS).
3
+ *
4
+ * `@babel/parser` opsiyonel peerdır; yoksa `getBabel()` kurulum mesajı fırlatır.
5
+ */
6
+ import { getBabel } from "./babel.mjs";
7
+
8
+ /**
9
+ * @param {string} code
10
+ * @param {string} [filename]
11
+ * @returns {import('@babel/types').File}
12
+ */
13
+ export function parseSource(code, filename = "file.tsx") {
14
+ const { parse } = getBabel();
15
+ return parse(code, {
16
+ sourceType: "module",
17
+ sourceFilename: filename,
18
+ plugins: [
19
+ "jsx",
20
+ "typescript",
21
+ "importAttributes",
22
+ "topLevelAwait",
23
+ ],
24
+ errorRecovery: false,
25
+ });
26
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Next App Router envanteri + engel listesi.
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import {
7
+ findAppDir,
8
+ findNextConfig,
9
+ inventoryApp,
10
+ findClientEnvKeys,
11
+ walkFiles,
12
+ } from "./fs-walk.mjs";
13
+ import { ensureBabel } from "./babel.mjs";
14
+ import { classifySource } from "./classify.mjs";
15
+
16
+ /**
17
+ * @param {string} root
18
+ * @returns {object}
19
+ */
20
+ export function scanProject(root) {
21
+ // Babel yoksa yüzlerce "parse error" yerine tek net kurulum mesajı.
22
+ ensureBabel(root);
23
+
24
+ const appDir = findAppDir(root);
25
+ const nextConfig = findNextConfig(root);
26
+ const clientEnv = findClientEnvKeys(root);
27
+
28
+ if (!appDir) {
29
+ return {
30
+ root,
31
+ appDir: null,
32
+ nextConfig,
33
+ clientEnv,
34
+ pages: [],
35
+ layouts: [],
36
+ clients: [],
37
+ components: [],
38
+ blockers: [
39
+ {
40
+ kind: "no-app-dir",
41
+ message: "No app/ or src/app/ directory found (App Router required).",
42
+ },
43
+ ],
44
+ pagesRouter: fs.existsSync(path.join(root, "pages")),
45
+ };
46
+ }
47
+
48
+ const inv = inventoryApp(appDir);
49
+ /** @type {object[]} */
50
+ const pages = [];
51
+ /** @type {object[]} */
52
+ const clients = [];
53
+ /** @type {object[]} */
54
+ const components = [];
55
+ /** @type {object[]} */
56
+ const blockers = [];
57
+
58
+ if (fs.existsSync(path.join(root, "pages"))) {
59
+ blockers.push({
60
+ kind: "pages-router",
61
+ message: "pages/ directory present — Pages Router is out of scope for this codemod.",
62
+ });
63
+ }
64
+
65
+ const nestedLayouts = inv.layouts.filter((l) => l.depth > 0);
66
+ if (nestedLayouts.length) {
67
+ blockers.push({
68
+ kind: "nested-layouts",
69
+ message: `${nestedLayouts.length} nested layout(s); JSkelet has a single layout — flatten manually.`,
70
+ files: nestedLayouts.map((l) => l.rel),
71
+ });
72
+ }
73
+
74
+ for (const page of inv.pages) {
75
+ const code = fs.readFileSync(page.abs, "utf8");
76
+ const classification = classifySource(code, page.rel);
77
+ pages.push({ ...page, classification });
78
+ if (classification.hasServerAction) {
79
+ blockers.push({
80
+ kind: "server-action",
81
+ file: page.rel,
82
+ message: "Server Actions have no equivalent — use app.post handlers.",
83
+ });
84
+ }
85
+ if (classification.hasSuspense) {
86
+ blockers.push({
87
+ kind: "suspense",
88
+ file: page.rel,
89
+ message: "Suspense / streaming is not supported — use fragment endpoints.",
90
+ });
91
+ }
92
+ }
93
+
94
+ for (const layout of inv.layouts) {
95
+ const code = fs.readFileSync(layout.abs, "utf8");
96
+ layout.classification = classifySource(code, layout.rel);
97
+ }
98
+
99
+ for (const src of inv.sources) {
100
+ const code = fs.readFileSync(src.abs, "utf8");
101
+ const classification = classifySource(code, src.rel);
102
+ if (classification.kind === "client" || classification.isClient) {
103
+ clients.push({ ...src, classification });
104
+ } else if (classification.kind === "server-component") {
105
+ components.push({ ...src, classification });
106
+ }
107
+ }
108
+
109
+ for (const dir of ["components", "src/components", "lib/components"]) {
110
+ const abs = path.join(root, dir);
111
+ if (!fs.existsSync(abs)) continue;
112
+ walkFiles(abs, (_rel, fileAbs) => {
113
+ if (!/\.(jsx?|tsx?)$/.test(fileAbs)) return;
114
+ const fromRoot = path.relative(root, fileAbs).replace(/\\/g, "/");
115
+ const code = fs.readFileSync(fileAbs, "utf8");
116
+ const classification = classifySource(code, fromRoot);
117
+ if (classification.kind === "client" || classification.isClient) {
118
+ clients.push({ abs: fileAbs, rel: fromRoot, classification });
119
+ } else if (classification.kind === "server-component") {
120
+ components.push({ abs: fileAbs, rel: fromRoot, classification });
121
+ }
122
+ });
123
+ }
124
+
125
+ return {
126
+ root,
127
+ appDir: path.relative(root, appDir).replace(/\\/g, "/"),
128
+ nextConfig: nextConfig ? path.relative(root, nextConfig).replace(/\\/g, "/") : null,
129
+ clientEnv,
130
+ pages,
131
+ layouts: inv.layouts,
132
+ clients,
133
+ components,
134
+ blockers,
135
+ pagesRouter: fs.existsSync(path.join(root, "pages")),
136
+ };
137
+ }
138
+
139
+ /**
140
+ * İnsan-okur özet.
141
+ * @param {ReturnType<typeof scanProject>} report
142
+ * @returns {string}
143
+ */
144
+ export function formatScanReport(report) {
145
+ const lines = [];
146
+ lines.push(`Next.js → JSkelet scan`);
147
+ lines.push(` app: ${report.appDir ?? "(missing)"}`);
148
+ lines.push(` config: ${report.nextConfig ?? "(none)"}`);
149
+ lines.push(` pages: ${report.pages.length}`);
150
+ lines.push(` layouts: ${report.layouts.length}`);
151
+ lines.push(` client modules: ${report.clients.length}`);
152
+ lines.push(` server components: ${report.components.length}`);
153
+ if (report.clientEnv.length) {
154
+ lines.push(` NEXT_PUBLIC_* → clientEnv candidates: ${report.clientEnv.join(", ")}`);
155
+ }
156
+ if (report.pages.length) {
157
+ lines.push("");
158
+ lines.push("Pages:");
159
+ for (const p of report.pages) {
160
+ const flags = [];
161
+ if (p.classification?.revalidate != null) flags.push(`revalidate=${p.classification.revalidate}`);
162
+ if (p.classification?.hasGenerateMetadata) flags.push("metadata");
163
+ if (p.classification?.hasGenerateStaticParams) flags.push("staticParams");
164
+ lines.push(` ${p.url} ← ${p.rel}${flags.length ? ` (${flags.join(", ")})` : ""}`);
165
+ }
166
+ }
167
+ if (report.blockers.length) {
168
+ lines.push("");
169
+ lines.push("Blockers:");
170
+ for (const b of report.blockers) {
171
+ lines.push(` ⚠ ${b.message}${b.file ? ` [${b.file}]` : ""}`);
172
+ }
173
+ }
174
+ lines.push("");
175
+ lines.push("Next: jskelet migrate apply [--write] | jskelet migrate config [--write]");
176
+ return lines.join("\n");
177
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Babel ifade → kaynak metin + JSK parseExpr doğrulaması.
3
+ * `@babel/generator` yok; JSX attr / {{ }} için yeterli alt küme basılır.
4
+ */
5
+ import { t } from "../babel.mjs";
6
+ import { parseExpr } from "../../compile/expr.js";
7
+
8
+ /**
9
+ * @param {import('@babel/types').Node} node
10
+ * @returns {string}
11
+ */
12
+ export function exprToSource(node) {
13
+ if (t.isTSAsExpression(node) || t.isTSTypeAssertion(node) || t.isTSNonNullExpression(node)) {
14
+ return exprToSource(/** @type {{ expression: import('@babel/types').Expression }} */ (node).expression);
15
+ }
16
+ if (t.isParenthesizedExpression(node)) {
17
+ return `(${exprToSource(node.expression)})`;
18
+ }
19
+ if (t.isIdentifier(node)) return node.name;
20
+ if (t.isStringLiteral(node)) return JSON.stringify(node.value);
21
+ if (t.isNumericLiteral(node)) return String(node.value);
22
+ if (t.isBooleanLiteral(node)) return node.value ? "true" : "false";
23
+ if (t.isNullLiteral(node)) return "null";
24
+ if (t.isBigIntLiteral(node)) return node.value + "n";
25
+
26
+ if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
27
+ const obj = exprToSource(node.object);
28
+ const opt = "optional" in node && node.optional ? "?." : ".";
29
+ if (node.computed) {
30
+ return `${obj}${opt === "?." ? "?." : ""}[${exprToSource(/** @type {import('@babel/types').Expression} */ (node.property))}]`;
31
+ }
32
+ if (t.isIdentifier(node.property)) {
33
+ return `${obj}${opt}${node.property.name}`;
34
+ }
35
+ return `${obj}.${exprToSource(node.property)}`;
36
+ }
37
+
38
+ if (t.isUnaryExpression(node)) {
39
+ return `${node.operator}${exprToSource(node.argument)}`;
40
+ }
41
+
42
+ if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
43
+ return `${exprToSource(/** @type {import('@babel/types').Expression} */ (node.left))} ${node.operator} ${exprToSource(node.right)}`;
44
+ }
45
+
46
+ if (t.isConditionalExpression(node)) {
47
+ return `${exprToSource(node.test)} ? ${exprToSource(node.consequent)} : ${exprToSource(node.alternate)}`;
48
+ }
49
+
50
+ if (t.isTemplateLiteral(node)) {
51
+ let out = "`";
52
+ for (let i = 0; i < node.quasis.length; i++) {
53
+ out += node.quasis[i].value.cooked ?? node.quasis[i].value.raw;
54
+ if (i < node.expressions.length) {
55
+ out += "${" + exprToSource(node.expressions[i]) + "}";
56
+ }
57
+ }
58
+ return out + "`";
59
+ }
60
+
61
+ if (t.isArrayExpression(node)) {
62
+ return `[${node.elements.map((el) => (el ? exprToSource(el) : "")).join(", ")}]`;
63
+ }
64
+
65
+ if (t.isObjectExpression(node)) {
66
+ const props = node.properties.map((p) => {
67
+ if (t.isSpreadElement(p)) return `...${exprToSource(p.argument)}`;
68
+ if (t.isObjectMethod(p)) return `/* method */`;
69
+ if (t.isObjectProperty(p)) {
70
+ const key = t.isIdentifier(p.key) && !p.computed ? p.key.name : exprToSource(p.key);
71
+ return `${key}: ${exprToSource(/** @type {import('@babel/types').Expression} */ (p.value))}`;
72
+ }
73
+ return "";
74
+ });
75
+ return `{ ${props.join(", ")} }`;
76
+ }
77
+
78
+ if (t.isCallExpression(node)) {
79
+ const callee = exprToSource(node.callee);
80
+ const args = node.arguments.map((a) => exprToSource(/** @type {import('@babel/types').Node} */ (a))).join(", ");
81
+ return `${callee}(${args})`;
82
+ }
83
+
84
+ if (t.isAwaitExpression(node)) {
85
+ return `await ${exprToSource(node.argument)}`;
86
+ }
87
+
88
+ if (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) {
89
+ return `/* function */`;
90
+ }
91
+
92
+ if (t.isThisExpression(node)) return "this";
93
+
94
+ return `/* unsupported:${node.type} */`;
95
+ }
96
+
97
+ /**
98
+ * @param {string} source
99
+ * @returns {boolean}
100
+ */
101
+ export function isJskExpr(source) {
102
+ if (!source || source.includes("/* unsupported")) return false;
103
+ try {
104
+ parseExpr(source);
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @param {import('@babel/types').Expression} expr
113
+ * @returns {{ ok: true, source: string } | { ok: false, reason: string }}
114
+ */
115
+ export function tryJskExpr(expr) {
116
+ if (t.isStringLiteral(expr)) {
117
+ return { ok: true, source: JSON.stringify(expr.value) };
118
+ }
119
+ if (t.isNumericLiteral(expr)) {
120
+ return { ok: true, source: String(expr.value) };
121
+ }
122
+ if (t.isBooleanLiteral(expr)) {
123
+ return { ok: true, source: expr.value ? "true" : "false" };
124
+ }
125
+ if (t.isNullLiteral(expr)) {
126
+ return { ok: true, source: "null" };
127
+ }
128
+
129
+ const source = exprToSource(expr);
130
+ if (isJskExpr(source)) return { ok: true, source };
131
+ return { ok: false, reason: `expression not valid in .jsk: ${source}` };
132
+ }
133
+
134
+ /**
135
+ * @param {import('@babel/types').Expression} expr
136
+ * @param {Set<string>} out
137
+ */
138
+ export function collectRootIdents(expr, out) {
139
+ if (t.isIdentifier(expr)) {
140
+ out.add(expr.name);
141
+ return;
142
+ }
143
+ if (t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) {
144
+ collectRootIdents(/** @type {import('@babel/types').Expression} */ (expr.object), out);
145
+ return;
146
+ }
147
+ if (t.isConditionalExpression(expr)) {
148
+ collectRootIdents(expr.test, out);
149
+ collectRootIdents(expr.consequent, out);
150
+ collectRootIdents(expr.alternate, out);
151
+ return;
152
+ }
153
+ if (t.isLogicalExpression(expr) || t.isBinaryExpression(expr)) {
154
+ collectRootIdents(/** @type {import('@babel/types').Expression} */ (expr.left), out);
155
+ collectRootIdents(expr.right, out);
156
+ return;
157
+ }
158
+ if (t.isUnaryExpression(expr)) {
159
+ collectRootIdents(expr.argument, out);
160
+ return;
161
+ }
162
+ if (t.isCallExpression(expr)) {
163
+ collectRootIdents(/** @type {import('@babel/types').Expression} */ (expr.callee), out);
164
+ for (const arg of expr.arguments) {
165
+ if (t.isExpression(arg)) collectRootIdents(arg, out);
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `"use client"` → island `mount()` iskeleti.
3
+ */
4
+ import path from "node:path";
5
+
6
+ /**
7
+ * @typedef {{ status: 'ok' | 'partial' | 'skipped', notes: string[], code: string | null, islandName: string }} IslandResult
8
+ */
9
+
10
+ /**
11
+ * @param {string} code
12
+ * @param {string} filename
13
+ * @param {{ name?: string }} [options]
14
+ * @returns {IslandResult}
15
+ */
16
+ export function transformIsland(code, filename, options = {}) {
17
+ const base = path.basename(filename).replace(/\.(jsx?|tsx?|mjs|cjs)$/, "");
18
+ const islandName = options.name ?? camel(base);
19
+ /** @type {IslandResult} */
20
+ const result = {
21
+ status: "partial",
22
+ notes: [],
23
+ code: null,
24
+ islandName,
25
+ };
26
+
27
+ const hasHooks = /\buse(State|Effect|Memo|Callback|Ref|Context|Reducer)\s*\(/.test(code);
28
+ if (hasHooks) {
29
+ result.notes.push("hooks require a manual port into mount()");
30
+ }
31
+ if (!/["']use client["']/.test(code) && !hasHooks) {
32
+ result.notes.push("no \"use client\" directive — treating as island candidate anyway");
33
+ }
34
+
35
+ result.code =
36
+ `/**\n` +
37
+ ` * Island migrated from ${filename.replace(/\\/g, "/")}\n` +
38
+ ` * Register: registerAll({ ${islandName}: () => import("./${islandName}.js") })\n` +
39
+ ` * Markup: <div data-island="${islandName}" data-island-props='{}'></div>\n` +
40
+ ` */\n` +
41
+ `/**\n` +
42
+ ` * @param {HTMLElement} el\n` +
43
+ ` * @param {Record<string, unknown>} [props]\n` +
44
+ ` */\n` +
45
+ `export function mount(el, props = {}) {\n` +
46
+ ` // TODO[jskelet-migrate]: port client logic from ${path.basename(filename)}\n` +
47
+ (hasHooks
48
+ ? ` // Original file used React hooks — rewrite with plain DOM + events.\n`
49
+ : "") +
50
+ ` el.textContent = "";\n` +
51
+ ` void props;\n` +
52
+ `}\n`;
53
+
54
+ return result;
55
+ }
56
+
57
+ /**
58
+ * @param {string} name
59
+ * @returns {string}
60
+ */
61
+ function camel(name) {
62
+ return name
63
+ .split(/[-_/]+/)
64
+ .filter(Boolean)
65
+ .map((p, i) => (i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1)))
66
+ .join("");
67
+ }