jskelet 0.6.4 → 0.6.6

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.
@@ -15,6 +15,7 @@ import fs from "node:fs";
15
15
  import path from "node:path";
16
16
  import process from "node:process";
17
17
  import * as esbuild from "esbuild";
18
+ import { chunkGroupsPlugin } from "../chunk-groups.mjs";
18
19
  import { paths, patchManifest, pruneAssets } from "../paths.mjs";
19
20
  import * as log from "../../log.mjs";
20
21
 
@@ -187,13 +188,34 @@ export async function buildClient(config, { watch = false } = {}) {
187
188
  .split(path.sep)
188
189
  .join("/");
189
190
 
191
+ const clientBuild = config.build?.client ?? {
192
+ splitting: true,
193
+ groups: {},
194
+ chunkGroup: null,
195
+ };
196
+ const splitting = clientBuild.splitting !== false;
197
+ const groups = splitting ? (clientBuild.groups ?? {}) : {};
198
+ const chunkGroup = splitting ? (clientBuild.chunkGroup ?? null) : null;
199
+
200
+ const groupPlugin = splitting
201
+ ? chunkGroupsPlugin({
202
+ root: config.root,
203
+ groups,
204
+ chunkGroup,
205
+ })
206
+ : null;
207
+
208
+ /** @type {esbuild.Plugin[]} */
209
+ const plugins = [aliasPlugin(config.root)];
210
+ if (groupPlugin) plugins.push(groupPlugin);
211
+
190
212
  /** @type {esbuild.BuildOptions} */
191
213
  const options = {
192
214
  entryPoints,
193
215
  outdir: outDir,
194
216
  absWorkingDir: config.root,
195
217
  bundle: true,
196
- splitting: true,
218
+ splitting,
197
219
  format: "esm",
198
220
  target: ["chrome111", "edge111", "firefox111", "safari16.4"],
199
221
  minify: true,
@@ -203,10 +225,15 @@ export async function buildClient(config, { watch = false } = {}) {
203
225
  entryNames: "[name].[hash]",
204
226
  chunkNames: "chunks/[name].[hash]",
205
227
  legalComments: "none",
206
- plugins: [aliasPlugin(config.root)],
228
+ plugins,
207
229
  define: {
208
230
  "process.env": JSON.stringify(publicEnv(config.clientEnv)),
209
231
  },
232
+ // Grup sarmalayıcısı her üyede `export { default }` yazar; named-only
233
+ // modüllerde bu uyarı gürültü — davranış yine doğru (default undefined).
234
+ ...(groupPlugin
235
+ ? { logOverride: { "import-is-undefined": "silent" } }
236
+ : {}),
210
237
  };
211
238
 
212
239
  if (watch) {
@@ -218,8 +245,7 @@ export async function buildClient(config, { watch = false } = {}) {
218
245
  await context.watch();
219
246
  writeMetafile(result.metafile);
220
247
  const manifest = toManifest(result.metafile, config, entryRoot);
221
- const entries = Object.keys(manifest).length;
222
- log.detail(`${entries} ${entries === 1 ? "entry" : "entries"}`);
248
+ log.detail(summarizeClientBuild(manifest, result.metafile));
223
249
  return manifest;
224
250
  }
225
251
 
@@ -230,11 +256,32 @@ export async function buildClient(config, { watch = false } = {}) {
230
256
  (sum, output) => sum + output.bytes,
231
257
  0,
232
258
  );
233
- const entries = Object.keys(manifest).length;
234
- log.detail(`${entries} ${entries === 1 ? "entry" : "entries"}, ${log.size(bytes)}`);
259
+ log.detail(
260
+ `${summarizeClientBuild(manifest, result.metafile)}, ${log.size(bytes)}`,
261
+ );
235
262
  return manifest;
236
263
  }
237
264
 
265
+ /**
266
+ * Entry + inject edilen group entry'ler metafile'da durur; teşhis için
267
+ * hem gerçek manifest entry sayısını hem toplam çıktı dosyasını yaz.
268
+ *
269
+ * @param {Record<string, string>} manifest
270
+ * @param {esbuild.Metafile} [metafile]
271
+ * @returns {string}
272
+ */
273
+ function summarizeClientBuild(manifest, metafile) {
274
+ const entries = Object.keys(manifest).length;
275
+ const entryLabel = `${entries} ${entries === 1 ? "entry" : "entries"}`;
276
+ if (!metafile) return entryLabel;
277
+
278
+ const outputs = Object.keys(metafile.outputs).filter(
279
+ (file) => file.endsWith(".js") || file.endsWith(".mjs"),
280
+ );
281
+ const chunks = outputs.length;
282
+ return `${entryLabel}, ${chunks} ${chunks === 1 ? "file" : "files"}`;
283
+ }
284
+
238
285
  /**
239
286
  * Metafile diske yazılır: dev panelindeki chunk analizi giriş/çıkış
240
287
  * kırılımını buradan okur. Çalışma zamanı bu dosyaya bağımlı değildir.
@@ -470,6 +470,31 @@ export const DEFAULT_SECURITY = {
470
470
  },
471
471
  };
472
472
 
473
+ /**
474
+ * Client bundle (esbuild) ayarları.
475
+ *
476
+ * `groups` / `chunkGroup` esbuild'in native manualChunks'ı olmadığı için
477
+ * framework plugin'i ile uygulanır — ayrıntı `src/build/chunk-groups.mjs`.
478
+ */
479
+ export const DEFAULT_BUILD = {
480
+ client: {
481
+ /** esbuild `splitting`; kapalıyken gruplama yok sayılır. */
482
+ splitting: true,
483
+ /**
484
+ * İsim → matcher listesi. Bare paket (`chart.js`), yol öneki
485
+ * (`./client/chrome/`) veya `@/` öneki.
486
+ * @type {Record<string, string[]>}
487
+ */
488
+ groups: {},
489
+ /**
490
+ * Modül kimliğini (mutlak yol veya bare specifier) grup adına map eder.
491
+ * `groups` ile çakışınca hook kazanır.
492
+ * @type {((id: string) => string | null | undefined) | null}
493
+ */
494
+ chunkGroup: null,
495
+ },
496
+ };
497
+
473
498
  /**
474
499
  * Build-zamanı görsel adımı + opsiyonel runtime uzak görsel proxy.
475
500
  *
@@ -47,6 +47,7 @@ import {
47
47
  ON_VISIT_CONCURRENCY_CEILING,
48
48
  ON_VISIT_PER_PAGE_CEILING,
49
49
  ON_VISIT_RPS_CEILING,
50
+ DEFAULT_BUILD,
50
51
  DEFAULT_IMAGES,
51
52
  DEFAULT_LOGS,
52
53
  DEFAULT_NAVIGATION,
@@ -160,6 +161,17 @@ const CONFIG_FILE = "jskelet.config.mjs";
160
161
  * @property {{ scan?: string[], dir: string } | false} icons
161
162
  * @property {ImagesConfig | false} images
162
163
  * @property {string[]} clientEnv Client bundle'a gömülecek env anahtarları.
164
+ * @property {BuildConfig} build Client bundling (esbuild splitting / chunk groups).
165
+ */
166
+
167
+ /**
168
+ * @typedef {object} BuildClientConfig
169
+ * @property {boolean} splitting
170
+ * @property {Record<string, string[]>} groups
171
+ * @property {((id: string) => string | null | undefined) | null} chunkGroup
172
+ *
173
+ * @typedef {object} BuildConfig
174
+ * @property {BuildClientConfig} client
163
175
  */
164
176
 
165
177
  /**
@@ -1139,6 +1151,118 @@ function normalizeSecurity(raw) {
1139
1151
  };
1140
1152
  }
1141
1153
 
1154
+ /**
1155
+ * Client build ayarları (`build.client`). Bozuk tipler uyarıyla varsayılana düşer.
1156
+ *
1157
+ * @param {unknown} raw
1158
+ * @returns {BuildConfig}
1159
+ */
1160
+ export function normalizeBuild(raw) {
1161
+ if (raw == null) {
1162
+ return {
1163
+ client: {
1164
+ splitting: DEFAULT_BUILD.client.splitting,
1165
+ groups: {},
1166
+ chunkGroup: null,
1167
+ },
1168
+ };
1169
+ }
1170
+
1171
+ if (typeof raw !== "object" || Array.isArray(raw)) {
1172
+ console.warn("[config] build must be an object, ignoring it");
1173
+ return {
1174
+ client: {
1175
+ splitting: DEFAULT_BUILD.client.splitting,
1176
+ groups: {},
1177
+ chunkGroup: null,
1178
+ },
1179
+ };
1180
+ }
1181
+
1182
+ const source = /** @type {Record<string, any>} */ (raw);
1183
+ const clientRaw = source.client;
1184
+
1185
+ if (clientRaw == null) {
1186
+ return {
1187
+ client: {
1188
+ splitting: DEFAULT_BUILD.client.splitting,
1189
+ groups: {},
1190
+ chunkGroup: null,
1191
+ },
1192
+ };
1193
+ }
1194
+
1195
+ if (typeof clientRaw !== "object" || Array.isArray(clientRaw)) {
1196
+ console.warn("[config] build.client must be an object, ignoring it");
1197
+ return {
1198
+ client: {
1199
+ splitting: DEFAULT_BUILD.client.splitting,
1200
+ groups: {},
1201
+ chunkGroup: null,
1202
+ },
1203
+ };
1204
+ }
1205
+
1206
+ const client = /** @type {Record<string, any>} */ (clientRaw);
1207
+ const splitting = client.splitting !== false;
1208
+
1209
+ /** @type {Record<string, string[]>} */
1210
+ const groups = {};
1211
+ if (client.groups != null) {
1212
+ if (typeof client.groups !== "object" || Array.isArray(client.groups)) {
1213
+ console.warn("[config] build.client.groups must be an object, ignoring it");
1214
+ } else {
1215
+ for (const [name, matchers] of Object.entries(client.groups)) {
1216
+ if (!isSafeGroupName(name)) {
1217
+ console.warn(
1218
+ `[config] build.client.groups key "${name}" is invalid ` +
1219
+ "(use letters, digits, _ or -); skipping",
1220
+ );
1221
+ continue;
1222
+ }
1223
+ const list = asArray(matchers, `build.client.groups.${name}`)
1224
+ .filter((entry) => typeof entry === "string" && entry.trim())
1225
+ .map((entry) => String(entry).trim());
1226
+ if (list.length) groups[name] = list;
1227
+ }
1228
+ }
1229
+ }
1230
+
1231
+ /** @type {((id: string) => string | null | undefined) | null} */
1232
+ let chunkGroup = null;
1233
+ if (client.chunkGroup != null) {
1234
+ if (typeof client.chunkGroup === "function") {
1235
+ chunkGroup = client.chunkGroup;
1236
+ } else {
1237
+ console.warn(
1238
+ "[config] build.client.chunkGroup must be a function, ignoring it",
1239
+ );
1240
+ }
1241
+ }
1242
+
1243
+ if (
1244
+ !splitting &&
1245
+ (Object.keys(groups).length > 0 || chunkGroup)
1246
+ ) {
1247
+ console.warn(
1248
+ "[config] build.client.groups / chunkGroup require splitting; " +
1249
+ "they are ignored while splitting is false",
1250
+ );
1251
+ }
1252
+
1253
+ return { client: { splitting, groups, chunkGroup } };
1254
+ }
1255
+
1256
+ /**
1257
+ * Chunk dosya adına güvenli grup adı.
1258
+ *
1259
+ * @param {string} name
1260
+ * @returns {boolean}
1261
+ */
1262
+ function isSafeGroupName(name) {
1263
+ return typeof name === "string" && /^[A-Za-z0-9_-]+$/.test(name);
1264
+ }
1265
+
1142
1266
  /**
1143
1267
  * İkon sprite ayarları. `false` → adım atlanır. `dir` varsayılanı `"icons"`:
1144
1268
  * o dizin varsa yalnızca yerel SVG'ler; yoksa Phosphor.
@@ -1423,6 +1547,7 @@ export async function loadConfig(options = {}) {
1423
1547
  icons: normalizeIcons(source.icons),
1424
1548
  images: normalizeImages(source.images),
1425
1549
  clientEnv: source.clientEnv ?? [],
1550
+ build: normalizeBuild(source.build),
1426
1551
  };
1427
1552
 
1428
1553
  if (