jskelet 0.6.2 → 0.6.3
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 +136 -136
- package/CHANGELOG.md +620 -596
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -309
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +661 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1443 -1423
- package/docs/07-yapilandirma.md +12 -6
- package/docs/08-build.md +429 -428
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +338 -338
- package/docs/12-panel-ve-oturum.md +478 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -328
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +669 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1453 -1431
- package/docs/en/07-configuration.md +1219 -1214
- package/docs/en/08-build.md +447 -446
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +340 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +488 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +17 -1
- package/src/config/index.js +13 -0
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +230 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -462
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -0
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1122
- package/src/server/image-optimizer.js +500 -407
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -66
- package/src/server/logs/pipeline.js +165 -158
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -100
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +356 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1058
- package/src/server/redis.js +588 -569
- package/src/server/render.js +4 -4
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +15 -1
- package/types/config/index.d.ts +8 -0
- package/types/server/cache-blob.d.ts +13 -0
- package/types/server/data-cache.d.ts +9 -0
- package/types/server/disk-cache.d.ts +36 -0
- package/types/server/html-cache.d.ts +26 -3
- package/types/server/logs/file-sink.d.ts +16 -5
- package/types/server/redis.d.ts +2 -1
|
@@ -1,435 +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
|
-
}
|
|
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
|
+
}
|