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.
Files changed (149) hide show
  1. package/AGENTS.md +18 -13
  2. package/CHANGELOG.md +387 -385
  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 +53 -25
  11. package/docs/08-build.md +40 -18
  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 +55 -27
  23. package/docs/en/08-build.md +43 -21
  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 +152 -18
  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 +31 -3
  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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.5.4",
4
- "description": "A framework that feels like no framework: Express 5 + build-time .jsk (or EJS) SSR, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
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
- "ejs",
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
- ".": "./src/index.js",
32
- "./server": "./src/index.js",
33
- "./client": "./src/client/index.js",
34
- "./html": "./src/views/helpers/html.js",
35
- "./tags": "./src/views/helpers/tags.js",
36
- "./cookies": "./src/http/cookies-entry.js",
37
- "./log": "./src/log.mjs",
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.ejs"
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
- "ejs": "^6.0.1",
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` 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)
@@ -1,12 +1,17 @@
1
1
  /**
2
- * Phosphor SVG sprite üretimi.
2
+ * SVG ikon sprite üretimi.
3
3
  *
4
- * `@phosphor-icons/core` içindeki tek tek SVG'lerden, **yalnızca kaynakta
5
- * kullanılan** ikonlar için `<symbol>` seti üretir. Tüm seti göndermek 1500+
6
- * ikon, yani birkaç megabayt; kullanım taraması sprite'ı tipik olarak 10-30
7
- * sembolde tutuyor.
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 readIconBody(coreAssets, name, weight) {
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
- const svg = fs.readFileSync(filePath, "utf8");
176
- const inner = svg.replace(/^[\s\S]*?<svg[^>]*>/, "").replace(/<\/svg>\s*$/, "");
177
- return inner.trim();
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 coreAssets = resolveIconAssets(config.root);
186
- if (!coreAssets) {
187
- log.detail("@phosphor-icons/core not installed, skipped");
188
- return {};
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 body = readIconBody(coreAssets, name, weight);
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="0 0 256 256">${body}</symbol>`,
343
+ `<symbol id="${name}-${weight}" viewBox="${parts.viewBox}">${parts.body}</symbol>`,
210
344
  );
211
345
  }
212
346
 
@@ -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() === "[") {