jskelet 0.6.3 → 0.6.4
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 +136 -136
- package/CHANGELOG.md +628 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +667 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +348 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +675 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +351 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +369 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +5 -0
|
@@ -1,349 +1,349 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Island bundle'ı: esbuild, ESM, code splitting.
|
|
3
|
-
*
|
|
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.
|
|
9
|
-
*
|
|
10
|
-
* Hedef tarayıcılar `package.json` → `browserslist` yerine burada sabit: ESM +
|
|
11
|
-
* dinamik import + `IntersectionObserver` island modelinin zaten alt sınırı,
|
|
12
|
-
* daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor.
|
|
13
|
-
*/
|
|
14
|
-
import fs from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import process from "node:process";
|
|
17
|
-
import * as esbuild from "esbuild";
|
|
18
|
-
import { paths, patchManifest, pruneAssets } from "../paths.mjs";
|
|
19
|
-
import * as log from "../../log.mjs";
|
|
20
|
-
|
|
21
|
-
/** Client entry ve `@/` alias için kabul edilen kaynak uzantıları. `.tsx` yok. */
|
|
22
|
-
const CLIENT_SOURCE_EXTS = [".js", ".ts", ".mts"];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* `@/` alias'ını proje köküne çözer — Node tarafındaki `alias-hooks.mjs` ile
|
|
26
|
-
* aynı davranış, böylece `lib/` altındaki modüller hem sunucuda hem
|
|
27
|
-
* tarayıcıda aynı import stilini kullanabilir.
|
|
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
|
-
*
|
|
32
|
-
* @param {string} root
|
|
33
|
-
* @returns {esbuild.Plugin}
|
|
34
|
-
*/
|
|
35
|
-
function aliasPlugin(root) {
|
|
36
|
-
return {
|
|
37
|
-
name: "jskelet-alias",
|
|
38
|
-
setup(build) {
|
|
39
|
-
build.onResolve({ filter: /^@\// }, (args) => ({
|
|
40
|
-
path: resolveWithExtension(path.join(root, args.path.slice(2))),
|
|
41
|
-
}));
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
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
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Tarayıcıda `process` yoktur; sunucuyla paylaşılan modüller yine de
|
|
65
|
-
* `process.env` okur. `clientEnv` ile bildirilen anahtarlar build zamanında
|
|
66
|
-
* gömülür — Next'teki `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın
|
|
67
|
-
* herkese açık olduğu isimden değil config'ten belli.
|
|
68
|
-
*
|
|
69
|
-
* `process.env`in tamamı tek nesne olarak define edilir: listede olmayan bir
|
|
70
|
-
* anahtar okunduğunda çökme yerine `undefined` döner.
|
|
71
|
-
*
|
|
72
|
-
* @param {string[]} keys
|
|
73
|
-
* @returns {Record<string, string>}
|
|
74
|
-
*/
|
|
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
|
-
|
|
84
|
-
/** @type {Record<string, string>} */
|
|
85
|
-
const env = { NODE_ENV: process.env.NODE_ENV ?? "production" };
|
|
86
|
-
for (const key of keys) {
|
|
87
|
-
const value = process.env[key];
|
|
88
|
-
if (value != null) env[key] = value;
|
|
89
|
-
}
|
|
90
|
-
return env;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @param {string} base
|
|
95
|
-
* @returns {string}
|
|
96
|
-
*/
|
|
97
|
-
export function resolveWithExtension(base) {
|
|
98
|
-
const candidates = [
|
|
99
|
-
base,
|
|
100
|
-
`${base}.js`,
|
|
101
|
-
`${base}.mjs`,
|
|
102
|
-
`${base}.ts`,
|
|
103
|
-
`${base}.mts`,
|
|
104
|
-
`${base}.json`,
|
|
105
|
-
path.join(base, "index.js"),
|
|
106
|
-
path.join(base, "index.ts"),
|
|
107
|
-
];
|
|
108
|
-
for (const candidate of candidates) {
|
|
109
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
110
|
-
}
|
|
111
|
-
return base;
|
|
112
|
-
}
|
|
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
|
-
|
|
161
|
-
/**
|
|
162
|
-
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
163
|
-
* @param {{ watch?: boolean }} [options]
|
|
164
|
-
* @returns {Promise<Record<string, string>>}
|
|
165
|
-
*/
|
|
166
|
-
export async function buildClient(config, { watch = false } = {}) {
|
|
167
|
-
const entryDir = path.join(config.dirs.client, "entries");
|
|
168
|
-
const outDir = path.join(paths.assets, "js");
|
|
169
|
-
|
|
170
|
-
if (!fs.existsSync(entryDir)) {
|
|
171
|
-
log.detail("no entries, skipped");
|
|
172
|
-
return {};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const entryPoints = listClientEntries(entryDir);
|
|
176
|
-
|
|
177
|
-
if (!entryPoints.length) {
|
|
178
|
-
log.detail("no entries, skipped");
|
|
179
|
-
return {};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
fs.rmSync(outDir, { recursive: true, force: true });
|
|
183
|
-
pruneAssets(["js/"]);
|
|
184
|
-
|
|
185
|
-
const entryRoot = path
|
|
186
|
-
.relative(config.root, entryDir)
|
|
187
|
-
.split(path.sep)
|
|
188
|
-
.join("/");
|
|
189
|
-
|
|
190
|
-
/** @type {esbuild.BuildOptions} */
|
|
191
|
-
const options = {
|
|
192
|
-
entryPoints,
|
|
193
|
-
outdir: outDir,
|
|
194
|
-
absWorkingDir: config.root,
|
|
195
|
-
bundle: true,
|
|
196
|
-
splitting: true,
|
|
197
|
-
format: "esm",
|
|
198
|
-
target: ["chrome111", "edge111", "firefox111", "safari16.4"],
|
|
199
|
-
minify: true,
|
|
200
|
-
// Prod'da map dosyaları `public/assets` altında herkese açık kalırdı.
|
|
201
|
-
sourcemap: process.env.NODE_ENV === "development",
|
|
202
|
-
metafile: true,
|
|
203
|
-
entryNames: "[name].[hash]",
|
|
204
|
-
chunkNames: "chunks/[name].[hash]",
|
|
205
|
-
legalComments: "none",
|
|
206
|
-
plugins: [aliasPlugin(config.root)],
|
|
207
|
-
define: {
|
|
208
|
-
"process.env": JSON.stringify(publicEnv(config.clientEnv)),
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
if (watch) {
|
|
213
|
-
const context = await esbuild.context({
|
|
214
|
-
...options,
|
|
215
|
-
plugins: [...options.plugins, rebuildReporter(config, entryRoot)],
|
|
216
|
-
});
|
|
217
|
-
const result = await context.rebuild();
|
|
218
|
-
await context.watch();
|
|
219
|
-
writeMetafile(result.metafile);
|
|
220
|
-
const manifest = toManifest(result.metafile, config, entryRoot);
|
|
221
|
-
const entries = Object.keys(manifest).length;
|
|
222
|
-
log.detail(`${entries} ${entries === 1 ? "entry" : "entries"}`);
|
|
223
|
-
return manifest;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const result = await esbuild.build(options);
|
|
227
|
-
writeMetafile(result.metafile);
|
|
228
|
-
const manifest = toManifest(result.metafile, config, entryRoot);
|
|
229
|
-
const bytes = Object.values(result.metafile.outputs).reduce(
|
|
230
|
-
(sum, output) => sum + output.bytes,
|
|
231
|
-
0,
|
|
232
|
-
);
|
|
233
|
-
const entries = Object.keys(manifest).length;
|
|
234
|
-
log.detail(`${entries} ${entries === 1 ? "entry" : "entries"}, ${log.size(bytes)}`);
|
|
235
|
-
return manifest;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Metafile diske yazılır: dev panelindeki chunk analizi giriş/çıkış
|
|
240
|
-
* kırılımını buradan okur. Çalışma zamanı bu dosyaya bağımlı değildir.
|
|
241
|
-
*
|
|
242
|
-
* @param {esbuild.Metafile} [metafile]
|
|
243
|
-
*/
|
|
244
|
-
function writeMetafile(metafile) {
|
|
245
|
-
if (!metafile) return;
|
|
246
|
-
try {
|
|
247
|
-
fs.mkdirSync(paths.generated, { recursive: true });
|
|
248
|
-
fs.writeFileSync(
|
|
249
|
-
path.join(paths.generated, "metafile.json"),
|
|
250
|
-
JSON.stringify(metafile),
|
|
251
|
-
);
|
|
252
|
-
} catch {
|
|
253
|
-
// Analiz verisi en iyi çaba; build'i düşürmez.
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Watch turlarını canlı olarak bildirir; ilk build zaten adım satırında görünür.
|
|
259
|
-
*
|
|
260
|
-
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
261
|
-
* @param {string} entryRoot
|
|
262
|
-
* @returns {esbuild.Plugin}
|
|
263
|
-
*/
|
|
264
|
-
function rebuildReporter(config, entryRoot) {
|
|
265
|
-
// İlk `rebuild()` ve `watch()`'un kendi açılış turu adım satırında zaten
|
|
266
|
-
// görünüyor; canlı bildirim gerçek değişikliklerle başlar.
|
|
267
|
-
let skip = 2;
|
|
268
|
-
let started = 0;
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
name: "jskelet-rebuild-log",
|
|
272
|
-
setup(build) {
|
|
273
|
-
build.onStart(() => {
|
|
274
|
-
started = Date.now();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
build.onEnd((result) => {
|
|
278
|
-
if (result.metafile) {
|
|
279
|
-
writeMetafile(result.metafile);
|
|
280
|
-
// Entry adları hash'li: yeni hash manifest'e yazılmazsa HTML eski
|
|
281
|
-
// (artık var olmayan) bundle'ı ister ve island'ların hiçbiri açılmaz.
|
|
282
|
-
for (const [key, url] of Object.entries(
|
|
283
|
-
toManifest(result.metafile, config, entryRoot),
|
|
284
|
-
)) {
|
|
285
|
-
patchManifest(key, url);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (skip > 0) {
|
|
290
|
-
skip -= 1;
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (result.errors.length) {
|
|
295
|
-
const [first] = result.errors;
|
|
296
|
-
log.errorBox({
|
|
297
|
-
title: "Client Build Error",
|
|
298
|
-
name: "BuildError",
|
|
299
|
-
message: first.text,
|
|
300
|
-
lines: [
|
|
301
|
-
first.location
|
|
302
|
-
? `${first.location.file}:${first.location.line}:${first.location.column}`
|
|
303
|
-
: "",
|
|
304
|
-
result.errors.length > 1
|
|
305
|
-
? `+${result.errors.length - 1} more errors`
|
|
306
|
-
: "",
|
|
307
|
-
].filter(Boolean),
|
|
308
|
-
});
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
log.event({
|
|
313
|
-
scope: "client",
|
|
314
|
-
message: "rebuilt",
|
|
315
|
-
time: Date.now() - started,
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
},
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* esbuild metafile → { "main.js": "/assets/js/main.abc.js" }
|
|
324
|
-
*
|
|
325
|
-
* Dinamik import'lar da `entryPoint` taşır; yalnızca gerçek entry'ler
|
|
326
|
-
* manifest'e girer, yoksa her island ayrı bir manifest anahtarı olurdu.
|
|
327
|
-
*
|
|
328
|
-
* @param {esbuild.Metafile} metafile
|
|
329
|
-
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
330
|
-
* @param {string} entryRoot
|
|
331
|
-
* @returns {Record<string, string>}
|
|
332
|
-
*/
|
|
333
|
-
function toManifest(metafile, config, entryRoot) {
|
|
334
|
-
/** @type {Record<string, string>} */
|
|
335
|
-
const manifest = {};
|
|
336
|
-
|
|
337
|
-
for (const [outputPath, output] of Object.entries(metafile.outputs)) {
|
|
338
|
-
if (!output.entryPoint?.startsWith(entryRoot)) continue;
|
|
339
|
-
|
|
340
|
-
const name = entryManifestName(output.entryPoint);
|
|
341
|
-
const absolute = path.resolve(config.root, outputPath);
|
|
342
|
-
manifest[name] = `/${path
|
|
343
|
-
.relative(config.dirs.public, absolute)
|
|
344
|
-
.split(path.sep)
|
|
345
|
-
.join("/")}`;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return manifest;
|
|
349
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Island bundle'ı: esbuild, ESM, code splitting.
|
|
3
|
+
*
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* Hedef tarayıcılar `package.json` → `browserslist` yerine burada sabit: ESM +
|
|
11
|
+
* dinamik import + `IntersectionObserver` island modelinin zaten alt sınırı,
|
|
12
|
+
* daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import process from "node:process";
|
|
17
|
+
import * as esbuild from "esbuild";
|
|
18
|
+
import { paths, patchManifest, pruneAssets } from "../paths.mjs";
|
|
19
|
+
import * as log from "../../log.mjs";
|
|
20
|
+
|
|
21
|
+
/** Client entry ve `@/` alias için kabul edilen kaynak uzantıları. `.tsx` yok. */
|
|
22
|
+
const CLIENT_SOURCE_EXTS = [".js", ".ts", ".mts"];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `@/` alias'ını proje köküne çözer — Node tarafındaki `alias-hooks.mjs` ile
|
|
26
|
+
* aynı davranış, böylece `lib/` altındaki modüller hem sunucuda hem
|
|
27
|
+
* tarayıcıda aynı import stilini kullanabilir.
|
|
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
|
+
*
|
|
32
|
+
* @param {string} root
|
|
33
|
+
* @returns {esbuild.Plugin}
|
|
34
|
+
*/
|
|
35
|
+
function aliasPlugin(root) {
|
|
36
|
+
return {
|
|
37
|
+
name: "jskelet-alias",
|
|
38
|
+
setup(build) {
|
|
39
|
+
build.onResolve({ filter: /^@\// }, (args) => ({
|
|
40
|
+
path: resolveWithExtension(path.join(root, args.path.slice(2))),
|
|
41
|
+
}));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
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
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tarayıcıda `process` yoktur; sunucuyla paylaşılan modüller yine de
|
|
65
|
+
* `process.env` okur. `clientEnv` ile bildirilen anahtarlar build zamanında
|
|
66
|
+
* gömülür — Next'teki `NEXT_PUBLIC_*` ile aynı sözleşme, ama hangi anahtarın
|
|
67
|
+
* herkese açık olduğu isimden değil config'ten belli.
|
|
68
|
+
*
|
|
69
|
+
* `process.env`in tamamı tek nesne olarak define edilir: listede olmayan bir
|
|
70
|
+
* anahtar okunduğunda çökme yerine `undefined` döner.
|
|
71
|
+
*
|
|
72
|
+
* @param {string[]} keys
|
|
73
|
+
* @returns {Record<string, string>}
|
|
74
|
+
*/
|
|
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
|
+
|
|
84
|
+
/** @type {Record<string, string>} */
|
|
85
|
+
const env = { NODE_ENV: process.env.NODE_ENV ?? "production" };
|
|
86
|
+
for (const key of keys) {
|
|
87
|
+
const value = process.env[key];
|
|
88
|
+
if (value != null) env[key] = value;
|
|
89
|
+
}
|
|
90
|
+
return env;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} base
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function resolveWithExtension(base) {
|
|
98
|
+
const candidates = [
|
|
99
|
+
base,
|
|
100
|
+
`${base}.js`,
|
|
101
|
+
`${base}.mjs`,
|
|
102
|
+
`${base}.ts`,
|
|
103
|
+
`${base}.mts`,
|
|
104
|
+
`${base}.json`,
|
|
105
|
+
path.join(base, "index.js"),
|
|
106
|
+
path.join(base, "index.ts"),
|
|
107
|
+
];
|
|
108
|
+
for (const candidate of candidates) {
|
|
109
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
110
|
+
}
|
|
111
|
+
return base;
|
|
112
|
+
}
|
|
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
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
163
|
+
* @param {{ watch?: boolean }} [options]
|
|
164
|
+
* @returns {Promise<Record<string, string>>}
|
|
165
|
+
*/
|
|
166
|
+
export async function buildClient(config, { watch = false } = {}) {
|
|
167
|
+
const entryDir = path.join(config.dirs.client, "entries");
|
|
168
|
+
const outDir = path.join(paths.assets, "js");
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(entryDir)) {
|
|
171
|
+
log.detail("no entries, skipped");
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const entryPoints = listClientEntries(entryDir);
|
|
176
|
+
|
|
177
|
+
if (!entryPoints.length) {
|
|
178
|
+
log.detail("no entries, skipped");
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
183
|
+
pruneAssets(["js/"]);
|
|
184
|
+
|
|
185
|
+
const entryRoot = path
|
|
186
|
+
.relative(config.root, entryDir)
|
|
187
|
+
.split(path.sep)
|
|
188
|
+
.join("/");
|
|
189
|
+
|
|
190
|
+
/** @type {esbuild.BuildOptions} */
|
|
191
|
+
const options = {
|
|
192
|
+
entryPoints,
|
|
193
|
+
outdir: outDir,
|
|
194
|
+
absWorkingDir: config.root,
|
|
195
|
+
bundle: true,
|
|
196
|
+
splitting: true,
|
|
197
|
+
format: "esm",
|
|
198
|
+
target: ["chrome111", "edge111", "firefox111", "safari16.4"],
|
|
199
|
+
minify: true,
|
|
200
|
+
// Prod'da map dosyaları `public/assets` altında herkese açık kalırdı.
|
|
201
|
+
sourcemap: process.env.NODE_ENV === "development",
|
|
202
|
+
metafile: true,
|
|
203
|
+
entryNames: "[name].[hash]",
|
|
204
|
+
chunkNames: "chunks/[name].[hash]",
|
|
205
|
+
legalComments: "none",
|
|
206
|
+
plugins: [aliasPlugin(config.root)],
|
|
207
|
+
define: {
|
|
208
|
+
"process.env": JSON.stringify(publicEnv(config.clientEnv)),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (watch) {
|
|
213
|
+
const context = await esbuild.context({
|
|
214
|
+
...options,
|
|
215
|
+
plugins: [...options.plugins, rebuildReporter(config, entryRoot)],
|
|
216
|
+
});
|
|
217
|
+
const result = await context.rebuild();
|
|
218
|
+
await context.watch();
|
|
219
|
+
writeMetafile(result.metafile);
|
|
220
|
+
const manifest = toManifest(result.metafile, config, entryRoot);
|
|
221
|
+
const entries = Object.keys(manifest).length;
|
|
222
|
+
log.detail(`${entries} ${entries === 1 ? "entry" : "entries"}`);
|
|
223
|
+
return manifest;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const result = await esbuild.build(options);
|
|
227
|
+
writeMetafile(result.metafile);
|
|
228
|
+
const manifest = toManifest(result.metafile, config, entryRoot);
|
|
229
|
+
const bytes = Object.values(result.metafile.outputs).reduce(
|
|
230
|
+
(sum, output) => sum + output.bytes,
|
|
231
|
+
0,
|
|
232
|
+
);
|
|
233
|
+
const entries = Object.keys(manifest).length;
|
|
234
|
+
log.detail(`${entries} ${entries === 1 ? "entry" : "entries"}, ${log.size(bytes)}`);
|
|
235
|
+
return manifest;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Metafile diske yazılır: dev panelindeki chunk analizi giriş/çıkış
|
|
240
|
+
* kırılımını buradan okur. Çalışma zamanı bu dosyaya bağımlı değildir.
|
|
241
|
+
*
|
|
242
|
+
* @param {esbuild.Metafile} [metafile]
|
|
243
|
+
*/
|
|
244
|
+
function writeMetafile(metafile) {
|
|
245
|
+
if (!metafile) return;
|
|
246
|
+
try {
|
|
247
|
+
fs.mkdirSync(paths.generated, { recursive: true });
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
path.join(paths.generated, "metafile.json"),
|
|
250
|
+
JSON.stringify(metafile),
|
|
251
|
+
);
|
|
252
|
+
} catch {
|
|
253
|
+
// Analiz verisi en iyi çaba; build'i düşürmez.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Watch turlarını canlı olarak bildirir; ilk build zaten adım satırında görünür.
|
|
259
|
+
*
|
|
260
|
+
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
261
|
+
* @param {string} entryRoot
|
|
262
|
+
* @returns {esbuild.Plugin}
|
|
263
|
+
*/
|
|
264
|
+
function rebuildReporter(config, entryRoot) {
|
|
265
|
+
// İlk `rebuild()` ve `watch()`'un kendi açılış turu adım satırında zaten
|
|
266
|
+
// görünüyor; canlı bildirim gerçek değişikliklerle başlar.
|
|
267
|
+
let skip = 2;
|
|
268
|
+
let started = 0;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
name: "jskelet-rebuild-log",
|
|
272
|
+
setup(build) {
|
|
273
|
+
build.onStart(() => {
|
|
274
|
+
started = Date.now();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
build.onEnd((result) => {
|
|
278
|
+
if (result.metafile) {
|
|
279
|
+
writeMetafile(result.metafile);
|
|
280
|
+
// Entry adları hash'li: yeni hash manifest'e yazılmazsa HTML eski
|
|
281
|
+
// (artık var olmayan) bundle'ı ister ve island'ların hiçbiri açılmaz.
|
|
282
|
+
for (const [key, url] of Object.entries(
|
|
283
|
+
toManifest(result.metafile, config, entryRoot),
|
|
284
|
+
)) {
|
|
285
|
+
patchManifest(key, url);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (skip > 0) {
|
|
290
|
+
skip -= 1;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (result.errors.length) {
|
|
295
|
+
const [first] = result.errors;
|
|
296
|
+
log.errorBox({
|
|
297
|
+
title: "Client Build Error",
|
|
298
|
+
name: "BuildError",
|
|
299
|
+
message: first.text,
|
|
300
|
+
lines: [
|
|
301
|
+
first.location
|
|
302
|
+
? `${first.location.file}:${first.location.line}:${first.location.column}`
|
|
303
|
+
: "",
|
|
304
|
+
result.errors.length > 1
|
|
305
|
+
? `+${result.errors.length - 1} more errors`
|
|
306
|
+
: "",
|
|
307
|
+
].filter(Boolean),
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
log.event({
|
|
313
|
+
scope: "client",
|
|
314
|
+
message: "rebuilt",
|
|
315
|
+
time: Date.now() - started,
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* esbuild metafile → { "main.js": "/assets/js/main.abc.js" }
|
|
324
|
+
*
|
|
325
|
+
* Dinamik import'lar da `entryPoint` taşır; yalnızca gerçek entry'ler
|
|
326
|
+
* manifest'e girer, yoksa her island ayrı bir manifest anahtarı olurdu.
|
|
327
|
+
*
|
|
328
|
+
* @param {esbuild.Metafile} metafile
|
|
329
|
+
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
330
|
+
* @param {string} entryRoot
|
|
331
|
+
* @returns {Record<string, string>}
|
|
332
|
+
*/
|
|
333
|
+
function toManifest(metafile, config, entryRoot) {
|
|
334
|
+
/** @type {Record<string, string>} */
|
|
335
|
+
const manifest = {};
|
|
336
|
+
|
|
337
|
+
for (const [outputPath, output] of Object.entries(metafile.outputs)) {
|
|
338
|
+
if (!output.entryPoint?.startsWith(entryRoot)) continue;
|
|
339
|
+
|
|
340
|
+
const name = entryManifestName(output.entryPoint);
|
|
341
|
+
const absolute = path.resolve(config.root, outputPath);
|
|
342
|
+
manifest[name] = `/${path
|
|
343
|
+
.relative(config.dirs.public, absolute)
|
|
344
|
+
.split(path.sep)
|
|
345
|
+
.join("/")}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return manifest;
|
|
349
|
+
}
|