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
package/src/migrate/config.mjs
CHANGED
|
@@ -1,126 +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
|
-
}
|
|
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
|
+
}
|
package/src/migrate/fs-walk.mjs
CHANGED
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Next App Router ağacında dosya keşfi ve path → URL eşlemesi.
|
|
3
|
-
*/
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
|
|
7
|
-
const PAGE_RE = /^page\.(jsx?|tsx?|mjs|cjs)$/;
|
|
8
|
-
const LAYOUT_RE = /^layout\.(jsx?|tsx?|mjs|cjs)$/;
|
|
9
|
-
const SOURCE_RE = /\.(jsx?|tsx?|mjs|cjs)$/;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @param {string} root
|
|
13
|
-
* @returns {string | null}
|
|
14
|
-
*/
|
|
15
|
-
export function findAppDir(root) {
|
|
16
|
-
for (const candidate of ["app", "src/app"]) {
|
|
17
|
-
const abs = path.join(root, candidate);
|
|
18
|
-
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return abs;
|
|
19
|
-
}
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {string} root
|
|
25
|
-
* @returns {string | null}
|
|
26
|
-
*/
|
|
27
|
-
export function findNextConfig(root) {
|
|
28
|
-
for (const name of [
|
|
29
|
-
"next.config.mjs",
|
|
30
|
-
"next.config.js",
|
|
31
|
-
"next.config.cjs",
|
|
32
|
-
"next.config.ts",
|
|
33
|
-
]) {
|
|
34
|
-
const abs = path.join(root, name);
|
|
35
|
-
if (fs.existsSync(abs)) return abs;
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} dir
|
|
42
|
-
* @param {(rel: string, abs: string) => void} visit
|
|
43
|
-
* @param {string} [base]
|
|
44
|
-
*/
|
|
45
|
-
export function walkFiles(dir, visit, base = dir) {
|
|
46
|
-
if (!fs.existsSync(dir)) return;
|
|
47
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
48
|
-
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
49
|
-
const abs = path.join(dir, entry.name);
|
|
50
|
-
if (entry.isDirectory()) {
|
|
51
|
-
walkFiles(abs, visit, base);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
visit(path.relative(base, abs).replace(/\\/g, "/"), abs);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* App Router segment yolunu Express path'ine çevirir.
|
|
60
|
-
* `news/[slug]` → `/news/:slug`, `docs/[...slug]` → `/docs/:slug*`
|
|
61
|
-
*
|
|
62
|
-
* @param {string} relativeDir app/ altındaki dizin (posix, trailing slash yok)
|
|
63
|
-
* @returns {string}
|
|
64
|
-
*/
|
|
65
|
-
export function segmentToExpressPath(relativeDir) {
|
|
66
|
-
if (!relativeDir || relativeDir === ".") return "/";
|
|
67
|
-
const parts = relativeDir.split("/").filter(Boolean);
|
|
68
|
-
const out = [];
|
|
69
|
-
for (const part of parts) {
|
|
70
|
-
if (part.startsWith("(") && part.endsWith(")")) continue; // route groups
|
|
71
|
-
if (part.startsWith("@")) continue; // parallel routes
|
|
72
|
-
const catchAll = part.match(/^\[\.\.\.(.+)\]$/);
|
|
73
|
-
if (catchAll) {
|
|
74
|
-
out.push(`:${catchAll[1]}*`);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const optional = part.match(/^\[\[\.\.\.(.+)\]\]$/);
|
|
78
|
-
if (optional) {
|
|
79
|
-
out.push(`:${optional[1]}*`);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const dyn = part.match(/^\[(.+)\]$/);
|
|
83
|
-
if (dyn) {
|
|
84
|
-
out.push(`:${dyn[1]}`);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
out.push(part);
|
|
88
|
-
}
|
|
89
|
-
return "/" + out.join("/");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Feature adı: ilk anlamlı segment veya `home`.
|
|
94
|
-
*
|
|
95
|
-
* @param {string} relativeDir
|
|
96
|
-
* @returns {string}
|
|
97
|
-
*/
|
|
98
|
-
export function featureNameFromDir(relativeDir) {
|
|
99
|
-
const parts = relativeDir
|
|
100
|
-
.split("/")
|
|
101
|
-
.filter(Boolean)
|
|
102
|
-
.filter((p) => !(p.startsWith("(") && p.endsWith(")")))
|
|
103
|
-
.filter((p) => !p.startsWith("@"))
|
|
104
|
-
.filter((p) => !p.startsWith("["));
|
|
105
|
-
if (parts.length === 0) return "home";
|
|
106
|
-
return parts[0].replace(/[^a-zA-Z0-9_-]/g, "-") || "home";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Sayfa dosya gövde adı.
|
|
111
|
-
*
|
|
112
|
-
* @param {string} relativeDir
|
|
113
|
-
* @returns {string}
|
|
114
|
-
*/
|
|
115
|
-
export function pageNameFromDir(relativeDir) {
|
|
116
|
-
const parts = relativeDir
|
|
117
|
-
.split("/")
|
|
118
|
-
.filter(Boolean)
|
|
119
|
-
.filter((p) => !(p.startsWith("(") && p.endsWith(")")))
|
|
120
|
-
.filter((p) => !p.startsWith("@"));
|
|
121
|
-
if (parts.length === 0) return "home";
|
|
122
|
-
const last = parts[parts.length - 1];
|
|
123
|
-
if (last.startsWith("[")) {
|
|
124
|
-
return parts.length > 1 ? parts[parts.length - 2] : "detail";
|
|
125
|
-
}
|
|
126
|
-
return last.replace(/[^a-zA-Z0-9_-]/g, "-") || "page";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* @param {string} appDir
|
|
131
|
-
* @returns {{ pages: Array<{ abs: string, rel: string, dir: string, url: string, feature: string, page: string }>, layouts: Array<{ abs: string, rel: string, dir: string, depth: number }>, sources: Array<{ abs: string, rel: string }> }}
|
|
132
|
-
*/
|
|
133
|
-
export function inventoryApp(appDir) {
|
|
134
|
-
/** @type {Array<{ abs: string, rel: string, dir: string, url: string, feature: string, page: string }>} */
|
|
135
|
-
const pages = [];
|
|
136
|
-
/** @type {Array<{ abs: string, rel: string, dir: string, depth: number }>} */
|
|
137
|
-
const layouts = [];
|
|
138
|
-
/** @type {Array<{ abs: string, rel: string }>} */
|
|
139
|
-
const sources = [];
|
|
140
|
-
|
|
141
|
-
walkFiles(appDir, (rel, abs) => {
|
|
142
|
-
const base = path.basename(rel);
|
|
143
|
-
const dir = path.posix.dirname(rel);
|
|
144
|
-
const dirKey = dir === "." ? "" : dir;
|
|
145
|
-
|
|
146
|
-
if (PAGE_RE.test(base)) {
|
|
147
|
-
pages.push({
|
|
148
|
-
abs,
|
|
149
|
-
rel,
|
|
150
|
-
dir: dirKey,
|
|
151
|
-
url: segmentToExpressPath(dirKey),
|
|
152
|
-
feature: featureNameFromDir(dirKey),
|
|
153
|
-
page: pageNameFromDir(dirKey),
|
|
154
|
-
});
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (LAYOUT_RE.test(base)) {
|
|
158
|
-
const depth = dirKey ? dirKey.split("/").length : 0;
|
|
159
|
-
layouts.push({ abs, rel, dir: dirKey, depth });
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (SOURCE_RE.test(base)) {
|
|
163
|
-
sources.push({ abs, rel });
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
layouts.sort((a, b) => a.depth - b.depth);
|
|
168
|
-
pages.sort((a, b) => a.url.localeCompare(b.url));
|
|
169
|
-
return { pages, layouts, sources };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* @param {string} root
|
|
174
|
-
* @returns {string[]}
|
|
175
|
-
*/
|
|
176
|
-
export function findClientEnvKeys(root) {
|
|
177
|
-
/** @type {Set<string>} */
|
|
178
|
-
const keys = new Set();
|
|
179
|
-
for (const name of [".env", ".env.local", ".env.production", ".env.development"]) {
|
|
180
|
-
const abs = path.join(root, name);
|
|
181
|
-
if (!fs.existsSync(abs)) continue;
|
|
182
|
-
const text = fs.readFileSync(abs, "utf8");
|
|
183
|
-
for (const line of text.split(/\r?\n/)) {
|
|
184
|
-
const m = line.match(/^\s*NEXT_PUBLIC_([A-Za-z0-9_]+)\s*=/);
|
|
185
|
-
if (m) keys.add(m[1]);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return [...keys].sort();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export { PAGE_RE, LAYOUT_RE, SOURCE_RE };
|
|
1
|
+
/**
|
|
2
|
+
* Next App Router ağacında dosya keşfi ve path → URL eşlemesi.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const PAGE_RE = /^page\.(jsx?|tsx?|mjs|cjs)$/;
|
|
8
|
+
const LAYOUT_RE = /^layout\.(jsx?|tsx?|mjs|cjs)$/;
|
|
9
|
+
const SOURCE_RE = /\.(jsx?|tsx?|mjs|cjs)$/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} root
|
|
13
|
+
* @returns {string | null}
|
|
14
|
+
*/
|
|
15
|
+
export function findAppDir(root) {
|
|
16
|
+
for (const candidate of ["app", "src/app"]) {
|
|
17
|
+
const abs = path.join(root, candidate);
|
|
18
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return abs;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} root
|
|
25
|
+
* @returns {string | null}
|
|
26
|
+
*/
|
|
27
|
+
export function findNextConfig(root) {
|
|
28
|
+
for (const name of [
|
|
29
|
+
"next.config.mjs",
|
|
30
|
+
"next.config.js",
|
|
31
|
+
"next.config.cjs",
|
|
32
|
+
"next.config.ts",
|
|
33
|
+
]) {
|
|
34
|
+
const abs = path.join(root, name);
|
|
35
|
+
if (fs.existsSync(abs)) return abs;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} dir
|
|
42
|
+
* @param {(rel: string, abs: string) => void} visit
|
|
43
|
+
* @param {string} [base]
|
|
44
|
+
*/
|
|
45
|
+
export function walkFiles(dir, visit, base = dir) {
|
|
46
|
+
if (!fs.existsSync(dir)) return;
|
|
47
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
48
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
49
|
+
const abs = path.join(dir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
walkFiles(abs, visit, base);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
visit(path.relative(base, abs).replace(/\\/g, "/"), abs);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* App Router segment yolunu Express path'ine çevirir.
|
|
60
|
+
* `news/[slug]` → `/news/:slug`, `docs/[...slug]` → `/docs/:slug*`
|
|
61
|
+
*
|
|
62
|
+
* @param {string} relativeDir app/ altındaki dizin (posix, trailing slash yok)
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function segmentToExpressPath(relativeDir) {
|
|
66
|
+
if (!relativeDir || relativeDir === ".") return "/";
|
|
67
|
+
const parts = relativeDir.split("/").filter(Boolean);
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
if (part.startsWith("(") && part.endsWith(")")) continue; // route groups
|
|
71
|
+
if (part.startsWith("@")) continue; // parallel routes
|
|
72
|
+
const catchAll = part.match(/^\[\.\.\.(.+)\]$/);
|
|
73
|
+
if (catchAll) {
|
|
74
|
+
out.push(`:${catchAll[1]}*`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const optional = part.match(/^\[\[\.\.\.(.+)\]\]$/);
|
|
78
|
+
if (optional) {
|
|
79
|
+
out.push(`:${optional[1]}*`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const dyn = part.match(/^\[(.+)\]$/);
|
|
83
|
+
if (dyn) {
|
|
84
|
+
out.push(`:${dyn[1]}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
out.push(part);
|
|
88
|
+
}
|
|
89
|
+
return "/" + out.join("/");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Feature adı: ilk anlamlı segment veya `home`.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} relativeDir
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function featureNameFromDir(relativeDir) {
|
|
99
|
+
const parts = relativeDir
|
|
100
|
+
.split("/")
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.filter((p) => !(p.startsWith("(") && p.endsWith(")")))
|
|
103
|
+
.filter((p) => !p.startsWith("@"))
|
|
104
|
+
.filter((p) => !p.startsWith("["));
|
|
105
|
+
if (parts.length === 0) return "home";
|
|
106
|
+
return parts[0].replace(/[^a-zA-Z0-9_-]/g, "-") || "home";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sayfa dosya gövde adı.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} relativeDir
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function pageNameFromDir(relativeDir) {
|
|
116
|
+
const parts = relativeDir
|
|
117
|
+
.split("/")
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.filter((p) => !(p.startsWith("(") && p.endsWith(")")))
|
|
120
|
+
.filter((p) => !p.startsWith("@"));
|
|
121
|
+
if (parts.length === 0) return "home";
|
|
122
|
+
const last = parts[parts.length - 1];
|
|
123
|
+
if (last.startsWith("[")) {
|
|
124
|
+
return parts.length > 1 ? parts[parts.length - 2] : "detail";
|
|
125
|
+
}
|
|
126
|
+
return last.replace(/[^a-zA-Z0-9_-]/g, "-") || "page";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} appDir
|
|
131
|
+
* @returns {{ pages: Array<{ abs: string, rel: string, dir: string, url: string, feature: string, page: string }>, layouts: Array<{ abs: string, rel: string, dir: string, depth: number }>, sources: Array<{ abs: string, rel: string }> }}
|
|
132
|
+
*/
|
|
133
|
+
export function inventoryApp(appDir) {
|
|
134
|
+
/** @type {Array<{ abs: string, rel: string, dir: string, url: string, feature: string, page: string }>} */
|
|
135
|
+
const pages = [];
|
|
136
|
+
/** @type {Array<{ abs: string, rel: string, dir: string, depth: number }>} */
|
|
137
|
+
const layouts = [];
|
|
138
|
+
/** @type {Array<{ abs: string, rel: string }>} */
|
|
139
|
+
const sources = [];
|
|
140
|
+
|
|
141
|
+
walkFiles(appDir, (rel, abs) => {
|
|
142
|
+
const base = path.basename(rel);
|
|
143
|
+
const dir = path.posix.dirname(rel);
|
|
144
|
+
const dirKey = dir === "." ? "" : dir;
|
|
145
|
+
|
|
146
|
+
if (PAGE_RE.test(base)) {
|
|
147
|
+
pages.push({
|
|
148
|
+
abs,
|
|
149
|
+
rel,
|
|
150
|
+
dir: dirKey,
|
|
151
|
+
url: segmentToExpressPath(dirKey),
|
|
152
|
+
feature: featureNameFromDir(dirKey),
|
|
153
|
+
page: pageNameFromDir(dirKey),
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (LAYOUT_RE.test(base)) {
|
|
158
|
+
const depth = dirKey ? dirKey.split("/").length : 0;
|
|
159
|
+
layouts.push({ abs, rel, dir: dirKey, depth });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (SOURCE_RE.test(base)) {
|
|
163
|
+
sources.push({ abs, rel });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
layouts.sort((a, b) => a.depth - b.depth);
|
|
168
|
+
pages.sort((a, b) => a.url.localeCompare(b.url));
|
|
169
|
+
return { pages, layouts, sources };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} root
|
|
174
|
+
* @returns {string[]}
|
|
175
|
+
*/
|
|
176
|
+
export function findClientEnvKeys(root) {
|
|
177
|
+
/** @type {Set<string>} */
|
|
178
|
+
const keys = new Set();
|
|
179
|
+
for (const name of [".env", ".env.local", ".env.production", ".env.development"]) {
|
|
180
|
+
const abs = path.join(root, name);
|
|
181
|
+
if (!fs.existsSync(abs)) continue;
|
|
182
|
+
const text = fs.readFileSync(abs, "utf8");
|
|
183
|
+
for (const line of text.split(/\r?\n/)) {
|
|
184
|
+
const m = line.match(/^\s*NEXT_PUBLIC_([A-Za-z0-9_]+)\s*=/);
|
|
185
|
+
if (m) keys.add(m[1]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [...keys].sort();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { PAGE_RE, LAYOUT_RE, SOURCE_RE };
|