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.
- package/AGENTS.md +18 -13
- package/CHANGELOG.md +387 -385
- package/README.md +9 -7
- package/bin/jskelet.mjs +24 -10
- package/docs/01-baslangic.md +4 -3
- package/docs/02-mimari.md +4 -3
- package/docs/03-routing.md +11 -6
- package/docs/04-render-ve-sablonlar.md +35 -43
- package/docs/05-islands.md +12 -8
- package/docs/07-yapilandirma.md +53 -25
- package/docs/08-build.md +40 -18
- package/docs/09-dev-araclari.md +5 -1
- package/docs/10-dagitim.md +6 -1
- package/docs/11-tasima.md +51 -17
- package/docs/12-panel-ve-oturum.md +10 -4
- package/docs/README.md +7 -5
- package/docs/en/01-getting-started.md +4 -3
- package/docs/en/02-architecture.md +5 -5
- package/docs/en/03-routing.md +12 -7
- package/docs/en/04-rendering.md +47 -59
- package/docs/en/05-islands.md +13 -8
- package/docs/en/07-configuration.md +55 -27
- package/docs/en/08-build.md +43 -21
- package/docs/en/09-dev-tools.md +6 -1
- package/docs/en/10-deployment.md +6 -1
- package/docs/en/11-migration.md +51 -16
- package/docs/en/12-dashboards-and-sessions.md +9 -4
- package/docs/en/README.md +7 -5
- package/package.json +49 -14
- package/src/build/tasks/client.mjs +91 -10
- package/src/build/tasks/icons.mjs +152 -18
- package/src/client/index.js +2 -2
- package/src/compile/codegen.js +4 -0
- package/src/compile/compile-all.js +12 -21
- package/src/compile/expr.js +5 -0
- package/src/compile/parse.js +64 -8
- package/src/compile/resolve.js +3 -0
- package/src/config/defaults.js +12 -2
- package/src/config/index.js +31 -3
- package/src/dev-server.mjs +26 -3
- package/src/http/cookies-entry.js +1 -0
- package/src/http/cookies.js +18 -0
- package/src/logo.png +0 -0
- package/src/migrate/apply.mjs +262 -0
- package/src/migrate/babel.mjs +79 -0
- package/src/migrate/classify.mjs +155 -0
- package/src/migrate/config.mjs +126 -0
- package/src/migrate/fs-walk.mjs +191 -0
- package/src/migrate/parse.mjs +26 -0
- package/src/migrate/scan.mjs +177 -0
- package/src/migrate/transform/expr-source.mjs +168 -0
- package/src/migrate/transform/island.mjs +67 -0
- package/src/migrate/transform/jsx-to-component.mjs +302 -0
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
- package/src/migrate/transform/page-split.mjs +435 -0
- package/src/migrate/write.mjs +81 -0
- package/src/migrate.mjs +171 -0
- package/src/server/auth/handoff.js +94 -11
- package/src/server/create-app.js +28 -10
- package/src/server/ejs-adapter.js +59 -0
- package/src/server/image-optimizer.js +94 -26
- package/src/server/port-guard.js +255 -0
- package/src/server/render.js +27 -9
- package/src/server/status-page.js +105 -4
- package/src/start.mjs +18 -3
- package/src/templates/layout.ejs +8 -28
- package/src/templates/layout.jsk +30 -0
- package/src/templates/layout.render.js +41 -0
- package/src/views/helpers/tags.js +86 -3
- package/types/build/resolve-peer.d.mts +13 -0
- package/types/client/dom.d.ts +55 -0
- package/types/client/form.d.ts +19 -0
- package/types/client/index.d.ts +20 -0
- package/types/client/registry.d.ts +53 -0
- package/types/client/safe-image.d.ts +19 -0
- package/types/client/shared-cookie.d.ts +82 -0
- package/types/client/store.d.ts +18 -0
- package/types/client/swap.d.ts +46 -0
- package/types/compile/codegen.d.ts +32 -0
- package/types/compile/compile-all.d.ts +42 -0
- package/types/compile/errors.d.ts +30 -0
- package/types/compile/expr.d.ts +67 -0
- package/types/compile/index.d.ts +10 -0
- package/types/compile/parse.d.ts +82 -0
- package/types/compile/resolve.d.ts +46 -0
- package/types/compile/scan-exports.d.ts +9 -0
- package/types/config/defaults.d.ts +449 -0
- package/types/config/index.d.ts +299 -0
- package/types/config/pattern.d.ts +38 -0
- package/types/http/control-flow.d.ts +45 -0
- package/types/http/cookies-entry.d.ts +5 -0
- package/types/http/cookies.d.ts +113 -0
- package/types/http/request-cache.d.ts +13 -0
- package/types/http/request-context.d.ts +67 -0
- package/types/http/shared-cookie.d.ts +73 -0
- package/types/index.d.ts +30 -0
- package/types/log.d.mts +153 -0
- package/types/server/admin/actions.d.ts +16 -0
- package/types/server/admin/auth.d.ts +52 -0
- package/types/server/admin/event-log.d.ts +38 -0
- package/types/server/admin/gate.d.ts +43 -0
- package/types/server/admin/inventory.d.ts +40 -0
- package/types/server/admin/mount.d.ts +6 -0
- package/types/server/admin/router.d.ts +6 -0
- package/types/server/admin/snapshot.d.ts +6 -0
- package/types/server/assets.d.ts +47 -0
- package/types/server/auth/handoff.d.ts +12 -0
- package/types/server/cache-deps.d.ts +16 -0
- package/types/server/cache-vary.d.ts +30 -0
- package/types/server/cloudflare.d.ts +163 -0
- package/types/server/create-app.d.ts +25 -0
- package/types/server/data-cache.d.ts +116 -0
- package/types/server/dev/devtools.d.ts +44 -0
- package/types/server/dev/report.d.ts +229 -0
- package/types/server/dev/socket.d.ts +17 -0
- package/types/server/dev/version-check.d.mts +15 -0
- package/types/server/ejs-adapter.d.ts +11 -0
- package/types/server/head-hints.d.ts +40 -0
- package/types/server/html-cache.d.ts +173 -0
- package/types/server/image-optimizer.d.ts +68 -0
- package/types/server/logs/access-middleware.d.ts +7 -0
- package/types/server/logs/file-sink.d.ts +17 -0
- package/types/server/logs/pipeline.d.ts +37 -0
- package/types/server/logs/s3-put.d.ts +85 -0
- package/types/server/logs/s3-sink.d.ts +26 -0
- package/types/server/metadata.d.ts +38 -0
- package/types/server/middleware/compression.d.ts +17 -0
- package/types/server/middleware/csrf.d.ts +4 -0
- package/types/server/middleware/dev-gate.d.ts +2 -0
- package/types/server/middleware/headers.d.ts +2 -0
- package/types/server/middleware/redirects.d.ts +2 -0
- package/types/server/middleware/static-precompressed.d.ts +5 -0
- package/types/server/middleware/trailing-slash.d.ts +11 -0
- package/types/server/middleware/upstream-proxy.d.ts +21 -0
- package/types/server/og-image.d.ts +149 -0
- package/types/server/port-guard.d.ts +50 -0
- package/types/server/prewarm.d.ts +128 -0
- package/types/server/redis.d.ts +163 -0
- package/types/server/render.d.ts +101 -0
- package/types/server/router.d.ts +5 -0
- package/types/server/status-page.d.ts +24 -0
- package/types/server/upstream-limiter.d.ts +123 -0
- package/types/server/upstream-tracking.d.ts +42 -0
- package/types/shared/cookie-domain.d.ts +29 -0
- package/types/templates/layout.render.d.ts +7 -0
- package/types/version.d.mts +10 -0
- package/types/views/components/loader.d.ts +5 -0
- package/types/views/helpers/html.d.ts +39 -0
- package/types/views/helpers/tags.d.ts +127 -0
package/src/compile/parse.js
CHANGED
|
@@ -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
|
-
|
|
258
|
+
this.blockStack.push("{/if}");
|
|
259
|
+
/** @type {AstNode[]} */
|
|
260
|
+
let consequent;
|
|
221
261
|
/** @type {AstNode[]} */
|
|
222
262
|
let alternate = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/compile/resolve.js
CHANGED
package/src/config/defaults.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
};
|
package/src/config/index.js
CHANGED
|
@@ -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.
|
|
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
|
};
|
package/src/dev-server.mjs
CHANGED
|
@@ -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
|
-
//
|
|
178
|
-
|
|
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
|
-
|
|
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() {
|
package/src/http/cookies.js
CHANGED
|
@@ -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
|
+
);
|