jskelet 0.5.4 → 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 +387 -385
- 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 +53 -25
- package/docs/08-build.md +40 -18
- 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 +55 -27
- package/docs/en/08-build.md +43 -21
- 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 +152 -18
- 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 +31 -3
- 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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A framework that feels like no framework: Express 5 + build-time .jsk (
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "A framework that feels like no framework: Express 5 + build-time .jsk SSR (optional EJS peer), vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Ayberk Enis",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"ssr",
|
|
18
18
|
"islands",
|
|
19
19
|
"express",
|
|
20
|
-
"
|
|
20
|
+
"jsk",
|
|
21
21
|
"esbuild",
|
|
22
22
|
"tailwind",
|
|
23
23
|
"seo",
|
|
@@ -28,19 +28,42 @@
|
|
|
28
28
|
},
|
|
29
29
|
"main": "./src/index.js",
|
|
30
30
|
"exports": {
|
|
31
|
-
".":
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"./
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./types/index.d.ts",
|
|
33
|
+
"default": "./src/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./server": {
|
|
36
|
+
"types": "./types/index.d.ts",
|
|
37
|
+
"default": "./src/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./client": {
|
|
40
|
+
"types": "./types/client/index.d.ts",
|
|
41
|
+
"default": "./src/client/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./html": {
|
|
44
|
+
"types": "./types/views/helpers/html.d.ts",
|
|
45
|
+
"default": "./src/views/helpers/html.js"
|
|
46
|
+
},
|
|
47
|
+
"./tags": {
|
|
48
|
+
"types": "./types/views/helpers/tags.d.ts",
|
|
49
|
+
"default": "./src/views/helpers/tags.js"
|
|
50
|
+
},
|
|
51
|
+
"./cookies": {
|
|
52
|
+
"types": "./types/http/cookies-entry.d.ts",
|
|
53
|
+
"default": "./src/http/cookies-entry.js"
|
|
54
|
+
},
|
|
55
|
+
"./log": {
|
|
56
|
+
"types": "./types/log.d.mts",
|
|
57
|
+
"default": "./src/log.mjs"
|
|
58
|
+
},
|
|
38
59
|
"./register": "./src/runtime/register.mjs",
|
|
39
|
-
"./layout": "./src/templates/layout.
|
|
60
|
+
"./layout": "./src/templates/layout.jsk",
|
|
61
|
+
"./layout/ejs": "./src/templates/layout.ejs"
|
|
40
62
|
},
|
|
41
63
|
"files": [
|
|
42
64
|
"bin",
|
|
43
65
|
"src",
|
|
66
|
+
"types",
|
|
44
67
|
"docs",
|
|
45
68
|
"README.md",
|
|
46
69
|
"AGENTS.md",
|
|
@@ -53,12 +76,16 @@
|
|
|
53
76
|
"scripts": {
|
|
54
77
|
"lint": "eslint",
|
|
55
78
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
79
|
+
"types": "tsc -p tsconfig.types.json",
|
|
80
|
+
"prepublishOnly": "npm run types",
|
|
81
|
+
"test:framework-layout": "node scripts/compile-framework-layout.mjs --check",
|
|
56
82
|
"example:minimal": "npm --prefix examples/minimal run dev",
|
|
57
83
|
"example:blog": "npm --prefix examples/blog run dev",
|
|
58
84
|
"example:dashboard": "npm --prefix examples/dashboard run dev"
|
|
59
85
|
},
|
|
60
86
|
"dependencies": {
|
|
61
|
-
"
|
|
87
|
+
"@babel/parser": "^8.0.6",
|
|
88
|
+
"@babel/types": "^8.0.6",
|
|
62
89
|
"esbuild": "^0.28.2",
|
|
63
90
|
"express": "^5.2.1",
|
|
64
91
|
"tailwind-merge": "^3.5.0"
|
|
@@ -66,6 +93,7 @@
|
|
|
66
93
|
"peerDependencies": {
|
|
67
94
|
"@phosphor-icons/core": "^2.1.1",
|
|
68
95
|
"@tailwindcss/postcss": "^4.3.3",
|
|
96
|
+
"ejs": "^6.0.1",
|
|
69
97
|
"ioredis": "^5.4.0 || ^6.0.0",
|
|
70
98
|
"lightningcss": "^1.32.0",
|
|
71
99
|
"postcss": "^8.5.26",
|
|
@@ -79,6 +107,9 @@
|
|
|
79
107
|
"@tailwindcss/postcss": {
|
|
80
108
|
"optional": true
|
|
81
109
|
},
|
|
110
|
+
"ejs": {
|
|
111
|
+
"optional": true
|
|
112
|
+
},
|
|
82
113
|
"ioredis": {
|
|
83
114
|
"optional": true
|
|
84
115
|
},
|
|
@@ -97,7 +128,11 @@
|
|
|
97
128
|
},
|
|
98
129
|
"devDependencies": {
|
|
99
130
|
"@eslint/js": "^9",
|
|
131
|
+
"@types/express": "^5.0.6",
|
|
132
|
+
"@types/node": "^26.6.1",
|
|
133
|
+
"ejs": "^6.0.1",
|
|
100
134
|
"eslint": "^9",
|
|
101
|
-
"globals": "^16"
|
|
135
|
+
"globals": "^16",
|
|
136
|
+
"typescript": "^7.0.2"
|
|
102
137
|
}
|
|
103
|
-
}
|
|
138
|
+
}
|
|
@@ -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)
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SVG ikon sprite üretimi.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Kaynak XOR seçilir: uygulama kökünde `icons/` (veya `icons.dir`) dizini
|
|
5
|
+
* varsa yalnızca oradaki düz SVG'ler; yoksa `@phosphor-icons/core`. İkisi
|
|
6
|
+
* birleştirilmez — boş bir `icons/` dizini Phosphor'a düşmez.
|
|
7
|
+
*
|
|
8
|
+
* Yalnızca kaynakta kullanılan ikonlar `<symbol>` olur. Tüm seti göndermek
|
|
9
|
+
* 1500+ ikon, yani birkaç megabayt; kullanım taraması sprite'ı tipik olarak
|
|
10
|
+
* 10-30 sembolde tutuyor. Çıktı `public/assets/` altında hash'li
|
|
11
|
+
* `sprite.svg` olduğu için mevcut precompress kapsamına girer.
|
|
8
12
|
*
|
|
9
13
|
* Sembol id'si: `<kebab-name>-<weight>` (örn. `arrow-right-bold`).
|
|
14
|
+
* Yerel dosya adı: `house.svg` → `house:regular`, `house-bold.svg` → `house:bold`.
|
|
10
15
|
*
|
|
11
16
|
* Tarama statik metin üzerinden yapıldığı için adı çalışma anında hesaplanan
|
|
12
17
|
* bir `icon()` çağrısı sprite'a girmez. Bu yüzden ad taşıyan yapılandırma
|
|
@@ -19,12 +24,15 @@ import { createRequire } from "node:module";
|
|
|
19
24
|
import { pruneAssets, writeAsset } from "../paths.mjs";
|
|
20
25
|
import * as log from "../../log.mjs";
|
|
21
26
|
|
|
22
|
-
const SCAN_EXTENSIONS = new Set([".ejs", ".jsk", ".js", ".mjs"]);
|
|
27
|
+
const SCAN_EXTENSIONS = new Set([".ejs", ".jsk", ".js", ".mjs", ".ts", ".mts"]);
|
|
23
28
|
|
|
24
29
|
/** `icon({ … })` çağrısının tamamı; `name:` ifadesi ayrıca çözümlenir. */
|
|
25
30
|
const ICON_CALL = /icon\(\s*\{([^}]*)\}/g;
|
|
26
31
|
const ICON_NAME_EXPR = /name:\s*([^,}]+)/;
|
|
27
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;
|
|
28
36
|
/** `data-icon="flag:fill"` ve JS nesnesindeki `"data-icon": "flag:fill"`. */
|
|
29
37
|
const ICON_ATTR = /data-icon"?\s*[:=]\s*["']([a-z0-9-]+)(?::([a-z]+))?["']/g;
|
|
30
38
|
|
|
@@ -41,6 +49,8 @@ const QUOTED_NAME = /["']([A-Z][A-Za-z0-9]*)["']/g;
|
|
|
41
49
|
|
|
42
50
|
const WEIGHTS = new Set(["thin", "light", "regular", "bold", "fill", "duotone"]);
|
|
43
51
|
|
|
52
|
+
const DEFAULT_VIEWBOX = "0 0 256 256";
|
|
53
|
+
|
|
44
54
|
/**
|
|
45
55
|
* `ArrowRightIcon` → `arrow-right`
|
|
46
56
|
* @param {string} name
|
|
@@ -54,6 +64,50 @@ function toKebab(name) {
|
|
|
54
64
|
.toLowerCase();
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Düz dizin dosya adı → `name` + `weight`.
|
|
69
|
+
* `house.svg` / `house-regular.svg` → regular; `arrow-right-bold.svg` → bold.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} fileName uzantılı veya uzantısız
|
|
72
|
+
* @returns {{ name: string, weight: string } | null}
|
|
73
|
+
*/
|
|
74
|
+
export function parseIconFileName(fileName) {
|
|
75
|
+
const base = String(fileName).replace(/\.svg$/i, "");
|
|
76
|
+
if (!base) return null;
|
|
77
|
+
|
|
78
|
+
let name = base;
|
|
79
|
+
let weight = "regular";
|
|
80
|
+
|
|
81
|
+
const dash = base.lastIndexOf("-");
|
|
82
|
+
if (dash > 0) {
|
|
83
|
+
const maybeWeight = base.slice(dash + 1);
|
|
84
|
+
if (WEIGHTS.has(maybeWeight)) {
|
|
85
|
+
name = base.slice(0, dash);
|
|
86
|
+
weight = maybeWeight;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// kebab: harf/rakam segmentleri, başta/sonda tire yok
|
|
91
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) return null;
|
|
92
|
+
|
|
93
|
+
return { name, weight };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
98
|
+
* @returns {string | null} mutlak dizin; yoksa veya dizin değilse null
|
|
99
|
+
*/
|
|
100
|
+
function resolveLocalIconsDir(config) {
|
|
101
|
+
const relative = config.icons?.dir ?? "icons";
|
|
102
|
+
const dir = path.resolve(config.root, relative);
|
|
103
|
+
try {
|
|
104
|
+
if (fs.statSync(dir).isDirectory()) return dir;
|
|
105
|
+
} catch {
|
|
106
|
+
// yok
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
57
111
|
/**
|
|
58
112
|
* Paket, framework'ün değil **uygulamanın** node_modules'ünden çözülür:
|
|
59
113
|
* ikon seti uygulamanın devDependency'si.
|
|
@@ -140,6 +194,13 @@ function scanUsedIcons(scanDirs) {
|
|
|
140
194
|
for (const name of names) used.add(`${toKebab(name)}:${weight}`);
|
|
141
195
|
}
|
|
142
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
|
+
|
|
143
204
|
for (const match of source.matchAll(ICON_NAME_PROP)) {
|
|
144
205
|
indirectNames.add(toKebab(match[1]));
|
|
145
206
|
}
|
|
@@ -160,21 +221,80 @@ function scanUsedIcons(scanDirs) {
|
|
|
160
221
|
return used;
|
|
161
222
|
}
|
|
162
223
|
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} svg
|
|
226
|
+
* @returns {{ body: string, viewBox: string }}
|
|
227
|
+
*/
|
|
228
|
+
function parseSvgParts(svg) {
|
|
229
|
+
const open = svg.match(/<svg\b[^>]*>/i)?.[0] ?? "";
|
|
230
|
+
const viewBox =
|
|
231
|
+
open.match(/\bviewBox\s*=\s*["']([^"']+)["']/i)?.[1]?.trim() ||
|
|
232
|
+
DEFAULT_VIEWBOX;
|
|
233
|
+
const body = svg
|
|
234
|
+
.replace(/^[\s\S]*?<svg[^>]*>/i, "")
|
|
235
|
+
.replace(/<\/svg>\s*$/i, "")
|
|
236
|
+
.trim();
|
|
237
|
+
return { body, viewBox };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Düz `icons/` altındaki `*.svg` dosyalarından `name:weight` → parça haritası.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} dir
|
|
244
|
+
* @returns {Map<string, { body: string, viewBox: string }>}
|
|
245
|
+
*/
|
|
246
|
+
function indexLocalIcons(dir) {
|
|
247
|
+
/** @type {Map<string, { body: string, viewBox: string }>} */
|
|
248
|
+
const index = new Map();
|
|
249
|
+
|
|
250
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
251
|
+
if (!entry.isFile() || path.extname(entry.name).toLowerCase() !== ".svg") {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const parsed = parseIconFileName(entry.name);
|
|
256
|
+
if (!parsed) {
|
|
257
|
+
log.warn(`icon file ignored (bad name) → ${entry.name}`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const key = `${parsed.name}:${parsed.weight}`;
|
|
262
|
+
const svg = fs.readFileSync(path.join(dir, entry.name), "utf8");
|
|
263
|
+
index.set(key, parseSvgParts(svg));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return index;
|
|
267
|
+
}
|
|
268
|
+
|
|
163
269
|
/**
|
|
164
270
|
* @param {string} coreAssets
|
|
165
271
|
* @param {string} name kebab
|
|
166
272
|
* @param {string} weight
|
|
167
|
-
* @returns {string | null}
|
|
273
|
+
* @returns {{ body: string, viewBox: string } | null}
|
|
168
274
|
*/
|
|
169
|
-
function
|
|
275
|
+
function readPhosphorIcon(coreAssets, name, weight) {
|
|
170
276
|
const fileName = weight === "regular" ? `${name}.svg` : `${name}-${weight}.svg`;
|
|
171
277
|
const filePath = path.join(coreAssets, weight, fileName);
|
|
172
278
|
|
|
173
279
|
if (!fs.existsSync(filePath)) return null;
|
|
174
280
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
281
|
+
return parseSvgParts(fs.readFileSync(filePath, "utf8"));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {Map<string, { body: string, viewBox: string }>} index
|
|
286
|
+
* @param {string} name
|
|
287
|
+
* @param {string} weight
|
|
288
|
+
* @returns {{ body: string, viewBox: string } | null}
|
|
289
|
+
*/
|
|
290
|
+
function readLocalIcon(index, name, weight) {
|
|
291
|
+
const exact = index.get(`${name}:${weight}`);
|
|
292
|
+
if (exact) return exact;
|
|
293
|
+
|
|
294
|
+
// `house.svg` regular sayılır; tarama `house:regular` ister.
|
|
295
|
+
if (weight === "regular") return index.get(`${name}:regular`) ?? null;
|
|
296
|
+
|
|
297
|
+
return null;
|
|
178
298
|
}
|
|
179
299
|
|
|
180
300
|
/**
|
|
@@ -182,10 +302,22 @@ function readIconBody(coreAssets, name, weight) {
|
|
|
182
302
|
* @returns {Promise<Record<string, string>>}
|
|
183
303
|
*/
|
|
184
304
|
export async function buildIconSprite(config) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
305
|
+
const localDir = resolveLocalIconsDir(config);
|
|
306
|
+
/** @type {Map<string, { body: string, viewBox: string }> | null} */
|
|
307
|
+
let localIndex = null;
|
|
308
|
+
/** @type {string | null} */
|
|
309
|
+
let coreAssets = null;
|
|
310
|
+
|
|
311
|
+
if (localDir) {
|
|
312
|
+
localIndex = indexLocalIcons(localDir);
|
|
313
|
+
const rel = path.relative(config.root, localDir) || ".";
|
|
314
|
+
log.detail(`icons from ${rel} (${localIndex.size} files)`);
|
|
315
|
+
} else {
|
|
316
|
+
coreAssets = resolveIconAssets(config.root);
|
|
317
|
+
if (!coreAssets) {
|
|
318
|
+
log.detail("@phosphor-icons/core not installed, skipped");
|
|
319
|
+
return {};
|
|
320
|
+
}
|
|
189
321
|
}
|
|
190
322
|
|
|
191
323
|
const scanDirs = (
|
|
@@ -198,15 +330,17 @@ export async function buildIconSprite(config) {
|
|
|
198
330
|
|
|
199
331
|
for (const entry of used) {
|
|
200
332
|
const [name, weight] = entry.split(":");
|
|
201
|
-
const
|
|
333
|
+
const parts = localIndex
|
|
334
|
+
? readLocalIcon(localIndex, name, weight)
|
|
335
|
+
: readPhosphorIcon(/** @type {string} */ (coreAssets), name, weight);
|
|
202
336
|
|
|
203
|
-
if (!body) {
|
|
337
|
+
if (!parts?.body) {
|
|
204
338
|
missing.push(entry);
|
|
205
339
|
continue;
|
|
206
340
|
}
|
|
207
341
|
|
|
208
342
|
symbols.push(
|
|
209
|
-
`<symbol id="${name}-${weight}" viewBox="
|
|
343
|
+
`<symbol id="${name}-${weight}" viewBox="${parts.viewBox}">${parts.body}</symbol>`,
|
|
210
344
|
);
|
|
211
345
|
}
|
|
212
346
|
|
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() === "[") {
|