@zenithbuild/cli 0.7.1 → 0.7.2

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 (52) hide show
  1. package/README.md +59 -1
  2. package/dist/adapters/adapter-netlify-static.d.ts +5 -0
  3. package/dist/adapters/adapter-netlify-static.js +39 -0
  4. package/dist/adapters/adapter-netlify.d.ts +5 -0
  5. package/dist/adapters/adapter-netlify.js +129 -0
  6. package/dist/adapters/adapter-node.d.ts +5 -0
  7. package/dist/adapters/adapter-node.js +121 -0
  8. package/dist/adapters/adapter-static.d.ts +5 -0
  9. package/dist/adapters/adapter-static.js +20 -0
  10. package/dist/adapters/adapter-types.d.ts +44 -0
  11. package/dist/adapters/adapter-types.js +65 -0
  12. package/dist/adapters/adapter-vercel-static.d.ts +5 -0
  13. package/dist/adapters/adapter-vercel-static.js +36 -0
  14. package/dist/adapters/adapter-vercel.d.ts +5 -0
  15. package/dist/adapters/adapter-vercel.js +99 -0
  16. package/dist/adapters/resolve-adapter.d.ts +5 -0
  17. package/dist/adapters/resolve-adapter.js +84 -0
  18. package/dist/adapters/route-rules.d.ts +7 -0
  19. package/dist/adapters/route-rules.js +88 -0
  20. package/dist/base-path-html.d.ts +2 -0
  21. package/dist/base-path-html.js +42 -0
  22. package/dist/base-path.d.ts +8 -0
  23. package/dist/base-path.js +74 -0
  24. package/dist/build/compiler-runtime.d.ts +2 -1
  25. package/dist/build/compiler-runtime.js +4 -1
  26. package/dist/build/page-loop.d.ts +2 -2
  27. package/dist/build/page-loop.js +3 -3
  28. package/dist/build-output-manifest.d.ts +28 -0
  29. package/dist/build-output-manifest.js +100 -0
  30. package/dist/build.js +42 -11
  31. package/dist/config.d.ts +10 -46
  32. package/dist/config.js +162 -28
  33. package/dist/dev-build-session.d.ts +1 -0
  34. package/dist/dev-build-session.js +4 -5
  35. package/dist/framework-components/Image.zen +31 -9
  36. package/dist/images/payload.d.ts +2 -1
  37. package/dist/images/payload.js +3 -2
  38. package/dist/images/runtime.js +6 -5
  39. package/dist/images/service.js +2 -2
  40. package/dist/images/shared.d.ts +4 -2
  41. package/dist/images/shared.js +8 -3
  42. package/dist/index.js +36 -15
  43. package/dist/manifest.d.ts +14 -2
  44. package/dist/manifest.js +49 -6
  45. package/dist/preview.js +61 -25
  46. package/dist/server-output.d.ts +26 -0
  47. package/dist/server-output.js +297 -0
  48. package/dist/server-runtime/node-server.d.ts +2 -0
  49. package/dist/server-runtime/node-server.js +354 -0
  50. package/dist/server-runtime/route-render.d.ts +64 -0
  51. package/dist/server-runtime/route-render.js +273 -0
  52. package/package.json +3 -2
package/dist/build.js CHANGED
@@ -1,4 +1,9 @@
1
- import { resolve } from 'node:path';
1
+ import { mkdir, rm } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
4
+ import { normalizeBasePath } from './base-path.js';
5
+ import { rewriteSoftNavigationHrefBasePathInHtmlFiles } from './base-path-html.js';
6
+ import { writeBuildOutputManifest } from './build-output-manifest.js';
2
7
  import { generateManifest } from './manifest.js';
3
8
  import { buildComponentRegistry } from './resolve-components.js';
4
9
  import { collectAssets, createCompilerWarningEmitter, runBundler } from './build/compiler-runtime.js';
@@ -8,6 +13,7 @@ import { materializeImageMarkupInHtmlFiles } from './images/materialize.js';
8
13
  import { buildImageArtifacts } from './images/service.js';
9
14
  import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } from './images/payload.js';
10
15
  import { createStartupProfiler } from './startup-profile.js';
16
+ import { writeServerOutput } from './server-output.js';
11
17
  import { resolveBundlerBin } from './toolchain-paths.js';
12
18
  import { createBundlerToolchain, createCompilerToolchain, ensureToolchainCompatibility, getActiveToolchainCandidate } from './toolchain-runner.js';
13
19
  import { maybeWarnAboutZenithVersionMismatch } from './version-check.js';
@@ -41,15 +47,18 @@ export async function build(options) {
41
47
  const { pagesDir, outDir, config = {}, logger = null, showBundlerInfo = true } = options;
42
48
  const startupProfile = createStartupProfiler('cli-build');
43
49
  const projectRoot = deriveProjectRootFromPagesDir(pagesDir);
50
+ const coreOutputDir = join(projectRoot, '.zenith-output');
51
+ const staticOutputDir = join(coreOutputDir, 'static');
44
52
  const srcDir = resolve(pagesDir, '..');
45
53
  const compilerBin = createCompilerToolchain({ projectRoot, logger });
46
54
  const bundlerBin = createBundlerToolchain({ projectRoot, logger });
47
55
  const compilerTotals = createCompilerTotals();
48
- const softNavigationEnabled = config.softNavigation === true || config.router === true;
56
+ const { target, adapter, mode } = resolveBuildAdapter(config);
57
+ const basePath = normalizeBasePath(config.basePath || '/');
58
+ const routerEnabled = config.router === true;
49
59
  const compilerOpts = {
50
60
  typescriptDefault: config.typescriptDefault === true,
51
- experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true
52
- || config.experimental?.embeddedMarkupExpressions === true,
61
+ experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true,
53
62
  strictDomLints: config.strictDomLints === true
54
63
  };
55
64
  ensureToolchainCompatibility(bundlerBin);
@@ -64,11 +73,16 @@ export async function build(options) {
64
73
  }
65
74
  const registry = startupProfile.measureSync('build_component_registry', () => buildComponentRegistry(srcDir));
66
75
  void RUNTIME_MARKUP_BINDING;
67
- const manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(pagesDir));
76
+ const manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(pagesDir, '.zen', { compilerOpts }));
77
+ if (mode !== 'legacy') {
78
+ adapter.validateRoutes(manifest);
79
+ }
68
80
  await startupProfile.measureAsync('ensure_zenith_type_declarations', () => ensureZenithTypeDeclarations({
69
81
  manifest,
70
82
  pagesDir
71
83
  }));
84
+ await startupProfile.measureAsync('reset_core_output', () => rm(coreOutputDir, { recursive: true, force: true }));
85
+ await startupProfile.measureAsync('prepare_core_output', () => mkdir(staticOutputDir, { recursive: true }));
72
86
  const emitCompilerWarning = createCompilerWarningEmitter((line) => {
73
87
  if (logger && typeof logger.warn === 'function') {
74
88
  logger.warn(line, { onceKey: `compiler-warning:${line}` });
@@ -83,29 +97,46 @@ export async function build(options) {
83
97
  registry,
84
98
  compilerOpts,
85
99
  compilerBin,
86
- softNavigationEnabled,
100
+ routerEnabled,
87
101
  startupProfile,
88
102
  compilerTotals,
89
103
  emitCompilerWarning
90
104
  });
91
105
  if (envelopes.length > 0) {
92
- await startupProfile.measureAsync('run_bundler', () => runBundler(envelopes, outDir, projectRoot, logger, showBundlerInfo, bundlerBin), { envelopes: envelopes.length });
106
+ await startupProfile.measureAsync('run_bundler', () => runBundler(envelopes, staticOutputDir, projectRoot, logger, showBundlerInfo, bundlerBin, { basePath }), { envelopes: envelopes.length });
93
107
  }
108
+ await startupProfile.measureAsync('rewrite_soft_navigation_base_path', () => rewriteSoftNavigationHrefBasePathInHtmlFiles(staticOutputDir, basePath));
94
109
  const { manifest: imageManifest } = await startupProfile.measureAsync('build_image_artifacts', () => buildImageArtifacts({
95
110
  projectRoot,
96
- outDir,
111
+ outDir: staticOutputDir,
97
112
  config: config.images
98
113
  }));
99
- const imageRuntimePayload = createImageRuntimePayload(config.images, imageManifest, 'passthrough');
114
+ const imageRuntimePayload = createImageRuntimePayload(config.images, imageManifest, 'passthrough', basePath);
100
115
  await startupProfile.measureAsync('materialize_image_markup', () => materializeImageMarkupInHtmlFiles({
101
- distDir: outDir,
116
+ distDir: staticOutputDir,
102
117
  payload: imageRuntimePayload
103
118
  }));
104
- await startupProfile.measureAsync('inject_image_runtime_payload', () => injectImageRuntimePayloadIntoHtmlFiles(outDir, imageRuntimePayload));
119
+ await startupProfile.measureAsync('inject_image_runtime_payload', () => injectImageRuntimePayloadIntoHtmlFiles(staticOutputDir, imageRuntimePayload));
120
+ const buildManifest = await startupProfile.measureAsync('write_core_manifest', () => writeBuildOutputManifest({
121
+ coreOutputDir,
122
+ staticDir: staticOutputDir,
123
+ target,
124
+ routeManifest: manifest,
125
+ basePath
126
+ }));
127
+ await startupProfile.measureAsync('write_server_output', () => writeServerOutput({
128
+ coreOutputDir,
129
+ staticDir: staticOutputDir,
130
+ projectRoot,
131
+ config,
132
+ basePath
133
+ }));
134
+ await startupProfile.measureAsync('adapt_output', () => adapter.adapt({ coreOutput: coreOutputDir, outDir, manifest: buildManifest, config }));
105
135
  const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
106
136
  startupProfile.emit('build_complete', {
107
137
  pages: manifest.length,
108
138
  assets: assets.length,
139
+ target,
109
140
  compilerTotals,
110
141
  expressionRewriteMetrics
111
142
  });
package/dist/config.d.ts CHANGED
@@ -1,55 +1,19 @@
1
- export function validateConfig(config: any): {
2
- images: {
3
- formats: string[];
4
- deviceSizes: number[];
5
- imageSizes: number[];
6
- remotePatterns: any[];
7
- quality: number;
8
- allowSvg: boolean;
9
- maxRemoteBytes: number;
10
- maxPixels: number;
11
- minimumCacheTTL: number;
12
- dangerouslyAllowLocalNetwork: boolean;
13
- };
14
- router: boolean;
15
- embeddedMarkupExpressions: boolean;
16
- types: boolean;
17
- typescriptDefault: boolean;
18
- outDir: string;
19
- pagesDir: string;
20
- experimental: {};
21
- strictDomLints: boolean;
22
- };
23
- export function loadConfig(projectRoot: any): Promise<{
24
- images: {
25
- formats: string[];
26
- deviceSizes: number[];
27
- imageSizes: number[];
28
- remotePatterns: any[];
29
- quality: number;
30
- allowSvg: boolean;
31
- maxRemoteBytes: number;
32
- maxPixels: number;
33
- minimumCacheTTL: number;
34
- dangerouslyAllowLocalNetwork: boolean;
35
- };
36
- router: boolean;
37
- embeddedMarkupExpressions: boolean;
38
- types: boolean;
39
- typescriptDefault: boolean;
40
- outDir: string;
41
- pagesDir: string;
42
- experimental: {};
43
- strictDomLints: boolean;
44
- }>;
1
+ export function hasExplicitConfigKey(config: any, key: any): boolean;
2
+ export function isLoadedConfig(config: any): boolean;
3
+ export function isConfigKeyExplicit(config: any, key: any): boolean;
4
+ export function resolveConfigPagesDir(projectRoot: any, config: any): string;
5
+ export function resolveConfigOutDir(projectRoot: any, config: any): string;
6
+ export function validateConfig(config: any): any;
7
+ export function loadConfig(projectRoot: any): Promise<any>;
45
8
  export namespace DEFAULT_CONFIG {
46
9
  let router: boolean;
47
10
  let embeddedMarkupExpressions: boolean;
48
- let types: boolean;
49
11
  let typescriptDefault: boolean;
50
12
  let outDir: string;
51
13
  let pagesDir: string;
52
- let experimental: {};
14
+ let basePath: string;
15
+ let target: string;
16
+ let adapter: null;
53
17
  let strictDomLints: boolean;
54
18
  let images: {
55
19
  formats: string[];
package/dist/config.js CHANGED
@@ -1,31 +1,166 @@
1
- import { join } from 'node:path';
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { join, resolve } from 'node:path';
2
5
  import { pathToFileURL } from 'node:url';
6
+ import { KNOWN_TARGETS } from './adapters/adapter-types.js';
7
+ import { normalizeBasePath } from './base-path.js';
3
8
  import { normalizeImageConfig } from './images/shared.js';
9
+ const PACKAGE_REQUIRE = createRequire(import.meta.url);
10
+ const CONFIG_FILES = ['zenith.config.ts', 'zenith.config.js'];
11
+ const CONFIG_META = Symbol('zenith.config.meta');
4
12
  export const DEFAULT_CONFIG = {
5
13
  router: false,
6
14
  embeddedMarkupExpressions: false,
7
- types: true,
8
15
  typescriptDefault: true,
9
16
  outDir: 'dist',
10
17
  pagesDir: 'pages',
11
- experimental: {},
18
+ basePath: '/',
19
+ target: 'static',
20
+ adapter: null,
12
21
  strictDomLints: false,
13
22
  images: normalizeImageConfig()
14
23
  };
15
24
  const TOP_LEVEL_SCHEMA = {
16
25
  router: 'boolean',
17
26
  embeddedMarkupExpressions: 'boolean',
18
- types: 'boolean',
19
27
  typescriptDefault: 'boolean',
20
28
  outDir: 'string',
21
29
  pagesDir: 'string',
22
- experimental: 'object',
30
+ basePath: 'string',
31
+ target: 'string',
32
+ adapter: 'object',
23
33
  strictDomLints: 'boolean',
24
34
  images: 'object'
25
35
  };
36
+ function attachConfigMeta(config, explicitKeys) {
37
+ Object.defineProperty(config, CONFIG_META, {
38
+ value: { explicitKeys: new Set(explicitKeys), loaded: false },
39
+ enumerable: false,
40
+ configurable: true,
41
+ writable: true
42
+ });
43
+ return config;
44
+ }
45
+ function markLoaded(config) {
46
+ if (config?.[CONFIG_META]) {
47
+ config[CONFIG_META].loaded = true;
48
+ }
49
+ return config;
50
+ }
51
+ function validateAdapterValue(value) {
52
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
53
+ throw new Error('[Zenith:Config] Key "adapter" must be a plain object');
54
+ }
55
+ if (typeof value.name !== 'string' || value.name.trim().length === 0) {
56
+ throw new Error('[Zenith:Config] Key "adapter.name" must be a non-empty string');
57
+ }
58
+ if (typeof value.validateRoutes !== 'function') {
59
+ throw new Error('[Zenith:Config] Key "adapter.validateRoutes" must be a function');
60
+ }
61
+ if (typeof value.adapt !== 'function') {
62
+ throw new Error('[Zenith:Config] Key "adapter.adapt" must be a function');
63
+ }
64
+ return value;
65
+ }
66
+ function resolveConfigFile(projectRoot) {
67
+ const matches = CONFIG_FILES
68
+ .map((name) => join(projectRoot, name))
69
+ .filter((candidate) => existsSync(candidate));
70
+ if (matches.length > 1) {
71
+ throw new Error(`[Zenith:Config] Multiple config files found. Keep exactly one of: ${CONFIG_FILES.join(', ')}`);
72
+ }
73
+ return matches[0] || null;
74
+ }
75
+ function resolveTypeScriptApi(projectRoot) {
76
+ try {
77
+ const projectRequire = createRequire(join(projectRoot, '__zenith_config_loader__.js'));
78
+ return projectRequire('typescript');
79
+ }
80
+ catch {
81
+ try {
82
+ return PACKAGE_REQUIRE('typescript');
83
+ }
84
+ catch {
85
+ throw new Error('[Zenith:Config] zenith.config.ts requires the `typescript` package to be installed.');
86
+ }
87
+ }
88
+ }
89
+ async function importTypescriptConfig(configPath, projectRoot) {
90
+ const source = await readFile(configPath, 'utf8');
91
+ const ts = resolveTypeScriptApi(projectRoot);
92
+ const transpiled = ts.transpileModule(source, {
93
+ compilerOptions: {
94
+ module: ts.ModuleKind.ESNext,
95
+ target: ts.ScriptTarget.ES2022,
96
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
97
+ esModuleInterop: true,
98
+ allowSyntheticDefaultImports: true
99
+ },
100
+ fileName: configPath
101
+ }).outputText;
102
+ const tempConfigPath = join(projectRoot, `.zenith.config.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.mjs`);
103
+ await writeFile(tempConfigPath, transpiled, 'utf8');
104
+ try {
105
+ return await import(`${pathToFileURL(tempConfigPath).href}?t=${Date.now()}`);
106
+ }
107
+ finally {
108
+ await rm(tempConfigPath, { force: true });
109
+ }
110
+ }
111
+ async function importJavascriptConfig(configPath, projectRoot) {
112
+ const source = await readFile(configPath, 'utf8');
113
+ const isCommonJs = /\bmodule\.exports\b|\bexports\./.test(source);
114
+ const tempConfigPath = join(projectRoot, `.zenith.config.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.${isCommonJs ? 'cjs' : 'mjs'}`);
115
+ await writeFile(tempConfigPath, source, 'utf8');
116
+ try {
117
+ if (isCommonJs) {
118
+ const projectRequire = createRequire(join(projectRoot, '__zenith_config_loader__.js'));
119
+ const resolvedPath = projectRequire.resolve(tempConfigPath);
120
+ const requireCache = projectRequire.cache || PACKAGE_REQUIRE.cache;
121
+ if (requireCache && resolvedPath in requireCache) {
122
+ delete requireCache[resolvedPath];
123
+ }
124
+ return projectRequire(tempConfigPath);
125
+ }
126
+ return await import(`${pathToFileURL(tempConfigPath).href}?t=${Date.now()}`);
127
+ }
128
+ finally {
129
+ await rm(tempConfigPath, { force: true });
130
+ }
131
+ }
132
+ export function hasExplicitConfigKey(config, key) {
133
+ return Boolean(config?.[CONFIG_META]?.explicitKeys?.has(key));
134
+ }
135
+ export function isLoadedConfig(config) {
136
+ return Boolean(config?.[CONFIG_META]?.loaded === true);
137
+ }
138
+ export function isConfigKeyExplicit(config, key) {
139
+ if (config && typeof config === 'object' && config[CONFIG_META]) {
140
+ return hasExplicitConfigKey(config, key);
141
+ }
142
+ return Boolean(config && Object.prototype.hasOwnProperty.call(config, key));
143
+ }
144
+ export function resolveConfigPagesDir(projectRoot, config) {
145
+ if (isConfigKeyExplicit(config, 'pagesDir')) {
146
+ return resolve(projectRoot, config.pagesDir);
147
+ }
148
+ const rootPagesDir = join(projectRoot, 'pages');
149
+ if (existsSync(rootPagesDir)) {
150
+ return rootPagesDir;
151
+ }
152
+ const srcPagesDir = join(projectRoot, 'src', 'pages');
153
+ if (existsSync(srcPagesDir)) {
154
+ return srcPagesDir;
155
+ }
156
+ return resolve(projectRoot, config?.pagesDir || DEFAULT_CONFIG.pagesDir);
157
+ }
158
+ export function resolveConfigOutDir(projectRoot, config) {
159
+ return resolve(projectRoot, config?.outDir || DEFAULT_CONFIG.outDir);
160
+ }
26
161
  export function validateConfig(config) {
27
162
  if (config === null || config === undefined) {
28
- return { ...DEFAULT_CONFIG, images: normalizeImageConfig() };
163
+ return attachConfigMeta({ ...DEFAULT_CONFIG, images: normalizeImageConfig() }, []);
29
164
  }
30
165
  if (typeof config !== 'object' || Array.isArray(config)) {
31
166
  throw new Error('[Zenith:Config] Config must be a plain object');
@@ -35,6 +170,9 @@ export function validateConfig(config) {
35
170
  throw new Error(`[Zenith:Config] Unknown key: "${key}"`);
36
171
  }
37
172
  }
173
+ if ('target' in config && 'adapter' in config) {
174
+ throw new Error('[Zenith:Config] Keys "target" and "adapter" are mutually exclusive');
175
+ }
38
176
  const result = {
39
177
  ...DEFAULT_CONFIG,
40
178
  images: normalizeImageConfig(DEFAULT_CONFIG.images)
@@ -48,11 +186,8 @@ export function validateConfig(config) {
48
186
  result.images = normalizeImageConfig(value);
49
187
  continue;
50
188
  }
51
- if (expectedType === 'object') {
52
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
53
- throw new Error(`[Zenith:Config] Key "${key}" must be a plain object`);
54
- }
55
- result[key] = { ...value };
189
+ if (key === 'adapter') {
190
+ result.adapter = validateAdapterValue(value);
56
191
  continue;
57
192
  }
58
193
  if (typeof value !== expectedType) {
@@ -61,26 +196,25 @@ export function validateConfig(config) {
61
196
  if (expectedType === 'string' && value.trim().length === 0) {
62
197
  throw new Error(`[Zenith:Config] Key "${key}" must be a non-empty string`);
63
198
  }
199
+ if (key === 'target' && !KNOWN_TARGETS.includes(value)) {
200
+ throw new Error(`[Zenith:Config] Unsupported target: "${value}"`);
201
+ }
202
+ if (key === 'basePath') {
203
+ result.basePath = normalizeBasePath(value);
204
+ continue;
205
+ }
64
206
  result[key] = value;
65
207
  }
66
- return result;
208
+ return attachConfigMeta(result, Object.keys(config));
67
209
  }
68
210
  export async function loadConfig(projectRoot) {
69
- const configPath = join(projectRoot, 'zenith.config.js');
70
- try {
71
- const url = pathToFileURL(configPath).href;
72
- const mod = await import(url);
73
- return validateConfig(mod.default || mod);
74
- }
75
- catch (error) {
76
- const code = typeof error?.code === 'string' ? error.code : '';
77
- const message = typeof error?.message === 'string' ? error.message : '';
78
- if (code === 'ERR_MODULE_NOT_FOUND'
79
- || code === 'ENOENT'
80
- || message.includes('Cannot find module')
81
- || message.includes('ENOENT')) {
82
- return validateConfig(null);
83
- }
84
- throw error;
211
+ const resolvedProjectRoot = resolve(projectRoot);
212
+ const configPath = resolveConfigFile(resolvedProjectRoot);
213
+ if (!configPath) {
214
+ return markLoaded(validateConfig(null));
85
215
  }
216
+ const mod = configPath.endsWith('.ts')
217
+ ? await importTypescriptConfig(configPath, resolvedProjectRoot)
218
+ : await importJavascriptConfig(configPath, resolvedProjectRoot);
219
+ return markLoaded(validateConfig(mod.default || mod));
86
220
  }
@@ -6,6 +6,7 @@ export function createDevBuildSession(options: any): {
6
6
  }>;
7
7
  getImageRuntimePayload(): {
8
8
  mode: string;
9
+ basePath: string;
9
10
  config: {
10
11
  formats: string[];
11
12
  deviceSizes: number[];
@@ -237,11 +237,10 @@ export function createDevBuildSession(options) {
237
237
  const srcDir = resolve(resolvedPagesDir, '..');
238
238
  const compilerBin = createCompilerToolchain({ projectRoot, logger });
239
239
  const bundlerBin = createBundlerToolchain({ projectRoot, logger });
240
- const softNavigationEnabled = config.softNavigation === true || config.router === true;
240
+ const routerEnabled = config.router === true;
241
241
  const compilerOpts = {
242
242
  typescriptDefault: config.typescriptDefault === true,
243
- experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true
244
- || config.experimental?.embeddedMarkupExpressions === true,
243
+ experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true,
245
244
  strictDomLints: config.strictDomLints === true
246
245
  };
247
246
  ensureToolchainCompatibility(bundlerBin);
@@ -308,7 +307,7 @@ export function createDevBuildSession(options) {
308
307
  registry: state.registry,
309
308
  compilerOpts,
310
309
  compilerBin,
311
- softNavigationEnabled,
310
+ routerEnabled,
312
311
  startupProfile,
313
312
  compilerTotals,
314
313
  emitCompilerWarning,
@@ -356,7 +355,7 @@ export function createDevBuildSession(options) {
356
355
  registry: state.registry,
357
356
  compilerOpts,
358
357
  compilerBin,
359
- softNavigationEnabled,
358
+ routerEnabled,
360
359
  startupProfile,
361
360
  compilerTotals,
362
361
  emitCompilerWarning,
@@ -32,6 +32,24 @@ function normalizeFormat(value: unknown): string {
32
32
  return String(value || "").trim().toLowerCase().replace(/^\./, "");
33
33
  }
34
34
 
35
+ function normalizeBasePath(value: unknown): string {
36
+ const raw = safeString(value);
37
+ if (!raw || raw === "/") return "/";
38
+ if (!raw.startsWith("/")) return "/";
39
+ const normalized = raw.replace(/\/{2,}/g, "/").replace(/\/+$/g, "");
40
+ return normalized || "/";
41
+ }
42
+
43
+ function prependBasePath(basePath: string, pathname: string): string {
44
+ const normalizedBasePath = normalizeBasePath(basePath);
45
+ if (normalizedBasePath === "/") return pathname;
46
+ if (pathname === "/") return normalizedBasePath;
47
+ if (pathname === normalizedBasePath || pathname.startsWith(normalizedBasePath + "/")) {
48
+ return pathname;
49
+ }
50
+ return `${normalizedBasePath}${pathname}`;
51
+ }
52
+
35
53
  function isRemoteUrl(value: string): boolean {
36
54
  return /^https?:\/\//i.test(value);
37
55
  }
@@ -71,17 +89,20 @@ function buildLocalImageKey(publicPath: string): string {
71
89
  return (hash >>> 0).toString(16).padStart(8, "0");
72
90
  }
73
91
 
74
- function buildLocalVariantPath(publicPath: string, width: number, quality: number, format: string): string {
75
- return `/_zenith/image/local/${buildLocalImageKey(publicPath)}/w${width}-q${quality}.${normalizeFormat(format)}`;
92
+ function buildLocalVariantPath(publicPath: string, width: number, quality: number, format: string, basePath: string): string {
93
+ return prependBasePath(
94
+ basePath,
95
+ `/_zenith/image/local/${buildLocalImageKey(publicPath)}/w${width}-q${quality}.${normalizeFormat(format)}`
96
+ );
76
97
  }
77
98
 
78
- function buildRemoteVariantPath(remoteUrl: string, width: number, quality: number, format: string): string {
99
+ function buildRemoteVariantPath(remoteUrl: string, width: number, quality: number, format: string, basePath: string): string {
79
100
  const query = new URLSearchParams();
80
101
  query.set("url", remoteUrl);
81
102
  query.set("w", String(width));
82
103
  query.set("q", String(quality));
83
104
  if (format) query.set("f", normalizeFormat(format));
84
- return `/_zenith/image?${query.toString()}`;
105
+ return `${prependBasePath(basePath, "/_zenith/image")}?${query.toString()}`;
85
106
  }
86
107
 
87
108
  function escapeRegex(value: string): string {
@@ -227,6 +248,7 @@ function renderImage(): string {
227
248
  if (!source || !alt) return "";
228
249
 
229
250
  const config = isPlainObject(payload.config) ? payload.config : {};
251
+ const basePath = normalizeBasePath((payload as any).basePath);
230
252
  const className = safeString(rawProps.class);
231
253
  const style = mergeStyle(rawProps.style, rawProps.fit, rawProps.position);
232
254
  const loading = rawProps.priority === true ? "eager" : safeString(rawProps.loading) || "lazy";
@@ -248,8 +270,8 @@ function renderImage(): string {
248
270
  const fallbackWidth = widths.length > 0 ? widths[widths.length - 1] : (width || manifestEntry?.width || 0);
249
271
  model = {
250
272
  src: rawProps.unoptimized === true
251
- ? source.path
252
- : buildLocalVariantPath(source.path, fallbackWidth, quality, fallbackFormat),
273
+ ? prependBasePath(basePath, source.path)
274
+ : buildLocalVariantPath(source.path, fallbackWidth, quality, fallbackFormat, basePath),
253
275
  width,
254
276
  height,
255
277
  sizes,
@@ -258,7 +280,7 @@ function renderImage(): string {
258
280
  : sourceFormats.map((format) => ({
259
281
  type: mimeTypeForFormat(format),
260
282
  sizes,
261
- srcset: widths.map((candidate) => `${buildLocalVariantPath(source.path, candidate, quality, format)} ${candidate}w`).join(", ")
283
+ srcset: widths.map((candidate) => `${buildLocalVariantPath(source.path, candidate, quality, format, basePath)} ${candidate}w`).join(", ")
262
284
  })).filter((entry) => entry.type && entry.srcset)
263
285
  };
264
286
  } else {
@@ -272,14 +294,14 @@ function renderImage(): string {
272
294
  model = { src: source.url, width, height, sizes, sources: [] };
273
295
  } else {
274
296
  model = {
275
- src: buildRemoteVariantPath(source.url, widths[widths.length - 1] || width, quality, ""),
297
+ src: buildRemoteVariantPath(source.url, widths[widths.length - 1] || width, quality, "", basePath),
276
298
  width,
277
299
  height,
278
300
  sizes,
279
301
  sources: ((config.formats as string[]) || []).map((format) => ({
280
302
  type: mimeTypeForFormat(format),
281
303
  sizes,
282
- srcset: widths.map((candidate) => `${buildRemoteVariantPath(source.url, candidate, quality, format)} ${candidate}w`).join(", ")
304
+ srcset: widths.map((candidate) => `${buildRemoteVariantPath(source.url, candidate, quality, format, basePath)} ${candidate}w`).join(", ")
283
305
  })).filter((entry) => entry.type && entry.srcset)
284
306
  };
285
307
  }
@@ -1,5 +1,6 @@
1
- export function createImageRuntimePayload(config: any, localImages: any, mode?: string): {
1
+ export function createImageRuntimePayload(config: any, localImages: any, mode?: string, basePath?: string): {
2
2
  mode: string;
3
+ basePath: string;
3
4
  config: {
4
5
  formats: string[];
5
6
  deviceSizes: number[];
@@ -1,9 +1,10 @@
1
1
  import { imageRuntimeGlobalName, normalizeImageConfig, normalizeImageRuntimePayload } from './shared.js';
2
2
  import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- export function createImageRuntimePayload(config, localImages, mode = 'passthrough') {
4
+ export function createImageRuntimePayload(config, localImages, mode = 'passthrough', basePath = '/') {
5
5
  return normalizeImageRuntimePayload({
6
6
  mode,
7
+ basePath,
7
8
  config: normalizeImageConfig(config),
8
9
  localImages: localImages && typeof localImages === 'object' ? localImages : {}
9
10
  });
@@ -17,7 +18,7 @@ function serializeInlineScriptJson(payload) {
17
18
  .replace(/\u2029/g, '\\u2029');
18
19
  }
19
20
  export function injectImageRuntimePayload(html, payload) {
20
- const safePayload = createImageRuntimePayload(payload?.config || {}, payload?.localImages || {}, payload?.mode || 'passthrough');
21
+ const safePayload = createImageRuntimePayload(payload?.config || {}, payload?.localImages || {}, payload?.mode || 'passthrough', payload?.basePath || '/');
21
22
  const globalName = imageRuntimeGlobalName();
22
23
  const serialized = serializeInlineScriptJson(safePayload);
23
24
  const scriptTag = `<script id="zenith-image-runtime">window.${globalName} = ${serialized};</script>`;
@@ -1,3 +1,4 @@
1
+ import { prependBasePath } from '../base-path.js';
1
2
  import { buildLocalVariantPath, buildRemoteVariantPath, imageRuntimeGlobalName, matchRemotePattern, normalizeImageRuntimePayload, normalizeImageSource, resolveWidthCandidates } from './shared.js';
2
3
  function safeString(value) {
3
4
  if (typeof value === 'string') {
@@ -128,14 +129,14 @@ function buildLocalImageModel(props, payload, source) {
128
129
  : true);
129
130
  const fallbackWidth = widths.length > 0 ? widths[widths.length - 1] : width;
130
131
  const imgSrc = props.unoptimized === true
131
- ? source.path
132
- : buildLocalVariantPath(source.path, fallbackWidth || width || manifestEntry?.width || 0, quality, fallbackFormat);
132
+ ? prependBasePath(payload?.basePath || '/', source.path)
133
+ : buildLocalVariantPath(source.path, fallbackWidth || width || manifestEntry?.width || 0, quality, fallbackFormat, payload?.basePath || '/');
133
134
  const sources = props.unoptimized === true
134
135
  ? []
135
136
  : sourceFormats.map((format) => ({
136
137
  type: mimeTypeForFormat(format),
137
138
  sizes,
138
- srcset: widths.map((candidate) => `${buildLocalVariantPath(source.path, candidate, quality, format)} ${candidate}w`).join(', ')
139
+ srcset: widths.map((candidate) => `${buildLocalVariantPath(source.path, candidate, quality, format, payload?.basePath || '/')} ${candidate}w`).join(', ')
139
140
  })).filter((entry) => entry.type && entry.srcset);
140
141
  return {
141
142
  src: imgSrc,
@@ -168,10 +169,10 @@ function buildRemoteImageModel(props, payload, source) {
168
169
  const sources = (config.formats || []).map((format) => ({
169
170
  type: mimeTypeForFormat(format),
170
171
  sizes,
171
- srcset: widths.map((candidate) => `${buildRemoteVariantPath(source.url, candidate, quality, format)} ${candidate}w`).join(', ')
172
+ srcset: widths.map((candidate) => `${buildRemoteVariantPath(source.url, candidate, quality, format, payload?.basePath || '/')} ${candidate}w`).join(', ')
172
173
  })).filter((entry) => entry.type && entry.srcset);
173
174
  return {
174
- src: buildRemoteVariantPath(source.url, widths[widths.length - 1] || width, quality, ''),
175
+ src: buildRemoteVariantPath(source.url, widths[widths.length - 1] || width, quality, '', payload?.basePath || '/'),
175
176
  width,
176
177
  height,
177
178
  sizes,
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { mkdir, readFile, stat, writeFile, readdir } from 'node:fs/promises';
4
4
  import { dirname, extname, join, relative, resolve } from 'node:path';
5
5
  import sharp from 'sharp';
6
- import { buildLocalImageKey, buildLocalVariantPath, matchRemotePattern, normalizeImageConfig, normalizeImageFormat } from './shared.js';
6
+ import { buildLocalImageKey, buildLocalVariantAssetPath, matchRemotePattern, normalizeImageConfig, normalizeImageFormat } from './shared.js';
7
7
  const RASTER_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.avif']);
8
8
  const MIME_BY_FORMAT = {
9
9
  avif: 'image/avif',
@@ -95,7 +95,7 @@ async function writeIfStale(sourcePath, targetPath, buffer) {
95
95
  await writeFile(targetPath, buffer);
96
96
  }
97
97
  function variantRelativePath(publicPath, width, quality, format) {
98
- return buildLocalVariantPath(publicPath, width, quality, format).replace(/^\//, '');
98
+ return buildLocalVariantAssetPath(publicPath, width, quality, format).replace(/^\//, '');
99
99
  }
100
100
  function createRemoteCacheKey(url, width, quality, format) {
101
101
  return buildLocalImageKey(`${url}|${width}|${quality}|${format || 'original'}`);
@@ -15,12 +15,14 @@ export function isRemoteImageUrl(value: any): boolean;
15
15
  export function normalizeImageSource(input: any): any;
16
16
  export function normalizeImageFormat(value: any): string;
17
17
  export function buildLocalImageKey(publicPath: any): string;
18
- export function buildLocalVariantPath(publicPath: any, width: any, quality: any, format: any): string;
19
- export function buildRemoteVariantPath(remoteUrl: any, width: any, quality: any, format: any): string;
18
+ export function buildLocalVariantAssetPath(publicPath: any, width: any, quality: any, format: any): string;
19
+ export function buildLocalVariantPath(publicPath: any, width: any, quality: any, format: any, basePath?: string): string;
20
+ export function buildRemoteVariantPath(remoteUrl: any, width: any, quality: any, format: any, basePath?: string): string;
20
21
  export function resolveWidthCandidates(width: any, sizes: any, config: any, manifestEntry: any): any[];
21
22
  export function imageRuntimeGlobalName(): string;
22
23
  export function normalizeImageRuntimePayload(payload: any): {
23
24
  mode: string;
25
+ basePath: string;
24
26
  config: {
25
27
  formats: string[];
26
28
  deviceSizes: number[];