jskelet 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/AGENTS.md +18 -13
  2. package/CHANGELOG.md +95 -0
  3. package/README.md +9 -7
  4. package/bin/jskelet.mjs +24 -10
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +4 -3
  7. package/docs/03-routing.md +11 -6
  8. package/docs/04-render-ve-sablonlar.md +35 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/07-yapilandirma.md +40 -18
  11. package/docs/08-build.md +15 -9
  12. package/docs/09-dev-araclari.md +5 -1
  13. package/docs/10-dagitim.md +6 -1
  14. package/docs/11-tasima.md +51 -17
  15. package/docs/12-panel-ve-oturum.md +10 -4
  16. package/docs/README.md +7 -5
  17. package/docs/en/01-getting-started.md +4 -3
  18. package/docs/en/02-architecture.md +5 -5
  19. package/docs/en/03-routing.md +12 -7
  20. package/docs/en/04-rendering.md +47 -59
  21. package/docs/en/05-islands.md +13 -8
  22. package/docs/en/07-configuration.md +40 -20
  23. package/docs/en/08-build.md +16 -10
  24. package/docs/en/09-dev-tools.md +6 -1
  25. package/docs/en/10-deployment.md +6 -1
  26. package/docs/en/11-migration.md +51 -16
  27. package/docs/en/12-dashboards-and-sessions.md +9 -4
  28. package/docs/en/README.md +7 -5
  29. package/package.json +49 -14
  30. package/src/build/tasks/client.mjs +91 -10
  31. package/src/build/tasks/icons.mjs +11 -1
  32. package/src/client/index.js +2 -2
  33. package/src/compile/codegen.js +4 -0
  34. package/src/compile/compile-all.js +12 -21
  35. package/src/compile/expr.js +5 -0
  36. package/src/compile/parse.js +64 -8
  37. package/src/compile/resolve.js +3 -0
  38. package/src/config/defaults.js +12 -2
  39. package/src/config/index.js +1 -1
  40. package/src/dev-server.mjs +26 -3
  41. package/src/http/cookies-entry.js +1 -0
  42. package/src/http/cookies.js +18 -0
  43. package/src/logo.png +0 -0
  44. package/src/migrate/apply.mjs +262 -0
  45. package/src/migrate/babel.mjs +79 -0
  46. package/src/migrate/classify.mjs +155 -0
  47. package/src/migrate/config.mjs +126 -0
  48. package/src/migrate/fs-walk.mjs +191 -0
  49. package/src/migrate/parse.mjs +26 -0
  50. package/src/migrate/scan.mjs +177 -0
  51. package/src/migrate/transform/expr-source.mjs +168 -0
  52. package/src/migrate/transform/island.mjs +67 -0
  53. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  54. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  55. package/src/migrate/transform/page-split.mjs +435 -0
  56. package/src/migrate/write.mjs +81 -0
  57. package/src/migrate.mjs +171 -0
  58. package/src/server/auth/handoff.js +94 -11
  59. package/src/server/create-app.js +28 -10
  60. package/src/server/ejs-adapter.js +59 -0
  61. package/src/server/image-optimizer.js +94 -26
  62. package/src/server/port-guard.js +255 -0
  63. package/src/server/render.js +27 -9
  64. package/src/server/status-page.js +105 -4
  65. package/src/start.mjs +18 -3
  66. package/src/templates/layout.ejs +8 -28
  67. package/src/templates/layout.jsk +30 -0
  68. package/src/templates/layout.render.js +41 -0
  69. package/src/views/helpers/tags.js +86 -3
  70. package/types/build/resolve-peer.d.mts +13 -0
  71. package/types/client/dom.d.ts +55 -0
  72. package/types/client/form.d.ts +19 -0
  73. package/types/client/index.d.ts +20 -0
  74. package/types/client/registry.d.ts +53 -0
  75. package/types/client/safe-image.d.ts +19 -0
  76. package/types/client/shared-cookie.d.ts +82 -0
  77. package/types/client/store.d.ts +18 -0
  78. package/types/client/swap.d.ts +46 -0
  79. package/types/compile/codegen.d.ts +32 -0
  80. package/types/compile/compile-all.d.ts +42 -0
  81. package/types/compile/errors.d.ts +30 -0
  82. package/types/compile/expr.d.ts +67 -0
  83. package/types/compile/index.d.ts +10 -0
  84. package/types/compile/parse.d.ts +82 -0
  85. package/types/compile/resolve.d.ts +46 -0
  86. package/types/compile/scan-exports.d.ts +9 -0
  87. package/types/config/defaults.d.ts +449 -0
  88. package/types/config/index.d.ts +299 -0
  89. package/types/config/pattern.d.ts +38 -0
  90. package/types/http/control-flow.d.ts +45 -0
  91. package/types/http/cookies-entry.d.ts +5 -0
  92. package/types/http/cookies.d.ts +113 -0
  93. package/types/http/request-cache.d.ts +13 -0
  94. package/types/http/request-context.d.ts +67 -0
  95. package/types/http/shared-cookie.d.ts +73 -0
  96. package/types/index.d.ts +30 -0
  97. package/types/log.d.mts +153 -0
  98. package/types/server/admin/actions.d.ts +16 -0
  99. package/types/server/admin/auth.d.ts +52 -0
  100. package/types/server/admin/event-log.d.ts +38 -0
  101. package/types/server/admin/gate.d.ts +43 -0
  102. package/types/server/admin/inventory.d.ts +40 -0
  103. package/types/server/admin/mount.d.ts +6 -0
  104. package/types/server/admin/router.d.ts +6 -0
  105. package/types/server/admin/snapshot.d.ts +6 -0
  106. package/types/server/assets.d.ts +47 -0
  107. package/types/server/auth/handoff.d.ts +12 -0
  108. package/types/server/cache-deps.d.ts +16 -0
  109. package/types/server/cache-vary.d.ts +30 -0
  110. package/types/server/cloudflare.d.ts +163 -0
  111. package/types/server/create-app.d.ts +25 -0
  112. package/types/server/data-cache.d.ts +116 -0
  113. package/types/server/dev/devtools.d.ts +44 -0
  114. package/types/server/dev/report.d.ts +229 -0
  115. package/types/server/dev/socket.d.ts +17 -0
  116. package/types/server/dev/version-check.d.mts +15 -0
  117. package/types/server/ejs-adapter.d.ts +11 -0
  118. package/types/server/head-hints.d.ts +40 -0
  119. package/types/server/html-cache.d.ts +173 -0
  120. package/types/server/image-optimizer.d.ts +68 -0
  121. package/types/server/logs/access-middleware.d.ts +7 -0
  122. package/types/server/logs/file-sink.d.ts +17 -0
  123. package/types/server/logs/pipeline.d.ts +37 -0
  124. package/types/server/logs/s3-put.d.ts +85 -0
  125. package/types/server/logs/s3-sink.d.ts +26 -0
  126. package/types/server/metadata.d.ts +38 -0
  127. package/types/server/middleware/compression.d.ts +17 -0
  128. package/types/server/middleware/csrf.d.ts +4 -0
  129. package/types/server/middleware/dev-gate.d.ts +2 -0
  130. package/types/server/middleware/headers.d.ts +2 -0
  131. package/types/server/middleware/redirects.d.ts +2 -0
  132. package/types/server/middleware/static-precompressed.d.ts +5 -0
  133. package/types/server/middleware/trailing-slash.d.ts +11 -0
  134. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  135. package/types/server/og-image.d.ts +149 -0
  136. package/types/server/port-guard.d.ts +50 -0
  137. package/types/server/prewarm.d.ts +128 -0
  138. package/types/server/redis.d.ts +163 -0
  139. package/types/server/render.d.ts +101 -0
  140. package/types/server/router.d.ts +5 -0
  141. package/types/server/status-page.d.ts +24 -0
  142. package/types/server/upstream-limiter.d.ts +123 -0
  143. package/types/server/upstream-tracking.d.ts +42 -0
  144. package/types/shared/cookie-domain.d.ts +29 -0
  145. package/types/templates/layout.render.d.ts +7 -0
  146. package/types/version.d.mts +10 -0
  147. package/types/views/components/loader.d.ts +5 -0
  148. package/types/views/helpers/html.d.ts +39 -0
  149. package/types/views/helpers/tags.d.ts +127 -0
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Island bundle'ı: esbuild, ESM, code splitting.
3
3
  *
4
- * `client/entries/*.js` içindeki her dosya bir entry'dir. `main.js` her sayfada
5
- * yüklenen ortak island bootstrap'ıdır; ek entry'ler yalnızca onları isteyen
6
- * sayfalarda (`controller` → `entries: ["chart.js"]`) yüklenir.
4
+ * `client/entries/*.{js,ts,mts}` içindeki her dosya bir entry'dir. `main.js`
5
+ * (veya `main.ts`) her sayfada yüklenen ortak island bootstrap'ıdır; ek
6
+ * entry'ler yalnızca onları isteyen sayfalarda
7
+ * (`controller` → `entries: ["chart.js"]`) yüklenir. Manifest anahtarı her
8
+ * zaman `*.js` kalır.
7
9
  *
8
10
  * Hedef tarayıcılar `package.json` → `browserslist` yerine burada sabit: ESM +
9
11
  * dinamik import + `IntersectionObserver` island modelinin zaten alt sınırı,
@@ -16,11 +18,17 @@ import * as esbuild from "esbuild";
16
18
  import { paths, patchManifest, pruneAssets } from "../paths.mjs";
17
19
  import * as log from "../../log.mjs";
18
20
 
21
+ /** Client entry ve `@/` alias için kabul edilen kaynak uzantıları. `.tsx` yok. */
22
+ const CLIENT_SOURCE_EXTS = [".js", ".ts", ".mts"];
23
+
19
24
  /**
20
25
  * `@/` alias'ını proje köküne çözer — Node tarafındaki `alias-hooks.mjs` ile
21
26
  * aynı davranış, böylece `lib/` altındaki modüller hem sunucuda hem
22
27
  * tarayıcıda aynı import stilini kullanabilir.
23
28
  *
29
+ * Not: sunucu runtime `.ts` çözmez; paylaşılan `@/lib` dosyaları `.js`
30
+ * kalmalıdır. `.ts` yalnızca esbuild client hattında anlamlıdır.
31
+ *
24
32
  * @param {string} root
25
33
  * @returns {esbuild.Plugin}
26
34
  */
@@ -35,6 +43,23 @@ function aliasPlugin(root) {
35
43
  };
36
44
  }
37
45
 
46
+ /**
47
+ * Secret benzeri isimler public bundle'a gömülmemeli. `PUBLIC` / `PUBLISHABLE`
48
+ * içerenler (örn. Stripe publishable key) muaf; diğer `SECRET`, `PASSWORD`,
49
+ * `TOKEN`, `API_KEY`, `PRIVATE` vb. reddedilir.
50
+ */
51
+ const SECRETISH_CLIENT_ENV =
52
+ /(?:^|_)(SECRET|PASSWORD|PASSWD|TOKEN|PRIVATE|CREDENTIAL|API[_-]?KEY)(?:_|$)|(?:^|_)(SECRET|PASSWORD|TOKEN|PRIVATE|KEY)$/i;
53
+
54
+ /**
55
+ * @param {string} key
56
+ * @returns {boolean}
57
+ */
58
+ export function isSecretLikeClientEnvKey(key) {
59
+ if (/PUBLIC|PUBLISHABLE/i.test(key)) return false;
60
+ return SECRETISH_CLIENT_ENV.test(key);
61
+ }
62
+
38
63
  /**
39
64
  * Tarayıcıda `process` yoktur; sunucuyla paylaşılan modüller yine de
40
65
  * `process.env` okur. `clientEnv` ile bildirilen anahtarlar build zamanında
@@ -48,6 +73,14 @@ function aliasPlugin(root) {
48
73
  * @returns {Record<string, string>}
49
74
  */
50
75
  function publicEnv(keys) {
76
+ const secretish = keys.filter((key) => isSecretLikeClientEnvKey(key));
77
+ if (secretish.length) {
78
+ throw new Error(
79
+ `[build] clientEnv must not include secret-like keys: ${secretish.join(", ")}. ` +
80
+ "Only values safe to publish in the browser bundle belong here.",
81
+ );
82
+ }
83
+
51
84
  /** @type {Record<string, string>} */
52
85
  const env = { NODE_ENV: process.env.NODE_ENV ?? "production" };
53
86
  for (const key of keys) {
@@ -61,13 +94,16 @@ function publicEnv(keys) {
61
94
  * @param {string} base
62
95
  * @returns {string}
63
96
  */
64
- function resolveWithExtension(base) {
97
+ export function resolveWithExtension(base) {
65
98
  const candidates = [
66
99
  base,
67
100
  `${base}.js`,
68
101
  `${base}.mjs`,
102
+ `${base}.ts`,
103
+ `${base}.mts`,
69
104
  `${base}.json`,
70
105
  path.join(base, "index.js"),
106
+ path.join(base, "index.ts"),
71
107
  ];
72
108
  for (const candidate of candidates) {
73
109
  if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
@@ -75,6 +111,53 @@ function resolveWithExtension(base) {
75
111
  return base;
76
112
  }
77
113
 
114
+ /**
115
+ * Entry dosya adını manifest anahtarına çevirir (`main.ts` → `main.js`).
116
+ * Layout / `entries: ["chart.js"]` sözleşmesi hash'siz `.js` anahtarları bekler.
117
+ *
118
+ * @param {string} entryPath
119
+ * @returns {string}
120
+ */
121
+ export function entryManifestName(entryPath) {
122
+ const base = path.basename(entryPath);
123
+ const ext = path.extname(base);
124
+ const stem = CLIENT_SOURCE_EXTS.includes(ext)
125
+ ? base.slice(0, -ext.length)
126
+ : path.basename(base, ".js");
127
+ return `${stem}.js`;
128
+ }
129
+
130
+ /**
131
+ * `client/entries/` altındaki kaynakları listeler. Aynı stem için birden fazla
132
+ * uzantı (`main.js` + `main.ts`) sessiz tercih yerine hata verir.
133
+ *
134
+ * @param {string} entryDir
135
+ * @returns {string[]}
136
+ */
137
+ export function listClientEntries(entryDir) {
138
+ /** @type {Map<string, string>} */
139
+ const byStem = new Map();
140
+
141
+ for (const file of fs.readdirSync(entryDir)) {
142
+ const ext = path.extname(file);
143
+ if (!CLIENT_SOURCE_EXTS.includes(ext)) continue;
144
+
145
+ const stem = file.slice(0, -ext.length);
146
+ const previous = byStem.get(stem);
147
+ if (previous) {
148
+ throw new Error(
149
+ `[build] conflicting client entries for "${stem}": ${previous} and ${file}. ` +
150
+ "Keep a single extension per entry name.",
151
+ );
152
+ }
153
+ byStem.set(stem, file);
154
+ }
155
+
156
+ return [...byStem.values()]
157
+ .sort()
158
+ .map((file) => path.join(entryDir, file));
159
+ }
160
+
78
161
  /**
79
162
  * @param {import('../../config/index.js').ResolvedConfig} config
80
163
  * @param {{ watch?: boolean }} [options]
@@ -89,10 +172,7 @@ export async function buildClient(config, { watch = false } = {}) {
89
172
  return {};
90
173
  }
91
174
 
92
- const entryPoints = fs
93
- .readdirSync(entryDir)
94
- .filter((file) => file.endsWith(".js"))
95
- .map((file) => path.join(entryDir, file));
175
+ const entryPoints = listClientEntries(entryDir);
96
176
 
97
177
  if (!entryPoints.length) {
98
178
  log.detail("no entries, skipped");
@@ -117,7 +197,8 @@ export async function buildClient(config, { watch = false } = {}) {
117
197
  format: "esm",
118
198
  target: ["chrome111", "edge111", "firefox111", "safari16.4"],
119
199
  minify: true,
120
- sourcemap: true,
200
+ // Prod'da map dosyaları `public/assets` altında herkese açık kalırdı.
201
+ sourcemap: process.env.NODE_ENV === "development",
121
202
  metafile: true,
122
203
  entryNames: "[name].[hash]",
123
204
  chunkNames: "chunks/[name].[hash]",
@@ -256,7 +337,7 @@ function toManifest(metafile, config, entryRoot) {
256
337
  for (const [outputPath, output] of Object.entries(metafile.outputs)) {
257
338
  if (!output.entryPoint?.startsWith(entryRoot)) continue;
258
339
 
259
- const name = `${path.basename(output.entryPoint, ".js")}.js`;
340
+ const name = entryManifestName(output.entryPoint);
260
341
  const absolute = path.resolve(config.root, outputPath);
261
342
  manifest[name] = `/${path
262
343
  .relative(config.dirs.public, absolute)
@@ -24,12 +24,15 @@ import { createRequire } from "node:module";
24
24
  import { pruneAssets, writeAsset } from "../paths.mjs";
25
25
  import * as log from "../../log.mjs";
26
26
 
27
- const SCAN_EXTENSIONS = new Set([".ejs", ".jsk", ".js", ".mjs"]);
27
+ const SCAN_EXTENSIONS = new Set([".ejs", ".jsk", ".js", ".mjs", ".ts", ".mts"]);
28
28
 
29
29
  /** `icon({ … })` çağrısının tamamı; `name:` ifadesi ayrıca çözümlenir. */
30
30
  const ICON_CALL = /icon\(\s*\{([^}]*)\}/g;
31
31
  const ICON_NAME_EXPR = /name:\s*([^,}]+)/;
32
32
  const ICON_WEIGHT = /weight:\s*["']([^"']+)["']/;
33
+ /** `.jsk` / JSX tarzı `<Icon name="Moon" />` ve isteğe bağlı `weight="bold"`. */
34
+ const ICON_TAG =
35
+ /<Icon\b[^>]*\bname=["']([A-Z][A-Za-z0-9]*)["'][^>]*(?:\bweight=["']([a-z]+)["'])?/gi;
33
36
  /** `data-icon="flag:fill"` ve JS nesnesindeki `"data-icon": "flag:fill"`. */
34
37
  const ICON_ATTR = /data-icon"?\s*[:=]\s*["']([a-z0-9-]+)(?::([a-z]+))?["']/g;
35
38
 
@@ -191,6 +194,13 @@ function scanUsedIcons(scanDirs) {
191
194
  for (const name of names) used.add(`${toKebab(name)}:${weight}`);
192
195
  }
193
196
 
197
+ for (const match of source.matchAll(ICON_TAG)) {
198
+ const weight = match[2] ?? "regular";
199
+ used.add(
200
+ `${toKebab(match[1])}:${WEIGHTS.has(weight) ? weight : "regular"}`,
201
+ );
202
+ }
203
+
194
204
  for (const match of source.matchAll(ICON_NAME_PROP)) {
195
205
  indirectNames.add(toKebab(match[1]));
196
206
  }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * JSkelet island runtime'ının tarayıcı tarafı API'si.
3
3
  *
4
- * `client/entries/main.js` içinde:
4
+ * `client/entries/main.js` (veya `main.ts`) içinde:
5
5
  *
6
6
  * import { registerAll, start } from "jskelet/client";
7
7
  *
8
8
  * registerAll({
9
- * counter: () => import("../islands/counter.js"),
9
+ * counter: () => import("../islands/counter.ts"),
10
10
  * });
11
11
  *
12
12
  * start();
@@ -21,6 +21,7 @@ export function codegen(ast, options) {
21
21
  return {
22
22
  code: ctx.finish(),
23
23
  includes: [...ctx.includes],
24
+ includeEntries: ctx.includeEntries,
24
25
  components: [...ctx.components],
25
26
  };
26
27
  }
@@ -58,6 +59,8 @@ class CodegenContext {
58
59
  this.knownComponents = options.knownComponents ?? null;
59
60
  /** @type {Set<string>} */
60
61
  this.includes = new Set();
62
+ /** @type {{ id: string, index: number }[]} */
63
+ this.includeEntries = [];
61
64
  /** @type {Set<string>} */
62
65
  this.components = new Set();
63
66
  /** @type {string[]} */
@@ -249,6 +252,7 @@ class CodegenContext {
249
252
  emitInclude(node) {
250
253
  const id = normalizeIncludeId(node.path);
251
254
  this.includes.add(id);
255
+ this.includeEntries.push({ id, index: node.index });
252
256
  const alias = includeAlias(id);
253
257
  this.append(`(${alias}(data, helpers) ?? "")`);
254
258
  }
@@ -19,7 +19,7 @@ import {
19
19
  * @param {{ quiet?: boolean }} [options]
20
20
  * @returns {Promise<{ count: number, manifest: Record<string, string>, outDir: string }>}
21
21
  */
22
- export async function compileAll(config, options = {}) {
22
+ export async function compileAll(config, _options = {}) {
23
23
  const outDir = path.join(config.dirs.generated, "templates");
24
24
  const viewRoots = getViewRoots(config);
25
25
  const jskFiles = discoverJskFiles(viewRoots);
@@ -32,7 +32,7 @@ export async function compileAll(config, options = {}) {
32
32
 
33
33
  /** @type {Record<string, string>} */
34
34
  const manifest = {};
35
- /** @type {Map<string, { code: string, includes: string[] }>} */
35
+ /** @type {Map<string, { code: string, includes: string[], includeEntries: { id: string, index: number }[], source: string, relative: string }>} */
36
36
  const compiled = new Map();
37
37
 
38
38
  for (const [viewId, file] of jskFiles) {
@@ -47,34 +47,25 @@ export async function compileAll(config, options = {}) {
47
47
  throw error;
48
48
  }
49
49
 
50
- const { code, includes, components: usedComponents } = codegen(ast, {
50
+ const { code, includes, includeEntries } = codegen(ast, {
51
51
  viewId,
52
52
  file: relative,
53
- knownComponents: null,
53
+ knownComponents,
54
54
  });
55
55
 
56
- for (const name of usedComponents) {
57
- if (knownComponents.has(name)) continue;
58
- const camel = name.charAt(0).toLowerCase() + name.slice(1);
59
- if (knownComponents.has(camel)) continue;
60
- if (!options.quiet) {
61
- console.warn(`[jsk] ${relative}: component "${name}" was not found at compile time`);
62
- }
63
- }
64
-
65
- compiled.set(viewId, { code, includes });
56
+ compiled.set(viewId, { code, includes, includeEntries, source, relative });
66
57
  }
67
58
 
68
59
  // Include hedefleri mevcut mu?
69
- for (const [viewId, { includes }] of compiled) {
70
- for (const id of includes) {
60
+ for (const [, { includeEntries, source, relative }] of compiled) {
61
+ for (const { id, index } of includeEntries) {
71
62
  const normalized = normalizeIncludeId(id);
72
63
  if (!compiled.has(normalized) && !jskFiles.has(normalized)) {
73
- const file = jskFiles.get(viewId);
74
- const relative = file
75
- ? path.relative(config.root, file).split(path.sep).join("/")
76
- : viewId;
77
- throw new CompileError(`Unknown include "${id}"`, { file: relative });
64
+ throw new CompileError(`Unknown include "${id}"`, {
65
+ file: relative,
66
+ source,
67
+ index,
68
+ });
78
69
  }
79
70
  }
80
71
  }
@@ -278,6 +278,11 @@ class ExprParser {
278
278
  let node = this.parsePrimary();
279
279
  for (;;) {
280
280
  this.skipWs();
281
+ if (this.peek() === "(") {
282
+ throw this.error(
283
+ "Function calls are not allowed in .jsk; move logic to a JS component or controller",
284
+ );
285
+ }
281
286
  if (this.match("?.")) {
282
287
  this.skipWs();
283
288
  if (this.peek() === "[") {
@@ -64,6 +64,8 @@ class TemplateParser {
64
64
  this.source = source;
65
65
  this.file = file;
66
66
  this.i = 0;
67
+ /** @type {string[]} Açık {#if}/{#each} kapanışları — EOF'ta öncelikli hata. */
68
+ this.blockStack = [];
67
69
  }
68
70
 
69
71
  /**
@@ -121,6 +123,13 @@ class TemplateParser {
121
123
  break;
122
124
  }
123
125
 
126
+ // `<!DOCTYPE …>` / `<![CDATA[…]]>` — etiket değil; metin olarak bas.
127
+ // İlerletmezsek `<` + `!` dalı parseText'te de takılıp sonsuz döngü olur.
128
+ if (this.startsWith("<!")) {
129
+ nodes.push(this.parseDeclaration());
130
+ continue;
131
+ }
132
+
124
133
  if (this.peek() === "<" && /[A-Za-z]/.test(this.peek(1))) {
125
134
  nodes.push(this.parseTag());
126
135
  continue;
@@ -131,6 +140,35 @@ class TemplateParser {
131
140
  return nodes;
132
141
  }
133
142
 
143
+ /**
144
+ * `<!DOCTYPE html>` gibi bildirimler. Kapanış `>` ilk tırnak dışı geçiş.
145
+ * @returns {TextNode}
146
+ */
147
+ parseDeclaration() {
148
+ const start = this.i;
149
+ this.i += 2; // <!
150
+ let quote = null;
151
+ while (!this.eof()) {
152
+ const ch = this.peek();
153
+ if (quote) {
154
+ if (ch === quote) quote = null;
155
+ this.i += 1;
156
+ continue;
157
+ }
158
+ if (ch === '"' || ch === "'") {
159
+ quote = ch;
160
+ this.i += 1;
161
+ continue;
162
+ }
163
+ if (ch === ">") {
164
+ this.i += 1;
165
+ break;
166
+ }
167
+ this.i += 1;
168
+ }
169
+ return { type: "Text", value: this.source.slice(start, this.i) };
170
+ }
171
+
134
172
  skipComment() {
135
173
  const end = this.source.indexOf("-->", this.i + 4);
136
174
  if (end === -1) throw this.error("Unterminated HTML comment");
@@ -217,13 +255,19 @@ class TemplateParser {
217
255
  });
218
256
  this.i = closeBrace + 1;
219
257
 
220
- const consequent = this.parseNodesUntil(["{#else}", "{/if}"]);
258
+ this.blockStack.push("{/if}");
259
+ /** @type {AstNode[]} */
260
+ let consequent;
221
261
  /** @type {AstNode[]} */
222
262
  let alternate = [];
223
-
224
- if (this.startsWith("{#else}")) {
225
- this.i += "{#else}".length;
226
- alternate = this.parseNodesUntil(["{/if}"]);
263
+ try {
264
+ consequent = this.parseNodesUntil(["{#else}", "{/if}"]);
265
+ if (this.startsWith("{#else}")) {
266
+ this.i += "{#else}".length;
267
+ alternate = this.parseNodesUntil(["{/if}"]);
268
+ }
269
+ } finally {
270
+ this.blockStack.pop();
227
271
  }
228
272
 
229
273
  if (!this.startsWith("{/if}")) {
@@ -263,6 +307,10 @@ class TemplateParser {
263
307
  continue;
264
308
  }
265
309
  if (this.startsWith("</")) break;
310
+ if (this.startsWith("<!")) {
311
+ nodes.push(this.parseDeclaration());
312
+ continue;
313
+ }
266
314
  if (this.peek() === "<" && /[A-Za-z]/.test(this.peek(1))) {
267
315
  nodes.push(this.parseTag());
268
316
  continue;
@@ -308,7 +356,14 @@ class TemplateParser {
308
356
  templateIndex: this.i,
309
357
  });
310
358
  this.i = closeBrace + 1;
311
- const children = this.parseNodesUntil(["{/each}"]);
359
+ this.blockStack.push("{/each}");
360
+ /** @type {AstNode[]} */
361
+ let children;
362
+ try {
363
+ children = this.parseNodesUntil(["{/each}"]);
364
+ } finally {
365
+ this.blockStack.pop();
366
+ }
312
367
  if (!this.startsWith("{/each}")) {
313
368
  throw this.error("Expected {/each}", this.i);
314
369
  }
@@ -369,9 +424,10 @@ class TemplateParser {
369
424
  let children = [];
370
425
  if (!selfClosing && !voidEl) {
371
426
  children = this.parseNodesUntil([`</${name}>`]);
372
- // Also allow parseNodes style — check closing tag
373
427
  if (!this.startsWith(`</${name}>`)) {
374
- // try case-sensitive exact
428
+ if (this.eof() && this.blockStack.length) {
429
+ throw this.error(`Expected ${this.blockStack.at(-1)}`, this.i);
430
+ }
375
431
  throw this.error(`Expected </${name}>`, this.i);
376
432
  }
377
433
  this.i += `</${name}>`.length;
@@ -131,6 +131,9 @@ export function collectKnownComponents(componentDirs, jskFiles) {
131
131
  "Icon",
132
132
  "CsrfField",
133
133
  "PreloadImage",
134
+ "Stylesheets",
135
+ "BodyScripts",
136
+ "JsonLd",
134
137
  ]);
135
138
 
136
139
  /** @type {Map<string, string>} export adı → göreli yol */
@@ -467,9 +467,19 @@ export const DEFAULT_BRAND = {
467
467
  */
468
468
  export const DEFAULT_AUTH = {
469
469
  /**
470
- * `true` veya `{ ttlSeconds?, path?, maxValueBytes? }`.
470
+ * `true` veya `{ allowedCookieNames, ttlSeconds?, path?, maxValueBytes?,
471
+ * maxPendingTickets?, maxMintsPerIpPerMinute? }`.
471
472
  * Açıkken `POST /_jskelet/auth/handoff` ve `?handoff=` redeem middleware'i.
472
- * @type {boolean | { enabled?: boolean, ttlSeconds?: number, path?: string, maxValueBytes?: number }}
473
+ * Mint için `allowedCookieNames` dolu olmalı (aksi halde 400).
474
+ * @type {boolean | {
475
+ * enabled?: boolean,
476
+ * allowedCookieNames?: string[],
477
+ * ttlSeconds?: number,
478
+ * path?: string,
479
+ * maxValueBytes?: number,
480
+ * maxPendingTickets?: number,
481
+ * maxMintsPerIpPerMinute?: number,
482
+ * }}
473
483
  */
474
484
  crossSubdomainHandoff: false,
475
485
  };
@@ -1138,7 +1138,7 @@ function resolveLayout(dirs, override) {
1138
1138
  const appLayout = path.join(dirs.views, "layout.ejs");
1139
1139
  if (fs.existsSync(appLayout)) return appLayout;
1140
1140
 
1141
- return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.ejs");
1141
+ return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
1142
1142
  }
1143
1143
 
1144
1144
  /**
@@ -138,6 +138,18 @@ function escapeRegExp(value) {
138
138
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
139
139
  }
140
140
 
141
+ /**
142
+ * Bekleyen hata kutusunu hemen bas. Alt süreç exit/close olduğunda 60 ms
143
+ * zamanlayıcıya güvenilmez: parent `shutdown` ile ölürse satır hiç görünmez.
144
+ */
145
+ function flushErrorNow() {
146
+ if (errorTimer) {
147
+ clearTimeout(errorTimer);
148
+ errorTimer = null;
149
+ }
150
+ flushError();
151
+ }
152
+
141
153
  /**
142
154
  * @param {string} line
143
155
  * @param {boolean} isError
@@ -174,8 +186,9 @@ function onServerLine(line, isError) {
174
186
 
175
187
  if (!line.trim()) return;
176
188
 
177
- // Hata gövdesi ve yığın satırları kutuya toplanır.
178
- if (isError || /^\s+at\s/.test(line) || /Error\b/.test(line)) {
189
+ // Yığın / Error: kutuya. Düz stderr (port dolu vb.) anında — süreç hemen
190
+ // exit 1 ile öldüğü için gecikmeli flush'a kalırsa satır kayboluyor.
191
+ if (/^\s+at\s/.test(line) || /Error\b/.test(line)) {
179
192
  errorBuffer.push(
180
193
  line.replace(/^\[(?:uncaughtException|unhandledRejection)\]\s*/, ""),
181
194
  );
@@ -183,6 +196,11 @@ function onServerLine(line, isError) {
183
196
  return;
184
197
  }
185
198
 
199
+ if (isError) {
200
+ log.error(line.trim());
201
+ return;
202
+ }
203
+
186
204
  log.event({ scope: "server", message: line });
187
205
  }
188
206
 
@@ -205,9 +223,12 @@ function run(args, label, passthrough) {
205
223
  passthrough ? onBuildLine(line) : onServerLine(line, true),
206
224
  );
207
225
 
208
- child.on("exit", (code) => {
226
+ // `close`: stdio boşaldıktan sonra. `exit` ile yarışınca port hatası
227
+ // stderr'de kalıp "server exited" dışında bir şey görünmüyordu.
228
+ child.on("close", (code) => {
209
229
  if (code === 0 || code === null) return;
210
230
  if (child[STOPPING]) return; // yeniden başlatma: beklenen çıkış
231
+ if (!passthrough) flushErrorNow();
211
232
  log.error(`${label} exited (code ${code})`);
212
233
  shutdown(code);
213
234
  });
@@ -249,6 +270,7 @@ let server = null;
249
270
 
250
271
  // `--import` modül belirteci bekler: Windows'ta `H:\…` yolu `h:` şemalı URL
251
272
  // sanılıp reddediliyor, bu yüzden file:// URL'e çevrilir.
273
+ // `--murder` sunucu sürecine iletilir: dolu portta dinleyen öldürülüp başlanır.
252
274
  const SERVER_ARGS = [
253
275
  // `.env` yoksa bayrak hiç geçilmez: `--env-file-if-exists` dosya yokken bir
254
276
  // bildirim satırı basıyor ve bu satır hata kutusuna dönüşüyordu.
@@ -256,6 +278,7 @@ const SERVER_ARGS = [
256
278
  "--import",
257
279
  pathToFileURL(path.join(SRC, "runtime", "register.mjs")).href,
258
280
  path.join(SRC, "start.mjs"),
281
+ ...(process.argv.includes("--murder") ? ["--murder"] : []),
259
282
  ];
260
283
 
261
284
  function startServerProcess() {
@@ -4,6 +4,7 @@
4
4
  export {
5
5
  clearCookie,
6
6
  getSignedCookie,
7
+ isValidCookieName,
7
8
  parseCookies,
8
9
  randomToken,
9
10
  safeEqual,
@@ -148,6 +148,17 @@ export function parseCookies(req) {
148
148
  * @property {"Strict" | "Lax" | "None"} [sameSite] Varsayılan `Lax`.
149
149
  */
150
150
 
151
+ /**
152
+ * RFC 6265 cookie-name (token). `;`, boşluk, CRLF gibi karakterler
153
+ * Set-Cookie enjeksiyonuna yol açardı — reddedilir.
154
+ *
155
+ * @param {unknown} name
156
+ * @returns {boolean}
157
+ */
158
+ export function isValidCookieName(name) {
159
+ return typeof name === "string" && /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
160
+ }
161
+
151
162
  /**
152
163
  * Varsayılanlar bilinçli olarak kısıtlayıcı: `HttpOnly` ile JS okuyamaz,
153
164
  * `SameSite=Lax` ile çapraz site POST'larında gönderilmez (CSRF'nin büyük
@@ -159,6 +170,13 @@ export function parseCookies(req) {
159
170
  * @returns {string}
160
171
  */
161
172
  export function serializeCookie(name, value, options = {}) {
173
+ if (!isValidCookieName(name)) {
174
+ throw new Error(
175
+ `[cookies] invalid cookie name ${JSON.stringify(name)}; ` +
176
+ "names must be RFC 6265 tokens (no spaces, semicolons, or control chars)",
177
+ );
178
+ }
179
+
162
180
  const parts = [`${name}=${encodeURIComponent(value)}`];
163
181
 
164
182
  parts.push(`Path=${options.path ?? "/"}`);
package/src/logo.png CHANGED
Binary file