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.
Files changed (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -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
+ }
@@ -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
+ }