@moku-labs/web 2.3.3 → 2.4.1
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/dist/browser.mjs +20 -8
- package/dist/index.cjs +73 -13
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +73 -13
- package/package.json +1 -1
package/dist/browser.mjs
CHANGED
|
@@ -3645,11 +3645,13 @@ function attachHistoryFallback(handlers, navigate = (pathname, _scrollToTop, sig
|
|
|
3645
3645
|
* @param navigation - The Navigation API object to attach the listener to.
|
|
3646
3646
|
* @param handlers - The navigation lifecycle callbacks.
|
|
3647
3647
|
* @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
|
|
3648
|
+
* @param isOwnPush - True while the kernel's programmatic navigation runs its own
|
|
3649
|
+
* `history.pushState`: the `navigate` event it fires is left alone, the kernel runs it once.
|
|
3648
3650
|
* @returns A teardown that removes the `navigate` listener.
|
|
3649
3651
|
* @example
|
|
3650
3652
|
* const dispose = attachNavigationApi(navigation, handlers);
|
|
3651
3653
|
*/
|
|
3652
|
-
function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal)) {
|
|
3654
|
+
function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal), isOwnPush = () => false) {
|
|
3653
3655
|
/**
|
|
3654
3656
|
* Handle a `navigate` event: classify, then intercept with fetch-and-swap.
|
|
3655
3657
|
*
|
|
@@ -3658,6 +3660,7 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
|
|
|
3658
3660
|
* navigation.addEventListener("navigate", onNavigate);
|
|
3659
3661
|
*/
|
|
3660
3662
|
const onNavigate = (navEvent) => {
|
|
3663
|
+
if (isOwnPush()) return;
|
|
3661
3664
|
const url = new URL(navEvent.destination.url);
|
|
3662
3665
|
if (!navEvent.canIntercept || navEvent.hashChange || navEvent.downloadRequest) return;
|
|
3663
3666
|
if (!isInternalLink(url)) return;
|
|
@@ -3690,13 +3693,15 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
|
|
|
3690
3693
|
*
|
|
3691
3694
|
* @param handlers - The navigation lifecycle callbacks the kernel supplies.
|
|
3692
3695
|
* @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
|
|
3696
|
+
* @param isOwnPush - True while the kernel runs its own `history.pushState` (Navigation API path
|
|
3697
|
+
* only; the History fallback never sees an event for it).
|
|
3693
3698
|
* @returns A teardown removing all attached listeners.
|
|
3694
3699
|
* @example
|
|
3695
|
-
* const dispose = attachRouter(handlers, navigate);
|
|
3700
|
+
* const dispose = attachRouter(handlers, navigate, () => pushingOwnUrl);
|
|
3696
3701
|
*/
|
|
3697
|
-
function attachRouter(handlers, navigate) {
|
|
3702
|
+
function attachRouter(handlers, navigate, isOwnPush) {
|
|
3698
3703
|
const navigation = getNavigation();
|
|
3699
|
-
return navigation ? attachNavigationApi(navigation, handlers, navigate) : attachHistoryFallback(handlers, navigate);
|
|
3704
|
+
return navigation ? attachNavigationApi(navigation, handlers, navigate, isOwnPush) : attachHistoryFallback(handlers, navigate);
|
|
3700
3705
|
}
|
|
3701
3706
|
//#endregion
|
|
3702
3707
|
//#region src/plugins/spa/state.ts
|
|
@@ -3861,6 +3866,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3861
3866
|
let progress;
|
|
3862
3867
|
let pendingScrollToTop = true;
|
|
3863
3868
|
let pendingTransition = resolveTransition(resolved.defaultTransition);
|
|
3869
|
+
let pushingOwnUrl = false;
|
|
3864
3870
|
/**
|
|
3865
3871
|
* Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
|
|
3866
3872
|
* consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
|
|
@@ -4173,8 +4179,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4173
4179
|
* Navigation API commits the address bar before the interceptor calls {@link navigate}; a
|
|
4174
4180
|
* programmatic call does NOT, so it must commit the URL itself first — otherwise the content
|
|
4175
4181
|
* swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
|
|
4176
|
-
* `history.pushState`
|
|
4177
|
-
*
|
|
4182
|
+
* Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
|
|
4183
|
+
* (Chrome, Edge, Safari 26): `pushingOwnUrl` marks it, so the interceptor does not run this
|
|
4184
|
+
* navigation a second time. The current scroll is saved first so a later back restores it.
|
|
4178
4185
|
*
|
|
4179
4186
|
* @param path - The destination path (pathname + optional search).
|
|
4180
4187
|
* @param scroll - The optional per-call scroll override.
|
|
@@ -4185,7 +4192,12 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4185
4192
|
if (typeof document === "undefined") return;
|
|
4186
4193
|
if (path !== currentLocationUrl()) {
|
|
4187
4194
|
saveScrollPosition(currentLocationUrl());
|
|
4188
|
-
|
|
4195
|
+
pushingOwnUrl = true;
|
|
4196
|
+
try {
|
|
4197
|
+
history.pushState({}, "", path);
|
|
4198
|
+
} finally {
|
|
4199
|
+
pushingOwnUrl = false;
|
|
4200
|
+
}
|
|
4189
4201
|
}
|
|
4190
4202
|
navigate(path, true, void 0, scroll).catch(() => {});
|
|
4191
4203
|
};
|
|
@@ -4214,7 +4226,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
4214
4226
|
if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
|
|
4215
4227
|
progress = createProgressBar(resolved.progressBar);
|
|
4216
4228
|
state.currentUrl = currentLocationUrl();
|
|
4217
|
-
state.destroyRouter = attachRouter(handlers, navigate);
|
|
4229
|
+
state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
|
|
4218
4230
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
4219
4231
|
const hit = deps.router.match(matchPath);
|
|
4220
4232
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
package/dist/index.cjs
CHANGED
|
@@ -2357,10 +2357,11 @@ const CSS_EXTERNAL_FONT_GLOBS = [
|
|
|
2357
2357
|
* @param options.naming.chunk - Naming template for lazy split chunks.
|
|
2358
2358
|
* @param options.naming.asset - Naming template for additional emitted assets.
|
|
2359
2359
|
* @param options.external - Import/url() globs left unresolved in the output.
|
|
2360
|
+
* @param options.define - Global names replaced with constant expressions.
|
|
2360
2361
|
* @returns The structural build result.
|
|
2361
2362
|
* @example
|
|
2362
2363
|
* ```ts
|
|
2363
|
-
* await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [] });
|
|
2364
|
+
* await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [], define: {} });
|
|
2364
2365
|
* ```
|
|
2365
2366
|
*/
|
|
2366
2367
|
async function defaultRunner(options) {
|
|
@@ -2372,6 +2373,33 @@ async function defaultRunner(options) {
|
|
|
2372
2373
|
return bun.build(options);
|
|
2373
2374
|
}
|
|
2374
2375
|
/**
|
|
2376
|
+
* The bundle-time constants for the `build.env` names. Each name becomes `process.env.<NAME>` and
|
|
2377
|
+
* `import.meta.env.<NAME>`, and the whole `import.meta.env` object is set too, because
|
|
2378
|
+
* `browserEnv()` spreads it. The value is the build process's own variable, or `""` when unset.
|
|
2379
|
+
* With `minify`, a branch on an unset flag is removed together with the dynamic `import()` chunk
|
|
2380
|
+
* it guards, so developer-only code never ships in a build without the flag.
|
|
2381
|
+
*
|
|
2382
|
+
* @param names - The `build.env` names.
|
|
2383
|
+
* @param source - Where the values come from; the build process's environment by default.
|
|
2384
|
+
* @returns The `define` map for `Bun.build`; empty without names.
|
|
2385
|
+
* @example
|
|
2386
|
+
* ```ts
|
|
2387
|
+
* envDefines(["IS_DEVELOPMENT"], { IS_DEVELOPMENT: "true" });
|
|
2388
|
+
* // { "process.env.IS_DEVELOPMENT": "\"true\"", "import.meta.env.IS_DEVELOPMENT": "\"true\"",
|
|
2389
|
+
* // "import.meta.env": "{\"IS_DEVELOPMENT\":\"true\"}" }
|
|
2390
|
+
* ```
|
|
2391
|
+
*/
|
|
2392
|
+
function envDefines(names, source = process.env) {
|
|
2393
|
+
if (names.length === 0) return {};
|
|
2394
|
+
const values = Object.fromEntries(names.map((name) => [name, source[name] ?? ""]));
|
|
2395
|
+
const define = { "import.meta.env": JSON.stringify(values) };
|
|
2396
|
+
for (const [name, value] of Object.entries(values)) {
|
|
2397
|
+
define[`process.env.${name}`] = JSON.stringify(value);
|
|
2398
|
+
define[`import.meta.env.${name}`] = JSON.stringify(value);
|
|
2399
|
+
}
|
|
2400
|
+
return define;
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2375
2403
|
* Resolve the first existing entry file from a candidate list (project-relative).
|
|
2376
2404
|
*
|
|
2377
2405
|
* @param candidates - Ordered candidate paths to probe.
|
|
@@ -2440,12 +2468,13 @@ function normalizeAssetPath(absolutePath, outDir) {
|
|
|
2440
2468
|
* @param outDir - The publish root; stored asset paths are made relative to it.
|
|
2441
2469
|
* @param outdir - The bundler output directory (`<outDir>/assets`).
|
|
2442
2470
|
* @param minify - Whether to minify.
|
|
2471
|
+
* @param define - Global names replaced with constant expressions.
|
|
2443
2472
|
* @example
|
|
2444
2473
|
* ```ts
|
|
2445
|
-
* await runOne(ctx, runner, "css", ["a.css"], "dist", true);
|
|
2474
|
+
* await runOne(ctx, runner, "css", ["a.css"], "dist", true, {});
|
|
2446
2475
|
* ```
|
|
2447
2476
|
*/
|
|
2448
|
-
async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
2477
|
+
async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify, define) {
|
|
2449
2478
|
if (entrypoints.length === 0) return;
|
|
2450
2479
|
const result = await runner({
|
|
2451
2480
|
entrypoints,
|
|
@@ -2454,7 +2483,8 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
|
2454
2483
|
splitting: true,
|
|
2455
2484
|
target: "browser",
|
|
2456
2485
|
naming: FINGERPRINT_NAMING,
|
|
2457
|
-
external: kind === "css" ? [...CSS_EXTERNAL_FONT_GLOBS] : []
|
|
2486
|
+
external: kind === "css" ? [...CSS_EXTERNAL_FONT_GLOBS] : [],
|
|
2487
|
+
define
|
|
2458
2488
|
});
|
|
2459
2489
|
if (!result.success) throw new Error(`[web] build.bundle ${kind} build failed`);
|
|
2460
2490
|
const hashed = {};
|
|
@@ -2489,10 +2519,11 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
|
2489
2519
|
async function bundle(ctx, options = {}) {
|
|
2490
2520
|
const runner = options.runner ?? defaultRunner;
|
|
2491
2521
|
const { minify, outDir } = ctx.config;
|
|
2522
|
+
const define = envDefines(ctx.config.env ?? []);
|
|
2492
2523
|
const assetsDir = node_path$1.default.join(outDir, "assets");
|
|
2493
2524
|
const cssEntrypoints = options.cssEntrypoints ?? resolveEntrypoints(CSS_ENTRY_CANDIDATES);
|
|
2494
2525
|
const jsEntrypoints = options.jsEntrypoints ?? resolveJsEntrypoints(ctx);
|
|
2495
|
-
await Promise.all([runOne(ctx, runner, "css", cssEntrypoints, outDir, assetsDir, minify), runOne(ctx, runner, "js", jsEntrypoints, outDir, assetsDir, minify)]);
|
|
2526
|
+
await Promise.all([runOne(ctx, runner, "css", cssEntrypoints, outDir, assetsDir, minify, define), runOne(ctx, runner, "js", jsEntrypoints, outDir, assetsDir, minify, define)]);
|
|
2496
2527
|
}
|
|
2497
2528
|
//#endregion
|
|
2498
2529
|
//#region src/plugins/build/phases/asset-tags.ts
|
|
@@ -5821,6 +5852,22 @@ function validateFonts(og) {
|
|
|
5821
5852
|
if (isMissingFontDir(og.fontDir)) throw new Error(`${ERROR_PREFIX$7}.ogImage: fontDir "${og.fontDir}" does not exist — provide a directory with at least one font.`);
|
|
5822
5853
|
if (!(0, node_fs.readdirSync)(og.fontDir).some((name) => FONT_EXTENSIONS.some((extension) => name.endsWith(extension)))) throw new Error(`${ERROR_PREFIX$7}.ogImage: fontDir "${og.fontDir}" contains no .ttf/.otf/.woff font files.`);
|
|
5823
5854
|
}
|
|
5855
|
+
/** An environment variable name: letters, digits and `_`, not starting with a digit. */
|
|
5856
|
+
const ENV_NAME = /^[A-Z_a-z]\w*$/;
|
|
5857
|
+
/**
|
|
5858
|
+
* Whether a `build.env` value lists environment variable names.
|
|
5859
|
+
*
|
|
5860
|
+
* @param value - The configured `build.env`.
|
|
5861
|
+
* @returns True for an array of valid names.
|
|
5862
|
+
* @example
|
|
5863
|
+
* ```ts
|
|
5864
|
+
* isEnvNameList(["IS_DEVELOPMENT"]); // true
|
|
5865
|
+
* isEnvNameList(["process.env.X"]); // false
|
|
5866
|
+
* ```
|
|
5867
|
+
*/
|
|
5868
|
+
function isEnvNameList(value) {
|
|
5869
|
+
return Array.isArray(value) && value.every((name) => typeof name === "string" && ENV_NAME.test(name));
|
|
5870
|
+
}
|
|
5824
5871
|
/**
|
|
5825
5872
|
* Validates `build` config synchronously in `onInit` (return value discarded).
|
|
5826
5873
|
* Throws an actionable `[web] build.<field>` error when `outDir` is empty, or
|
|
@@ -5837,6 +5884,7 @@ function validateConfig$2(config) {
|
|
|
5837
5884
|
if (config.publicDir !== void 0 && typeof config.publicDir !== "string") throw new Error(`${ERROR_PREFIX$7}.publicDir: must be a string when set.`);
|
|
5838
5885
|
if (config.template !== void 0 && typeof config.template !== "string") throw new Error(`${ERROR_PREFIX$7}.template: must be a string path when set.`);
|
|
5839
5886
|
if (config.clientEntry !== void 0 && typeof config.clientEntry !== "string") throw new Error(`${ERROR_PREFIX$7}.clientEntry: must be a string path when set.`);
|
|
5887
|
+
if (config.env !== void 0 && !isEnvNameList(config.env)) throw new Error(`${ERROR_PREFIX$7}.env: must list environment variable names.\n Use names like "IS_DEVELOPMENT".`);
|
|
5840
5888
|
if (config.ogImage) validateFonts(config.ogImage);
|
|
5841
5889
|
}
|
|
5842
5890
|
//#endregion
|
|
@@ -10691,11 +10739,13 @@ function attachHistoryFallback(handlers, navigate = (pathname, _scrollToTop, sig
|
|
|
10691
10739
|
* @param navigation - The Navigation API object to attach the listener to.
|
|
10692
10740
|
* @param handlers - The navigation lifecycle callbacks.
|
|
10693
10741
|
* @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
|
|
10742
|
+
* @param isOwnPush - True while the kernel's programmatic navigation runs its own
|
|
10743
|
+
* `history.pushState`: the `navigate` event it fires is left alone, the kernel runs it once.
|
|
10694
10744
|
* @returns A teardown that removes the `navigate` listener.
|
|
10695
10745
|
* @example
|
|
10696
10746
|
* const dispose = attachNavigationApi(navigation, handlers);
|
|
10697
10747
|
*/
|
|
10698
|
-
function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal)) {
|
|
10748
|
+
function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal), isOwnPush = () => false) {
|
|
10699
10749
|
/**
|
|
10700
10750
|
* Handle a `navigate` event: classify, then intercept with fetch-and-swap.
|
|
10701
10751
|
*
|
|
@@ -10704,6 +10754,7 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
|
|
|
10704
10754
|
* navigation.addEventListener("navigate", onNavigate);
|
|
10705
10755
|
*/
|
|
10706
10756
|
const onNavigate = (navEvent) => {
|
|
10757
|
+
if (isOwnPush()) return;
|
|
10707
10758
|
const url = new URL(navEvent.destination.url);
|
|
10708
10759
|
if (!navEvent.canIntercept || navEvent.hashChange || navEvent.downloadRequest) return;
|
|
10709
10760
|
if (!isInternalLink(url)) return;
|
|
@@ -10736,13 +10787,15 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
|
|
|
10736
10787
|
*
|
|
10737
10788
|
* @param handlers - The navigation lifecycle callbacks the kernel supplies.
|
|
10738
10789
|
* @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
|
|
10790
|
+
* @param isOwnPush - True while the kernel runs its own `history.pushState` (Navigation API path
|
|
10791
|
+
* only; the History fallback never sees an event for it).
|
|
10739
10792
|
* @returns A teardown removing all attached listeners.
|
|
10740
10793
|
* @example
|
|
10741
|
-
* const dispose = attachRouter(handlers, navigate);
|
|
10794
|
+
* const dispose = attachRouter(handlers, navigate, () => pushingOwnUrl);
|
|
10742
10795
|
*/
|
|
10743
|
-
function attachRouter(handlers, navigate) {
|
|
10796
|
+
function attachRouter(handlers, navigate, isOwnPush) {
|
|
10744
10797
|
const navigation = getNavigation();
|
|
10745
|
-
return navigation ? attachNavigationApi(navigation, handlers, navigate) : attachHistoryFallback(handlers, navigate);
|
|
10798
|
+
return navigation ? attachNavigationApi(navigation, handlers, navigate, isOwnPush) : attachHistoryFallback(handlers, navigate);
|
|
10746
10799
|
}
|
|
10747
10800
|
//#endregion
|
|
10748
10801
|
//#region src/plugins/spa/state.ts
|
|
@@ -10907,6 +10960,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10907
10960
|
let progress;
|
|
10908
10961
|
let pendingScrollToTop = true;
|
|
10909
10962
|
let pendingTransition = resolveTransition(resolved.defaultTransition);
|
|
10963
|
+
let pushingOwnUrl = false;
|
|
10910
10964
|
/**
|
|
10911
10965
|
* Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
|
|
10912
10966
|
* consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
|
|
@@ -11219,8 +11273,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11219
11273
|
* Navigation API commits the address bar before the interceptor calls {@link navigate}; a
|
|
11220
11274
|
* programmatic call does NOT, so it must commit the URL itself first — otherwise the content
|
|
11221
11275
|
* swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
|
|
11222
|
-
* `history.pushState`
|
|
11223
|
-
*
|
|
11276
|
+
* Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
|
|
11277
|
+
* (Chrome, Edge, Safari 26): `pushingOwnUrl` marks it, so the interceptor does not run this
|
|
11278
|
+
* navigation a second time. The current scroll is saved first so a later back restores it.
|
|
11224
11279
|
*
|
|
11225
11280
|
* @param path - The destination path (pathname + optional search).
|
|
11226
11281
|
* @param scroll - The optional per-call scroll override.
|
|
@@ -11231,7 +11286,12 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11231
11286
|
if (typeof document === "undefined") return;
|
|
11232
11287
|
if (path !== currentLocationUrl()) {
|
|
11233
11288
|
saveScrollPosition(currentLocationUrl());
|
|
11234
|
-
|
|
11289
|
+
pushingOwnUrl = true;
|
|
11290
|
+
try {
|
|
11291
|
+
history.pushState({}, "", path);
|
|
11292
|
+
} finally {
|
|
11293
|
+
pushingOwnUrl = false;
|
|
11294
|
+
}
|
|
11235
11295
|
}
|
|
11236
11296
|
navigate(path, true, void 0, scroll).catch(() => {});
|
|
11237
11297
|
};
|
|
@@ -11260,7 +11320,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11260
11320
|
if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
|
|
11261
11321
|
progress = createProgressBar(resolved.progressBar);
|
|
11262
11322
|
state.currentUrl = currentLocationUrl();
|
|
11263
|
-
state.destroyRouter = attachRouter(handlers, navigate);
|
|
11323
|
+
state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
|
|
11264
11324
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
11265
11325
|
const hit = deps.router.match(matchPath);
|
|
11266
11326
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
package/dist/index.d.cts
CHANGED
|
@@ -1715,6 +1715,13 @@ type Config$3 = {
|
|
|
1715
1715
|
}; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
|
|
1716
1716
|
localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
|
|
1717
1717
|
clientEntry?: string;
|
|
1718
|
+
/**
|
|
1719
|
+
* Environment variable names the client bundle reads, e.g. `["IS_DEVELOPMENT", "IS_LOCAL"]`.
|
|
1720
|
+
* Each becomes a constant under `process.env.<NAME>` and `import.meta.env.<NAME>`: the value
|
|
1721
|
+
* the build process has, or `""` when unset. With `minify`, a branch on an unset flag is dropped
|
|
1722
|
+
* together with the dynamic `import()` chunk it guards. Default: none.
|
|
1723
|
+
*/
|
|
1724
|
+
env?: readonly string[];
|
|
1718
1725
|
/**
|
|
1719
1726
|
* Path to a custom HTML document shell, giving the app full control over the
|
|
1720
1727
|
* scaffold (charset, viewport, `<html lang>`, body attributes, wrapper markup).
|
package/dist/index.d.mts
CHANGED
|
@@ -1713,6 +1713,13 @@ type Config$3 = {
|
|
|
1713
1713
|
}; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
|
|
1714
1714
|
localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
|
|
1715
1715
|
clientEntry?: string;
|
|
1716
|
+
/**
|
|
1717
|
+
* Environment variable names the client bundle reads, e.g. `["IS_DEVELOPMENT", "IS_LOCAL"]`.
|
|
1718
|
+
* Each becomes a constant under `process.env.<NAME>` and `import.meta.env.<NAME>`: the value
|
|
1719
|
+
* the build process has, or `""` when unset. With `minify`, a branch on an unset flag is dropped
|
|
1720
|
+
* together with the dynamic `import()` chunk it guards. Default: none.
|
|
1721
|
+
*/
|
|
1722
|
+
env?: readonly string[];
|
|
1716
1723
|
/**
|
|
1717
1724
|
* Path to a custom HTML document shell, giving the app full control over the
|
|
1718
1725
|
* scaffold (charset, viewport, `<html lang>`, body attributes, wrapper markup).
|
package/dist/index.mjs
CHANGED
|
@@ -2313,10 +2313,11 @@ const CSS_EXTERNAL_FONT_GLOBS = [
|
|
|
2313
2313
|
* @param options.naming.chunk - Naming template for lazy split chunks.
|
|
2314
2314
|
* @param options.naming.asset - Naming template for additional emitted assets.
|
|
2315
2315
|
* @param options.external - Import/url() globs left unresolved in the output.
|
|
2316
|
+
* @param options.define - Global names replaced with constant expressions.
|
|
2316
2317
|
* @returns The structural build result.
|
|
2317
2318
|
* @example
|
|
2318
2319
|
* ```ts
|
|
2319
|
-
* await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [] });
|
|
2320
|
+
* await defaultRunner({ entrypoints: ["a.css"], outdir: "dist", minify: true, splitting: true, target: "browser", naming: FINGERPRINT_NAMING, external: [], define: {} });
|
|
2320
2321
|
* ```
|
|
2321
2322
|
*/
|
|
2322
2323
|
async function defaultRunner(options) {
|
|
@@ -2328,6 +2329,33 @@ async function defaultRunner(options) {
|
|
|
2328
2329
|
return bun.build(options);
|
|
2329
2330
|
}
|
|
2330
2331
|
/**
|
|
2332
|
+
* The bundle-time constants for the `build.env` names. Each name becomes `process.env.<NAME>` and
|
|
2333
|
+
* `import.meta.env.<NAME>`, and the whole `import.meta.env` object is set too, because
|
|
2334
|
+
* `browserEnv()` spreads it. The value is the build process's own variable, or `""` when unset.
|
|
2335
|
+
* With `minify`, a branch on an unset flag is removed together with the dynamic `import()` chunk
|
|
2336
|
+
* it guards, so developer-only code never ships in a build without the flag.
|
|
2337
|
+
*
|
|
2338
|
+
* @param names - The `build.env` names.
|
|
2339
|
+
* @param source - Where the values come from; the build process's environment by default.
|
|
2340
|
+
* @returns The `define` map for `Bun.build`; empty without names.
|
|
2341
|
+
* @example
|
|
2342
|
+
* ```ts
|
|
2343
|
+
* envDefines(["IS_DEVELOPMENT"], { IS_DEVELOPMENT: "true" });
|
|
2344
|
+
* // { "process.env.IS_DEVELOPMENT": "\"true\"", "import.meta.env.IS_DEVELOPMENT": "\"true\"",
|
|
2345
|
+
* // "import.meta.env": "{\"IS_DEVELOPMENT\":\"true\"}" }
|
|
2346
|
+
* ```
|
|
2347
|
+
*/
|
|
2348
|
+
function envDefines(names, source = process.env) {
|
|
2349
|
+
if (names.length === 0) return {};
|
|
2350
|
+
const values = Object.fromEntries(names.map((name) => [name, source[name] ?? ""]));
|
|
2351
|
+
const define = { "import.meta.env": JSON.stringify(values) };
|
|
2352
|
+
for (const [name, value] of Object.entries(values)) {
|
|
2353
|
+
define[`process.env.${name}`] = JSON.stringify(value);
|
|
2354
|
+
define[`import.meta.env.${name}`] = JSON.stringify(value);
|
|
2355
|
+
}
|
|
2356
|
+
return define;
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2331
2359
|
* Resolve the first existing entry file from a candidate list (project-relative).
|
|
2332
2360
|
*
|
|
2333
2361
|
* @param candidates - Ordered candidate paths to probe.
|
|
@@ -2396,12 +2424,13 @@ function normalizeAssetPath(absolutePath, outDir) {
|
|
|
2396
2424
|
* @param outDir - The publish root; stored asset paths are made relative to it.
|
|
2397
2425
|
* @param outdir - The bundler output directory (`<outDir>/assets`).
|
|
2398
2426
|
* @param minify - Whether to minify.
|
|
2427
|
+
* @param define - Global names replaced with constant expressions.
|
|
2399
2428
|
* @example
|
|
2400
2429
|
* ```ts
|
|
2401
|
-
* await runOne(ctx, runner, "css", ["a.css"], "dist", true);
|
|
2430
|
+
* await runOne(ctx, runner, "css", ["a.css"], "dist", true, {});
|
|
2402
2431
|
* ```
|
|
2403
2432
|
*/
|
|
2404
|
-
async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
2433
|
+
async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify, define) {
|
|
2405
2434
|
if (entrypoints.length === 0) return;
|
|
2406
2435
|
const result = await runner({
|
|
2407
2436
|
entrypoints,
|
|
@@ -2410,7 +2439,8 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
|
2410
2439
|
splitting: true,
|
|
2411
2440
|
target: "browser",
|
|
2412
2441
|
naming: FINGERPRINT_NAMING,
|
|
2413
|
-
external: kind === "css" ? [...CSS_EXTERNAL_FONT_GLOBS] : []
|
|
2442
|
+
external: kind === "css" ? [...CSS_EXTERNAL_FONT_GLOBS] : [],
|
|
2443
|
+
define
|
|
2414
2444
|
});
|
|
2415
2445
|
if (!result.success) throw new Error(`[web] build.bundle ${kind} build failed`);
|
|
2416
2446
|
const hashed = {};
|
|
@@ -2445,10 +2475,11 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
|
2445
2475
|
async function bundle(ctx, options = {}) {
|
|
2446
2476
|
const runner = options.runner ?? defaultRunner;
|
|
2447
2477
|
const { minify, outDir } = ctx.config;
|
|
2478
|
+
const define = envDefines(ctx.config.env ?? []);
|
|
2448
2479
|
const assetsDir = path.join(outDir, "assets");
|
|
2449
2480
|
const cssEntrypoints = options.cssEntrypoints ?? resolveEntrypoints(CSS_ENTRY_CANDIDATES);
|
|
2450
2481
|
const jsEntrypoints = options.jsEntrypoints ?? resolveJsEntrypoints(ctx);
|
|
2451
|
-
await Promise.all([runOne(ctx, runner, "css", cssEntrypoints, outDir, assetsDir, minify), runOne(ctx, runner, "js", jsEntrypoints, outDir, assetsDir, minify)]);
|
|
2482
|
+
await Promise.all([runOne(ctx, runner, "css", cssEntrypoints, outDir, assetsDir, minify, define), runOne(ctx, runner, "js", jsEntrypoints, outDir, assetsDir, minify, define)]);
|
|
2452
2483
|
}
|
|
2453
2484
|
//#endregion
|
|
2454
2485
|
//#region src/plugins/build/phases/asset-tags.ts
|
|
@@ -5777,6 +5808,22 @@ function validateFonts(og) {
|
|
|
5777
5808
|
if (isMissingFontDir(og.fontDir)) throw new Error(`${ERROR_PREFIX$7}.ogImage: fontDir "${og.fontDir}" does not exist — provide a directory with at least one font.`);
|
|
5778
5809
|
if (!readdirSync(og.fontDir).some((name) => FONT_EXTENSIONS.some((extension) => name.endsWith(extension)))) throw new Error(`${ERROR_PREFIX$7}.ogImage: fontDir "${og.fontDir}" contains no .ttf/.otf/.woff font files.`);
|
|
5779
5810
|
}
|
|
5811
|
+
/** An environment variable name: letters, digits and `_`, not starting with a digit. */
|
|
5812
|
+
const ENV_NAME = /^[A-Z_a-z]\w*$/;
|
|
5813
|
+
/**
|
|
5814
|
+
* Whether a `build.env` value lists environment variable names.
|
|
5815
|
+
*
|
|
5816
|
+
* @param value - The configured `build.env`.
|
|
5817
|
+
* @returns True for an array of valid names.
|
|
5818
|
+
* @example
|
|
5819
|
+
* ```ts
|
|
5820
|
+
* isEnvNameList(["IS_DEVELOPMENT"]); // true
|
|
5821
|
+
* isEnvNameList(["process.env.X"]); // false
|
|
5822
|
+
* ```
|
|
5823
|
+
*/
|
|
5824
|
+
function isEnvNameList(value) {
|
|
5825
|
+
return Array.isArray(value) && value.every((name) => typeof name === "string" && ENV_NAME.test(name));
|
|
5826
|
+
}
|
|
5780
5827
|
/**
|
|
5781
5828
|
* Validates `build` config synchronously in `onInit` (return value discarded).
|
|
5782
5829
|
* Throws an actionable `[web] build.<field>` error when `outDir` is empty, or
|
|
@@ -5793,6 +5840,7 @@ function validateConfig$2(config) {
|
|
|
5793
5840
|
if (config.publicDir !== void 0 && typeof config.publicDir !== "string") throw new Error(`${ERROR_PREFIX$7}.publicDir: must be a string when set.`);
|
|
5794
5841
|
if (config.template !== void 0 && typeof config.template !== "string") throw new Error(`${ERROR_PREFIX$7}.template: must be a string path when set.`);
|
|
5795
5842
|
if (config.clientEntry !== void 0 && typeof config.clientEntry !== "string") throw new Error(`${ERROR_PREFIX$7}.clientEntry: must be a string path when set.`);
|
|
5843
|
+
if (config.env !== void 0 && !isEnvNameList(config.env)) throw new Error(`${ERROR_PREFIX$7}.env: must list environment variable names.\n Use names like "IS_DEVELOPMENT".`);
|
|
5796
5844
|
if (config.ogImage) validateFonts(config.ogImage);
|
|
5797
5845
|
}
|
|
5798
5846
|
//#endregion
|
|
@@ -10647,11 +10695,13 @@ function attachHistoryFallback(handlers, navigate = (pathname, _scrollToTop, sig
|
|
|
10647
10695
|
* @param navigation - The Navigation API object to attach the listener to.
|
|
10648
10696
|
* @param handlers - The navigation lifecycle callbacks.
|
|
10649
10697
|
* @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
|
|
10698
|
+
* @param isOwnPush - True while the kernel's programmatic navigation runs its own
|
|
10699
|
+
* `history.pushState`: the `navigate` event it fires is left alone, the kernel runs it once.
|
|
10650
10700
|
* @returns A teardown that removes the `navigate` listener.
|
|
10651
10701
|
* @example
|
|
10652
10702
|
* const dispose = attachNavigationApi(navigation, handlers);
|
|
10653
10703
|
*/
|
|
10654
|
-
function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal)) {
|
|
10704
|
+
function attachNavigationApi(navigation, handlers, navigate = (pathname, _scrollToTop, signal) => performNavigation(pathname, handlers, signal), isOwnPush = () => false) {
|
|
10655
10705
|
/**
|
|
10656
10706
|
* Handle a `navigate` event: classify, then intercept with fetch-and-swap.
|
|
10657
10707
|
*
|
|
@@ -10660,6 +10710,7 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
|
|
|
10660
10710
|
* navigation.addEventListener("navigate", onNavigate);
|
|
10661
10711
|
*/
|
|
10662
10712
|
const onNavigate = (navEvent) => {
|
|
10713
|
+
if (isOwnPush()) return;
|
|
10663
10714
|
const url = new URL(navEvent.destination.url);
|
|
10664
10715
|
if (!navEvent.canIntercept || navEvent.hashChange || navEvent.downloadRequest) return;
|
|
10665
10716
|
if (!isInternalLink(url)) return;
|
|
@@ -10692,13 +10743,15 @@ function attachNavigationApi(navigation, handlers, navigate = (pathname, _scroll
|
|
|
10692
10743
|
*
|
|
10693
10744
|
* @param handlers - The navigation lifecycle callbacks the kernel supplies.
|
|
10694
10745
|
* @param navigate - The navigation strategy (defaults to HTML-over-fetch via `performNavigation`).
|
|
10746
|
+
* @param isOwnPush - True while the kernel runs its own `history.pushState` (Navigation API path
|
|
10747
|
+
* only; the History fallback never sees an event for it).
|
|
10695
10748
|
* @returns A teardown removing all attached listeners.
|
|
10696
10749
|
* @example
|
|
10697
|
-
* const dispose = attachRouter(handlers, navigate);
|
|
10750
|
+
* const dispose = attachRouter(handlers, navigate, () => pushingOwnUrl);
|
|
10698
10751
|
*/
|
|
10699
|
-
function attachRouter(handlers, navigate) {
|
|
10752
|
+
function attachRouter(handlers, navigate, isOwnPush) {
|
|
10700
10753
|
const navigation = getNavigation();
|
|
10701
|
-
return navigation ? attachNavigationApi(navigation, handlers, navigate) : attachHistoryFallback(handlers, navigate);
|
|
10754
|
+
return navigation ? attachNavigationApi(navigation, handlers, navigate, isOwnPush) : attachHistoryFallback(handlers, navigate);
|
|
10702
10755
|
}
|
|
10703
10756
|
//#endregion
|
|
10704
10757
|
//#region src/plugins/spa/state.ts
|
|
@@ -10863,6 +10916,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
10863
10916
|
let progress;
|
|
10864
10917
|
let pendingScrollToTop = true;
|
|
10865
10918
|
let pendingTransition = resolveTransition(resolved.defaultTransition);
|
|
10919
|
+
let pushingOwnUrl = false;
|
|
10866
10920
|
/**
|
|
10867
10921
|
* Resolve a {@link TransitionMode} into the runtime {@link SwapTransition} the swap
|
|
10868
10922
|
* consumes: `"none"` ⇒ disabled; `"crossfade"` ⇒ enabled, no named types (default root
|
|
@@ -11175,8 +11229,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11175
11229
|
* Navigation API commits the address bar before the interceptor calls {@link navigate}; a
|
|
11176
11230
|
* programmatic call does NOT, so it must commit the URL itself first — otherwise the content
|
|
11177
11231
|
* swaps but the address bar (and the back button / refresh / deep link) stays on the old URL.
|
|
11178
|
-
* `history.pushState`
|
|
11179
|
-
*
|
|
11232
|
+
* Where the Navigation API exists, `history.pushState` fires a `navigate` event synchronously
|
|
11233
|
+
* (Chrome, Edge, Safari 26): `pushingOwnUrl` marks it, so the interceptor does not run this
|
|
11234
|
+
* navigation a second time. The current scroll is saved first so a later back restores it.
|
|
11180
11235
|
*
|
|
11181
11236
|
* @param path - The destination path (pathname + optional search).
|
|
11182
11237
|
* @param scroll - The optional per-call scroll override.
|
|
@@ -11187,7 +11242,12 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11187
11242
|
if (typeof document === "undefined") return;
|
|
11188
11243
|
if (path !== currentLocationUrl()) {
|
|
11189
11244
|
saveScrollPosition(currentLocationUrl());
|
|
11190
|
-
|
|
11245
|
+
pushingOwnUrl = true;
|
|
11246
|
+
try {
|
|
11247
|
+
history.pushState({}, "", path);
|
|
11248
|
+
} finally {
|
|
11249
|
+
pushingOwnUrl = false;
|
|
11250
|
+
}
|
|
11191
11251
|
}
|
|
11192
11252
|
navigate(path, true, void 0, scroll).catch(() => {});
|
|
11193
11253
|
};
|
|
@@ -11216,7 +11276,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11216
11276
|
if (state.started) throw new Error(`${ERROR_PREFIX} spa kernel already started.\n Call app.stop() before booting again (single boot per app).`);
|
|
11217
11277
|
progress = createProgressBar(resolved.progressBar);
|
|
11218
11278
|
state.currentUrl = currentLocationUrl();
|
|
11219
|
-
state.destroyRouter = attachRouter(handlers, navigate);
|
|
11279
|
+
state.destroyRouter = attachRouter(handlers, navigate, () => pushingOwnUrl);
|
|
11220
11280
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
11221
11281
|
const hit = deps.router.match(matchPath);
|
|
11222
11282
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
package/package.json
CHANGED