jskelet 0.6.3 → 0.6.4
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 +628 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +667 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +348 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +675 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +351 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -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 +541 -534
- package/src/config/index.js +1500 -1469
- 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 +232 -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 -70
- package/src/server/cache-control.js +45 -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 -553
- 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 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- 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 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +369 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- 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 +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +5 -0
package/src/config/pattern.js
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `next.config` `source`/`destination` sözdiziminin küçük bir derleyicisi.
|
|
3
|
-
*
|
|
4
|
-
* Desteklenen biçimler:
|
|
5
|
-
* `/haber/:slug` → tek segment yakalar
|
|
6
|
-
* `/:path*` → sıfır veya daha fazla segment yakalar
|
|
7
|
-
* `/:path*.svg` → joker + sabit son ek (uzantı kuralları böyle yazılır)
|
|
8
|
-
* `/etiket-:slug` → segment ortasında parametre
|
|
9
|
-
*
|
|
10
|
-
* Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
|
|
11
|
-
* Next'in tam `path-to-regexp` yüzeyi değil; config'te fiilen kullanılan alt
|
|
12
|
-
* küme bilinçli olarak seçildi ve tanınmayan bir sözdizimi sessizce literal
|
|
13
|
-
* kabul edilmez, uyarı üretir.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/** @typedef {{ regex: RegExp, keys: string[], source: string }} CompiledPattern */
|
|
17
|
-
|
|
18
|
-
const PARAM = /^:([A-Za-z_][A-Za-z0-9_]*)(\*)?/;
|
|
19
|
-
|
|
20
|
-
/** @param {string} text */
|
|
21
|
-
function escapeLiteral(text) {
|
|
22
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {string} source
|
|
27
|
-
* @returns {CompiledPattern | null}
|
|
28
|
-
*/
|
|
29
|
-
export function compilePattern(source) {
|
|
30
|
-
if (typeof source !== "string" || !source.startsWith("/")) {
|
|
31
|
-
console.warn(`[config] invalid source (must start with \`/\`): ${source}`);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** @type {string[]} */
|
|
36
|
-
const keys = [];
|
|
37
|
-
let pattern = "";
|
|
38
|
-
let index = 0;
|
|
39
|
-
|
|
40
|
-
while (index < source.length) {
|
|
41
|
-
const char = source[index];
|
|
42
|
-
|
|
43
|
-
if (char !== ":") {
|
|
44
|
-
pattern += escapeLiteral(char);
|
|
45
|
-
index += 1;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const match = source.slice(index).match(PARAM);
|
|
50
|
-
if (!match) {
|
|
51
|
-
pattern += escapeLiteral(char);
|
|
52
|
-
index += 1;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const [raw, name, wildcard] = match;
|
|
57
|
-
keys.push(name);
|
|
58
|
-
|
|
59
|
-
if (!wildcard) {
|
|
60
|
-
// Tek segmentli parametre segment sınırını aşmaz.
|
|
61
|
-
pattern += "([^/]+)";
|
|
62
|
-
index += raw.length;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Joker parametre segment sınırlarını aşar ve **sıfır** segment de
|
|
67
|
-
// yakalar: `/hesabim/:path*` kuralı `/hesabim`i de kapsamalı, aksi hâlde
|
|
68
|
-
// bir bölümün tamamını kapatmak isteyen kural kök yolu atlıyor. Bunun için
|
|
69
|
-
// hemen öncesindeki `/` de opsiyonele alınır.
|
|
70
|
-
const trailingSlash = pattern.endsWith("/");
|
|
71
|
-
pattern = trailingSlash ? `${pattern.slice(0, -1)}(?:/(.*))?` : `${pattern}(.*)`;
|
|
72
|
-
index += raw.length;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { regex: new RegExp(`^${pattern}$`), keys, source };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @param {CompiledPattern} compiled
|
|
80
|
-
* @param {string} pathname
|
|
81
|
-
* @returns {Record<string, string> | null}
|
|
82
|
-
*/
|
|
83
|
-
export function matchPattern(compiled, pathname) {
|
|
84
|
-
const match = compiled.regex.exec(pathname);
|
|
85
|
-
if (!match) return null;
|
|
86
|
-
|
|
87
|
-
/** @type {Record<string, string>} */
|
|
88
|
-
const params = {};
|
|
89
|
-
compiled.keys.forEach((key, i) => {
|
|
90
|
-
params[key] = match[i + 1] ?? "";
|
|
91
|
-
});
|
|
92
|
-
return params;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* `destination` içindeki `:param` yer tutucularını doldurur.
|
|
97
|
-
*
|
|
98
|
-
* @param {string} destination
|
|
99
|
-
* @param {Record<string, string>} params
|
|
100
|
-
* @returns {string}
|
|
101
|
-
*/
|
|
102
|
-
export function fillDestination(destination, params) {
|
|
103
|
-
return destination.replace(
|
|
104
|
-
/:([A-Za-z_][A-Za-z0-9_]*)(\*)?/g,
|
|
105
|
-
(raw, name) => params[name] ?? raw,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `next.config` `source`/`destination` sözdiziminin küçük bir derleyicisi.
|
|
3
|
+
*
|
|
4
|
+
* Desteklenen biçimler:
|
|
5
|
+
* `/haber/:slug` → tek segment yakalar
|
|
6
|
+
* `/:path*` → sıfır veya daha fazla segment yakalar
|
|
7
|
+
* `/:path*.svg` → joker + sabit son ek (uzantı kuralları böyle yazılır)
|
|
8
|
+
* `/etiket-:slug` → segment ortasında parametre
|
|
9
|
+
*
|
|
10
|
+
* Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
|
|
11
|
+
* Next'in tam `path-to-regexp` yüzeyi değil; config'te fiilen kullanılan alt
|
|
12
|
+
* küme bilinçli olarak seçildi ve tanınmayan bir sözdizimi sessizce literal
|
|
13
|
+
* kabul edilmez, uyarı üretir.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @typedef {{ regex: RegExp, keys: string[], source: string }} CompiledPattern */
|
|
17
|
+
|
|
18
|
+
const PARAM = /^:([A-Za-z_][A-Za-z0-9_]*)(\*)?/;
|
|
19
|
+
|
|
20
|
+
/** @param {string} text */
|
|
21
|
+
function escapeLiteral(text) {
|
|
22
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} source
|
|
27
|
+
* @returns {CompiledPattern | null}
|
|
28
|
+
*/
|
|
29
|
+
export function compilePattern(source) {
|
|
30
|
+
if (typeof source !== "string" || !source.startsWith("/")) {
|
|
31
|
+
console.warn(`[config] invalid source (must start with \`/\`): ${source}`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @type {string[]} */
|
|
36
|
+
const keys = [];
|
|
37
|
+
let pattern = "";
|
|
38
|
+
let index = 0;
|
|
39
|
+
|
|
40
|
+
while (index < source.length) {
|
|
41
|
+
const char = source[index];
|
|
42
|
+
|
|
43
|
+
if (char !== ":") {
|
|
44
|
+
pattern += escapeLiteral(char);
|
|
45
|
+
index += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const match = source.slice(index).match(PARAM);
|
|
50
|
+
if (!match) {
|
|
51
|
+
pattern += escapeLiteral(char);
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [raw, name, wildcard] = match;
|
|
57
|
+
keys.push(name);
|
|
58
|
+
|
|
59
|
+
if (!wildcard) {
|
|
60
|
+
// Tek segmentli parametre segment sınırını aşmaz.
|
|
61
|
+
pattern += "([^/]+)";
|
|
62
|
+
index += raw.length;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Joker parametre segment sınırlarını aşar ve **sıfır** segment de
|
|
67
|
+
// yakalar: `/hesabim/:path*` kuralı `/hesabim`i de kapsamalı, aksi hâlde
|
|
68
|
+
// bir bölümün tamamını kapatmak isteyen kural kök yolu atlıyor. Bunun için
|
|
69
|
+
// hemen öncesindeki `/` de opsiyonele alınır.
|
|
70
|
+
const trailingSlash = pattern.endsWith("/");
|
|
71
|
+
pattern = trailingSlash ? `${pattern.slice(0, -1)}(?:/(.*))?` : `${pattern}(.*)`;
|
|
72
|
+
index += raw.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { regex: new RegExp(`^${pattern}$`), keys, source };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {CompiledPattern} compiled
|
|
80
|
+
* @param {string} pathname
|
|
81
|
+
* @returns {Record<string, string> | null}
|
|
82
|
+
*/
|
|
83
|
+
export function matchPattern(compiled, pathname) {
|
|
84
|
+
const match = compiled.regex.exec(pathname);
|
|
85
|
+
if (!match) return null;
|
|
86
|
+
|
|
87
|
+
/** @type {Record<string, string>} */
|
|
88
|
+
const params = {};
|
|
89
|
+
compiled.keys.forEach((key, i) => {
|
|
90
|
+
params[key] = match[i + 1] ?? "";
|
|
91
|
+
});
|
|
92
|
+
return params;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `destination` içindeki `:param` yer tutucularını doldurur.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} destination
|
|
99
|
+
* @param {Record<string, string>} params
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
export function fillDestination(destination, params) {
|
|
103
|
+
return destination.replace(
|
|
104
|
+
/:([A-Za-z_][A-Za-z0-9_]*)(\*)?/g,
|
|
105
|
+
(raw, name) => params[name] ?? raw,
|
|
106
|
+
);
|
|
107
|
+
}
|
package/src/generate.mjs
CHANGED
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `jskelet generate` — feature / page / island iskeleti.
|
|
3
|
-
*
|
|
4
|
-
* Var olan dosyaların üzerine yazmaz. Route kaydı hâlâ açık `register`
|
|
5
|
-
* sözleşmesine bağlıdır; filesystem URL türetme yoktur.
|
|
6
|
-
*/
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import process from "node:process";
|
|
10
|
-
import * as log from "./log.mjs";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param {string} name
|
|
14
|
-
* @returns {string}
|
|
15
|
-
*/
|
|
16
|
-
function toPascal(name) {
|
|
17
|
-
return name
|
|
18
|
-
.split(/[-_/]+/)
|
|
19
|
-
.filter(Boolean)
|
|
20
|
-
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
21
|
-
.join("");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {string} file
|
|
26
|
-
* @param {string} contents
|
|
27
|
-
* @returns {boolean} yazıldı mı
|
|
28
|
-
*/
|
|
29
|
-
function writeIfMissing(file, contents) {
|
|
30
|
-
if (fs.existsSync(file)) {
|
|
31
|
-
log.line(`skip ${path.relative(process.cwd(), file)} (exists)`);
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
-
fs.writeFileSync(file, contents, "utf8");
|
|
36
|
-
log.line(`create ${path.relative(process.cwd(), file)}`);
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} root
|
|
42
|
-
* @param {string[]} args
|
|
43
|
-
* @returns {Promise<void>}
|
|
44
|
-
*/
|
|
45
|
-
export async function generate(root, args) {
|
|
46
|
-
const [kind, target] = args;
|
|
47
|
-
if (!kind || !target) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
"usage: jskelet generate <feature|page|island> <name>\n" +
|
|
50
|
-
" feature features/<name>/{index.js,server,views,client}\n" +
|
|
51
|
-
" page features/<feature>/views/pages/<name>.jsk (name: feature/page)\n" +
|
|
52
|
-
" island features/<feature>/client/<name>.js (name: feature/island)",
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (kind === "feature") {
|
|
57
|
-
await generateFeature(root, target);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (kind === "page") {
|
|
61
|
-
const [feature, page] = splitTarget(target);
|
|
62
|
-
await generatePage(root, feature, page);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (kind === "island") {
|
|
66
|
-
const [feature, island] = splitTarget(target);
|
|
67
|
-
await generateIsland(root, feature, island);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
throw new Error(`unknown generate kind: ${kind}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @param {string} target
|
|
76
|
-
* @returns {[string, string]}
|
|
77
|
-
*/
|
|
78
|
-
function splitTarget(target) {
|
|
79
|
-
const parts = target.split("/").filter(Boolean);
|
|
80
|
-
if (parts.length < 2) {
|
|
81
|
-
throw new Error(`expected <feature>/<name>, got "${target}"`);
|
|
82
|
-
}
|
|
83
|
-
return [parts[0], parts.slice(1).join("/")];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {string} root
|
|
88
|
-
* @param {string} name
|
|
89
|
-
*/
|
|
90
|
-
async function generateFeature(root, name) {
|
|
91
|
-
const base = path.join(root, "features", name);
|
|
92
|
-
writeIfMissing(
|
|
93
|
-
path.join(base, "index.js"),
|
|
94
|
-
`/**
|
|
95
|
-
* Feature route registration. Explicit paths only — no filesystem URL routing.
|
|
96
|
-
* @param {import('express').Express} app
|
|
97
|
-
* @param {{ route: Function }} api
|
|
98
|
-
*/
|
|
99
|
-
export default function register(app, { route }) {
|
|
100
|
-
app.get(
|
|
101
|
-
"/${name}",
|
|
102
|
-
route(async () => ({
|
|
103
|
-
view: "pages/${name}",
|
|
104
|
-
metadata: { title: "${toPascal(name)}" },
|
|
105
|
-
data: {},
|
|
106
|
-
})),
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
`,
|
|
110
|
-
);
|
|
111
|
-
writeIfMissing(path.join(base, "server", ".gitkeep"), "");
|
|
112
|
-
writeIfMissing(
|
|
113
|
-
path.join(base, "views", "pages", `${name}.jsk`),
|
|
114
|
-
`<section class="wrapper">
|
|
115
|
-
<h1>{{ metadata.title }}</h1>
|
|
116
|
-
</section>
|
|
117
|
-
`,
|
|
118
|
-
);
|
|
119
|
-
writeIfMissing(path.join(base, "client", ".gitkeep"), "");
|
|
120
|
-
log.ready({ label: `Feature ${name}` });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @param {string} root
|
|
125
|
-
* @param {string} feature
|
|
126
|
-
* @param {string} page
|
|
127
|
-
*/
|
|
128
|
-
async function generatePage(root, feature, page) {
|
|
129
|
-
const file = path.join(root, "features", feature, "views", "pages", `${page}.jsk`);
|
|
130
|
-
writeIfMissing(
|
|
131
|
-
file,
|
|
132
|
-
`<section class="wrapper">
|
|
133
|
-
<h1>{{ metadata.title }}</h1>
|
|
134
|
-
</section>
|
|
135
|
-
`,
|
|
136
|
-
);
|
|
137
|
-
log.ready({ label: `Page ${feature}/${page}` });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* @param {string} root
|
|
142
|
-
* @param {string} feature
|
|
143
|
-
* @param {string} island
|
|
144
|
-
*/
|
|
145
|
-
async function generateIsland(root, feature, island) {
|
|
146
|
-
const file = path.join(root, "features", feature, "client", `${island}.js`);
|
|
147
|
-
writeIfMissing(
|
|
148
|
-
file,
|
|
149
|
-
`/**
|
|
150
|
-
* @param {HTMLElement} element
|
|
151
|
-
* @param {Record<string, unknown>} props
|
|
152
|
-
* @returns {void | (() => void)}
|
|
153
|
-
*/
|
|
154
|
-
export function mount(element, props) {
|
|
155
|
-
element.textContent = String(props.label ?? "${island}");
|
|
156
|
-
}
|
|
157
|
-
`,
|
|
158
|
-
);
|
|
159
|
-
log.line(
|
|
160
|
-
`Register in client/entries/main.js: "${island}": () => import("../../features/${feature}/client/${island}.js")`,
|
|
161
|
-
);
|
|
162
|
-
log.ready({ label: `Island ${feature}/${island}` });
|
|
163
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet generate` — feature / page / island iskeleti.
|
|
3
|
+
*
|
|
4
|
+
* Var olan dosyaların üzerine yazmaz. Route kaydı hâlâ açık `register`
|
|
5
|
+
* sözleşmesine bağlıdır; filesystem URL türetme yoktur.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import * as log from "./log.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} name
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function toPascal(name) {
|
|
17
|
+
return name
|
|
18
|
+
.split(/[-_/]+/)
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
21
|
+
.join("");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} file
|
|
26
|
+
* @param {string} contents
|
|
27
|
+
* @returns {boolean} yazıldı mı
|
|
28
|
+
*/
|
|
29
|
+
function writeIfMissing(file, contents) {
|
|
30
|
+
if (fs.existsSync(file)) {
|
|
31
|
+
log.line(`skip ${path.relative(process.cwd(), file)} (exists)`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
+
fs.writeFileSync(file, contents, "utf8");
|
|
36
|
+
log.line(`create ${path.relative(process.cwd(), file)}`);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} root
|
|
42
|
+
* @param {string[]} args
|
|
43
|
+
* @returns {Promise<void>}
|
|
44
|
+
*/
|
|
45
|
+
export async function generate(root, args) {
|
|
46
|
+
const [kind, target] = args;
|
|
47
|
+
if (!kind || !target) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"usage: jskelet generate <feature|page|island> <name>\n" +
|
|
50
|
+
" feature features/<name>/{index.js,server,views,client}\n" +
|
|
51
|
+
" page features/<feature>/views/pages/<name>.jsk (name: feature/page)\n" +
|
|
52
|
+
" island features/<feature>/client/<name>.js (name: feature/island)",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (kind === "feature") {
|
|
57
|
+
await generateFeature(root, target);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (kind === "page") {
|
|
61
|
+
const [feature, page] = splitTarget(target);
|
|
62
|
+
await generatePage(root, feature, page);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (kind === "island") {
|
|
66
|
+
const [feature, island] = splitTarget(target);
|
|
67
|
+
await generateIsland(root, feature, island);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(`unknown generate kind: ${kind}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} target
|
|
76
|
+
* @returns {[string, string]}
|
|
77
|
+
*/
|
|
78
|
+
function splitTarget(target) {
|
|
79
|
+
const parts = target.split("/").filter(Boolean);
|
|
80
|
+
if (parts.length < 2) {
|
|
81
|
+
throw new Error(`expected <feature>/<name>, got "${target}"`);
|
|
82
|
+
}
|
|
83
|
+
return [parts[0], parts.slice(1).join("/")];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} root
|
|
88
|
+
* @param {string} name
|
|
89
|
+
*/
|
|
90
|
+
async function generateFeature(root, name) {
|
|
91
|
+
const base = path.join(root, "features", name);
|
|
92
|
+
writeIfMissing(
|
|
93
|
+
path.join(base, "index.js"),
|
|
94
|
+
`/**
|
|
95
|
+
* Feature route registration. Explicit paths only — no filesystem URL routing.
|
|
96
|
+
* @param {import('express').Express} app
|
|
97
|
+
* @param {{ route: Function }} api
|
|
98
|
+
*/
|
|
99
|
+
export default function register(app, { route }) {
|
|
100
|
+
app.get(
|
|
101
|
+
"/${name}",
|
|
102
|
+
route(async () => ({
|
|
103
|
+
view: "pages/${name}",
|
|
104
|
+
metadata: { title: "${toPascal(name)}" },
|
|
105
|
+
data: {},
|
|
106
|
+
})),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
`,
|
|
110
|
+
);
|
|
111
|
+
writeIfMissing(path.join(base, "server", ".gitkeep"), "");
|
|
112
|
+
writeIfMissing(
|
|
113
|
+
path.join(base, "views", "pages", `${name}.jsk`),
|
|
114
|
+
`<section class="wrapper">
|
|
115
|
+
<h1>{{ metadata.title }}</h1>
|
|
116
|
+
</section>
|
|
117
|
+
`,
|
|
118
|
+
);
|
|
119
|
+
writeIfMissing(path.join(base, "client", ".gitkeep"), "");
|
|
120
|
+
log.ready({ label: `Feature ${name}` });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} root
|
|
125
|
+
* @param {string} feature
|
|
126
|
+
* @param {string} page
|
|
127
|
+
*/
|
|
128
|
+
async function generatePage(root, feature, page) {
|
|
129
|
+
const file = path.join(root, "features", feature, "views", "pages", `${page}.jsk`);
|
|
130
|
+
writeIfMissing(
|
|
131
|
+
file,
|
|
132
|
+
`<section class="wrapper">
|
|
133
|
+
<h1>{{ metadata.title }}</h1>
|
|
134
|
+
</section>
|
|
135
|
+
`,
|
|
136
|
+
);
|
|
137
|
+
log.ready({ label: `Page ${feature}/${page}` });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} root
|
|
142
|
+
* @param {string} feature
|
|
143
|
+
* @param {string} island
|
|
144
|
+
*/
|
|
145
|
+
async function generateIsland(root, feature, island) {
|
|
146
|
+
const file = path.join(root, "features", feature, "client", `${island}.js`);
|
|
147
|
+
writeIfMissing(
|
|
148
|
+
file,
|
|
149
|
+
`/**
|
|
150
|
+
* @param {HTMLElement} element
|
|
151
|
+
* @param {Record<string, unknown>} props
|
|
152
|
+
* @returns {void | (() => void)}
|
|
153
|
+
*/
|
|
154
|
+
export function mount(element, props) {
|
|
155
|
+
element.textContent = String(props.label ?? "${island}");
|
|
156
|
+
}
|
|
157
|
+
`,
|
|
158
|
+
);
|
|
159
|
+
log.line(
|
|
160
|
+
`Register in client/entries/main.js: "${island}": () => import("../../features/${feature}/client/${island}.js")`,
|
|
161
|
+
);
|
|
162
|
+
log.ready({ label: `Island ${feature}/${island}` });
|
|
163
|
+
}
|
package/src/http/control-flow.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `next/navigation` içindeki notFound/redirect kontrol akışının karşılığı.
|
|
3
|
-
* Derinlerdeki bir fonksiyon throw eder, Express error handler yakalar.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export class NotFoundError extends Error {
|
|
7
|
-
constructor(message = "Not Found") {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = "NotFoundError";
|
|
10
|
-
this.statusCode = 404;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class RedirectError extends Error {
|
|
15
|
-
/**
|
|
16
|
-
* @param {string} location
|
|
17
|
-
* @param {301 | 302 | 303 | 307 | 308} [statusCode]
|
|
18
|
-
*/
|
|
19
|
-
constructor(location, statusCode = 307) {
|
|
20
|
-
super(`Redirect to ${location}`);
|
|
21
|
-
this.name = "RedirectError";
|
|
22
|
-
this.location = location;
|
|
23
|
-
this.statusCode = statusCode;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** @returns {never} */
|
|
28
|
-
export function notFound() {
|
|
29
|
-
throw new NotFoundError();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {string} location
|
|
34
|
-
* @returns {never}
|
|
35
|
-
*/
|
|
36
|
-
export function permanentRedirect(location) {
|
|
37
|
-
throw new RedirectError(location, 308);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} location
|
|
42
|
-
* @returns {never}
|
|
43
|
-
*/
|
|
44
|
-
export function redirect(location) {
|
|
45
|
-
throw new RedirectError(location, 307);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* POST sonrası yönlendirme (303 See Other).
|
|
50
|
-
*
|
|
51
|
-
* `redirect()` 307 kullanır ve 307 **metodu korur**: bir POST handler'ından
|
|
52
|
-
* çağrıldığında tarayıcı hedefe yeniden POST eder. Form gönderiminden sonra
|
|
53
|
-
* sayfayı GET olarak açmak — yani geri tuşunun formu yeniden göndermediği
|
|
54
|
-
* klasik "post/redirect/get" akışı — 303 gerektiriyor.
|
|
55
|
-
*
|
|
56
|
-
* @param {string} location
|
|
57
|
-
* @returns {never}
|
|
58
|
-
*/
|
|
59
|
-
export function seeOther(location) {
|
|
60
|
-
throw new RedirectError(location, 303);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** @param {unknown} error */
|
|
64
|
-
export function isNotFoundError(error) {
|
|
65
|
-
return error instanceof NotFoundError;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** @param {unknown} error */
|
|
69
|
-
export function isRedirectError(error) {
|
|
70
|
-
return error instanceof RedirectError;
|
|
71
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `next/navigation` içindeki notFound/redirect kontrol akışının karşılığı.
|
|
3
|
+
* Derinlerdeki bir fonksiyon throw eder, Express error handler yakalar.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class NotFoundError extends Error {
|
|
7
|
+
constructor(message = "Not Found") {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "NotFoundError";
|
|
10
|
+
this.statusCode = 404;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class RedirectError extends Error {
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} location
|
|
17
|
+
* @param {301 | 302 | 303 | 307 | 308} [statusCode]
|
|
18
|
+
*/
|
|
19
|
+
constructor(location, statusCode = 307) {
|
|
20
|
+
super(`Redirect to ${location}`);
|
|
21
|
+
this.name = "RedirectError";
|
|
22
|
+
this.location = location;
|
|
23
|
+
this.statusCode = statusCode;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @returns {never} */
|
|
28
|
+
export function notFound() {
|
|
29
|
+
throw new NotFoundError();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} location
|
|
34
|
+
* @returns {never}
|
|
35
|
+
*/
|
|
36
|
+
export function permanentRedirect(location) {
|
|
37
|
+
throw new RedirectError(location, 308);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} location
|
|
42
|
+
* @returns {never}
|
|
43
|
+
*/
|
|
44
|
+
export function redirect(location) {
|
|
45
|
+
throw new RedirectError(location, 307);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* POST sonrası yönlendirme (303 See Other).
|
|
50
|
+
*
|
|
51
|
+
* `redirect()` 307 kullanır ve 307 **metodu korur**: bir POST handler'ından
|
|
52
|
+
* çağrıldığında tarayıcı hedefe yeniden POST eder. Form gönderiminden sonra
|
|
53
|
+
* sayfayı GET olarak açmak — yani geri tuşunun formu yeniden göndermediği
|
|
54
|
+
* klasik "post/redirect/get" akışı — 303 gerektiriyor.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} location
|
|
57
|
+
* @returns {never}
|
|
58
|
+
*/
|
|
59
|
+
export function seeOther(location) {
|
|
60
|
+
throw new RedirectError(location, 303);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {unknown} error */
|
|
64
|
+
export function isNotFoundError(error) {
|
|
65
|
+
return error instanceof NotFoundError;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {unknown} error */
|
|
69
|
+
export function isRedirectError(error) {
|
|
70
|
+
return error instanceof RedirectError;
|
|
71
|
+
}
|