jskelet 0.5.5 → 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 +95 -0
- 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 +40 -18
- package/docs/08-build.md +15 -9
- 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 +40 -20
- package/docs/en/08-build.md +16 -10
- 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 +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 +12 -2
- package/src/config/index.js +1 -1
- 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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next `page.*` → controller + `.jsk` gövde.
|
|
3
|
+
*/
|
|
4
|
+
import { t } from "../babel.mjs";
|
|
5
|
+
import { parseSource } from "../parse.mjs";
|
|
6
|
+
import { jsxToJsk } from "./jsx-to-jsk.mjs";
|
|
7
|
+
import { exprToSource } from "./expr-source.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* status: 'ok' | 'partial' | 'skipped',
|
|
12
|
+
* notes: string[],
|
|
13
|
+
* controller: string | null,
|
|
14
|
+
* jsk: string | null,
|
|
15
|
+
* viewId: string,
|
|
16
|
+
* dataKeys: string[],
|
|
17
|
+
* }} PageSplitResult
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} code
|
|
22
|
+
* @param {string} filename
|
|
23
|
+
* @param {{ url: string, feature: string, page: string, viewId?: string }} meta
|
|
24
|
+
* @returns {PageSplitResult}
|
|
25
|
+
*/
|
|
26
|
+
export function splitPage(code, filename, meta) {
|
|
27
|
+
const viewId = meta.viewId ?? `pages/${meta.page}`;
|
|
28
|
+
/** @type {PageSplitResult} */
|
|
29
|
+
const result = {
|
|
30
|
+
status: "ok",
|
|
31
|
+
notes: [],
|
|
32
|
+
controller: null,
|
|
33
|
+
jsk: null,
|
|
34
|
+
viewId,
|
|
35
|
+
dataKeys: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let ast;
|
|
39
|
+
try {
|
|
40
|
+
ast = parseSource(code, filename);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
result.status = "skipped";
|
|
43
|
+
result.notes.push(error instanceof Error ? error.message : String(error));
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const revalidate = readRevalidate(ast);
|
|
48
|
+
const pageFn = findDefaultPage(ast);
|
|
49
|
+
if (!pageFn) {
|
|
50
|
+
result.status = "skipped";
|
|
51
|
+
result.notes.push("no default export page function found");
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const jsx = findReturnJsx(pageFn.body);
|
|
56
|
+
if (!jsx) {
|
|
57
|
+
result.status = "skipped";
|
|
58
|
+
result.notes.push("page does not return JSX");
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Import map: Image from next/image, Link from next/link → keep tag names
|
|
63
|
+
const { source: jsk, report } = jsxToJsk(jsx);
|
|
64
|
+
result.jsk = `{# Migrated from ${filename.replace(/\\/g, "/")} #}\n${jsk.trim()}\n`;
|
|
65
|
+
result.notes.push(...report.notes);
|
|
66
|
+
if (report.status === "partial") result.status = "partial";
|
|
67
|
+
if (report.status === "skipped") result.status = "skipped";
|
|
68
|
+
|
|
69
|
+
const locals = extractControllerLocals(pageFn.body);
|
|
70
|
+
const dataKeys = [...new Set([...locals.dataKeys, ...report.bindings])].filter(
|
|
71
|
+
(k) => !["params", "searchParams", "props", "query"].includes(k),
|
|
72
|
+
);
|
|
73
|
+
result.dataKeys = dataKeys;
|
|
74
|
+
|
|
75
|
+
const metadataExpr = trySimpleMetadata(ast);
|
|
76
|
+
const imports = collectDataImports(ast);
|
|
77
|
+
|
|
78
|
+
const usesNotFound = code.includes("notFound(");
|
|
79
|
+
const usesRedirect = /\bredirect\s*\(/.test(code);
|
|
80
|
+
const jskeletImports = [];
|
|
81
|
+
if (usesNotFound) jskeletImports.push("notFound");
|
|
82
|
+
if (usesRedirect) jskeletImports.push("redirect");
|
|
83
|
+
|
|
84
|
+
/** @type {string[]} */
|
|
85
|
+
const controllerLines = [];
|
|
86
|
+
controllerLines.push(`/** Migrated from ${filename.replace(/\\/g, "/")} */`);
|
|
87
|
+
for (const line of imports) controllerLines.push(line);
|
|
88
|
+
if (jskeletImports.length) {
|
|
89
|
+
controllerLines.push(`import { ${jskeletImports.join(", ")} } from "jskelet";`);
|
|
90
|
+
}
|
|
91
|
+
controllerLines.push("");
|
|
92
|
+
controllerLines.push("export default function register(app, { route }) {");
|
|
93
|
+
const routeOpts = revalidate != null ? `, { revalidate: ${revalidate} }` : "";
|
|
94
|
+
controllerLines.push(` app.get(`);
|
|
95
|
+
controllerLines.push(` ${JSON.stringify(meta.url)},`);
|
|
96
|
+
controllerLines.push(` route(`);
|
|
97
|
+
controllerLines.push(` async ({ params, query }) => {`);
|
|
98
|
+
for (const stmt of locals.prelude) {
|
|
99
|
+
controllerLines.push(` ${stmt}`);
|
|
100
|
+
}
|
|
101
|
+
const dataObj = dataKeys.length > 0 ? `{ ${dataKeys.join(", ")} }` : "{}";
|
|
102
|
+
controllerLines.push(` return {`);
|
|
103
|
+
controllerLines.push(` view: ${JSON.stringify(viewId)},`);
|
|
104
|
+
controllerLines.push(` data: ${dataObj},`);
|
|
105
|
+
if (metadataExpr) {
|
|
106
|
+
controllerLines.push(` metadata: ${metadataExpr},`);
|
|
107
|
+
} else if (code.includes("generateMetadata")) {
|
|
108
|
+
controllerLines.push(
|
|
109
|
+
` // TODO[jskelet-migrate]: port generateMetadata() into metadata`,
|
|
110
|
+
);
|
|
111
|
+
if (result.status !== "skipped") result.status = "partial";
|
|
112
|
+
result.notes.push("generateMetadata needs manual port");
|
|
113
|
+
}
|
|
114
|
+
controllerLines.push(` };`);
|
|
115
|
+
controllerLines.push(` }${routeOpts},`);
|
|
116
|
+
controllerLines.push(` ),`);
|
|
117
|
+
controllerLines.push(` );`);
|
|
118
|
+
controllerLines.push(`}`);
|
|
119
|
+
controllerLines.push("");
|
|
120
|
+
|
|
121
|
+
result.controller = controllerLines.join("\n");
|
|
122
|
+
if (result.notes.length && result.status === "ok") result.status = "partial";
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {import('@babel/types').File} ast
|
|
128
|
+
* @returns {number | null}
|
|
129
|
+
*/
|
|
130
|
+
function readRevalidate(ast) {
|
|
131
|
+
for (const node of ast.program.body) {
|
|
132
|
+
if (
|
|
133
|
+
t.isExportNamedDeclaration(node) &&
|
|
134
|
+
node.declaration &&
|
|
135
|
+
t.isVariableDeclaration(node.declaration)
|
|
136
|
+
) {
|
|
137
|
+
for (const d of node.declaration.declarations) {
|
|
138
|
+
if (t.isIdentifier(d.id) && d.id.name === "revalidate" && t.isNumericLiteral(d.init)) {
|
|
139
|
+
return d.init.value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {import('@babel/types').File} ast
|
|
149
|
+
* @returns {{ body: import('@babel/types').BlockStatement, params: import('@babel/types').FunctionDeclaration['params'] } | null}
|
|
150
|
+
*/
|
|
151
|
+
function findDefaultPage(ast) {
|
|
152
|
+
for (const node of ast.program.body) {
|
|
153
|
+
if (t.isExportDefaultDeclaration(node)) {
|
|
154
|
+
const d = node.declaration;
|
|
155
|
+
if (t.isFunctionDeclaration(d) && d.body) {
|
|
156
|
+
return { body: d.body, params: d.params };
|
|
157
|
+
}
|
|
158
|
+
if (t.isArrowFunctionExpression(d) || t.isFunctionExpression(d)) {
|
|
159
|
+
if (t.isBlockStatement(d.body)) return { body: d.body, params: d.params };
|
|
160
|
+
}
|
|
161
|
+
if (t.isIdentifier(d)) {
|
|
162
|
+
// export default Page — find function Page
|
|
163
|
+
const name = d.name;
|
|
164
|
+
for (const other of ast.program.body) {
|
|
165
|
+
if (t.isFunctionDeclaration(other) && other.id?.name === name && other.body) {
|
|
166
|
+
return { body: other.body, params: other.params };
|
|
167
|
+
}
|
|
168
|
+
if (t.isVariableDeclaration(other)) {
|
|
169
|
+
for (const decl of other.declarations) {
|
|
170
|
+
if (t.isIdentifier(decl.id) && decl.id.name === name && decl.init) {
|
|
171
|
+
if (
|
|
172
|
+
(t.isArrowFunctionExpression(decl.init) || t.isFunctionExpression(decl.init)) &&
|
|
173
|
+
t.isBlockStatement(decl.init.body)
|
|
174
|
+
) {
|
|
175
|
+
return { body: decl.init.body, params: decl.init.params };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {import('@babel/types').BlockStatement} body
|
|
189
|
+
* @returns {import('@babel/types').JSXElement | import('@babel/types').JSXFragment | null}
|
|
190
|
+
*/
|
|
191
|
+
function findReturnJsx(body) {
|
|
192
|
+
for (const stmt of body.body) {
|
|
193
|
+
if (!t.isReturnStatement(stmt) || !stmt.argument) continue;
|
|
194
|
+
let arg = stmt.argument;
|
|
195
|
+
if (t.isParenthesizedExpression(arg)) arg = arg.expression;
|
|
196
|
+
if (t.isJSXElement(arg) || t.isJSXFragment(arg)) return arg;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Return JSX'ten önceki await/const satırlarını controller'a kopyala.
|
|
203
|
+
*
|
|
204
|
+
* @param {import('@babel/types').BlockStatement} body
|
|
205
|
+
* @returns {{ prelude: string[], dataKeys: string[] }}
|
|
206
|
+
*/
|
|
207
|
+
function extractControllerLocals(body) {
|
|
208
|
+
/** @type {string[]} */
|
|
209
|
+
const prelude = [];
|
|
210
|
+
/** @type {string[]} */
|
|
211
|
+
const dataKeys = [];
|
|
212
|
+
|
|
213
|
+
for (const stmt of body.body) {
|
|
214
|
+
if (t.isReturnStatement(stmt)) break;
|
|
215
|
+
|
|
216
|
+
if (t.isVariableDeclaration(stmt)) {
|
|
217
|
+
for (const d of stmt.declarations) {
|
|
218
|
+
if (t.isIdentifier(d.id)) dataKeys.push(d.id.name);
|
|
219
|
+
else if (t.isObjectPattern(d.id)) {
|
|
220
|
+
for (const p of d.id.properties) {
|
|
221
|
+
if (!t.isObjectProperty(p)) continue;
|
|
222
|
+
if (t.isIdentifier(p.value)) dataKeys.push(p.value.name);
|
|
223
|
+
else if (p.shorthand && t.isIdentifier(p.key)) dataKeys.push(p.key.name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// params destructure: const { slug } = await params / = params
|
|
228
|
+
const rewritten = rewriteParamsAccess(stmtToSource(stmt));
|
|
229
|
+
prelude.push(rewritten);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (t.isIfStatement(stmt) || t.isExpressionStatement(stmt)) {
|
|
234
|
+
prelude.push(stmtToSource(stmt));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { prelude, dataKeys };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Next 15 `await params` → JSkelet `params` (zaten düz obje).
|
|
243
|
+
* @param {string} src
|
|
244
|
+
* @returns {string}
|
|
245
|
+
*/
|
|
246
|
+
function rewriteParamsAccess(src) {
|
|
247
|
+
return src
|
|
248
|
+
.replace(/=\s*await\s+params\b/g, "= params")
|
|
249
|
+
.replace(/=\s*await\s+searchParams\b/g, "= query");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* VariableDeclaration için exprToSource yeterli değil — basit printer.
|
|
254
|
+
* @param {import('@babel/types').Statement} stmt
|
|
255
|
+
* @returns {string}
|
|
256
|
+
*/
|
|
257
|
+
function stmtToSource(stmt) {
|
|
258
|
+
if (t.isIfStatement(stmt)) {
|
|
259
|
+
const test = exprToSource(stmt.test);
|
|
260
|
+
let cons = "";
|
|
261
|
+
if (t.isBlockStatement(stmt.consequent)) {
|
|
262
|
+
cons = stmt.consequent.body.map((s) => stmtToSource(s)).join(" ");
|
|
263
|
+
} else {
|
|
264
|
+
cons = stmtToSource(stmt.consequent);
|
|
265
|
+
}
|
|
266
|
+
return `if (${test}) { ${cons} }`;
|
|
267
|
+
}
|
|
268
|
+
if (t.isExpressionStatement(stmt)) {
|
|
269
|
+
return exprToSource(stmt.expression) + ";";
|
|
270
|
+
}
|
|
271
|
+
if (t.isVariableDeclaration(stmt)) {
|
|
272
|
+
const kind = stmt.kind;
|
|
273
|
+
const decls = stmt.declarations
|
|
274
|
+
.map((d) => {
|
|
275
|
+
const id = t.isIdentifier(d.id)
|
|
276
|
+
? d.id.name
|
|
277
|
+
: t.isObjectPattern(d.id)
|
|
278
|
+
? `{ ${d.id.properties
|
|
279
|
+
.map((p) => {
|
|
280
|
+
if (t.isObjectProperty(p) && t.isIdentifier(p.key)) {
|
|
281
|
+
if (p.shorthand) return p.key.name;
|
|
282
|
+
if (t.isIdentifier(p.value)) return `${p.key.name}: ${p.value.name}`;
|
|
283
|
+
}
|
|
284
|
+
return "...";
|
|
285
|
+
})
|
|
286
|
+
.join(", ")} }`
|
|
287
|
+
: "/* pat */";
|
|
288
|
+
const init = d.init ? ` = ${exprToSource(d.init)}` : "";
|
|
289
|
+
return id + init;
|
|
290
|
+
})
|
|
291
|
+
.join(", ");
|
|
292
|
+
return `${kind} ${decls};`;
|
|
293
|
+
}
|
|
294
|
+
return `/* TODO[jskelet-migrate]: ${stmt.type} */`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @param {import('@babel/types').File} ast
|
|
299
|
+
* @returns {string[]}
|
|
300
|
+
*/
|
|
301
|
+
function collectDataImports(ast) {
|
|
302
|
+
/** @type {string[]} */
|
|
303
|
+
const lines = [];
|
|
304
|
+
for (const node of ast.program.body) {
|
|
305
|
+
if (!t.isImportDeclaration(node)) continue;
|
|
306
|
+
const src = node.source.value;
|
|
307
|
+
if (
|
|
308
|
+
src.startsWith("next/") ||
|
|
309
|
+
src === "react" ||
|
|
310
|
+
src === "react/jsx-runtime" ||
|
|
311
|
+
src === "react/jsx-dev-runtime"
|
|
312
|
+
) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Keep app data imports; rewrite @/ if present (same in JSkelet)
|
|
316
|
+
const specs = node.specifiers
|
|
317
|
+
.map((s) => {
|
|
318
|
+
if (t.isImportDefaultSpecifier(s)) return s.local.name;
|
|
319
|
+
if (t.isImportNamespaceSpecifier(s)) return `* as ${s.local.name}`;
|
|
320
|
+
if (t.isImportSpecifier(s) && t.isIdentifier(s.imported)) {
|
|
321
|
+
return s.imported.name === s.local.name
|
|
322
|
+
? s.local.name
|
|
323
|
+
: `${s.imported.name} as ${s.local.name}`;
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
})
|
|
327
|
+
.filter(Boolean);
|
|
328
|
+
if (specs.length === 0) continue;
|
|
329
|
+
const hasDefault = node.specifiers.some((s) => t.isImportDefaultSpecifier(s));
|
|
330
|
+
const hasNs = node.specifiers.some((s) => t.isImportNamespaceSpecifier(s));
|
|
331
|
+
const named = node.specifiers.filter((s) => t.isImportSpecifier(s));
|
|
332
|
+
if (hasNs) {
|
|
333
|
+
lines.push(`import ${specs[0]} from ${JSON.stringify(src)};`);
|
|
334
|
+
} else if (hasDefault && named.length === 0) {
|
|
335
|
+
lines.push(`import ${specs[0]} from ${JSON.stringify(ensureJsExt(src))};`);
|
|
336
|
+
} else if (hasDefault) {
|
|
337
|
+
const def = node.specifiers.find((s) => t.isImportDefaultSpecifier(s));
|
|
338
|
+
const n = named
|
|
339
|
+
.map((s) => {
|
|
340
|
+
if (!t.isImportSpecifier(s) || !t.isIdentifier(s.imported)) return null;
|
|
341
|
+
return s.imported.name === s.local.name
|
|
342
|
+
? s.local.name
|
|
343
|
+
: `${s.imported.name} as ${s.local.name}`;
|
|
344
|
+
})
|
|
345
|
+
.filter(Boolean);
|
|
346
|
+
lines.push(
|
|
347
|
+
`import ${def && t.isImportDefaultSpecifier(def) ? def.local.name : "X"}, { ${n.join(", ")} } from ${JSON.stringify(ensureJsExt(src))};`,
|
|
348
|
+
);
|
|
349
|
+
} else {
|
|
350
|
+
lines.push(`import { ${specs.join(", ")} } from ${JSON.stringify(ensureJsExt(src))};`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return lines;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @param {string} src
|
|
358
|
+
* @returns {string}
|
|
359
|
+
*/
|
|
360
|
+
function ensureJsExt(src) {
|
|
361
|
+
if (src.startsWith(".") || src.startsWith("@/")) {
|
|
362
|
+
if (!/\.(js|mjs|cjs|ts|tsx|jsx)$/.test(src)) return src + ".js";
|
|
363
|
+
}
|
|
364
|
+
return src;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* generateMetadata basit obje return ediyorsa metadata literal'e çevir.
|
|
369
|
+
*
|
|
370
|
+
* @param {import('@babel/types').File} ast
|
|
371
|
+
* @returns {string | null}
|
|
372
|
+
*/
|
|
373
|
+
function trySimpleMetadata(ast) {
|
|
374
|
+
for (const node of ast.program.body) {
|
|
375
|
+
if (!t.isExportNamedDeclaration(node)) continue;
|
|
376
|
+
let fn = null;
|
|
377
|
+
if (t.isFunctionDeclaration(node.declaration) && node.declaration.id?.name === "generateMetadata") {
|
|
378
|
+
fn = node.declaration;
|
|
379
|
+
}
|
|
380
|
+
if (
|
|
381
|
+
node.declaration &&
|
|
382
|
+
t.isVariableDeclaration(node.declaration)
|
|
383
|
+
) {
|
|
384
|
+
for (const d of node.declaration.declarations) {
|
|
385
|
+
if (t.isIdentifier(d.id) && d.id.name === "generateMetadata" && d.init) {
|
|
386
|
+
if (t.isArrowFunctionExpression(d.init) || t.isFunctionExpression(d.init)) {
|
|
387
|
+
fn = d.init;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!fn) continue;
|
|
393
|
+
const body = t.isBlockStatement(fn.body) ? fn.body : null;
|
|
394
|
+
if (!body) continue;
|
|
395
|
+
for (const stmt of body.body) {
|
|
396
|
+
if (t.isReturnStatement(stmt) && stmt.argument && t.isObjectExpression(stmt.argument)) {
|
|
397
|
+
// Only if no await in function and object is simple enough
|
|
398
|
+
const hasAwait = body.body.some(
|
|
399
|
+
(s) => t.isVariableDeclaration(s) && s.declarations.some((d) => d.init && t.isAwaitExpression(d.init)),
|
|
400
|
+
);
|
|
401
|
+
if (hasAwait) return null;
|
|
402
|
+
return objectToMetadata(stmt.argument);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* @param {import('@babel/types').ObjectExpression} obj
|
|
411
|
+
* @returns {string}
|
|
412
|
+
*/
|
|
413
|
+
function objectToMetadata(obj) {
|
|
414
|
+
/** @type {string[]} */
|
|
415
|
+
const parts = [];
|
|
416
|
+
for (const p of obj.properties) {
|
|
417
|
+
if (!t.isObjectProperty(p)) continue;
|
|
418
|
+
const key = t.isIdentifier(p.key) ? p.key.name : t.isStringLiteral(p.key) ? p.key.value : null;
|
|
419
|
+
if (!key || !t.isExpression(p.value)) continue;
|
|
420
|
+
if (key === "alternates" && t.isObjectExpression(p.value)) {
|
|
421
|
+
const canon = p.value.properties.find(
|
|
422
|
+
(x) =>
|
|
423
|
+
t.isObjectProperty(x) &&
|
|
424
|
+
((t.isIdentifier(x.key) && x.key.name === "canonical") ||
|
|
425
|
+
(t.isStringLiteral(x.key) && x.key.value === "canonical")),
|
|
426
|
+
);
|
|
427
|
+
if (canon && t.isObjectProperty(canon) && t.isExpression(canon.value)) {
|
|
428
|
+
parts.push(`canonical: ${exprToSource(canon.value)}`);
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
parts.push(`${key}: ${exprToSource(/** @type {import('@babel/types').Expression} */ (p.value))}`);
|
|
433
|
+
}
|
|
434
|
+
return `{ ${parts.join(", ")} }`;
|
|
435
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dry-run / no-overwrite yazıcı.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import * as log from "../log.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{ path: string, contents: string, action: 'create' | 'skip' | 'conflict' }} WritePlan
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} absPath
|
|
15
|
+
* @param {string} contents
|
|
16
|
+
* @param {{ write?: boolean, cwd?: string }} options
|
|
17
|
+
* @returns {WritePlan}
|
|
18
|
+
*/
|
|
19
|
+
export function planWrite(absPath, contents, options = {}) {
|
|
20
|
+
const cwd = options.cwd ?? process.cwd();
|
|
21
|
+
const rel = path.relative(cwd, absPath).replace(/\\/g, "/");
|
|
22
|
+
if (fs.existsSync(absPath)) {
|
|
23
|
+
return { path: rel, contents, action: "conflict" };
|
|
24
|
+
}
|
|
25
|
+
return { path: rel, contents, action: options.write ? "create" : "create" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {WritePlan[]} plans
|
|
30
|
+
* @param {{ write?: boolean, cwd?: string }} options
|
|
31
|
+
* @returns {WritePlan[]}
|
|
32
|
+
*/
|
|
33
|
+
export function commitWrites(plans, options = {}) {
|
|
34
|
+
const cwd = options.cwd ?? process.cwd();
|
|
35
|
+
/** @type {WritePlan[]} */
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const plan of plans) {
|
|
38
|
+
const abs = path.isAbsolute(plan.path) ? plan.path : path.join(cwd, plan.path);
|
|
39
|
+
if (fs.existsSync(abs)) {
|
|
40
|
+
const conflict = {
|
|
41
|
+
path: path.relative(cwd, abs).replace(/\\/g, "/"),
|
|
42
|
+
contents: plan.contents,
|
|
43
|
+
action: /** @type {const} */ ("conflict"),
|
|
44
|
+
};
|
|
45
|
+
// .migrate soneki dene
|
|
46
|
+
const alt = abs.replace(/(\.\w+)?$/, (m) => `.migrate${m || ""}`);
|
|
47
|
+
if (!fs.existsSync(alt) && options.write) {
|
|
48
|
+
fs.mkdirSync(path.dirname(alt), { recursive: true });
|
|
49
|
+
fs.writeFileSync(alt, plan.contents, "utf8");
|
|
50
|
+
log.line(`create ${path.relative(cwd, alt).replace(/\\/g, "/")} (conflict → .migrate)`);
|
|
51
|
+
out.push({
|
|
52
|
+
path: path.relative(cwd, alt).replace(/\\/g, "/"),
|
|
53
|
+
contents: plan.contents,
|
|
54
|
+
action: "create",
|
|
55
|
+
});
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
log.line(`skip ${conflict.path} (exists)`);
|
|
59
|
+
out.push(conflict);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (options.write) {
|
|
63
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
64
|
+
fs.writeFileSync(abs, plan.contents, "utf8");
|
|
65
|
+
log.line(`create ${path.relative(cwd, abs).replace(/\\/g, "/")}`);
|
|
66
|
+
out.push({
|
|
67
|
+
path: path.relative(cwd, abs).replace(/\\/g, "/"),
|
|
68
|
+
contents: plan.contents,
|
|
69
|
+
action: "create",
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
log.line(`would create ${path.relative(cwd, abs).replace(/\\/g, "/")}`);
|
|
73
|
+
out.push({
|
|
74
|
+
path: path.relative(cwd, abs).replace(/\\/g, "/"),
|
|
75
|
+
contents: plan.contents,
|
|
76
|
+
action: "create",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
package/src/migrate.mjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet migrate` — Next.js App Router → JSkelet codemod.
|
|
3
|
+
*
|
|
4
|
+
* jskelet migrate scan + apply --dry-run özeti
|
|
5
|
+
* jskelet migrate scan [dir]
|
|
6
|
+
* jskelet migrate apply [dir] [--out .] [--write] [--only pages|components|islands] [--strict] [--json]
|
|
7
|
+
* jskelet migrate config [dir] [--out .] [--write] [--json]
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
import * as log from "./log.mjs";
|
|
13
|
+
import { scanProject, formatScanReport } from "./migrate/scan.mjs";
|
|
14
|
+
import { draftConfig } from "./migrate/config.mjs";
|
|
15
|
+
import { applyMigrate, formatApplyReport } from "./migrate/apply.mjs";
|
|
16
|
+
import { commitWrites } from "./migrate/write.mjs";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} cwd
|
|
20
|
+
* @param {string[]} args
|
|
21
|
+
* @returns {Promise<void>}
|
|
22
|
+
*/
|
|
23
|
+
export async function migrate(cwd, args) {
|
|
24
|
+
const { command, positionals, flags } = parseArgs(args);
|
|
25
|
+
|
|
26
|
+
if (command === "help" || flags.help) {
|
|
27
|
+
printUsage();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sourceRoot = path.resolve(cwd, positionals[0] ?? ".");
|
|
32
|
+
const outRoot = path.resolve(cwd, flags.out ?? (command === "apply" || command === "config" ? "." : sourceRoot));
|
|
33
|
+
|
|
34
|
+
if (command === "scan" || command === "default") {
|
|
35
|
+
const report = scanProject(sourceRoot);
|
|
36
|
+
if (flags.json) {
|
|
37
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
38
|
+
} else {
|
|
39
|
+
process.stdout.write(formatScanReport(report) + "\n");
|
|
40
|
+
}
|
|
41
|
+
if (command === "default") {
|
|
42
|
+
process.stdout.write("\n--- apply (dry-run) ---\n");
|
|
43
|
+
const { reports } = await applyMigrate(sourceRoot, outRoot, {
|
|
44
|
+
write: false,
|
|
45
|
+
only: flags.only,
|
|
46
|
+
});
|
|
47
|
+
if (flags.json) {
|
|
48
|
+
process.stdout.write(JSON.stringify({ apply: reports }, null, 2) + "\n");
|
|
49
|
+
} else {
|
|
50
|
+
process.stdout.write(formatApplyReport(reports) + "\n");
|
|
51
|
+
}
|
|
52
|
+
exitStrict(reports, flags.strict);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (command === "config") {
|
|
58
|
+
const draft = await draftConfig(sourceRoot);
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
process.stdout.write(JSON.stringify(draft, null, 2) + "\n");
|
|
61
|
+
} else {
|
|
62
|
+
for (const n of draft.notes) log.warn(n);
|
|
63
|
+
if (!flags.write) {
|
|
64
|
+
process.stdout.write(draft.code ?? "");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (draft.code) {
|
|
68
|
+
const dest = path.join(outRoot, "jskelet.config.mjs");
|
|
69
|
+
commitWrites([{ path: dest, contents: draft.code, action: "create" }], {
|
|
70
|
+
write: Boolean(flags.write),
|
|
71
|
+
cwd: outRoot,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (flags.strict && draft.status !== "ok") process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (command === "apply") {
|
|
79
|
+
if (!fs.existsSync(sourceRoot)) {
|
|
80
|
+
throw new Error(`source not found: ${sourceRoot}`);
|
|
81
|
+
}
|
|
82
|
+
const { reports } = await applyMigrate(sourceRoot, outRoot, {
|
|
83
|
+
write: Boolean(flags.write),
|
|
84
|
+
only: flags.only,
|
|
85
|
+
strict: flags.strict,
|
|
86
|
+
});
|
|
87
|
+
if (flags.json) {
|
|
88
|
+
process.stdout.write(JSON.stringify({ reports }, null, 2) + "\n");
|
|
89
|
+
} else {
|
|
90
|
+
process.stdout.write(formatApplyReport(reports) + "\n");
|
|
91
|
+
if (!flags.write) {
|
|
92
|
+
log.line("dry-run only — pass --write to create files (never overwrites; conflicts get .migrate suffix)");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exitStrict(reports, flags.strict);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new Error(`unknown migrate command: ${command}\n\n${usageText()}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string[]} args
|
|
104
|
+
*/
|
|
105
|
+
function parseArgs(args) {
|
|
106
|
+
/** @type {string[]} */
|
|
107
|
+
const positionals = [];
|
|
108
|
+
/** @type {{ write?: boolean, json?: boolean, strict?: boolean, help?: boolean, out?: string, only?: Set<string> }} */
|
|
109
|
+
const flags = {};
|
|
110
|
+
let command = "default";
|
|
111
|
+
let i = 0;
|
|
112
|
+
if (args[0] && !args[0].startsWith("-")) {
|
|
113
|
+
if (["scan", "apply", "config", "help"].includes(args[0])) {
|
|
114
|
+
command = args[0];
|
|
115
|
+
i = 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (; i < args.length; i++) {
|
|
119
|
+
const a = args[i];
|
|
120
|
+
if (a === "--write") flags.write = true;
|
|
121
|
+
else if (a === "--dry-run") flags.write = false;
|
|
122
|
+
else if (a === "--json") flags.json = true;
|
|
123
|
+
else if (a === "--strict") flags.strict = true;
|
|
124
|
+
else if (a === "--help" || a === "-h") flags.help = true;
|
|
125
|
+
else if (a === "--out") {
|
|
126
|
+
flags.out = args[++i];
|
|
127
|
+
} else if (a.startsWith("--out=")) {
|
|
128
|
+
flags.out = a.slice(6);
|
|
129
|
+
} else if (a === "--only") {
|
|
130
|
+
const raw = args[++i] ?? "";
|
|
131
|
+
flags.only = new Set(raw.split(",").map((s) => s.trim()).filter(Boolean));
|
|
132
|
+
} else if (a.startsWith("--only=")) {
|
|
133
|
+
flags.only = new Set(a.slice(7).split(",").map((s) => s.trim()).filter(Boolean));
|
|
134
|
+
} else if (a.startsWith("-")) {
|
|
135
|
+
throw new Error(`unknown flag: ${a}`);
|
|
136
|
+
} else {
|
|
137
|
+
positionals.push(a);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { command, positionals, flags };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {{ status: string }[]} reports
|
|
145
|
+
* @param {boolean} [strict]
|
|
146
|
+
*/
|
|
147
|
+
function exitStrict(reports, strict) {
|
|
148
|
+
if (!strict) return;
|
|
149
|
+
if (reports.some((r) => r.status !== "ok")) process.exitCode = 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function printUsage() {
|
|
153
|
+
process.stdout.write(usageText() + "\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function usageText() {
|
|
157
|
+
return (
|
|
158
|
+
"usage: jskelet migrate [scan|apply|config] [dir] [options]\n\n" +
|
|
159
|
+
" scan Inventory App Router pages, layouts, blockers\n" +
|
|
160
|
+
" apply Codemod: JSX pages → controller + .jsk, components, islands\n" +
|
|
161
|
+
" config Draft jskelet.config.mjs from next.config\n\n" +
|
|
162
|
+
"Options:\n" +
|
|
163
|
+
" --out <dir> Output root (default: cwd)\n" +
|
|
164
|
+
" --write Write files (default: dry-run)\n" +
|
|
165
|
+
" --only <list> pages,components,islands (comma-separated)\n" +
|
|
166
|
+
" --json Machine-readable report\n" +
|
|
167
|
+
" --strict Exit 1 if any partial/skipped\n\n" +
|
|
168
|
+
"Requires optional peers (install in the project you migrate):\n" +
|
|
169
|
+
" npm i -D @babel/parser @babel/types\n"
|
|
170
|
+
);
|
|
171
|
+
}
|