@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.
- package/README.md +124 -60
- package/dist/assets/imageOptimizer.js +10 -15
- package/dist/assets/precompression.js +1 -1
- package/dist/builders/contentBuilder.js +102 -90
- package/dist/builders/cssBuilder.js +25 -19
- package/dist/builders/htmlBuilder.js +57 -42
- package/dist/builders/index.js +1 -1
- package/dist/builders/jsBuilder.js +219 -76
- package/dist/builders/staticAssetsBuilder.js +27 -9
- package/dist/builders/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -30
- package/dist/config/manifest.js +7 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -6
- package/dist/config/setup.js +1 -1
- package/dist/config/workspace.js +11 -9
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +5 -5
- package/dist/core/diagnostics.js +1 -1
- package/dist/core/pages.js +4 -4
- package/dist/hooks.js +3 -3
- package/dist/html/criticalCss.js +6 -3
- package/dist/html/htmlSecurity.d.ts +6 -1
- package/dist/html/htmlSecurity.js +28 -14
- package/dist/html/lazyLoad.js +1 -1
- package/dist/html/pageScaffold.js +1 -1
- package/dist/html/resourceHints.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +110 -0
- package/dist/modes/ssg/metadata.js +4 -4
- package/dist/modes/ssg/routing.js +2 -5
- package/dist/modes/ssg/seo.js +5 -5
- package/dist/modes/ssg/views.js +17 -11
- package/dist/operations.js +18 -10
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.js +6 -1
- package/dist/provider.js +28 -24
- package/dist/runtime/boundary.d.ts +28 -0
- package/dist/runtime/boundary.js +247 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils/fs.d.ts +11 -10
- package/dist/utils/fs.js +48 -20
- package/dist/utils/glob.d.ts +8 -0
- package/dist/utils/glob.js +21 -0
- package/dist/utils/hash.js +1 -2
- package/dist/utils/pagePaths.js +2 -2
- package/package.json +19 -14
- package/scripts/publish.sh +2 -94
- package/scripts/update-contract.sh +12 -10
- package/src/assets/assetManifest.ts +39 -29
- package/src/assets/imageOptimizer.ts +91 -82
- package/src/assets/precompression.ts +22 -16
- package/src/builders/contentBuilder.ts +1224 -1149
- package/src/builders/cssBuilder.ts +466 -417
- package/src/builders/htmlBuilder.ts +511 -448
- package/src/builders/index.ts +7 -7
- package/src/builders/jsBuilder.ts +538 -280
- package/src/builders/staticAssetsBuilder.ts +166 -135
- package/src/builders/types.ts +7 -6
- package/src/cli.ts +66 -90
- package/src/config/manifest.ts +16 -14
- package/src/config/paths.ts +5 -5
- package/src/config/schema.ts +38 -37
- package/src/config/setup.ts +7 -7
- package/src/config/workspace.ts +118 -116
- package/src/config/workspaceManifest.ts +14 -14
- package/src/core/constants.ts +62 -62
- package/src/core/diagnostics.ts +26 -26
- package/src/core/pages.ts +19 -19
- package/src/hooks.ts +128 -118
- package/src/html/criticalCss.ts +84 -77
- package/src/html/htmlSecurity.ts +107 -66
- package/src/html/lazyLoad.ts +22 -19
- package/src/html/pageScaffold.ts +37 -28
- package/src/html/resourceHints.ts +83 -74
- package/src/index.ts +2 -0
- package/src/inspect.ts +158 -0
- package/src/modes/ssg/metadata.ts +53 -51
- package/src/modes/ssg/routing.ts +177 -177
- package/src/modes/ssg/seo.ts +208 -200
- package/src/modes/ssg/validation.ts +31 -25
- package/src/modes/ssg/views.ts +257 -238
- package/src/operations.ts +105 -95
- package/src/pipeline.ts +81 -69
- package/src/provider.ts +184 -176
- package/src/runtime/boundary.ts +325 -0
- package/src/runtime/index.ts +1 -0
- package/src/types.ts +107 -48
- package/src/utils/changedFile.ts +22 -22
- package/src/utils/fs.ts +73 -26
- package/src/utils/glob.ts +38 -0
- package/src/utils/hash.ts +2 -4
- package/src/utils/pagePaths.ts +35 -23
- package/src/utils/pathMatch.ts +26 -23
- package/tests/add-page-defaults.test.js +44 -39
- package/tests/bundlerParity.test.js +252 -0
- package/tests/cli.contract.test.js +13 -0
- package/tests/content-pages.test.js +108 -13
- package/tests/css-app-imports.test.js +22 -11
- package/tests/css-page-imports.test.js +26 -13
- package/tests/diagnostics.test.js +39 -36
- package/tests/features.test.js +48 -43
- package/tests/hooks.test.js +58 -42
- package/tests/htmlSecurity.test.js +66 -0
- package/tests/inspect.test.js +148 -0
- package/tests/provider.integration.test.js +71 -20
- package/tests/runtime.test.js +493 -0
- package/tests/ssg-defaults.test.js +284 -177
- package/tests/ssg-guardrails.test.js +51 -51
- package/tsconfig.json +3 -10
- package/dist/watch/frontendFiles.d.ts +0 -3
- package/dist/watch/frontendFiles.js +0 -25
- package/dist/watch/hotUpdateTracker.d.ts +0 -51
- package/dist/watch/hotUpdateTracker.js +0 -205
- package/dist/watch/pipelineHelpers.d.ts +0 -26
- package/dist/watch/pipelineHelpers.js +0 -177
- package/dist/watch/types.d.ts +0 -27
- package/dist/watch/types.js +0 -1
- package/dist/watch/watchCoordinator.d.ts +0 -36
- package/dist/watch/watchCoordinator.js +0 -551
- package/dist/watch/watchDaemon.d.ts +0 -17
- package/dist/watch/watchDaemon.js +0 -127
- package/dist/watch/watchReporter.d.ts +0 -21
- package/dist/watch/watchReporter.js +0 -64
- package/scripts/smoke.mjs +0 -35
- package/src/watch/frontendFiles.ts +0 -32
- package/src/watch/hotUpdateTracker.ts +0 -285
- package/src/watch/pipelineHelpers.ts +0 -242
- package/src/watch/types.ts +0 -23
- package/src/watch/watchCoordinator.ts +0 -666
- package/src/watch/watchDaemon.ts +0 -144
- 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 {
|
|
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(
|
|
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(
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
114
|
-
|
|
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 =
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
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) {
|
package/dist/builders/types.d.ts
CHANGED
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
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);
|
package/dist/config/manifest.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
11
|
-
await
|
|
11
|
+
await writeFile(tempPath, serialized);
|
|
12
|
+
await rename(tempPath, options.outputPath);
|
|
12
13
|
}
|
|
13
14
|
export async function readConfigManifest(manifestPath) {
|
|
14
|
-
const json = await
|
|
15
|
+
const json = await readFile(manifestPath);
|
|
15
16
|
const parsed = JSON.parse(json);
|
|
16
17
|
return frontendConfigSchema.parse(parsed);
|
|
17
18
|
}
|
package/dist/config/paths.js
CHANGED
|
@@ -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) {
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -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
|
};
|