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.
- package/AGENTS.md +19 -15
- package/CHANGELOG.md +165 -15
- package/README.md +16 -21
- package/bin/jskelet.mjs +23 -9
- package/docs/01-baslangic.md +4 -3
- package/docs/02-mimari.md +10 -4
- package/docs/03-routing.md +14 -7
- package/docs/04-render-ve-sablonlar.md +60 -43
- package/docs/05-islands.md +12 -8
- package/docs/06-cache.md +18 -7
- package/docs/07-yapilandirma.md +69 -27
- package/docs/08-build.md +15 -9
- package/docs/09-dev-araclari.md +22 -8
- package/docs/10-dagitim.md +14 -13
- package/docs/11-tasima.md +51 -17
- package/docs/12-panel-ve-oturum.md +10 -4
- package/docs/README.md +10 -33
- package/docs/en/01-getting-started.md +4 -3
- package/docs/en/02-architecture.md +12 -6
- package/docs/en/03-routing.md +15 -8
- package/docs/en/04-rendering.md +71 -59
- package/docs/en/05-islands.md +13 -8
- package/docs/en/06-caching.md +21 -7
- package/docs/en/07-configuration.md +69 -29
- package/docs/en/08-build.md +16 -10
- package/docs/en/09-dev-tools.md +24 -8
- package/docs/en/10-deployment.md +14 -14
- package/docs/en/11-migration.md +51 -16
- package/docs/en/12-dashboards-and-sessions.md +9 -4
- package/docs/en/README.md +10 -35
- package/package.json +48 -13
- package/src/build/tasks/client.mjs +91 -10
- package/src/build/tasks/icons.mjs +11 -1
- 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 +48 -5
- package/src/config/index.js +138 -27
- 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 +37 -10
- package/src/server/ejs-adapter.js +59 -0
- package/src/server/html-cache.js +178 -32
- package/src/server/image-optimizer.js +94 -26
- package/src/server/middleware/dev-gate.js +21 -8
- package/src/server/middleware/robots-txt.js +341 -0
- package/src/server/port-guard.js +255 -0
- package/src/server/prewarm.js +137 -51
- package/src/server/render.js +30 -10
- 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 +477 -0
- package/types/config/index.d.ts +304 -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 +207 -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/robots-txt.d.ts +33 -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 +131 -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
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bir kaynak dosyasını page / layout / client / server-component olarak sınıflandırır.
|
|
3
|
+
*/
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { t } from "./babel.mjs";
|
|
6
|
+
import { parseSource } from "./parse.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {'page' | 'layout' | 'client' | 'server-component' | 'unknown'} FileKind
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* kind: FileKind,
|
|
12
|
+
* isClient: boolean,
|
|
13
|
+
* hasHooks: boolean,
|
|
14
|
+
* hasServerAction: boolean,
|
|
15
|
+
* hasSuspense: boolean,
|
|
16
|
+
* revalidate: number | null,
|
|
17
|
+
* hasGenerateMetadata: boolean,
|
|
18
|
+
* hasGenerateStaticParams: boolean,
|
|
19
|
+
* reasons: string[],
|
|
20
|
+
* }} Classification
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} code
|
|
25
|
+
* @param {string} filename
|
|
26
|
+
* @returns {Classification}
|
|
27
|
+
*/
|
|
28
|
+
export function classifySource(code, filename) {
|
|
29
|
+
const base = path.basename(filename);
|
|
30
|
+
/** @type {Classification} */
|
|
31
|
+
const out = {
|
|
32
|
+
kind: "unknown",
|
|
33
|
+
isClient: false,
|
|
34
|
+
hasHooks: false,
|
|
35
|
+
hasServerAction: false,
|
|
36
|
+
hasSuspense: false,
|
|
37
|
+
revalidate: null,
|
|
38
|
+
hasGenerateMetadata: false,
|
|
39
|
+
hasGenerateStaticParams: false,
|
|
40
|
+
reasons: [],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (/^["']use client["'];?/.test(code.trim()) || /\n["']use client["'];?/.test(code)) {
|
|
44
|
+
out.isClient = true;
|
|
45
|
+
}
|
|
46
|
+
if (code.includes('"use server"') || code.includes("'use server'")) {
|
|
47
|
+
out.hasServerAction = true;
|
|
48
|
+
out.reasons.push("contains \"use server\"");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let ast;
|
|
52
|
+
try {
|
|
53
|
+
ast = parseSource(code, filename);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
out.reasons.push(
|
|
56
|
+
`parse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
57
|
+
);
|
|
58
|
+
if (out.isClient) out.kind = "client";
|
|
59
|
+
else if (/^page\./.test(base)) out.kind = "page";
|
|
60
|
+
else if (/^layout\./.test(base)) out.kind = "layout";
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const node of ast.program.directives ?? []) {
|
|
65
|
+
if (node.value?.value === "use client") out.isClient = true;
|
|
66
|
+
if (node.value?.value === "use server") {
|
|
67
|
+
out.hasServerAction = true;
|
|
68
|
+
out.reasons.push("directive \"use server\"");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const node of ast.program.body) {
|
|
73
|
+
if (t.isImportDeclaration(node)) {
|
|
74
|
+
const src = node.source.value;
|
|
75
|
+
if (src === "react" || src === "react/jsx-runtime") {
|
|
76
|
+
for (const spec of node.specifiers) {
|
|
77
|
+
const name =
|
|
78
|
+
(t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)
|
|
79
|
+
? spec.imported.name
|
|
80
|
+
: null) ||
|
|
81
|
+
(t.isImportDefaultSpecifier(spec) ? "default" : null);
|
|
82
|
+
if (name === "Suspense") out.hasSuspense = true;
|
|
83
|
+
if (
|
|
84
|
+
name &&
|
|
85
|
+
/^(use[A-Z]|useState|useEffect|useMemo|useCallback|useRef|useContext|useReducer|useLayoutEffect|useId|useTransition|useDeferredValue|useSyncExternalStore)/.test(
|
|
86
|
+
name,
|
|
87
|
+
)
|
|
88
|
+
) {
|
|
89
|
+
out.hasHooks = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
t.isExportNamedDeclaration(node) &&
|
|
97
|
+
node.declaration &&
|
|
98
|
+
t.isVariableDeclaration(node.declaration)
|
|
99
|
+
) {
|
|
100
|
+
for (const decl of node.declaration.declarations) {
|
|
101
|
+
if (t.isIdentifier(decl.id) && decl.id.name === "revalidate") {
|
|
102
|
+
if (t.isNumericLiteral(decl.init)) out.revalidate = decl.init.value;
|
|
103
|
+
else if (t.isLiteral(decl.init) && typeof decl.init.value === "number") {
|
|
104
|
+
out.revalidate = /** @type {number} */ (decl.init.value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (t.isExportNamedDeclaration(node)) {
|
|
111
|
+
const id = namedExportName(node);
|
|
112
|
+
if (id === "generateMetadata") out.hasGenerateMetadata = true;
|
|
113
|
+
if (id === "generateStaticParams") out.hasGenerateStaticParams = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Hook çağrıları (import alias olmadan da)
|
|
118
|
+
if (/\buse(State|Effect|Memo|Callback|Ref|Context|Reducer)\s*\(/.test(code)) {
|
|
119
|
+
out.hasHooks = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (out.isClient || out.hasHooks) {
|
|
123
|
+
out.kind = "client";
|
|
124
|
+
} else if (/^page\./.test(base)) {
|
|
125
|
+
out.kind = "page";
|
|
126
|
+
} else if (/^layout\./.test(base)) {
|
|
127
|
+
out.kind = "layout";
|
|
128
|
+
} else {
|
|
129
|
+
out.kind = "server-component";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {import('@babel/types').ExportNamedDeclaration} node
|
|
137
|
+
* @returns {string | null}
|
|
138
|
+
*/
|
|
139
|
+
function namedExportName(node) {
|
|
140
|
+
if (node.declaration) {
|
|
141
|
+
if (t.isFunctionDeclaration(node.declaration) && node.declaration.id) {
|
|
142
|
+
return node.declaration.id.name;
|
|
143
|
+
}
|
|
144
|
+
if (t.isVariableDeclaration(node.declaration)) {
|
|
145
|
+
const id = node.declaration.declarations[0]?.id;
|
|
146
|
+
if (t.isIdentifier(id)) return id.name;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const spec of node.specifiers) {
|
|
150
|
+
if (t.isExportSpecifier(spec) && t.isIdentifier(spec.exported)) {
|
|
151
|
+
return spec.exported.name;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* next.config → jskelet.config.mjs taslağı.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { findNextConfig, findClientEnvKeys } from "./fs-walk.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} root
|
|
11
|
+
* @returns {Promise<{ status: 'ok' | 'partial' | 'skipped', notes: string[], code: string | null }>}
|
|
12
|
+
*/
|
|
13
|
+
export async function draftConfig(root) {
|
|
14
|
+
/** @type {string[]} */
|
|
15
|
+
const notes = [];
|
|
16
|
+
const configPath = findNextConfig(root);
|
|
17
|
+
const clientEnv = findClientEnvKeys(root);
|
|
18
|
+
|
|
19
|
+
/** @type {Record<string, unknown>} */
|
|
20
|
+
let next = {};
|
|
21
|
+
|
|
22
|
+
if (!configPath) {
|
|
23
|
+
notes.push("no next.config.* — emitting minimal jskelet.config.mjs");
|
|
24
|
+
} else if (configPath.endsWith(".ts")) {
|
|
25
|
+
notes.push("next.config.ts cannot be imported — copy headers/redirects/rewrites manually");
|
|
26
|
+
} else {
|
|
27
|
+
try {
|
|
28
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
29
|
+
next = mod.default ?? mod;
|
|
30
|
+
if (typeof next === "function") {
|
|
31
|
+
next = await next({ default: {} });
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
notes.push(
|
|
35
|
+
`could not load next.config: ${error instanceof Error ? error.message : String(error)}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const trailingSlash = Boolean(next.trailingSlash);
|
|
41
|
+
const deviceSizes =
|
|
42
|
+
next.images && typeof next.images === "object" && Array.isArray(next.images.deviceSizes)
|
|
43
|
+
? next.images.deviceSizes
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
if (next.experimental) {
|
|
47
|
+
notes.push("experimental.* has no JSkelet equivalent — ignored");
|
|
48
|
+
}
|
|
49
|
+
if (fs.existsSync(path.join(root, "middleware.ts")) || fs.existsSync(path.join(root, "middleware.js"))) {
|
|
50
|
+
notes.push("middleware.ts/js → port to Express middleware manually");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const headersSrc = fnSource(next.headers, "headers", notes);
|
|
54
|
+
const redirectsSrc = fnSource(next.redirects, "redirects", notes);
|
|
55
|
+
const rewritesSrc = fnSource(next.rewrites, "rewrites", notes);
|
|
56
|
+
|
|
57
|
+
const clientEnvList =
|
|
58
|
+
clientEnv.length > 0
|
|
59
|
+
? clientEnv.map((k) => JSON.stringify(k)).join(", ")
|
|
60
|
+
: "";
|
|
61
|
+
|
|
62
|
+
const imagesBlock = deviceSizes
|
|
63
|
+
? `\n images: {\n widths: ${JSON.stringify(deviceSizes)},\n },\n`
|
|
64
|
+
: "";
|
|
65
|
+
|
|
66
|
+
const code =
|
|
67
|
+
`/**\n` +
|
|
68
|
+
` * Drafted by \`jskelet migrate config\` from ${configPath ? path.basename(configPath) : "defaults"}.\n` +
|
|
69
|
+
` * Review before use — complex path-to-regexp patterns are not supported.\n` +
|
|
70
|
+
` */\n` +
|
|
71
|
+
`export default {\n` +
|
|
72
|
+
` brand: { lang: "en" },\n` +
|
|
73
|
+
(trailingSlash ? ` trailingSlash: true,\n` : "") +
|
|
74
|
+
(clientEnvList ? ` clientEnv: [${clientEnvList}],\n` : "") +
|
|
75
|
+
imagesBlock +
|
|
76
|
+
(headersSrc ? `\n async headers() {\n${headersSrc}\n },\n` : "") +
|
|
77
|
+
(redirectsSrc ? `\n async redirects() {\n${redirectsSrc}\n },\n` : "") +
|
|
78
|
+
(rewritesSrc ? `\n async rewrites() {\n${rewritesSrc}\n },\n` : "") +
|
|
79
|
+
`\n hooks: {\n` +
|
|
80
|
+
` metadata() {\n` +
|
|
81
|
+
` return {\n` +
|
|
82
|
+
` titleTemplate: "%s | Site",\n` +
|
|
83
|
+
` description: "",\n` +
|
|
84
|
+
` };\n` +
|
|
85
|
+
` },\n` +
|
|
86
|
+
` layoutContext() {\n` +
|
|
87
|
+
` return { bodyClass: "min-h-full" };\n` +
|
|
88
|
+
` },\n` +
|
|
89
|
+
` notFound() {\n` +
|
|
90
|
+
` return {\n` +
|
|
91
|
+
` view: "pages/not-found",\n` +
|
|
92
|
+
` metadata: { title: "Page not found", robots: { index: false } },\n` +
|
|
93
|
+
` };\n` +
|
|
94
|
+
` },\n` +
|
|
95
|
+
` },\n` +
|
|
96
|
+
`};\n`;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
status: notes.length ? "partial" : "ok",
|
|
100
|
+
notes,
|
|
101
|
+
code,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {unknown} value
|
|
107
|
+
* @param {string} name
|
|
108
|
+
* @param {string[]} notes
|
|
109
|
+
* @returns {string | null}
|
|
110
|
+
*/
|
|
111
|
+
function fnSource(value, name, notes) {
|
|
112
|
+
if (typeof value !== "function") return null;
|
|
113
|
+
try {
|
|
114
|
+
const src = Function.prototype.toString.call(value);
|
|
115
|
+
// "async headers() { ... }" or "headers() { ... }" or arrow
|
|
116
|
+
const bodyMatch = src.match(/^[^{]*\{([\s\S]*)\}$/);
|
|
117
|
+
if (!bodyMatch) {
|
|
118
|
+
notes.push(`${name}() could not be inlined — copy manually`);
|
|
119
|
+
return ` // TODO[jskelet-migrate]: paste ${name}() from next.config\n return [];`;
|
|
120
|
+
}
|
|
121
|
+
return bodyMatch[1].replace(/^\n/, "").replace(/\n$/, "");
|
|
122
|
+
} catch {
|
|
123
|
+
notes.push(`${name}() could not be serialized`);
|
|
124
|
+
return ` // TODO[jskelet-migrate]: paste ${name}() from next.config\n return [];`;
|
|
125
|
+
}
|
|
126
|
+
}
|