@webstir-io/webstir-frontend 0.1.40 → 0.1.41

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 (138) hide show
  1. package/README.md +124 -60
  2. package/dist/assets/imageOptimizer.js +10 -15
  3. package/dist/assets/precompression.js +1 -1
  4. package/dist/builders/contentBuilder.js +102 -90
  5. package/dist/builders/cssBuilder.js +25 -19
  6. package/dist/builders/htmlBuilder.js +57 -42
  7. package/dist/builders/index.js +1 -1
  8. package/dist/builders/jsBuilder.js +219 -76
  9. package/dist/builders/staticAssetsBuilder.js +27 -9
  10. package/dist/builders/types.d.ts +1 -0
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +6 -30
  13. package/dist/config/manifest.js +7 -6
  14. package/dist/config/paths.js +2 -2
  15. package/dist/config/schema.d.ts +8 -0
  16. package/dist/config/schema.js +7 -6
  17. package/dist/config/setup.js +1 -1
  18. package/dist/config/workspace.js +11 -9
  19. package/dist/core/constants.d.ts +1 -1
  20. package/dist/core/constants.js +5 -5
  21. package/dist/core/diagnostics.js +1 -1
  22. package/dist/core/pages.js +4 -4
  23. package/dist/hooks.js +3 -3
  24. package/dist/html/criticalCss.js +6 -3
  25. package/dist/html/htmlSecurity.d.ts +6 -1
  26. package/dist/html/htmlSecurity.js +28 -14
  27. package/dist/html/lazyLoad.js +1 -1
  28. package/dist/html/pageScaffold.js +1 -1
  29. package/dist/html/resourceHints.js +5 -2
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +2 -0
  32. package/dist/inspect.d.ts +2 -0
  33. package/dist/inspect.js +110 -0
  34. package/dist/modes/ssg/metadata.js +4 -4
  35. package/dist/modes/ssg/routing.js +2 -5
  36. package/dist/modes/ssg/seo.js +5 -5
  37. package/dist/modes/ssg/views.js +17 -11
  38. package/dist/operations.js +18 -10
  39. package/dist/pipeline.d.ts +1 -0
  40. package/dist/pipeline.js +6 -1
  41. package/dist/provider.js +28 -24
  42. package/dist/runtime/boundary.d.ts +28 -0
  43. package/dist/runtime/boundary.js +247 -0
  44. package/dist/runtime/index.d.ts +1 -0
  45. package/dist/runtime/index.js +1 -0
  46. package/dist/types.d.ts +52 -0
  47. package/dist/utils/fs.d.ts +11 -10
  48. package/dist/utils/fs.js +48 -20
  49. package/dist/utils/glob.d.ts +8 -0
  50. package/dist/utils/glob.js +21 -0
  51. package/dist/utils/hash.js +1 -2
  52. package/dist/utils/pagePaths.js +2 -2
  53. package/package.json +19 -14
  54. package/scripts/publish.sh +2 -94
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/assets/assetManifest.ts +39 -29
  57. package/src/assets/imageOptimizer.ts +91 -82
  58. package/src/assets/precompression.ts +22 -16
  59. package/src/builders/contentBuilder.ts +1224 -1149
  60. package/src/builders/cssBuilder.ts +466 -417
  61. package/src/builders/htmlBuilder.ts +511 -448
  62. package/src/builders/index.ts +7 -7
  63. package/src/builders/jsBuilder.ts +538 -280
  64. package/src/builders/staticAssetsBuilder.ts +166 -135
  65. package/src/builders/types.ts +7 -6
  66. package/src/cli.ts +66 -90
  67. package/src/config/manifest.ts +16 -14
  68. package/src/config/paths.ts +5 -5
  69. package/src/config/schema.ts +38 -37
  70. package/src/config/setup.ts +7 -7
  71. package/src/config/workspace.ts +118 -116
  72. package/src/config/workspaceManifest.ts +14 -14
  73. package/src/core/constants.ts +62 -62
  74. package/src/core/diagnostics.ts +26 -26
  75. package/src/core/pages.ts +19 -19
  76. package/src/hooks.ts +128 -118
  77. package/src/html/criticalCss.ts +84 -77
  78. package/src/html/htmlSecurity.ts +107 -66
  79. package/src/html/lazyLoad.ts +22 -19
  80. package/src/html/pageScaffold.ts +37 -28
  81. package/src/html/resourceHints.ts +83 -74
  82. package/src/index.ts +2 -0
  83. package/src/inspect.ts +158 -0
  84. package/src/modes/ssg/metadata.ts +53 -51
  85. package/src/modes/ssg/routing.ts +177 -177
  86. package/src/modes/ssg/seo.ts +208 -200
  87. package/src/modes/ssg/validation.ts +31 -25
  88. package/src/modes/ssg/views.ts +257 -238
  89. package/src/operations.ts +105 -95
  90. package/src/pipeline.ts +81 -69
  91. package/src/provider.ts +184 -176
  92. package/src/runtime/boundary.ts +325 -0
  93. package/src/runtime/index.ts +1 -0
  94. package/src/types.ts +107 -48
  95. package/src/utils/changedFile.ts +22 -22
  96. package/src/utils/fs.ts +73 -26
  97. package/src/utils/glob.ts +38 -0
  98. package/src/utils/hash.ts +2 -4
  99. package/src/utils/pagePaths.ts +35 -23
  100. package/src/utils/pathMatch.ts +26 -23
  101. package/tests/add-page-defaults.test.js +44 -39
  102. package/tests/bundlerParity.test.js +252 -0
  103. package/tests/cli.contract.test.js +13 -0
  104. package/tests/content-pages.test.js +108 -13
  105. package/tests/css-app-imports.test.js +22 -11
  106. package/tests/css-page-imports.test.js +26 -13
  107. package/tests/diagnostics.test.js +39 -36
  108. package/tests/features.test.js +48 -43
  109. package/tests/hooks.test.js +58 -42
  110. package/tests/htmlSecurity.test.js +66 -0
  111. package/tests/inspect.test.js +148 -0
  112. package/tests/provider.integration.test.js +71 -20
  113. package/tests/runtime.test.js +493 -0
  114. package/tests/ssg-defaults.test.js +284 -177
  115. package/tests/ssg-guardrails.test.js +51 -51
  116. package/tsconfig.json +3 -10
  117. package/dist/watch/frontendFiles.d.ts +0 -3
  118. package/dist/watch/frontendFiles.js +0 -25
  119. package/dist/watch/hotUpdateTracker.d.ts +0 -51
  120. package/dist/watch/hotUpdateTracker.js +0 -205
  121. package/dist/watch/pipelineHelpers.d.ts +0 -26
  122. package/dist/watch/pipelineHelpers.js +0 -177
  123. package/dist/watch/types.d.ts +0 -27
  124. package/dist/watch/types.js +0 -1
  125. package/dist/watch/watchCoordinator.d.ts +0 -36
  126. package/dist/watch/watchCoordinator.js +0 -551
  127. package/dist/watch/watchDaemon.d.ts +0 -17
  128. package/dist/watch/watchDaemon.js +0 -127
  129. package/dist/watch/watchReporter.d.ts +0 -21
  130. package/dist/watch/watchReporter.js +0 -64
  131. package/scripts/smoke.mjs +0 -35
  132. package/src/watch/frontendFiles.ts +0 -32
  133. package/src/watch/hotUpdateTracker.ts +0 -285
  134. package/src/watch/pipelineHelpers.ts +0 -242
  135. package/src/watch/types.ts +0 -23
  136. package/src/watch/watchCoordinator.ts +0 -666
  137. package/src/watch/watchDaemon.ts +0 -144
  138. package/src/watch/watchReporter.ts +0 -98
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { build as esbuild } from 'esbuild';
3
- import { glob } from 'glob';
4
3
  import { FOLDERS, FILES, EXTENSIONS } from '../core/constants.js';
5
4
  import { getPages } from '../core/pages.js';
6
5
  import { ensureDir, pathExists, copy, remove, stat } from '../utils/fs.js';
7
- import { updatePageManifest, updateSharedAssets, readSharedAssets } from '../assets/assetManifest.js';
6
+ import { scanGlob } from '../utils/glob.js';
7
+ import { updatePageManifest, updateSharedAssets, readSharedAssets, } from '../assets/assetManifest.js';
8
8
  import { createCompressedVariants } from '../assets/precompression.js';
9
9
  import { shouldProcess } from '../utils/changedFile.js';
10
10
  import { findPageFromChangedFile } from '../utils/pathMatch.js';
@@ -18,24 +18,24 @@ export function createJavaScriptBuilder(context) {
18
18
  },
19
19
  async publish() {
20
20
  await bundleJavaScript(context, true);
21
- }
21
+ },
22
22
  };
23
23
  }
24
24
  async function bundleJavaScript(context, isProduction) {
25
25
  const { config } = context;
26
+ const bundler = resolveJavaScriptBundler(context.env);
26
27
  if (!shouldProcess(context, [
27
28
  {
28
29
  directory: config.paths.src.frontend,
29
- extensions: [EXTENSIONS.ts, EXTENSIONS.js, '.tsx', '.jsx']
30
- }
30
+ extensions: [EXTENSIONS.ts, EXTENSIONS.js, '.tsx', '.jsx'],
31
+ },
31
32
  ])) {
32
33
  return;
33
34
  }
34
35
  const targetPage = findPageFromChangedFile(context.changedFile, config.paths.src.pages);
35
36
  const pages = await getPages(config.paths.src.pages);
36
- let builtAny = false;
37
37
  await assertFeatureModulesPresent(config, context.enable);
38
- await compileAppTypeScript(config, isProduction);
38
+ await compileAppTypeScript(context, isProduction, bundler);
39
39
  for (const page of pages) {
40
40
  if (targetPage && page.name !== targetPage) {
41
41
  continue;
@@ -44,12 +44,11 @@ async function bundleJavaScript(context, isProduction) {
44
44
  if (!entryPoint) {
45
45
  continue;
46
46
  }
47
- builtAny = true;
48
47
  if (isProduction) {
49
- await buildForProduction(config, page.name, entryPoint);
48
+ await buildForProduction(config, page.name, entryPoint, bundler);
50
49
  }
51
50
  else {
52
- await buildForDevelopment(config, page.name, entryPoint);
51
+ await buildForDevelopment(config, page.name, entryPoint, bundler);
53
52
  }
54
53
  }
55
54
  // Always copy dev runtime scripts in dev builds to support HMR/refresh even when no page JS exists.
@@ -57,7 +56,8 @@ async function bundleJavaScript(context, isProduction) {
57
56
  await copyRuntimeScripts(config, context.enable, isProduction);
58
57
  }
59
58
  }
60
- async function compileAppTypeScript(config, isProduction) {
59
+ async function compileAppTypeScript(context, isProduction, bundler) {
60
+ const { config } = context;
61
61
  const appRoot = config.paths.src.app;
62
62
  if (!(await pathExists(appRoot))) {
63
63
  return;
@@ -69,24 +69,9 @@ async function compileAppTypeScript(config, isProduction) {
69
69
  }
70
70
  const outputDir = path.join(config.paths.dist.frontend, FOLDERS.app);
71
71
  await ensureDir(outputDir);
72
- const result = await esbuild({
73
- entryPoints: [entryPoint],
74
- outdir: outputDir,
75
- format: 'esm',
76
- target: 'es2020',
77
- platform: 'browser',
78
- minify: true,
79
- sourcemap: false,
80
- bundle: true,
81
- entryNames: 'app-[hash]',
82
- assetNames: 'assets/[name]-[hash]',
83
- metafile: true,
84
- logLevel: 'silent'
85
- });
86
- const fileName = await resolveAppBundleName(outputDir, entryPoint, result.metafile);
87
- if (!fileName) {
88
- throw new Error(`esbuild did not produce an app bundle for ${entryPoint}.`);
89
- }
72
+ const fileName = bundler === 'bun'
73
+ ? await buildAppForProductionWithBun(outputDir, entryPoint)
74
+ : await buildAppForProductionWithEsbuild(outputDir, entryPoint);
90
75
  const absolutePath = path.join(outputDir, fileName);
91
76
  if (config.features.precompression) {
92
77
  await createCompressedVariants(absolutePath);
@@ -94,7 +79,7 @@ async function compileAppTypeScript(config, isProduction) {
94
79
  else {
95
80
  await Promise.all([
96
81
  remove(`${absolutePath}${EXTENSIONS.br}`).catch(() => undefined),
97
- remove(`${absolutePath}${EXTENSIONS.gz}`).catch(() => undefined)
82
+ remove(`${absolutePath}${EXTENSIONS.gz}`).catch(() => undefined),
98
83
  ]);
99
84
  }
100
85
  const existing = await readSharedAssets(config.paths.dist.frontend);
@@ -105,37 +90,30 @@ async function compileAppTypeScript(config, isProduction) {
105
90
  await remove(`${previousPath}${EXTENSIONS.br}`).catch(() => undefined);
106
91
  await remove(`${previousPath}${EXTENSIONS.gz}`).catch(() => undefined);
107
92
  }
108
- await updateSharedAssets(config.paths.dist.frontend, shared => {
93
+ await updateSharedAssets(config.paths.dist.frontend, (shared) => {
109
94
  shared.js = fileName;
110
95
  });
111
96
  return;
112
97
  }
113
- const entryPoints = (await glob('**/*.{ts,tsx}', { cwd: appRoot, nodir: true }))
114
- .filter((relativePath) => !relativePath.endsWith('.d.ts'))
115
- .map((relativePath) => path.join(appRoot, relativePath));
116
- if (entryPoints.length === 0) {
98
+ const entryPoint = await resolveAppEntry(appRoot);
99
+ if (!entryPoint) {
117
100
  return;
118
101
  }
119
- const outdir = isProduction
120
- ? path.join(config.paths.dist.frontend, FOLDERS.app)
121
- : path.join(config.paths.build.frontend, FOLDERS.app);
102
+ const outdir = path.join(config.paths.build.frontend, FOLDERS.app);
122
103
  await ensureDir(outdir);
123
- await esbuild({
124
- entryPoints,
125
- outdir,
126
- format: 'esm',
127
- target: 'es2020',
128
- platform: 'browser',
129
- sourcemap: !isProduction,
130
- minify: isProduction,
131
- bundle: false,
132
- outbase: appRoot,
133
- logLevel: 'silent'
134
- });
104
+ if (bundler === 'bun') {
105
+ await buildForDevelopmentWithBun(outdir, entryPoint);
106
+ return;
107
+ }
108
+ await buildAppForDevelopmentWithEsbuild(outdir, entryPoint);
135
109
  }
136
- async function buildForDevelopment(config, pageName, entryPoint) {
110
+ async function buildForDevelopment(config, pageName, entryPoint, bundler) {
137
111
  const outputDir = path.join(config.paths.build.pages, pageName);
138
112
  await ensureDir(outputDir);
113
+ if (bundler === 'bun') {
114
+ await buildForDevelopmentWithBun(outputDir, entryPoint);
115
+ return;
116
+ }
139
117
  const outfile = path.join(outputDir, `${FILES.index}${EXTENSIONS.js}`);
140
118
  await esbuild({
141
119
  entryPoints: [entryPoint],
@@ -145,12 +123,94 @@ async function buildForDevelopment(config, pageName, entryPoint) {
145
123
  platform: 'browser',
146
124
  sourcemap: true,
147
125
  outfile,
148
- logLevel: 'silent'
126
+ logLevel: 'silent',
149
127
  });
150
128
  }
151
- async function buildForProduction(config, pageName, entryPoint) {
129
+ async function buildForProduction(config, pageName, entryPoint, bundler) {
152
130
  const outputDir = path.join(config.paths.dist.pages, pageName);
153
131
  await ensureDir(outputDir);
132
+ const fileName = bundler === 'bun'
133
+ ? await buildPageForProductionWithBun(outputDir, pageName, entryPoint)
134
+ : await buildPageForProductionWithEsbuild(outputDir, pageName, entryPoint);
135
+ const absolutePath = path.join(outputDir, fileName);
136
+ if (config.features.precompression) {
137
+ await createCompressedVariants(absolutePath);
138
+ }
139
+ else {
140
+ await Promise.all([
141
+ remove(`${absolutePath}${EXTENSIONS.br}`).catch(() => undefined),
142
+ remove(`${absolutePath}${EXTENSIONS.gz}`).catch(() => undefined),
143
+ ]);
144
+ }
145
+ await updatePageManifest(outputDir, pageName, (manifest) => {
146
+ manifest.js = fileName;
147
+ });
148
+ }
149
+ async function buildAppForProductionWithEsbuild(outputDir, entryPoint) {
150
+ const result = await esbuild({
151
+ entryPoints: [entryPoint],
152
+ outdir: outputDir,
153
+ format: 'esm',
154
+ target: 'es2020',
155
+ platform: 'browser',
156
+ minify: true,
157
+ sourcemap: false,
158
+ bundle: true,
159
+ entryNames: 'app-[hash]',
160
+ assetNames: 'assets/[name]-[hash]',
161
+ metafile: true,
162
+ logLevel: 'silent',
163
+ });
164
+ const fileName = await resolveAppBundleName(outputDir, entryPoint, result.metafile);
165
+ if (!fileName) {
166
+ throw new Error(`esbuild did not produce an app bundle for ${entryPoint}.`);
167
+ }
168
+ return fileName;
169
+ }
170
+ async function buildAppForProductionWithBun(outputDir, entryPoint) {
171
+ const result = await runBunBrowserBuild({
172
+ entryPoint,
173
+ root: path.dirname(entryPoint),
174
+ outputDir,
175
+ minify: true,
176
+ sourcemap: 'none',
177
+ naming: {
178
+ entry: 'app-[hash].js',
179
+ asset: 'assets/[name]-[hash].[ext]',
180
+ },
181
+ });
182
+ ensureBunBuildSucceeded(result, `app bundle ${entryPoint}`);
183
+ const fileName = resolveBunEntryOutputName(result.outputs, outputDir, (name) => {
184
+ return name.startsWith('app-') && name.endsWith(EXTENSIONS.js);
185
+ });
186
+ if (!fileName) {
187
+ throw new Error(`Bun.build() did not produce an app bundle for ${entryPoint}.`);
188
+ }
189
+ return fileName;
190
+ }
191
+ async function buildForDevelopmentWithBun(outputDir, entryPoint) {
192
+ const result = await runBunBrowserBuild({
193
+ entryPoint,
194
+ root: path.dirname(entryPoint),
195
+ outputDir,
196
+ minify: false,
197
+ sourcemap: 'linked',
198
+ });
199
+ ensureBunBuildSucceeded(result, `development bundle ${entryPoint}`);
200
+ }
201
+ async function buildAppForDevelopmentWithEsbuild(outputDir, entryPoint) {
202
+ await esbuild({
203
+ entryPoints: [entryPoint],
204
+ outdir: outputDir,
205
+ format: 'esm',
206
+ target: 'es2020',
207
+ platform: 'browser',
208
+ sourcemap: 'linked',
209
+ bundle: true,
210
+ logLevel: 'silent',
211
+ });
212
+ }
213
+ async function buildPageForProductionWithEsbuild(outputDir, pageName, entryPoint) {
154
214
  const result = await esbuild({
155
215
  entryPoints: [entryPoint],
156
216
  bundle: true,
@@ -163,33 +223,41 @@ async function buildForProduction(config, pageName, entryPoint) {
163
223
  entryNames: `${FILES.index}-[hash]`,
164
224
  assetNames: 'assets/[name]-[hash]',
165
225
  metafile: true,
166
- logLevel: 'silent'
226
+ logLevel: 'silent',
167
227
  });
168
228
  const outputs = result.metafile?.outputs ?? {};
169
229
  const scriptPath = Object.keys(outputs).find((file) => file.endsWith('.js'));
170
230
  if (!scriptPath) {
171
231
  throw new Error(`esbuild did not produce a JavaScript bundle for page '${pageName}'.`);
172
232
  }
173
- const fileName = path.basename(scriptPath);
174
- const absolutePath = path.join(outputDir, fileName);
175
- if (config.features.precompression) {
176
- await createCompressedVariants(absolutePath);
177
- }
178
- else {
179
- await Promise.all([
180
- remove(`${absolutePath}${EXTENSIONS.br}`).catch(() => undefined),
181
- remove(`${absolutePath}${EXTENSIONS.gz}`).catch(() => undefined)
182
- ]);
183
- }
184
- await updatePageManifest(outputDir, pageName, (manifest) => {
185
- manifest.js = fileName;
233
+ return path.basename(scriptPath);
234
+ }
235
+ async function buildPageForProductionWithBun(outputDir, pageName, entryPoint) {
236
+ const result = await runBunBrowserBuild({
237
+ entryPoint,
238
+ root: path.dirname(entryPoint),
239
+ outputDir,
240
+ minify: true,
241
+ sourcemap: 'none',
242
+ naming: {
243
+ entry: `${FILES.index}-[hash].js`,
244
+ asset: 'assets/[name]-[hash].[ext]',
245
+ },
246
+ });
247
+ ensureBunBuildSucceeded(result, `page bundle '${pageName}'`);
248
+ const fileName = resolveBunEntryOutputName(result.outputs, outputDir, (name) => {
249
+ return name.startsWith(`${FILES.index}-`) && name.endsWith(EXTENSIONS.js);
186
250
  });
251
+ if (!fileName) {
252
+ throw new Error(`Bun.build() did not produce a JavaScript bundle for page '${pageName}'.`);
253
+ }
254
+ return fileName;
187
255
  }
188
- async function copyRuntimeScripts(config, enable, isProduction) {
256
+ async function copyRuntimeScripts(config, _enable, isProduction) {
189
257
  const scripts = [
190
258
  // Always copy dev runtime in dev builds to support live reload, even if no page JS exists.
191
259
  { name: FILES.refreshJs, copyToDist: false, required: !isProduction },
192
- { name: FILES.hmrJs, copyToDist: false, required: !isProduction }
260
+ { name: FILES.hmrJs, copyToDist: false, required: !isProduction },
193
261
  ];
194
262
  for (const script of scripts) {
195
263
  if (!script.required) {
@@ -244,15 +312,13 @@ async function assertFeatureModulesPresent(config, enable) {
244
312
  if (missing.length === 0) {
245
313
  return;
246
314
  }
247
- const expected = missing
248
- .map((name) => `src/frontend/app/scripts/features/${name}.ts`)
249
- .join(', ');
315
+ const expected = missing.map((name) => `src/frontend/app/scripts/features/${name}.ts`).join(', ');
250
316
  throw new Error(`Enabled feature module(s) missing: ${missing.join(', ')}. Run 'webstir enable <feature>' to scaffold them (expected: ${expected}).`);
251
317
  }
252
318
  async function hasFeatureModule(config, name) {
253
319
  const root = path.join(config.paths.src.app, 'scripts', 'features');
254
- return await pathExists(path.join(root, `${name}${EXTENSIONS.ts}`))
255
- || await pathExists(path.join(root, `${name}${EXTENSIONS.js}`));
320
+ return ((await pathExists(path.join(root, `${name}${EXTENSIONS.ts}`))) ||
321
+ (await pathExists(path.join(root, `${name}${EXTENSIONS.js}`))));
256
322
  }
257
323
  async function resolveAppBundleName(outputDir, entryPoint, metafile) {
258
324
  const outputs = metafile?.outputs ?? {};
@@ -266,7 +332,7 @@ async function resolveAppBundleName(outputDir, entryPoint, metafile) {
266
332
  if (entryOutput) {
267
333
  return path.basename(entryOutput[0]);
268
334
  }
269
- const matches = await glob('app-*.js', { cwd: outputDir, nodir: true });
335
+ const matches = await scanGlob('app-*.js', { cwd: outputDir });
270
336
  if (matches.length === 0) {
271
337
  return null;
272
338
  }
@@ -283,12 +349,89 @@ async function resolveAppBundleName(outputDir, entryPoint, metafile) {
283
349
  }
284
350
  return latest?.name ?? matches[0] ?? null;
285
351
  }
352
+ async function runBunBrowserBuild(options) {
353
+ const build = getBunBuild();
354
+ if (!build) {
355
+ throw new Error('Bun.build() is not available in the current runtime.');
356
+ }
357
+ return await build({
358
+ entrypoints: [options.entryPoint],
359
+ root: options.root,
360
+ outdir: options.outputDir,
361
+ target: 'browser',
362
+ format: 'esm',
363
+ minify: options.minify,
364
+ sourcemap: options.sourcemap,
365
+ splitting: false,
366
+ naming: options.naming,
367
+ throw: false,
368
+ });
369
+ }
370
+ function resolveJavaScriptBundler(env) {
371
+ const requestedBundler = normalizeJavaScriptBundler(env?.WEBSTIR_FRONTEND_BUNDLER);
372
+ if (requestedBundler !== 'bun') {
373
+ return 'esbuild';
374
+ }
375
+ if (!getBunBuild()) {
376
+ console.warn('[webstir-frontend] WEBSTIR_FRONTEND_BUNDLER=bun requested outside a Bun runtime; falling back to esbuild.');
377
+ return 'esbuild';
378
+ }
379
+ return 'bun';
380
+ }
381
+ function normalizeJavaScriptBundler(rawBundler) {
382
+ return typeof rawBundler === 'string' && rawBundler.trim().toLowerCase() === 'bun'
383
+ ? 'bun'
384
+ : 'esbuild';
385
+ }
386
+ function resolveBunEntryOutputName(outputs, outputDir, matcher) {
387
+ const normalizedOutputDir = path.resolve(outputDir);
388
+ for (const output of outputs ?? []) {
389
+ if (output.kind !== 'entry-point') {
390
+ continue;
391
+ }
392
+ if (path.resolve(path.dirname(output.path)) !== normalizedOutputDir) {
393
+ continue;
394
+ }
395
+ const fileName = path.basename(output.path);
396
+ if (matcher(fileName)) {
397
+ return fileName;
398
+ }
399
+ }
400
+ return null;
401
+ }
402
+ function ensureBunBuildSucceeded(result, label) {
403
+ const errors = (result.logs ?? [])
404
+ .filter((entry) => entry.level === 'error')
405
+ .map((entry) => formatBunBuildMessage(entry));
406
+ if (!result.success || errors.length > 0) {
407
+ throw new Error(errors[0] ?? `Bun.build() failed for ${label}.`);
408
+ }
409
+ }
410
+ function formatBunBuildMessage(entry) {
411
+ const text = typeof entry.message === 'string'
412
+ ? entry.message
413
+ : typeof entry.text === 'string'
414
+ ? entry.text
415
+ : 'Bun.build() failed.';
416
+ const position = entry.position;
417
+ if (position?.file) {
418
+ const line = typeof position.line === 'number' ? position.line : 1;
419
+ const column = typeof position.column === 'number' ? position.column : 1;
420
+ return `${position.file}:${line}:${column} ${text}`;
421
+ }
422
+ return text;
423
+ }
424
+ function getBunBuild() {
425
+ const runtime = globalThis;
426
+ const build = runtime.Bun?.build;
427
+ return typeof build === 'function' ? build.bind(runtime.Bun) : undefined;
428
+ }
286
429
  async function resolveAppEntry(appRoot) {
287
430
  const candidates = [
288
431
  `${APP_ENTRY_BASENAME}${EXTENSIONS.ts}`,
289
432
  `${APP_ENTRY_BASENAME}.tsx`,
290
433
  `${APP_ENTRY_BASENAME}${EXTENSIONS.js}`,
291
- `${APP_ENTRY_BASENAME}.jsx`
434
+ `${APP_ENTRY_BASENAME}.jsx`,
292
435
  ];
293
436
  for (const candidate of candidates) {
294
437
  const fullPath = path.join(appRoot, candidate);
@@ -11,14 +11,14 @@ const IMAGE_EXTENSIONS = [
11
11
  EXTENSIONS.gif,
12
12
  EXTENSIONS.svg,
13
13
  EXTENSIONS.webp,
14
- EXTENSIONS.ico
14
+ EXTENSIONS.ico,
15
15
  ];
16
16
  const FONT_EXTENSIONS = [
17
17
  EXTENSIONS.woff,
18
18
  EXTENSIONS.woff2,
19
19
  EXTENSIONS.ttf,
20
20
  EXTENSIONS.otf,
21
- EXTENSIONS.eot
21
+ EXTENSIONS.eot,
22
22
  ];
23
23
  const MEDIA_EXTENSIONS = [
24
24
  EXTENSIONS.mp3,
@@ -27,7 +27,7 @@ const MEDIA_EXTENSIONS = [
27
27
  EXTENSIONS.ogg,
28
28
  EXTENSIONS.mp4,
29
29
  EXTENSIONS.webm,
30
- EXTENSIONS.mov
30
+ EXTENSIONS.mov,
31
31
  ];
32
32
  const ALLOW_ALL_ROBOTS = 'User-agent: *\nAllow: /\n';
33
33
  export function createStaticAssetsBuilder(context) {
@@ -38,7 +38,7 @@ export function createStaticAssetsBuilder(context) {
38
38
  },
39
39
  async publish() {
40
40
  await copyStaticAssets(context, true);
41
- }
41
+ },
42
42
  };
43
43
  }
44
44
  async function copyStaticAssets(context, isProduction) {
@@ -46,14 +46,32 @@ async function copyStaticAssets(context, isProduction) {
46
46
  if (!shouldProcess(context, [
47
47
  { directory: config.paths.src.images, extensions: IMAGE_EXTENSIONS },
48
48
  { directory: config.paths.src.fonts, extensions: FONT_EXTENSIONS },
49
- { directory: config.paths.src.media, extensions: MEDIA_EXTENSIONS }
49
+ { directory: config.paths.src.media, extensions: MEDIA_EXTENSIONS },
50
50
  ])) {
51
51
  return;
52
52
  }
53
53
  const targets = [
54
- { source: config.paths.src.images, build: config.paths.build.frontend, dist: config.paths.dist.frontend, folder: FOLDERS.images, extensions: IMAGE_EXTENSIONS },
55
- { source: config.paths.src.fonts, build: config.paths.build.frontend, dist: config.paths.dist.frontend, folder: FOLDERS.fonts, extensions: FONT_EXTENSIONS },
56
- { source: config.paths.src.media, build: config.paths.build.frontend, dist: config.paths.dist.frontend, folder: FOLDERS.media, extensions: MEDIA_EXTENSIONS }
54
+ {
55
+ source: config.paths.src.images,
56
+ build: config.paths.build.frontend,
57
+ dist: config.paths.dist.frontend,
58
+ folder: FOLDERS.images,
59
+ extensions: IMAGE_EXTENSIONS,
60
+ },
61
+ {
62
+ source: config.paths.src.fonts,
63
+ build: config.paths.build.frontend,
64
+ dist: config.paths.dist.frontend,
65
+ folder: FOLDERS.fonts,
66
+ extensions: FONT_EXTENSIONS,
67
+ },
68
+ {
69
+ source: config.paths.src.media,
70
+ build: config.paths.build.frontend,
71
+ dist: config.paths.dist.frontend,
72
+ folder: FOLDERS.media,
73
+ extensions: MEDIA_EXTENSIONS,
74
+ },
57
75
  ];
58
76
  for (const target of targets) {
59
77
  if (!(await pathExists(target.source))) {
@@ -131,7 +149,7 @@ async function syncImageWithoutOptimization(buildRoot, distRoot, relativePath) {
131
149
  }
132
150
  await Promise.all([
133
151
  remove(`${destinationPath}${EXTENSIONS.webp}`).catch(() => undefined),
134
- remove(`${destinationPath}${EXTENSIONS.avif}`).catch(() => undefined)
152
+ remove(`${destinationPath}${EXTENSIONS.avif}`).catch(() => undefined),
135
153
  ]);
136
154
  }
137
155
  async function syncRobotsTxt(config, isProduction) {
@@ -3,6 +3,7 @@ export interface BuilderContext {
3
3
  readonly config: FrontendConfig;
4
4
  readonly changedFile?: string;
5
5
  readonly enable?: EnableFlags;
6
+ readonly env?: Record<string, string | undefined>;
6
7
  }
7
8
  export interface Builder {
8
9
  readonly name: string;
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  export {};
package/dist/cli.js CHANGED
@@ -1,11 +1,8 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  import { Command } from 'commander';
3
3
  import { runAddPage, runBuild, runPublish, runRebuild } from './operations.js';
4
- import { WatchDaemon } from './watch/watchDaemon.js';
5
4
  const program = new Command();
6
- program
7
- .name('webstir-frontend')
8
- .description('Webstir frontend build orchestrator');
5
+ program.name('webstir-frontend').description('Webstir frontend build orchestrator');
9
6
  program
10
7
  .command('build')
11
8
  .description('Build frontend assets for development workflows')
@@ -15,7 +12,7 @@ program
15
12
  try {
16
13
  await runBuild({
17
14
  workspaceRoot: cmd.workspace,
18
- changedFile: cmd.changedFile ?? undefined
15
+ changedFile: cmd.changedFile ?? undefined,
19
16
  });
20
17
  }
21
18
  catch (error) {
@@ -31,7 +28,7 @@ program
31
28
  try {
32
29
  await runPublish({
33
30
  workspaceRoot: cmd.workspace,
34
- publishMode: cmd.mode === 'ssg' ? 'ssg' : 'bundle'
31
+ publishMode: cmd.mode === 'ssg' ? 'ssg' : 'bundle',
35
32
  });
36
33
  }
37
34
  catch (error) {
@@ -47,7 +44,7 @@ program
47
44
  try {
48
45
  await runRebuild({
49
46
  workspaceRoot: cmd.workspace,
50
- changedFile: cmd.changedFile ?? undefined
47
+ changedFile: cmd.changedFile ?? undefined,
51
48
  });
52
49
  }
53
50
  catch (error) {
@@ -65,29 +62,8 @@ program
65
62
  await runAddPage({
66
63
  workspaceRoot: cmd.workspace,
67
64
  pageName: name,
68
- ssg: rawMode === 'ssg' ? true : rawMode === 'standard' ? false : undefined
69
- });
70
- }
71
- catch (error) {
72
- handleError(error);
73
- }
74
- });
75
- program
76
- .command('watch-daemon')
77
- .description('Run the persistent frontend watch daemon')
78
- .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
79
- .option('--no-auto-start', 'Defer startup until a start command is received')
80
- .option('-v, --verbose', 'Enable verbose watch diagnostics')
81
- .option('--hmr-verbose', 'Log detailed hot-update diagnostics')
82
- .action(async (cmd) => {
83
- try {
84
- const daemon = new WatchDaemon({
85
- workspaceRoot: cmd.workspace,
86
- autoStart: cmd.autoStart,
87
- verbose: cmd.verbose === true,
88
- hmrVerbose: cmd.hmrVerbose === true
65
+ ssg: rawMode === 'ssg' ? true : rawMode === 'standard' ? false : undefined,
89
66
  });
90
- await daemon.run();
91
67
  }
92
68
  catch (error) {
93
69
  handleError(error);
@@ -1,17 +1,18 @@
1
- import { promises as fs } from 'fs';
2
- import path from 'path';
1
+ import { rename } from 'node:fs/promises';
2
+ import path from 'node:path';
3
3
  import { frontendConfigSchema } from './schema.js';
4
+ import { ensureDir, readFile, writeFile } from '../utils/fs.js';
4
5
  export async function writeConfigManifest(options) {
5
6
  const parsed = frontendConfigSchema.parse(options.data);
6
7
  const directory = path.dirname(options.outputPath);
7
- await fs.mkdir(directory, { recursive: true });
8
+ await ensureDir(directory);
8
9
  const serialized = JSON.stringify(parsed, undefined, 2);
9
10
  const tempPath = path.join(directory, `.webstir-frontend-${process.pid}-${Date.now()}.tmp`);
10
- await fs.writeFile(tempPath, serialized, 'utf8');
11
- await fs.rename(tempPath, options.outputPath);
11
+ await writeFile(tempPath, serialized);
12
+ await rename(tempPath, options.outputPath);
12
13
  }
13
14
  export async function readConfigManifest(manifestPath) {
14
- const json = await fs.readFile(manifestPath, 'utf8');
15
+ const json = await readFile(manifestPath);
15
16
  const parsed = JSON.parse(json);
16
17
  return frontendConfigSchema.parse(parsed);
17
18
  }
@@ -1,5 +1,5 @@
1
- import path from 'path';
2
- import { promises as fs } from 'fs';
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
3
  import { FOLDERS } from '../core/constants.js';
4
4
  export const FRONTEND_MANIFEST_FILENAME = 'frontend-manifest.json';
5
5
  export function resolveManifestPath(workspaceRoot) {
@@ -152,14 +152,17 @@ export declare const frontendPathSchema: z.ZodObject<{
152
152
  }>;
153
153
  export declare const frontendFeatureFlagsSchema: z.ZodObject<{
154
154
  htmlSecurity: z.ZodDefault<z.ZodBoolean>;
155
+ externalResourceIntegrity: z.ZodDefault<z.ZodBoolean>;
155
156
  imageOptimization: z.ZodDefault<z.ZodBoolean>;
156
157
  precompression: z.ZodDefault<z.ZodBoolean>;
157
158
  }, "strip", z.ZodTypeAny, {
158
159
  htmlSecurity: boolean;
160
+ externalResourceIntegrity: boolean;
159
161
  imageOptimization: boolean;
160
162
  precompression: boolean;
161
163
  }, {
162
164
  htmlSecurity?: boolean | undefined;
165
+ externalResourceIntegrity?: boolean | undefined;
163
166
  imageOptimization?: boolean | undefined;
164
167
  precompression?: boolean | undefined;
165
168
  }>;
@@ -318,20 +321,24 @@ export declare const frontendConfigSchema: z.ZodObject<{
318
321
  }>;
319
322
  features: z.ZodObject<{
320
323
  htmlSecurity: z.ZodDefault<z.ZodBoolean>;
324
+ externalResourceIntegrity: z.ZodDefault<z.ZodBoolean>;
321
325
  imageOptimization: z.ZodDefault<z.ZodBoolean>;
322
326
  precompression: z.ZodDefault<z.ZodBoolean>;
323
327
  }, "strip", z.ZodTypeAny, {
324
328
  htmlSecurity: boolean;
329
+ externalResourceIntegrity: boolean;
325
330
  imageOptimization: boolean;
326
331
  precompression: boolean;
327
332
  }, {
328
333
  htmlSecurity?: boolean | undefined;
334
+ externalResourceIntegrity?: boolean | undefined;
329
335
  imageOptimization?: boolean | undefined;
330
336
  precompression?: boolean | undefined;
331
337
  }>;
332
338
  }, "strip", z.ZodTypeAny, {
333
339
  features: {
334
340
  htmlSecurity: boolean;
341
+ externalResourceIntegrity: boolean;
335
342
  imageOptimization: boolean;
336
343
  precompression: boolean;
337
344
  };
@@ -372,6 +379,7 @@ export declare const frontendConfigSchema: z.ZodObject<{
372
379
  }, {
373
380
  features: {
374
381
  htmlSecurity?: boolean | undefined;
382
+ externalResourceIntegrity?: boolean | undefined;
375
383
  imageOptimization?: boolean | undefined;
376
384
  precompression?: boolean | undefined;
377
385
  };