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.
- package/AGENTS.md +18 -13
- package/CHANGELOG.md +95 -0
- package/README.md +9 -7
- package/bin/jskelet.mjs +24 -10
- package/docs/01-baslangic.md +4 -3
- package/docs/02-mimari.md +4 -3
- package/docs/03-routing.md +11 -6
- package/docs/04-render-ve-sablonlar.md +35 -43
- package/docs/05-islands.md +12 -8
- package/docs/07-yapilandirma.md +40 -18
- package/docs/08-build.md +15 -9
- package/docs/09-dev-araclari.md +5 -1
- package/docs/10-dagitim.md +6 -1
- package/docs/11-tasima.md +51 -17
- package/docs/12-panel-ve-oturum.md +10 -4
- package/docs/README.md +7 -5
- package/docs/en/01-getting-started.md +4 -3
- package/docs/en/02-architecture.md +5 -5
- package/docs/en/03-routing.md +12 -7
- package/docs/en/04-rendering.md +47 -59
- package/docs/en/05-islands.md +13 -8
- package/docs/en/07-configuration.md +40 -20
- package/docs/en/08-build.md +16 -10
- package/docs/en/09-dev-tools.md +6 -1
- package/docs/en/10-deployment.md +6 -1
- package/docs/en/11-migration.md +51 -16
- package/docs/en/12-dashboards-and-sessions.md +9 -4
- package/docs/en/README.md +7 -5
- package/package.json +49 -14
- package/src/build/tasks/client.mjs +91 -10
- package/src/build/tasks/icons.mjs +11 -1
- package/src/client/index.js +2 -2
- package/src/compile/codegen.js +4 -0
- package/src/compile/compile-all.js +12 -21
- package/src/compile/expr.js +5 -0
- package/src/compile/parse.js +64 -8
- package/src/compile/resolve.js +3 -0
- package/src/config/defaults.js +12 -2
- package/src/config/index.js +1 -1
- package/src/dev-server.mjs +26 -3
- package/src/http/cookies-entry.js +1 -0
- package/src/http/cookies.js +18 -0
- package/src/logo.png +0 -0
- package/src/migrate/apply.mjs +262 -0
- package/src/migrate/babel.mjs +79 -0
- package/src/migrate/classify.mjs +155 -0
- package/src/migrate/config.mjs +126 -0
- package/src/migrate/fs-walk.mjs +191 -0
- package/src/migrate/parse.mjs +26 -0
- package/src/migrate/scan.mjs +177 -0
- package/src/migrate/transform/expr-source.mjs +168 -0
- package/src/migrate/transform/island.mjs +67 -0
- package/src/migrate/transform/jsx-to-component.mjs +302 -0
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
- package/src/migrate/transform/page-split.mjs +435 -0
- package/src/migrate/write.mjs +81 -0
- package/src/migrate.mjs +171 -0
- package/src/server/auth/handoff.js +94 -11
- package/src/server/create-app.js +28 -10
- package/src/server/ejs-adapter.js +59 -0
- package/src/server/image-optimizer.js +94 -26
- package/src/server/port-guard.js +255 -0
- package/src/server/render.js +27 -9
- package/src/server/status-page.js +105 -4
- package/src/start.mjs +18 -3
- package/src/templates/layout.ejs +8 -28
- package/src/templates/layout.jsk +30 -0
- package/src/templates/layout.render.js +41 -0
- package/src/views/helpers/tags.js +86 -3
- package/types/build/resolve-peer.d.mts +13 -0
- package/types/client/dom.d.ts +55 -0
- package/types/client/form.d.ts +19 -0
- package/types/client/index.d.ts +20 -0
- package/types/client/registry.d.ts +53 -0
- package/types/client/safe-image.d.ts +19 -0
- package/types/client/shared-cookie.d.ts +82 -0
- package/types/client/store.d.ts +18 -0
- package/types/client/swap.d.ts +46 -0
- package/types/compile/codegen.d.ts +32 -0
- package/types/compile/compile-all.d.ts +42 -0
- package/types/compile/errors.d.ts +30 -0
- package/types/compile/expr.d.ts +67 -0
- package/types/compile/index.d.ts +10 -0
- package/types/compile/parse.d.ts +82 -0
- package/types/compile/resolve.d.ts +46 -0
- package/types/compile/scan-exports.d.ts +9 -0
- package/types/config/defaults.d.ts +449 -0
- package/types/config/index.d.ts +299 -0
- package/types/config/pattern.d.ts +38 -0
- package/types/http/control-flow.d.ts +45 -0
- package/types/http/cookies-entry.d.ts +5 -0
- package/types/http/cookies.d.ts +113 -0
- package/types/http/request-cache.d.ts +13 -0
- package/types/http/request-context.d.ts +67 -0
- package/types/http/shared-cookie.d.ts +73 -0
- package/types/index.d.ts +30 -0
- package/types/log.d.mts +153 -0
- package/types/server/admin/actions.d.ts +16 -0
- package/types/server/admin/auth.d.ts +52 -0
- package/types/server/admin/event-log.d.ts +38 -0
- package/types/server/admin/gate.d.ts +43 -0
- package/types/server/admin/inventory.d.ts +40 -0
- package/types/server/admin/mount.d.ts +6 -0
- package/types/server/admin/router.d.ts +6 -0
- package/types/server/admin/snapshot.d.ts +6 -0
- package/types/server/assets.d.ts +47 -0
- package/types/server/auth/handoff.d.ts +12 -0
- package/types/server/cache-deps.d.ts +16 -0
- package/types/server/cache-vary.d.ts +30 -0
- package/types/server/cloudflare.d.ts +163 -0
- package/types/server/create-app.d.ts +25 -0
- package/types/server/data-cache.d.ts +116 -0
- package/types/server/dev/devtools.d.ts +44 -0
- package/types/server/dev/report.d.ts +229 -0
- package/types/server/dev/socket.d.ts +17 -0
- package/types/server/dev/version-check.d.mts +15 -0
- package/types/server/ejs-adapter.d.ts +11 -0
- package/types/server/head-hints.d.ts +40 -0
- package/types/server/html-cache.d.ts +173 -0
- package/types/server/image-optimizer.d.ts +68 -0
- package/types/server/logs/access-middleware.d.ts +7 -0
- package/types/server/logs/file-sink.d.ts +17 -0
- package/types/server/logs/pipeline.d.ts +37 -0
- package/types/server/logs/s3-put.d.ts +85 -0
- package/types/server/logs/s3-sink.d.ts +26 -0
- package/types/server/metadata.d.ts +38 -0
- package/types/server/middleware/compression.d.ts +17 -0
- package/types/server/middleware/csrf.d.ts +4 -0
- package/types/server/middleware/dev-gate.d.ts +2 -0
- package/types/server/middleware/headers.d.ts +2 -0
- package/types/server/middleware/redirects.d.ts +2 -0
- package/types/server/middleware/static-precompressed.d.ts +5 -0
- package/types/server/middleware/trailing-slash.d.ts +11 -0
- package/types/server/middleware/upstream-proxy.d.ts +21 -0
- package/types/server/og-image.d.ts +149 -0
- package/types/server/port-guard.d.ts +50 -0
- package/types/server/prewarm.d.ts +128 -0
- package/types/server/redis.d.ts +163 -0
- package/types/server/render.d.ts +101 -0
- package/types/server/router.d.ts +5 -0
- package/types/server/status-page.d.ts +24 -0
- package/types/server/upstream-limiter.d.ts +123 -0
- package/types/server/upstream-tracking.d.ts +42 -0
- package/types/shared/cookie-domain.d.ts +29 -0
- package/types/templates/layout.render.d.ts +7 -0
- package/types/version.d.mts +10 -0
- package/types/views/components/loader.d.ts +5 -0
- package/types/views/helpers/html.d.ts +39 -0
- 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`
|
|
5
|
-
* yüklenen ortak island bootstrap'ıdır; ek
|
|
6
|
-
*
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
}
|
package/src/client/index.js
CHANGED
|
@@ -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.
|
|
9
|
+
* counter: () => import("../islands/counter.ts"),
|
|
10
10
|
* });
|
|
11
11
|
*
|
|
12
12
|
* start();
|
package/src/compile/codegen.js
CHANGED
|
@@ -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,
|
|
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,
|
|
50
|
+
const { code, includes, includeEntries } = codegen(ast, {
|
|
51
51
|
viewId,
|
|
52
52
|
file: relative,
|
|
53
|
-
knownComponents
|
|
53
|
+
knownComponents,
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
|
|
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 [
|
|
70
|
-
for (const id of
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
throw new CompileError(`Unknown include "${id}"`, {
|
|
65
|
+
file: relative,
|
|
66
|
+
source,
|
|
67
|
+
index,
|
|
68
|
+
});
|
|
78
69
|
}
|
|
79
70
|
}
|
|
80
71
|
}
|
package/src/compile/expr.js
CHANGED
|
@@ -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() === "[") {
|
package/src/compile/parse.js
CHANGED
|
@@ -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
|
-
|
|
258
|
+
this.blockStack.push("{/if}");
|
|
259
|
+
/** @type {AstNode[]} */
|
|
260
|
+
let consequent;
|
|
221
261
|
/** @type {AstNode[]} */
|
|
222
262
|
let alternate = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/compile/resolve.js
CHANGED
package/src/config/defaults.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
};
|
package/src/config/index.js
CHANGED
|
@@ -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.
|
|
1141
|
+
return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
|
|
1142
1142
|
}
|
|
1143
1143
|
|
|
1144
1144
|
/**
|
package/src/dev-server.mjs
CHANGED
|
@@ -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
|
-
//
|
|
178
|
-
|
|
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
|
-
|
|
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() {
|
package/src/http/cookies.js
CHANGED
|
@@ -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
|