jskelet 0.5.4 → 0.6.0

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 (149) hide show
  1. package/AGENTS.md +18 -13
  2. package/CHANGELOG.md +387 -385
  3. package/README.md +9 -7
  4. package/bin/jskelet.mjs +24 -10
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +4 -3
  7. package/docs/03-routing.md +11 -6
  8. package/docs/04-render-ve-sablonlar.md +35 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/07-yapilandirma.md +53 -25
  11. package/docs/08-build.md +40 -18
  12. package/docs/09-dev-araclari.md +5 -1
  13. package/docs/10-dagitim.md +6 -1
  14. package/docs/11-tasima.md +51 -17
  15. package/docs/12-panel-ve-oturum.md +10 -4
  16. package/docs/README.md +7 -5
  17. package/docs/en/01-getting-started.md +4 -3
  18. package/docs/en/02-architecture.md +5 -5
  19. package/docs/en/03-routing.md +12 -7
  20. package/docs/en/04-rendering.md +47 -59
  21. package/docs/en/05-islands.md +13 -8
  22. package/docs/en/07-configuration.md +55 -27
  23. package/docs/en/08-build.md +43 -21
  24. package/docs/en/09-dev-tools.md +6 -1
  25. package/docs/en/10-deployment.md +6 -1
  26. package/docs/en/11-migration.md +51 -16
  27. package/docs/en/12-dashboards-and-sessions.md +9 -4
  28. package/docs/en/README.md +7 -5
  29. package/package.json +49 -14
  30. package/src/build/tasks/client.mjs +91 -10
  31. package/src/build/tasks/icons.mjs +152 -18
  32. package/src/client/index.js +2 -2
  33. package/src/compile/codegen.js +4 -0
  34. package/src/compile/compile-all.js +12 -21
  35. package/src/compile/expr.js +5 -0
  36. package/src/compile/parse.js +64 -8
  37. package/src/compile/resolve.js +3 -0
  38. package/src/config/defaults.js +12 -2
  39. package/src/config/index.js +31 -3
  40. package/src/dev-server.mjs +26 -3
  41. package/src/http/cookies-entry.js +1 -0
  42. package/src/http/cookies.js +18 -0
  43. package/src/logo.png +0 -0
  44. package/src/migrate/apply.mjs +262 -0
  45. package/src/migrate/babel.mjs +79 -0
  46. package/src/migrate/classify.mjs +155 -0
  47. package/src/migrate/config.mjs +126 -0
  48. package/src/migrate/fs-walk.mjs +191 -0
  49. package/src/migrate/parse.mjs +26 -0
  50. package/src/migrate/scan.mjs +177 -0
  51. package/src/migrate/transform/expr-source.mjs +168 -0
  52. package/src/migrate/transform/island.mjs +67 -0
  53. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  54. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  55. package/src/migrate/transform/page-split.mjs +435 -0
  56. package/src/migrate/write.mjs +81 -0
  57. package/src/migrate.mjs +171 -0
  58. package/src/server/auth/handoff.js +94 -11
  59. package/src/server/create-app.js +28 -10
  60. package/src/server/ejs-adapter.js +59 -0
  61. package/src/server/image-optimizer.js +94 -26
  62. package/src/server/port-guard.js +255 -0
  63. package/src/server/render.js +27 -9
  64. package/src/server/status-page.js +105 -4
  65. package/src/start.mjs +18 -3
  66. package/src/templates/layout.ejs +8 -28
  67. package/src/templates/layout.jsk +30 -0
  68. package/src/templates/layout.render.js +41 -0
  69. package/src/views/helpers/tags.js +86 -3
  70. package/types/build/resolve-peer.d.mts +13 -0
  71. package/types/client/dom.d.ts +55 -0
  72. package/types/client/form.d.ts +19 -0
  73. package/types/client/index.d.ts +20 -0
  74. package/types/client/registry.d.ts +53 -0
  75. package/types/client/safe-image.d.ts +19 -0
  76. package/types/client/shared-cookie.d.ts +82 -0
  77. package/types/client/store.d.ts +18 -0
  78. package/types/client/swap.d.ts +46 -0
  79. package/types/compile/codegen.d.ts +32 -0
  80. package/types/compile/compile-all.d.ts +42 -0
  81. package/types/compile/errors.d.ts +30 -0
  82. package/types/compile/expr.d.ts +67 -0
  83. package/types/compile/index.d.ts +10 -0
  84. package/types/compile/parse.d.ts +82 -0
  85. package/types/compile/resolve.d.ts +46 -0
  86. package/types/compile/scan-exports.d.ts +9 -0
  87. package/types/config/defaults.d.ts +449 -0
  88. package/types/config/index.d.ts +299 -0
  89. package/types/config/pattern.d.ts +38 -0
  90. package/types/http/control-flow.d.ts +45 -0
  91. package/types/http/cookies-entry.d.ts +5 -0
  92. package/types/http/cookies.d.ts +113 -0
  93. package/types/http/request-cache.d.ts +13 -0
  94. package/types/http/request-context.d.ts +67 -0
  95. package/types/http/shared-cookie.d.ts +73 -0
  96. package/types/index.d.ts +30 -0
  97. package/types/log.d.mts +153 -0
  98. package/types/server/admin/actions.d.ts +16 -0
  99. package/types/server/admin/auth.d.ts +52 -0
  100. package/types/server/admin/event-log.d.ts +38 -0
  101. package/types/server/admin/gate.d.ts +43 -0
  102. package/types/server/admin/inventory.d.ts +40 -0
  103. package/types/server/admin/mount.d.ts +6 -0
  104. package/types/server/admin/router.d.ts +6 -0
  105. package/types/server/admin/snapshot.d.ts +6 -0
  106. package/types/server/assets.d.ts +47 -0
  107. package/types/server/auth/handoff.d.ts +12 -0
  108. package/types/server/cache-deps.d.ts +16 -0
  109. package/types/server/cache-vary.d.ts +30 -0
  110. package/types/server/cloudflare.d.ts +163 -0
  111. package/types/server/create-app.d.ts +25 -0
  112. package/types/server/data-cache.d.ts +116 -0
  113. package/types/server/dev/devtools.d.ts +44 -0
  114. package/types/server/dev/report.d.ts +229 -0
  115. package/types/server/dev/socket.d.ts +17 -0
  116. package/types/server/dev/version-check.d.mts +15 -0
  117. package/types/server/ejs-adapter.d.ts +11 -0
  118. package/types/server/head-hints.d.ts +40 -0
  119. package/types/server/html-cache.d.ts +173 -0
  120. package/types/server/image-optimizer.d.ts +68 -0
  121. package/types/server/logs/access-middleware.d.ts +7 -0
  122. package/types/server/logs/file-sink.d.ts +17 -0
  123. package/types/server/logs/pipeline.d.ts +37 -0
  124. package/types/server/logs/s3-put.d.ts +85 -0
  125. package/types/server/logs/s3-sink.d.ts +26 -0
  126. package/types/server/metadata.d.ts +38 -0
  127. package/types/server/middleware/compression.d.ts +17 -0
  128. package/types/server/middleware/csrf.d.ts +4 -0
  129. package/types/server/middleware/dev-gate.d.ts +2 -0
  130. package/types/server/middleware/headers.d.ts +2 -0
  131. package/types/server/middleware/redirects.d.ts +2 -0
  132. package/types/server/middleware/static-precompressed.d.ts +5 -0
  133. package/types/server/middleware/trailing-slash.d.ts +11 -0
  134. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  135. package/types/server/og-image.d.ts +149 -0
  136. package/types/server/port-guard.d.ts +50 -0
  137. package/types/server/prewarm.d.ts +128 -0
  138. package/types/server/redis.d.ts +163 -0
  139. package/types/server/render.d.ts +101 -0
  140. package/types/server/router.d.ts +5 -0
  141. package/types/server/status-page.d.ts +24 -0
  142. package/types/server/upstream-limiter.d.ts +123 -0
  143. package/types/server/upstream-tracking.d.ts +42 -0
  144. package/types/shared/cookie-domain.d.ts +29 -0
  145. package/types/templates/layout.render.d.ts +7 -0
  146. package/types/version.d.mts +10 -0
  147. package/types/views/components/loader.d.ts +5 -0
  148. package/types/views/helpers/html.d.ts +39 -0
  149. package/types/views/helpers/tags.d.ts +127 -0
@@ -64,6 +64,8 @@ class TemplateParser {
64
64
  this.source = source;
65
65
  this.file = file;
66
66
  this.i = 0;
67
+ /** @type {string[]} Açık {#if}/{#each} kapanışları — EOF'ta öncelikli hata. */
68
+ this.blockStack = [];
67
69
  }
68
70
 
69
71
  /**
@@ -121,6 +123,13 @@ class TemplateParser {
121
123
  break;
122
124
  }
123
125
 
126
+ // `<!DOCTYPE …>` / `<![CDATA[…]]>` — etiket değil; metin olarak bas.
127
+ // İlerletmezsek `<` + `!` dalı parseText'te de takılıp sonsuz döngü olur.
128
+ if (this.startsWith("<!")) {
129
+ nodes.push(this.parseDeclaration());
130
+ continue;
131
+ }
132
+
124
133
  if (this.peek() === "<" && /[A-Za-z]/.test(this.peek(1))) {
125
134
  nodes.push(this.parseTag());
126
135
  continue;
@@ -131,6 +140,35 @@ class TemplateParser {
131
140
  return nodes;
132
141
  }
133
142
 
143
+ /**
144
+ * `<!DOCTYPE html>` gibi bildirimler. Kapanış `>` ilk tırnak dışı geçiş.
145
+ * @returns {TextNode}
146
+ */
147
+ parseDeclaration() {
148
+ const start = this.i;
149
+ this.i += 2; // <!
150
+ let quote = null;
151
+ while (!this.eof()) {
152
+ const ch = this.peek();
153
+ if (quote) {
154
+ if (ch === quote) quote = null;
155
+ this.i += 1;
156
+ continue;
157
+ }
158
+ if (ch === '"' || ch === "'") {
159
+ quote = ch;
160
+ this.i += 1;
161
+ continue;
162
+ }
163
+ if (ch === ">") {
164
+ this.i += 1;
165
+ break;
166
+ }
167
+ this.i += 1;
168
+ }
169
+ return { type: "Text", value: this.source.slice(start, this.i) };
170
+ }
171
+
134
172
  skipComment() {
135
173
  const end = this.source.indexOf("-->", this.i + 4);
136
174
  if (end === -1) throw this.error("Unterminated HTML comment");
@@ -217,13 +255,19 @@ class TemplateParser {
217
255
  });
218
256
  this.i = closeBrace + 1;
219
257
 
220
- const consequent = this.parseNodesUntil(["{#else}", "{/if}"]);
258
+ this.blockStack.push("{/if}");
259
+ /** @type {AstNode[]} */
260
+ let consequent;
221
261
  /** @type {AstNode[]} */
222
262
  let alternate = [];
223
-
224
- if (this.startsWith("{#else}")) {
225
- this.i += "{#else}".length;
226
- alternate = this.parseNodesUntil(["{/if}"]);
263
+ try {
264
+ consequent = this.parseNodesUntil(["{#else}", "{/if}"]);
265
+ if (this.startsWith("{#else}")) {
266
+ this.i += "{#else}".length;
267
+ alternate = this.parseNodesUntil(["{/if}"]);
268
+ }
269
+ } finally {
270
+ this.blockStack.pop();
227
271
  }
228
272
 
229
273
  if (!this.startsWith("{/if}")) {
@@ -263,6 +307,10 @@ class TemplateParser {
263
307
  continue;
264
308
  }
265
309
  if (this.startsWith("</")) break;
310
+ if (this.startsWith("<!")) {
311
+ nodes.push(this.parseDeclaration());
312
+ continue;
313
+ }
266
314
  if (this.peek() === "<" && /[A-Za-z]/.test(this.peek(1))) {
267
315
  nodes.push(this.parseTag());
268
316
  continue;
@@ -308,7 +356,14 @@ class TemplateParser {
308
356
  templateIndex: this.i,
309
357
  });
310
358
  this.i = closeBrace + 1;
311
- const children = this.parseNodesUntil(["{/each}"]);
359
+ this.blockStack.push("{/each}");
360
+ /** @type {AstNode[]} */
361
+ let children;
362
+ try {
363
+ children = this.parseNodesUntil(["{/each}"]);
364
+ } finally {
365
+ this.blockStack.pop();
366
+ }
312
367
  if (!this.startsWith("{/each}")) {
313
368
  throw this.error("Expected {/each}", this.i);
314
369
  }
@@ -369,9 +424,10 @@ class TemplateParser {
369
424
  let children = [];
370
425
  if (!selfClosing && !voidEl) {
371
426
  children = this.parseNodesUntil([`</${name}>`]);
372
- // Also allow parseNodes style — check closing tag
373
427
  if (!this.startsWith(`</${name}>`)) {
374
- // try case-sensitive exact
428
+ if (this.eof() && this.blockStack.length) {
429
+ throw this.error(`Expected ${this.blockStack.at(-1)}`, this.i);
430
+ }
375
431
  throw this.error(`Expected </${name}>`, this.i);
376
432
  }
377
433
  this.i += `</${name}>`.length;
@@ -131,6 +131,9 @@ export function collectKnownComponents(componentDirs, jskFiles) {
131
131
  "Icon",
132
132
  "CsrfField",
133
133
  "PreloadImage",
134
+ "Stylesheets",
135
+ "BodyScripts",
136
+ "JsonLd",
134
137
  ]);
135
138
 
136
139
  /** @type {Map<string, string>} export adı → göreli yol */
@@ -467,9 +467,19 @@ export const DEFAULT_BRAND = {
467
467
  */
468
468
  export const DEFAULT_AUTH = {
469
469
  /**
470
- * `true` veya `{ ttlSeconds?, path?, maxValueBytes? }`.
470
+ * `true` veya `{ allowedCookieNames, ttlSeconds?, path?, maxValueBytes?,
471
+ * maxPendingTickets?, maxMintsPerIpPerMinute? }`.
471
472
  * Açıkken `POST /_jskelet/auth/handoff` ve `?handoff=` redeem middleware'i.
472
- * @type {boolean | { enabled?: boolean, ttlSeconds?: number, path?: string, maxValueBytes?: number }}
473
+ * Mint için `allowedCookieNames` dolu olmalı (aksi halde 400).
474
+ * @type {boolean | {
475
+ * enabled?: boolean,
476
+ * allowedCookieNames?: string[],
477
+ * ttlSeconds?: number,
478
+ * path?: string,
479
+ * maxValueBytes?: number,
480
+ * maxPendingTickets?: number,
481
+ * maxMintsPerIpPerMinute?: number,
482
+ * }}
473
483
  */
474
484
  crossSubdomainHandoff: false,
475
485
  };
@@ -141,7 +141,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
141
141
  * @property {string[]} prewarmSkip
142
142
  * @property {string[]} watch Dev sunucusunun izlediği ek dizinler.
143
143
  * @property {{ family: string, slug?: string, weights: number[] }[]} fonts
144
- * @property {{ scan?: string[] } | false} icons
144
+ * @property {{ scan?: string[], dir: string } | false} icons
145
145
  * @property {ImagesConfig | false} images
146
146
  * @property {string[]} clientEnv Client bundle'a gömülecek env anahtarları.
147
147
  */
@@ -987,6 +987,34 @@ function normalizeSecurity(raw) {
987
987
  };
988
988
  }
989
989
 
990
+ /**
991
+ * İkon sprite ayarları. `false` → adım atlanır. `dir` varsayılanı `"icons"`:
992
+ * o dizin varsa yalnızca yerel SVG'ler; yoksa Phosphor.
993
+ *
994
+ * @param {unknown} raw
995
+ * @returns {{ scan?: string[], dir: string } | false}
996
+ */
997
+ function normalizeIcons(raw) {
998
+ if (raw === false) return false;
999
+
1000
+ const source = /** @type {Record<string, any>} */ (raw ?? {});
1001
+ const dir =
1002
+ typeof source.dir === "string" && source.dir.trim()
1003
+ ? source.dir.trim()
1004
+ : "icons";
1005
+
1006
+ /** @type {{ scan?: string[], dir: string }} */
1007
+ const icons = { dir };
1008
+
1009
+ if (source.scan != null) {
1010
+ icons.scan = asArray(source.scan, "icons.scan")
1011
+ .filter((entry) => typeof entry === "string" && entry.trim())
1012
+ .map((entry) => String(entry).trim());
1013
+ }
1014
+
1015
+ return icons;
1016
+ }
1017
+
990
1018
  /**
991
1019
  * Build + runtime görsel ayarları. `false` → her iki yüzey de kapalı.
992
1020
  * `remote.allowHosts` boşsa remote kapalı kalır (açık proxy olmasın).
@@ -1110,7 +1138,7 @@ function resolveLayout(dirs, override) {
1110
1138
  const appLayout = path.join(dirs.views, "layout.ejs");
1111
1139
  if (fs.existsSync(appLayout)) return appLayout;
1112
1140
 
1113
- return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.ejs");
1141
+ return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
1114
1142
  }
1115
1143
 
1116
1144
  /**
@@ -1237,7 +1265,7 @@ export async function loadConfig(options = {}) {
1237
1265
  // Build tarafı ayarları. Sunucu bunları okumaz ama config tek dosya
1238
1266
  // olsun diye aynı yerden geçer.
1239
1267
  fonts: source.fonts ?? [],
1240
- icons: source.icons ?? {},
1268
+ icons: normalizeIcons(source.icons),
1241
1269
  images: normalizeImages(source.images),
1242
1270
  clientEnv: source.clientEnv ?? [],
1243
1271
  };
@@ -138,6 +138,18 @@ function escapeRegExp(value) {
138
138
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
139
139
  }
140
140
 
141
+ /**
142
+ * Bekleyen hata kutusunu hemen bas. Alt süreç exit/close olduğunda 60 ms
143
+ * zamanlayıcıya güvenilmez: parent `shutdown` ile ölürse satır hiç görünmez.
144
+ */
145
+ function flushErrorNow() {
146
+ if (errorTimer) {
147
+ clearTimeout(errorTimer);
148
+ errorTimer = null;
149
+ }
150
+ flushError();
151
+ }
152
+
141
153
  /**
142
154
  * @param {string} line
143
155
  * @param {boolean} isError
@@ -174,8 +186,9 @@ function onServerLine(line, isError) {
174
186
 
175
187
  if (!line.trim()) return;
176
188
 
177
- // Hata gövdesi ve yığın satırları kutuya toplanır.
178
- if (isError || /^\s+at\s/.test(line) || /Error\b/.test(line)) {
189
+ // Yığın / Error: kutuya. Düz stderr (port dolu vb.) anında — süreç hemen
190
+ // exit 1 ile öldüğü için gecikmeli flush'a kalırsa satır kayboluyor.
191
+ if (/^\s+at\s/.test(line) || /Error\b/.test(line)) {
179
192
  errorBuffer.push(
180
193
  line.replace(/^\[(?:uncaughtException|unhandledRejection)\]\s*/, ""),
181
194
  );
@@ -183,6 +196,11 @@ function onServerLine(line, isError) {
183
196
  return;
184
197
  }
185
198
 
199
+ if (isError) {
200
+ log.error(line.trim());
201
+ return;
202
+ }
203
+
186
204
  log.event({ scope: "server", message: line });
187
205
  }
188
206
 
@@ -205,9 +223,12 @@ function run(args, label, passthrough) {
205
223
  passthrough ? onBuildLine(line) : onServerLine(line, true),
206
224
  );
207
225
 
208
- child.on("exit", (code) => {
226
+ // `close`: stdio boşaldıktan sonra. `exit` ile yarışınca port hatası
227
+ // stderr'de kalıp "server exited" dışında bir şey görünmüyordu.
228
+ child.on("close", (code) => {
209
229
  if (code === 0 || code === null) return;
210
230
  if (child[STOPPING]) return; // yeniden başlatma: beklenen çıkış
231
+ if (!passthrough) flushErrorNow();
211
232
  log.error(`${label} exited (code ${code})`);
212
233
  shutdown(code);
213
234
  });
@@ -249,6 +270,7 @@ let server = null;
249
270
 
250
271
  // `--import` modül belirteci bekler: Windows'ta `H:\…` yolu `h:` şemalı URL
251
272
  // sanılıp reddediliyor, bu yüzden file:// URL'e çevrilir.
273
+ // `--murder` sunucu sürecine iletilir: dolu portta dinleyen öldürülüp başlanır.
252
274
  const SERVER_ARGS = [
253
275
  // `.env` yoksa bayrak hiç geçilmez: `--env-file-if-exists` dosya yokken bir
254
276
  // bildirim satırı basıyor ve bu satır hata kutusuna dönüşüyordu.
@@ -256,6 +278,7 @@ const SERVER_ARGS = [
256
278
  "--import",
257
279
  pathToFileURL(path.join(SRC, "runtime", "register.mjs")).href,
258
280
  path.join(SRC, "start.mjs"),
281
+ ...(process.argv.includes("--murder") ? ["--murder"] : []),
259
282
  ];
260
283
 
261
284
  function startServerProcess() {
@@ -4,6 +4,7 @@
4
4
  export {
5
5
  clearCookie,
6
6
  getSignedCookie,
7
+ isValidCookieName,
7
8
  parseCookies,
8
9
  randomToken,
9
10
  safeEqual,
@@ -148,6 +148,17 @@ export function parseCookies(req) {
148
148
  * @property {"Strict" | "Lax" | "None"} [sameSite] Varsayılan `Lax`.
149
149
  */
150
150
 
151
+ /**
152
+ * RFC 6265 cookie-name (token). `;`, boşluk, CRLF gibi karakterler
153
+ * Set-Cookie enjeksiyonuna yol açardı — reddedilir.
154
+ *
155
+ * @param {unknown} name
156
+ * @returns {boolean}
157
+ */
158
+ export function isValidCookieName(name) {
159
+ return typeof name === "string" && /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
160
+ }
161
+
151
162
  /**
152
163
  * Varsayılanlar bilinçli olarak kısıtlayıcı: `HttpOnly` ile JS okuyamaz,
153
164
  * `SameSite=Lax` ile çapraz site POST'larında gönderilmez (CSRF'nin büyük
@@ -159,6 +170,13 @@ export function parseCookies(req) {
159
170
  * @returns {string}
160
171
  */
161
172
  export function serializeCookie(name, value, options = {}) {
173
+ if (!isValidCookieName(name)) {
174
+ throw new Error(
175
+ `[cookies] invalid cookie name ${JSON.stringify(name)}; ` +
176
+ "names must be RFC 6265 tokens (no spaces, semicolons, or control chars)",
177
+ );
178
+ }
179
+
162
180
  const parts = [`${name}=${encodeURIComponent(value)}`];
163
181
 
164
182
  parts.push(`Path=${options.path ?? "/"}`);
package/src/logo.png CHANGED
Binary file
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Codemod apply: pages / components / islands → JSkelet ağacı.
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { scanProject } from "./scan.mjs";
7
+ import { splitPage } from "./transform/page-split.mjs";
8
+ import { transformComponent } from "./transform/jsx-to-component.mjs";
9
+ import { transformIsland } from "./transform/island.mjs";
10
+ import { jsxToJsk } from "./transform/jsx-to-jsk.mjs";
11
+ import { parseSource } from "./parse.mjs";
12
+ import { t } from "./babel.mjs";
13
+ import { commitWrites } from "./write.mjs";
14
+
15
+ /**
16
+ * @typedef {{
17
+ * file: string,
18
+ * kind: string,
19
+ * status: 'ok' | 'partial' | 'skipped',
20
+ * notes: string[],
21
+ * outputs: string[],
22
+ * }} FileReport
23
+ */
24
+
25
+ /**
26
+ * @param {string} sourceRoot Next projesi
27
+ * @param {string} outRoot JSkelet hedefi
28
+ * @param {{ write?: boolean, only?: Set<string>, strict?: boolean }} options
29
+ * @returns {Promise<{ reports: FileReport[], writes: object[] }>}
30
+ */
31
+ export async function applyMigrate(sourceRoot, outRoot, options = {}) {
32
+ const scan = scanProject(sourceRoot);
33
+ /** @type {FileReport[]} */
34
+ const reports = [];
35
+ /** @type {{ path: string, contents: string }[]} */
36
+ const pending = [];
37
+
38
+ const only = options.only ?? null;
39
+ const want = (/** @type {string} */ k) => !only || only.has(k);
40
+
41
+ if (want("pages")) {
42
+ for (const page of scan.pages) {
43
+ const code = fs.readFileSync(page.abs, "utf8");
44
+ if (page.classification?.isClient || page.classification?.hasHooks) {
45
+ reports.push({
46
+ file: page.rel,
47
+ kind: "page",
48
+ status: "skipped",
49
+ notes: ["page is a client component — convert to island + server shell manually"],
50
+ outputs: [],
51
+ });
52
+ continue;
53
+ }
54
+ const viewId = `pages/${page.page}`;
55
+ const result = splitPage(code, page.rel, {
56
+ url: page.url,
57
+ feature: page.feature,
58
+ page: page.page,
59
+ viewId,
60
+ });
61
+ /** @type {string[]} */
62
+ const outputs = [];
63
+ if (result.controller) {
64
+ const dest = path.join(outRoot, "features", page.feature, "index.js");
65
+ pending.push({ path: dest, contents: result.controller });
66
+ outputs.push(path.relative(outRoot, dest).replace(/\\/g, "/"));
67
+ }
68
+ if (result.jsk) {
69
+ const dest = path.join(
70
+ outRoot,
71
+ "features",
72
+ page.feature,
73
+ "views",
74
+ "pages",
75
+ `${page.page}.jsk`,
76
+ );
77
+ pending.push({ path: dest, contents: result.jsk });
78
+ outputs.push(path.relative(outRoot, dest).replace(/\\/g, "/"));
79
+ }
80
+ reports.push({
81
+ file: page.rel,
82
+ kind: "page",
83
+ status: result.status,
84
+ notes: result.notes,
85
+ outputs,
86
+ });
87
+ }
88
+
89
+ // Root layout only (depth 0)
90
+ const rootLayout = scan.layouts.find((l) => l.depth === 0);
91
+ if (rootLayout) {
92
+ const code = fs.readFileSync(rootLayout.abs, "utf8");
93
+ const layoutResult = transformRootLayout(code, rootLayout.rel);
94
+ if (layoutResult.jsk) {
95
+ const dest = path.join(outRoot, "views", "layout.jsk");
96
+ pending.push({ path: dest, contents: layoutResult.jsk });
97
+ reports.push({
98
+ file: rootLayout.rel,
99
+ kind: "layout",
100
+ status: layoutResult.status,
101
+ notes: layoutResult.notes,
102
+ outputs: ["views/layout.jsk"],
103
+ });
104
+ } else {
105
+ reports.push({
106
+ file: rootLayout.rel,
107
+ kind: "layout",
108
+ status: layoutResult.status,
109
+ notes: layoutResult.notes,
110
+ outputs: [],
111
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ if (want("components")) {
117
+ for (const comp of scan.components) {
118
+ const code = fs.readFileSync(comp.abs, "utf8");
119
+ const result = transformComponent(code, comp.rel);
120
+ /** @type {string[]} */
121
+ const outputs = [];
122
+ if (result.code && result.name) {
123
+ const fileName = camelToKebab(result.name) + ".js";
124
+ const dest = path.join(outRoot, "views", "components", fileName);
125
+ pending.push({ path: dest, contents: result.code });
126
+ outputs.push(path.relative(outRoot, dest).replace(/\\/g, "/"));
127
+ }
128
+ reports.push({
129
+ file: comp.rel,
130
+ kind: "component",
131
+ status: result.status,
132
+ notes: result.notes,
133
+ outputs,
134
+ });
135
+ }
136
+ }
137
+
138
+ if (want("islands")) {
139
+ for (const client of scan.clients) {
140
+ const code = fs.readFileSync(client.abs, "utf8");
141
+ const result = transformIsland(code, client.rel);
142
+ /** @type {string[]} */
143
+ const outputs = [];
144
+ if (result.code) {
145
+ const dest = path.join(outRoot, "features", "migrated", "client", `${result.islandName}.js`);
146
+ pending.push({ path: dest, contents: result.code });
147
+ outputs.push(path.relative(outRoot, dest).replace(/\\/g, "/"));
148
+ }
149
+ reports.push({
150
+ file: client.rel,
151
+ kind: "island",
152
+ status: result.status,
153
+ notes: result.notes,
154
+ outputs,
155
+ });
156
+ }
157
+ }
158
+
159
+ const writes = commitWrites(
160
+ pending.map((p) => ({ path: p.path, contents: p.contents, action: /** @type {const} */ ("create") })),
161
+ { write: Boolean(options.write), cwd: outRoot },
162
+ );
163
+
164
+ return { reports, writes, scan };
165
+ }
166
+
167
+ /**
168
+ * @param {string} code
169
+ * @param {string} filename
170
+ * @returns {{ status: 'ok' | 'partial' | 'skipped', notes: string[], jsk: string | null }}
171
+ */
172
+ function transformRootLayout(code, filename) {
173
+ /** @type {string[]} */
174
+ const notes = [];
175
+ try {
176
+ const ast = parseSource(code, filename);
177
+ const pageFn = findDefaultExportJsx(ast);
178
+ if (!pageFn) {
179
+ return {
180
+ status: "skipped",
181
+ notes: ["root layout: no default export JSX (children slot?) — copy framework layout and edit"],
182
+ jsk: null,
183
+ };
184
+ }
185
+ // Replace {children} with {{{ body }}}
186
+ const { source, report } = jsxToJsk(pageFn);
187
+ let jsk = source.replace(/\{\{\s*children\s*\}\}/g, "{{{ body }}}");
188
+ notes.push(...report.notes);
189
+ notes.push("review layout: wire <Stylesheets />, <BodyScripts />, hooks.layoutContext");
190
+ return {
191
+ status: report.status === "ok" ? "partial" : report.status,
192
+ notes,
193
+ jsk: `{# Migrated from ${filename} — prefer jskelet/layout as a starting point #}\n${jsk.trim()}\n`,
194
+ };
195
+ } catch (error) {
196
+ return {
197
+ status: "skipped",
198
+ notes: [error instanceof Error ? error.message : String(error)],
199
+ jsk: null,
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * @param {import('@babel/types').File} ast
206
+ * @returns {import('@babel/types').JSXElement | import('@babel/types').JSXFragment | null}
207
+ */
208
+ function findDefaultExportJsx(ast) {
209
+ for (const node of ast.program.body) {
210
+ if (!t.isExportDefaultDeclaration(node)) continue;
211
+ const d = node.declaration;
212
+ let body = null;
213
+ if (t.isFunctionDeclaration(d)) body = d.body;
214
+ else if ((t.isArrowFunctionExpression(d) || t.isFunctionExpression(d)) && t.isBlockStatement(d.body)) {
215
+ body = d.body;
216
+ } else if ((t.isArrowFunctionExpression(d) || t.isFunctionExpression(d)) && (t.isJSXElement(d.body) || t.isJSXFragment(d.body))) {
217
+ return d.body;
218
+ }
219
+ if (!body) continue;
220
+ for (const stmt of body.body) {
221
+ if (t.isReturnStatement(stmt) && stmt.argument) {
222
+ let arg = stmt.argument;
223
+ if (t.isParenthesizedExpression(arg)) arg = arg.expression;
224
+ if (t.isJSXElement(arg) || t.isJSXFragment(arg)) return arg;
225
+ }
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+
231
+ /**
232
+ * @param {string} name
233
+ * @returns {string}
234
+ */
235
+ function camelToKebab(name) {
236
+ return name
237
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
238
+ .replace(/_/g, "-")
239
+ .toLowerCase();
240
+ }
241
+
242
+ /**
243
+ * @param {FileReport[]} reports
244
+ * @returns {string}
245
+ */
246
+ export function formatApplyReport(reports) {
247
+ const lines = ["Migrate apply report", ""];
248
+ let ok = 0;
249
+ let partial = 0;
250
+ let skipped = 0;
251
+ for (const r of reports) {
252
+ if (r.status === "ok") ok++;
253
+ else if (r.status === "partial") partial++;
254
+ else skipped++;
255
+ const outs = r.outputs.length ? ` → ${r.outputs.join(", ")}` : "";
256
+ lines.push(`[${r.status}] ${r.kind} ${r.file}${outs}`);
257
+ for (const n of r.notes) lines.push(` - ${n}`);
258
+ }
259
+ lines.push("");
260
+ lines.push(`Summary: ${ok} ok, ${partial} partial, ${skipped} skipped`);
261
+ return lines.join("\n");
262
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Migrate (JSX/TSX → .jsk) için Babel — opsiyonel peer.
3
+ *
4
+ * Normal `build` / `start` / `dev` bunları indirmez. İlk parse anında paket
5
+ * yoksa kurulum komutunu gösteren net bir hata verir.
6
+ */
7
+ import { createRequire } from "node:module";
8
+ import path from "node:path";
9
+ import process from "node:process";
10
+ import { FRAMEWORK_ROOT } from "../config/index.js";
11
+
12
+ const MISSING =
13
+ "`jskelet migrate` requires optional peer dependencies `@babel/parser` and `@babel/types`.\n" +
14
+ "Install them in the project you are migrating:\n" +
15
+ " npm i -D @babel/parser @babel/types";
16
+
17
+ /**
18
+ * @typedef {{
19
+ * parse: typeof import('@babel/parser').parse,
20
+ * types: typeof import('@babel/types'),
21
+ * }} BabelApi
22
+ */
23
+
24
+ /** @type {BabelApi | null | undefined} */
25
+ let cached;
26
+
27
+ /**
28
+ * @param {string} [root] Hedef proje kökü (varsayılan: cwd).
29
+ * @returns {BabelApi}
30
+ */
31
+ export function getBabel(root = process.cwd()) {
32
+ if (cached) return cached;
33
+ if (cached === null) throw new Error(MISSING);
34
+
35
+ for (const base of [root, FRAMEWORK_ROOT]) {
36
+ try {
37
+ const require = createRequire(path.join(base, "package.json"));
38
+ const parser = require("@babel/parser");
39
+ const types = require("@babel/types");
40
+ cached = {
41
+ parse: parser.parse.bind(parser),
42
+ types,
43
+ };
44
+ return cached;
45
+ } catch {
46
+ // Sonraki köke bak.
47
+ }
48
+ }
49
+
50
+ cached = null;
51
+ throw new Error(MISSING);
52
+ }
53
+
54
+ /**
55
+ * Migrate girişinde erken ve anlaşılır hata için.
56
+ *
57
+ * @param {string} [root]
58
+ */
59
+ export function ensureBabel(root) {
60
+ getBabel(root);
61
+ }
62
+
63
+ /**
64
+ * `@babel/types` — ilk erişimde peer'ları yükler.
65
+ * Metodlar doğru `this` ile bağlanır (`t.isIdentifier(node)`).
66
+ *
67
+ * @type {typeof import('@babel/types')}
68
+ */
69
+ export const t = /** @type {typeof import('@babel/types')} */ (
70
+ new Proxy(/** @type {object} */ ({}), {
71
+ get(_target, prop) {
72
+ // Promise thenable tuzağına düşmesin.
73
+ if (prop === "then") return undefined;
74
+ const types = getBabel().types;
75
+ const value = Reflect.get(types, prop, types);
76
+ return typeof value === "function" ? value.bind(types) : value;
77
+ },
78
+ })
79
+ );