jskelet 0.2.5 → 0.3.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 (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +293 -287
  7. package/docs/03-routing.md +486 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
package/src/init.mjs CHANGED
@@ -1,221 +1,221 @@
1
- /**
2
- * `jskelet init` — bulunduğun dizine çalışan bir minimum iskelet kurar.
3
- *
4
- * Var olan dosyaların üzerine yazmaz: komutu ikinci kez çalıştırmak yalnızca
5
- * eksikleri tamamlar. Amaç, "kurulum yaptım ama hiçbir şey çalışmıyor"
6
- * aşamasını tamamen atlamak — `jskelet dev` hemen ardından çalışır.
7
- */
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import * as log from "./log.mjs";
11
-
12
- /** @type {Record<string, string>} */
13
- const FILES = {
14
- "jskelet.config.mjs": `/**
15
- * JSkelet configuration. Every field is optional; the app still runs on
16
- * defaults if you delete this file.
17
- *
18
- * Details: node_modules/jskelet/docs/en/07-configuration.md
19
- */
20
- export default {
21
- brand: { lang: "en" },
22
-
23
- /** Third-party origins; emitted as preconnect in \`<head>\`. */
24
- preconnect: [],
25
-
26
- async cache() {
27
- return {
28
- /** How long a page's HTML stays in the cache (seconds). */
29
- html: { "/": 60 },
30
- };
31
- },
32
-
33
- hooks: {
34
- /** Metadata defaults for every page. */
35
- metadata() {
36
- return {
37
- titleTemplate: "%s | JSkelet",
38
- description: "A site built with JSkelet.",
39
- };
40
- },
41
-
42
- /** Locals added to the layout on every render. */
43
- layoutContext() {
44
- return { bodyClass: "min-h-full" };
45
- },
46
-
47
- /** 404 page. */
48
- notFound() {
49
- return {
50
- view: "pages/not-found",
51
- metadata: { title: "Page not found", robots: { index: false } },
52
- };
53
- },
54
- },
55
- };
56
- `,
57
-
58
- "routes/10-pages.mjs": `/**
59
- * Route module. The default export receives \`(app, api)\`; \`api.route()\` wraps
60
- * the controller with the HTML cache, the notFound/redirect flow and
61
- * compression.
62
- *
63
- * The numeric prefix in the file name sets load order: catch-all routes
64
- * (like "/:slug") belong to a higher number.
65
- */
66
- export default function register(app, { route }) {
67
- app.get(
68
- "/",
69
- route(
70
- async () => ({
71
- view: "pages/home",
72
- metadata: { title: "Home" },
73
- data: { message: "JSkelet is running." },
74
- }),
75
- { revalidate: 60 },
76
- ),
77
- );
78
- }
79
- `,
80
-
81
- "views/pages/home.ejs": `<section class="wrapper">
82
- <h1><%= metadata.title %></h1>
83
- <p><%= message %></p>
84
- <div data-island="counter" data-island-props='{"start":0}'></div>
85
- </section>
86
- `,
87
-
88
- "views/pages/not-found.ejs": `<section class="wrapper">
89
- <h1>404</h1>
90
- <p>The page you are looking for was not found.</p>
91
- <p><a href="/">Back to home</a></p>
92
- </section>
93
- `,
94
-
95
- "views/components/button.js": `import { attrs, esc } from "jskelet/html";
96
-
97
- /**
98
- * Every named export under \`views/components/**\` is usable directly in
99
- * templates: \`<%- button({ text: "Save" }) %>\`. No import needed.
100
- *
101
- * @param {{ text: string, href?: string, class?: string }} props
102
- * @returns {string}
103
- */
104
- export function button({ text, href, class: className }) {
105
- const tag = href ? "a" : "button";
106
- return \`<\${tag}\${attrs({ href, class: className })}>\${esc(text)}</\${tag}>\`;
107
- }
108
- `,
109
-
110
- "client/entries/main.js": `import { registerAll, start } from "jskelet/client";
111
-
112
- /**
113
- * Island registry. Values are dynamic imports: a module is downloaded only if
114
- * that island is actually on the page and becomes visible.
115
- */
116
- registerAll({
117
- counter: () => import("../islands/counter.js"),
118
- });
119
-
120
- start();
121
- `,
122
-
123
- "client/islands/counter.js": `/**
124
- * Island contract: a named export called \`mount(element, props)\`.
125
- * The returned function, if any, is reserved for cleanup.
126
- *
127
- * @param {HTMLElement} element
128
- * @param {{ start?: number }} props
129
- */
130
- export function mount(element, props) {
131
- let value = props.start ?? 0;
132
-
133
- const button = document.createElement("button");
134
- button.type = "button";
135
-
136
- const paint = () => {
137
- button.textContent = \`Clicks: \${value}\`;
138
- };
139
-
140
- button.addEventListener("click", () => {
141
- value += 1;
142
- paint();
143
- });
144
-
145
- paint();
146
- element.append(button);
147
- }
148
- `,
149
-
150
- "styles/globals.css": `@import "tailwindcss" source(none);
151
-
152
- /**
153
- * Tailwind's class scanning depends on these directives. Automatic detection
154
- * only scans the directory holding this file; variants used in templates
155
- * (like data-[active=false]:…) would otherwise be dropped silently.
156
- */
157
- @source "../views";
158
- @source "../client";
159
- @source "../routes";
160
-
161
- body {
162
- margin: 0;
163
- font-family: system-ui, sans-serif;
164
- }
165
-
166
- .wrapper {
167
- max-width: 64rem;
168
- margin-inline: auto;
169
- padding: 2rem 1rem;
170
- }
171
- `,
172
-
173
- "jsconfig.json": `{
174
- "compilerOptions": {
175
- "checkJs": true,
176
- "module": "esnext",
177
- "moduleResolution": "bundler",
178
- "target": "es2022",
179
- "baseUrl": ".",
180
- "paths": { "@/*": ["./*"] }
181
- },
182
- "exclude": ["node_modules", "public/assets"]
183
- }
184
- `,
185
-
186
- ".gitignore": `node_modules/
187
- .jskelet/
188
- public/assets/
189
- .env
190
- `,
191
- };
192
-
193
- /**
194
- * @param {string} root
195
- * @returns {Promise<void>}
196
- */
197
- export async function init(root) {
198
- log.section("init");
199
-
200
- const created = [];
201
- const skipped = [];
202
-
203
- for (const [relative, contents] of Object.entries(FILES)) {
204
- const target = path.join(root, relative);
205
-
206
- if (fs.existsSync(target)) {
207
- skipped.push(relative);
208
- continue;
209
- }
210
-
211
- fs.mkdirSync(path.dirname(target), { recursive: true });
212
- fs.writeFileSync(target, contents);
213
- created.push(relative);
214
- }
215
-
216
- for (const file of created) log.line(`+ ${file}`);
217
- if (skipped.length) log.warn(`${skipped.length} files already existed, skipped`);
218
-
219
- log.line("");
220
- log.line("next step: npx jskelet dev");
221
- }
1
+ /**
2
+ * `jskelet init` — bulunduğun dizine çalışan bir minimum iskelet kurar.
3
+ *
4
+ * Var olan dosyaların üzerine yazmaz: komutu ikinci kez çalıştırmak yalnızca
5
+ * eksikleri tamamlar. Amaç, "kurulum yaptım ama hiçbir şey çalışmıyor"
6
+ * aşamasını tamamen atlamak — `jskelet dev` hemen ardından çalışır.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import * as log from "./log.mjs";
11
+
12
+ /** @type {Record<string, string>} */
13
+ const FILES = {
14
+ "jskelet.config.mjs": `/**
15
+ * JSkelet configuration. Every field is optional; the app still runs on
16
+ * defaults if you delete this file.
17
+ *
18
+ * Details: node_modules/jskelet/docs/en/07-configuration.md
19
+ */
20
+ export default {
21
+ brand: { lang: "en" },
22
+
23
+ /** Third-party origins; emitted as preconnect in \`<head>\`. */
24
+ preconnect: [],
25
+
26
+ async cache() {
27
+ return {
28
+ /** How long a page's HTML stays in the cache (seconds). */
29
+ html: { "/": 60 },
30
+ };
31
+ },
32
+
33
+ hooks: {
34
+ /** Metadata defaults for every page. */
35
+ metadata() {
36
+ return {
37
+ titleTemplate: "%s | JSkelet",
38
+ description: "A site built with JSkelet.",
39
+ };
40
+ },
41
+
42
+ /** Locals added to the layout on every render. */
43
+ layoutContext() {
44
+ return { bodyClass: "min-h-full" };
45
+ },
46
+
47
+ /** 404 page. */
48
+ notFound() {
49
+ return {
50
+ view: "pages/not-found",
51
+ metadata: { title: "Page not found", robots: { index: false } },
52
+ };
53
+ },
54
+ },
55
+ };
56
+ `,
57
+
58
+ "routes/10-pages.mjs": `/**
59
+ * Route module. The default export receives \`(app, api)\`; \`api.route()\` wraps
60
+ * the controller with the HTML cache, the notFound/redirect flow and
61
+ * compression.
62
+ *
63
+ * The numeric prefix in the file name sets load order: catch-all routes
64
+ * (like "/:slug") belong to a higher number.
65
+ */
66
+ export default function register(app, { route }) {
67
+ app.get(
68
+ "/",
69
+ route(
70
+ async () => ({
71
+ view: "pages/home",
72
+ metadata: { title: "Home" },
73
+ data: { message: "JSkelet is running." },
74
+ }),
75
+ { revalidate: 60 },
76
+ ),
77
+ );
78
+ }
79
+ `,
80
+
81
+ "views/pages/home.ejs": `<section class="wrapper">
82
+ <h1><%= metadata.title %></h1>
83
+ <p><%= message %></p>
84
+ <div data-island="counter" data-island-props='{"start":0}'></div>
85
+ </section>
86
+ `,
87
+
88
+ "views/pages/not-found.ejs": `<section class="wrapper">
89
+ <h1>404</h1>
90
+ <p>The page you are looking for was not found.</p>
91
+ <p><a href="/">Back to home</a></p>
92
+ </section>
93
+ `,
94
+
95
+ "views/components/button.js": `import { attrs, esc } from "jskelet/html";
96
+
97
+ /**
98
+ * Every named export under \`views/components/**\` is usable directly in
99
+ * templates: \`<%- button({ text: "Save" }) %>\`. No import needed.
100
+ *
101
+ * @param {{ text: string, href?: string, class?: string }} props
102
+ * @returns {string}
103
+ */
104
+ export function button({ text, href, class: className }) {
105
+ const tag = href ? "a" : "button";
106
+ return \`<\${tag}\${attrs({ href, class: className })}>\${esc(text)}</\${tag}>\`;
107
+ }
108
+ `,
109
+
110
+ "client/entries/main.js": `import { registerAll, start } from "jskelet/client";
111
+
112
+ /**
113
+ * Island registry. Values are dynamic imports: a module is downloaded only if
114
+ * that island is actually on the page and becomes visible.
115
+ */
116
+ registerAll({
117
+ counter: () => import("../islands/counter.js"),
118
+ });
119
+
120
+ start();
121
+ `,
122
+
123
+ "client/islands/counter.js": `/**
124
+ * Island contract: a named export called \`mount(element, props)\`.
125
+ * The returned function, if any, is reserved for cleanup.
126
+ *
127
+ * @param {HTMLElement} element
128
+ * @param {{ start?: number }} props
129
+ */
130
+ export function mount(element, props) {
131
+ let value = props.start ?? 0;
132
+
133
+ const button = document.createElement("button");
134
+ button.type = "button";
135
+
136
+ const paint = () => {
137
+ button.textContent = \`Clicks: \${value}\`;
138
+ };
139
+
140
+ button.addEventListener("click", () => {
141
+ value += 1;
142
+ paint();
143
+ });
144
+
145
+ paint();
146
+ element.append(button);
147
+ }
148
+ `,
149
+
150
+ "styles/globals.css": `@import "tailwindcss" source(none);
151
+
152
+ /**
153
+ * Tailwind's class scanning depends on these directives. Automatic detection
154
+ * only scans the directory holding this file; variants used in templates
155
+ * (like data-[active=false]:…) would otherwise be dropped silently.
156
+ */
157
+ @source "../views";
158
+ @source "../client";
159
+ @source "../routes";
160
+
161
+ body {
162
+ margin: 0;
163
+ font-family: system-ui, sans-serif;
164
+ }
165
+
166
+ .wrapper {
167
+ max-width: 64rem;
168
+ margin-inline: auto;
169
+ padding: 2rem 1rem;
170
+ }
171
+ `,
172
+
173
+ "jsconfig.json": `{
174
+ "compilerOptions": {
175
+ "checkJs": true,
176
+ "module": "esnext",
177
+ "moduleResolution": "bundler",
178
+ "target": "es2022",
179
+ "baseUrl": ".",
180
+ "paths": { "@/*": ["./*"] }
181
+ },
182
+ "exclude": ["node_modules", "public/assets"]
183
+ }
184
+ `,
185
+
186
+ ".gitignore": `node_modules/
187
+ .jskelet/
188
+ public/assets/
189
+ .env
190
+ `,
191
+ };
192
+
193
+ /**
194
+ * @param {string} root
195
+ * @returns {Promise<void>}
196
+ */
197
+ export async function init(root) {
198
+ log.section("init");
199
+
200
+ const created = [];
201
+ const skipped = [];
202
+
203
+ for (const [relative, contents] of Object.entries(FILES)) {
204
+ const target = path.join(root, relative);
205
+
206
+ if (fs.existsSync(target)) {
207
+ skipped.push(relative);
208
+ continue;
209
+ }
210
+
211
+ fs.mkdirSync(path.dirname(target), { recursive: true });
212
+ fs.writeFileSync(target, contents);
213
+ created.push(relative);
214
+ }
215
+
216
+ for (const file of created) log.line(`+ ${file}`);
217
+ if (skipped.length) log.warn(`${skipped.length} files already existed, skipped`);
218
+
219
+ log.line("");
220
+ log.line("next step: npx jskelet dev");
221
+ }
package/src/log.mjs CHANGED
@@ -253,6 +253,14 @@ export function event({ symbol = symbols.ok, scope, message, note, time = null }
253
253
  `${message}${note ? ` ${c.dim(note)}` : ""}${padding}` +
254
254
  `${c.gray((time == null ? "" : ms(time)).padStart(TIME))}\n`,
255
255
  );
256
+
257
+ emitLog({
258
+ kind: "event",
259
+ scope,
260
+ message,
261
+ note: note ?? null,
262
+ ms: time ?? null,
263
+ });
256
264
  }
257
265
 
258
266
  /**
@@ -271,6 +279,15 @@ export function http(info) {
271
279
  `${tint(String(info.status))} ${c.gray(ms(info.ms).padStart(TIME))}` +
272
280
  `${info.cache === "HIT" ? ` ${c.dim("cached")}` : ""}\n`,
273
281
  );
282
+
283
+ emitLog({
284
+ kind: "http",
285
+ method: info.method,
286
+ url: info.url,
287
+ status: info.status,
288
+ ms: info.ms,
289
+ cache: info.cache ?? null,
290
+ });
274
291
  }
275
292
 
276
293
  /**
@@ -337,6 +354,14 @@ export function errorBox({ title, name, message, lines = [] }) {
337
354
  }
338
355
  line("");
339
356
  write(`${c.red(bottom)}\n\n`);
357
+
358
+ emitLog({
359
+ kind: "error",
360
+ scope: title,
361
+ message: `${name}: ${message}`,
362
+ note: lines.length ? lines.join(" · ") : null,
363
+ ms: null,
364
+ });
340
365
  }
341
366
 
342
367
  /**
@@ -361,3 +386,36 @@ function wrap(text, max) {
361
386
  if (current) out.push(current);
362
387
  return out.length ? out : [""];
363
388
  }
389
+
390
+ /* -------------------------------------------------------------- aboneler */
391
+
392
+ /**
393
+ * Admin paneli gibi dinleyiciler. Abone yokken `emitLog` boş Set üzerinde
394
+ * döner — stdout yoluna maliyet eklemez.
395
+ *
396
+ * @type {Set<(entry: Record<string, unknown>) => void>}
397
+ */
398
+ const listeners = new Set();
399
+
400
+ /**
401
+ * @param {(entry: Record<string, unknown>) => void} listener
402
+ * @returns {() => void} Aboneliği iptal eden fonksiyon.
403
+ */
404
+ export function subscribe(listener) {
405
+ listeners.add(listener);
406
+ return () => listeners.delete(listener);
407
+ }
408
+
409
+ /**
410
+ * @param {Record<string, unknown>} entry
411
+ */
412
+ function emitLog(entry) {
413
+ if (!listeners.size) return;
414
+ for (const listener of listeners) {
415
+ try {
416
+ listener(entry);
417
+ } catch {
418
+ // Dinleyici paneli düşürmesin.
419
+ }
420
+ }
421
+ }