@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,21 +0,0 @@
1
- import type { BuildResult, Message } from 'esbuild';
2
- import type { DiagnosticEvent } from '../core/diagnostics.js';
3
- export interface WatchReporterOptions {
4
- readonly verbose: boolean;
5
- }
6
- export interface SerializedMessage {
7
- readonly text: string;
8
- readonly location?: {
9
- readonly file?: string;
10
- readonly line?: number;
11
- readonly column?: number;
12
- };
13
- }
14
- export declare class WatchReporter {
15
- private readonly verbose;
16
- constructor(options: WatchReporterOptions);
17
- emit(event: DiagnosticEvent): void;
18
- emitVerbose(event: DiagnosticEvent): void;
19
- emitJavaScriptStats(pageName: string, result: BuildResult, durationMs: number): void;
20
- }
21
- export declare function serializeMessages(messages: readonly Message[]): SerializedMessage[];
@@ -1,64 +0,0 @@
1
- import { emitDiagnostic } from '../core/diagnostics.js';
2
- export class WatchReporter {
3
- verbose;
4
- constructor(options) {
5
- this.verbose = options.verbose;
6
- }
7
- emit(event) {
8
- emitDiagnostic(event);
9
- }
10
- emitVerbose(event) {
11
- if (!this.verbose) {
12
- return;
13
- }
14
- emitDiagnostic(event);
15
- }
16
- emitJavaScriptStats(pageName, result, durationMs) {
17
- if (!this.verbose) {
18
- return;
19
- }
20
- const stats = extractMetafileStats(result);
21
- const data = {
22
- page: pageName,
23
- durationMs: Number(durationMs.toFixed(1))
24
- };
25
- if (stats) {
26
- data.inputs = stats.inputs;
27
- data.outputs = stats.outputs;
28
- data.bytes = stats.bytes;
29
- }
30
- emitDiagnostic({
31
- code: 'frontend.watch.javascript.build.stats',
32
- kind: 'watch-daemon',
33
- stage: 'javascript',
34
- severity: 'info',
35
- message: `JavaScript rebuild stats for '${pageName}' (${durationMs.toFixed(1)}ms).`,
36
- data
37
- });
38
- }
39
- }
40
- export function serializeMessages(messages) {
41
- return messages.map((message) => ({
42
- text: message.text,
43
- location: message.location
44
- ? {
45
- file: message.location.file,
46
- line: message.location.line,
47
- column: message.location.column
48
- }
49
- : undefined
50
- }));
51
- }
52
- function extractMetafileStats(result) {
53
- if (!result.metafile) {
54
- return null;
55
- }
56
- const inputs = Object.keys(result.metafile.inputs ?? {}).length;
57
- const outputsEntries = Object.entries(result.metafile.outputs ?? {});
58
- const bytes = outputsEntries.reduce((sum, [, output]) => sum + (output.bytes ?? 0), 0);
59
- return {
60
- inputs,
61
- outputs: outputsEntries.length,
62
- bytes
63
- };
64
- }
package/scripts/smoke.mjs DELETED
@@ -1,35 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
- import { frontendProvider } from '../dist/index.js';
5
-
6
- async function createWorkspace() {
7
- const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-smoke-'));
8
- const appDir = path.join(root, 'src', 'frontend', 'app');
9
- const pageDir = path.join(root, 'src', 'frontend', 'pages', 'home');
10
- await fs.mkdir(appDir, { recursive: true });
11
- await fs.mkdir(pageDir, { recursive: true });
12
- await fs.writeFile(path.join(appDir, 'app.html'), '<!DOCTYPE html><html><head><title>App</title></head><body><main></main></body></html>', 'utf8');
13
- await fs.writeFile(path.join(pageDir, 'index.html'), '<head></head><main><section>Home</section></main>', 'utf8');
14
- await fs.writeFile(path.join(pageDir, 'index.ts'), 'console.log("home")', 'utf8');
15
- return root;
16
- }
17
-
18
- async function main() {
19
- const workspace = await createWorkspace();
20
- console.info('[smoke:frontend] build mode');
21
- const build = await frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false });
22
- console.info('[smoke:frontend] build entries:', build.manifest.entryPoints);
23
- console.info('[smoke:frontend] build diagnostics:', build.manifest.diagnostics.map(d => d.message));
24
-
25
- console.info('[smoke:frontend] publish mode');
26
- const publish = await frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'publish' }, incremental: false });
27
- console.info('[smoke:frontend] publish entries:', publish.manifest.entryPoints);
28
- console.info('[smoke:frontend] publish diagnostics:', publish.manifest.diagnostics.map(d => d.message));
29
- }
30
-
31
- main().catch((err) => {
32
- console.error(err);
33
- process.exit(1);
34
- });
35
-
@@ -1,32 +0,0 @@
1
- import path from 'node:path';
2
- import { ensureDir, pathExists, copy } from '../utils/fs.js';
3
- import { FILES, EXTENSIONS } from '../core/constants.js';
4
- import type { EnableFlags, FrontendConfig } from '../types.js';
5
-
6
- export async function resolveEntryPoint(pageDirectory: string): Promise<string | null> {
7
- const candidates = [`${FILES.index}${EXTENSIONS.ts}`, `${FILES.index}.tsx`, `${FILES.index}${EXTENSIONS.js}`, `${FILES.index}.jsx`];
8
-
9
- for (const candidate of candidates) {
10
- const file = path.join(pageDirectory, candidate);
11
- if (await pathExists(file)) {
12
- return file;
13
- }
14
- }
15
-
16
- return null;
17
- }
18
-
19
- export async function copyRefreshScript(config: FrontendConfig, enable?: EnableFlags): Promise<void> {
20
- const runtimeScripts: string[] = [FILES.refreshJs, FILES.hmrJs];
21
-
22
- for (const scriptName of runtimeScripts) {
23
- const source = path.join(config.paths.src.app, scriptName);
24
- if (!(await pathExists(source))) {
25
- continue;
26
- }
27
-
28
- const destination = path.join(config.paths.build.frontend, scriptName);
29
- await ensureDir(path.dirname(destination));
30
- await copy(source, destination);
31
- }
32
- }
@@ -1,285 +0,0 @@
1
- import path from 'node:path';
2
- import { createHash } from 'node:crypto';
3
- import { readFile } from 'node:fs/promises';
4
- import type { BuildResult, Metafile } from 'esbuild';
5
- import { FOLDERS, FILES, EXTENSIONS } from '../core/constants.js';
6
- import { emitDiagnostic } from '../core/diagnostics.js';
7
- import type { FrontendConfig } from '../types.js';
8
- import type { BuilderContext } from '../builders/types.js';
9
- import { pathExists } from '../utils/fs.js';
10
- import { isPathInside } from '../utils/changedFile.js';
11
- import { findPageFromChangedFile } from '../utils/pathMatch.js';
12
-
13
- export interface HotAsset {
14
- readonly type: 'js' | 'css';
15
- readonly path: string;
16
- readonly relativePath: string;
17
- readonly url: string;
18
- }
19
-
20
- export interface HotUpdateDetails {
21
- readonly modules: readonly HotAsset[];
22
- readonly styles: readonly HotAsset[];
23
- readonly requiresReload: boolean;
24
- readonly fallbackReasons: readonly string[];
25
- readonly changedFile?: string;
26
- readonly stats?: HotUpdateStats;
27
- }
28
-
29
- export interface HotUpdateStats {
30
- readonly hotUpdates: number;
31
- readonly reloadFallbacks: number;
32
- }
33
-
34
- interface ProcessJavaScriptResult {
35
- readonly modules: readonly HotAsset[];
36
- readonly requiresReload: boolean;
37
- readonly fallbackReasons: readonly string[];
38
- }
39
-
40
- interface CollectCssResult {
41
- readonly styles: readonly HotAsset[];
42
- readonly requiresReload: boolean;
43
- readonly fallbackReasons: readonly string[];
44
- }
45
-
46
- interface HotUpdateTrackerOptions {
47
- readonly workspaceRoot: string;
48
- }
49
-
50
- export class HotUpdateTracker {
51
- private readonly workspaceRoot: string;
52
- private readonly pageOutputHashes = new Map<string, Map<string, { hash: string }>>();
53
- private readonly assetFingerprints = new Map<string, string>();
54
-
55
- public constructor(options: HotUpdateTrackerOptions) {
56
- this.workspaceRoot = options.workspaceRoot;
57
- }
58
-
59
- public reset(): void {
60
- this.pageOutputHashes.clear();
61
- this.assetFingerprints.clear();
62
- }
63
-
64
- public removePage(pageName: string): void {
65
- this.pageOutputHashes.delete(pageName);
66
- }
67
-
68
- public async processJavaScriptResult(
69
- pageName: string,
70
- result: BuildResult,
71
- config: FrontendConfig
72
- ): Promise<ProcessJavaScriptResult> {
73
- const modules: HotAsset[] = [];
74
- let requiresReload = false;
75
- const fallbackReasons: string[] = [];
76
- const metafile = result.metafile as Metafile | undefined;
77
-
78
- if (!metafile) {
79
- fallbackReasons.push('javascript.metafile.missing');
80
- return { modules, requiresReload: true, fallbackReasons };
81
- }
82
-
83
- const buildRoot = config.paths.build.frontend;
84
- const currentOutputs = new Set<string>();
85
- const previousOutputs = this.pageOutputHashes.get(pageName) ?? new Map<string, { hash: string }>();
86
-
87
- for (const outputPath of Object.keys(metafile.outputs)) {
88
- const extension = path.extname(outputPath).toLowerCase();
89
- if (extension !== EXTENSIONS.js && extension !== '.mjs') {
90
- continue;
91
- }
92
-
93
- const absoluteOutput = this.resolveOutputPath(outputPath);
94
- currentOutputs.add(absoluteOutput);
95
-
96
- const fingerprint = await this.computeAssetFingerprint(absoluteOutput, buildRoot, 'js');
97
- if (!fingerprint) {
98
- if (this.assetFingerprints.has(absoluteOutput)) {
99
- this.assetFingerprints.delete(absoluteOutput);
100
- }
101
- if (previousOutputs.has(absoluteOutput)) {
102
- previousOutputs.delete(absoluteOutput);
103
- requiresReload = true;
104
- fallbackReasons.push('javascript.output.missing');
105
- }
106
- continue;
107
- }
108
-
109
- if (fingerprint.requiresReload) {
110
- requiresReload = true;
111
- fallbackReasons.push('javascript.fingerprint.error');
112
- }
113
-
114
- if (fingerprint.changed) {
115
- modules.push(fingerprint.asset);
116
- }
117
-
118
- if (fingerprint.hash) {
119
- previousOutputs.set(absoluteOutput, { hash: fingerprint.hash });
120
- } else if (previousOutputs.has(absoluteOutput)) {
121
- previousOutputs.delete(absoluteOutput);
122
- }
123
- }
124
-
125
- for (const known of Array.from(previousOutputs.keys())) {
126
- if (!currentOutputs.has(known)) {
127
- previousOutputs.delete(known);
128
- requiresReload = true;
129
- fallbackReasons.push('javascript.output.removed');
130
- }
131
- }
132
-
133
- this.pageOutputHashes.set(pageName, previousOutputs);
134
- return { modules, requiresReload, fallbackReasons: uniqueReasons(fallbackReasons) };
135
- }
136
-
137
- public async collectCssChanges(
138
- context: BuilderContext,
139
- pageNames: readonly string[]
140
- ): Promise<CollectCssResult> {
141
- const { config, changedFile } = context;
142
- const buildRoot = config.paths.build.frontend;
143
- const candidates = new Set<string>();
144
-
145
- if (!changedFile) {
146
- for (const page of pageNames) {
147
- candidates.add(this.getPageCssOutputPath(config, page));
148
- }
149
- candidates.add(this.getAppCssOutputPath(config));
150
- } else {
151
- const normalized = path.resolve(changedFile);
152
- const extension = path.extname(normalized).toLowerCase();
153
- if (extension === EXTENSIONS.css) {
154
- if (isPathInside(normalized, config.paths.src.app)) {
155
- for (const page of pageNames) {
156
- candidates.add(this.getPageCssOutputPath(config, page));
157
- }
158
- candidates.add(this.getAppCssOutputPath(config));
159
- } else if (isPathInside(normalized, config.paths.src.pages)) {
160
- const page = findPageFromChangedFile(normalized, config.paths.src.pages);
161
- if (page) {
162
- candidates.add(this.getPageCssOutputPath(config, page));
163
- }
164
- } else if (isPathInside(normalized, config.paths.src.frontend)) {
165
- for (const page of pageNames) {
166
- candidates.add(this.getPageCssOutputPath(config, page));
167
- }
168
- candidates.add(this.getAppCssOutputPath(config));
169
- }
170
- }
171
- }
172
-
173
- if (candidates.size === 0 && !changedFile) {
174
- candidates.add(this.getAppCssOutputPath(config));
175
- for (const page of pageNames) {
176
- candidates.add(this.getPageCssOutputPath(config, page));
177
- }
178
- }
179
-
180
- const styles: HotAsset[] = [];
181
- let requiresReload = !changedFile;
182
- const fallbackReasons: string[] = [];
183
-
184
- if (!changedFile) {
185
- fallbackReasons.push('css.full-rebuild');
186
- }
187
-
188
- for (const candidate of candidates) {
189
- const fingerprint = await this.computeAssetFingerprint(candidate, buildRoot, 'css');
190
- if (!fingerprint) {
191
- if (this.assetFingerprints.has(path.resolve(candidate))) {
192
- this.assetFingerprints.delete(path.resolve(candidate));
193
- requiresReload = true;
194
- fallbackReasons.push('css.asset.missing');
195
- }
196
- continue;
197
- }
198
-
199
- if (fingerprint.requiresReload) {
200
- requiresReload = true;
201
- fallbackReasons.push('css.fingerprint.error');
202
- }
203
-
204
- if (fingerprint.changed) {
205
- styles.push(fingerprint.asset);
206
- }
207
- }
208
-
209
- return { styles, requiresReload, fallbackReasons: uniqueReasons(fallbackReasons) };
210
- }
211
-
212
- private async computeAssetFingerprint(
213
- filePath: string,
214
- buildRoot: string,
215
- type: HotAsset['type']
216
- ): Promise<{ asset: HotAsset; changed: boolean; requiresReload: boolean; hash?: string } | null> {
217
- const absolutePath = path.resolve(filePath);
218
- if (!(await pathExists(absolutePath))) {
219
- return null;
220
- }
221
-
222
- try {
223
- const contents = await readFile(absolutePath);
224
- const hash = createHash('sha1').update(contents).digest('hex');
225
- const previous = this.assetFingerprints.get(absolutePath);
226
- const changed = previous !== hash;
227
- this.assetFingerprints.set(absolutePath, hash);
228
- return {
229
- asset: this.createHotAsset(absolutePath, buildRoot, type),
230
- changed,
231
- requiresReload: false,
232
- hash
233
- };
234
- } catch (error) {
235
- emitDiagnostic({
236
- code: 'frontend.watch.unexpected',
237
- kind: 'watch-daemon',
238
- stage: 'css-fingerprint',
239
- severity: 'error',
240
- message: `Failed to fingerprint asset '${absolutePath}': ${error instanceof Error ? error.message : String(error)}`
241
- });
242
-
243
- return {
244
- asset: this.createHotAsset(absolutePath, buildRoot, type),
245
- changed: false,
246
- requiresReload: true
247
- };
248
- }
249
- }
250
-
251
- private getPageCssOutputPath(config: FrontendConfig, pageName: string): string {
252
- return path.join(config.paths.build.frontend, FOLDERS.pages, pageName, `${FILES.index}${EXTENSIONS.css}`);
253
- }
254
-
255
- private getAppCssOutputPath(config: FrontendConfig): string {
256
- return path.join(config.paths.build.frontend, FOLDERS.app, 'app.css');
257
- }
258
-
259
- private createHotAsset(filePath: string, buildRoot: string, type: HotAsset['type']): HotAsset {
260
- const relativePath = path.relative(buildRoot, filePath);
261
- const webPath = this.toWebPath(relativePath);
262
- return {
263
- type,
264
- path: filePath,
265
- relativePath,
266
- url: webPath.startsWith('/') ? webPath : `/${webPath}`
267
- };
268
- }
269
-
270
- private resolveOutputPath(outputPath: string): string {
271
- if (path.isAbsolute(outputPath)) {
272
- return outputPath;
273
- }
274
-
275
- return path.resolve(this.workspaceRoot, outputPath);
276
- }
277
-
278
- private toWebPath(relativePath: string): string {
279
- return relativePath.split(path.sep).join('/') || '';
280
- }
281
- }
282
-
283
- function uniqueReasons(reasons: readonly string[]): readonly string[] {
284
- return Array.from(new Set(reasons.filter(Boolean)));
285
- }
@@ -1,242 +0,0 @@
1
- import type { BuildFailure, Message } from 'esbuild';
2
- import { emitDiagnostic } from '../core/diagnostics.js';
3
- import type { DiagnosticSeverity } from '../core/diagnostics.js';
4
- import type { Builder, BuilderContext } from '../builders/types.js';
5
- import { WatchReporter, serializeMessages, type SerializedMessage } from './watchReporter.js';
6
- import type { HotAsset, HotUpdateDetails } from './hotUpdateTracker.js';
7
-
8
- const BUILDER_DISPLAY_NAMES: Record<string, string> = {
9
- css: 'CSS',
10
- html: 'HTML',
11
- 'static-assets': 'Static assets'
12
- } as const;
13
-
14
- export interface JavaScriptBuildSummary {
15
- readonly pagesBuilt: readonly string[];
16
- readonly warnings: readonly SerializedMessage[];
17
- readonly modules: readonly HotAsset[];
18
- readonly requiresReload: boolean;
19
- readonly fallbackReasons: readonly string[];
20
- }
21
-
22
- export interface AdditionalBuildResult {
23
- readonly succeeded: boolean;
24
- readonly assets: readonly string[];
25
- readonly styles: readonly HotAsset[];
26
- readonly requiresReload: boolean;
27
- readonly fallbackReasons: readonly string[];
28
- }
29
-
30
- export async function runBuilderWithDiagnostics(
31
- builder: Builder,
32
- reporter: WatchReporter,
33
- context: BuilderContext,
34
- changedFile: string | undefined,
35
- relativeChange: string | undefined
36
- ): Promise<boolean> {
37
- const displayName = BUILDER_DISPLAY_NAMES[builder.name] ?? builder.name;
38
- const messageContext = relativeChange ? ` (${relativeChange})` : '';
39
-
40
- reporter.emitVerbose({
41
- code: `frontend.watch.${builder.name}.build.start`,
42
- kind: 'watch-daemon',
43
- stage: builder.name,
44
- severity: 'info',
45
- message: `Starting ${displayName} rebuild${messageContext}.`,
46
- data: changedFile ? { changedFile, builder: builder.name } : { builder: builder.name }
47
- });
48
-
49
- try {
50
- await builder.build(context);
51
- reporter.emitVerbose({
52
- code: `frontend.watch.${builder.name}.build.success`,
53
- kind: 'watch-daemon',
54
- stage: builder.name,
55
- severity: 'info',
56
- message: `${displayName} rebuild completed${messageContext}.`,
57
- data: changedFile ? { changedFile, builder: builder.name } : { builder: builder.name }
58
- });
59
- return true;
60
- } catch (error) {
61
- const details: Record<string, unknown> = { builder: builder.name };
62
- if (changedFile) {
63
- details.changedFile = changedFile;
64
- }
65
- if (error instanceof Error) {
66
- details.error = error.message;
67
- } else {
68
- details.error = String(error);
69
- }
70
-
71
- emitDiagnostic({
72
- code: `frontend.watch.${builder.name}.build.failure`,
73
- kind: 'watch-daemon',
74
- stage: builder.name,
75
- severity: 'error',
76
- message: `${displayName} rebuild failed${messageContext}.`,
77
- data: details
78
- });
79
-
80
- return false;
81
- }
82
- }
83
-
84
- export function emitPipelineSuccess(
85
- summary: JavaScriptBuildSummary,
86
- assetsResult: AdditionalBuildResult,
87
- changedFile: string | undefined,
88
- relativeChange: string | undefined,
89
- hotUpdate: HotUpdateDetails
90
- ): void {
91
- const message = `Frontend rebuild pipeline completed${relativeChange ? ` (${relativeChange})` : ''}.`;
92
-
93
- const data: Record<string, unknown> = {
94
- pages: summary.pagesBuilt,
95
- assets: assetsResult.assets,
96
- hotUpdate: serializeHotUpdate(hotUpdate, relativeChange)
97
- };
98
-
99
- if (relativeChange) {
100
- data.changedFile = relativeChange;
101
- } else if (changedFile) {
102
- data.changedFile = changedFile;
103
- }
104
-
105
- if (summary.warnings.length > 0) {
106
- data.javascriptWarnings = summary.warnings;
107
- }
108
-
109
- emitDiagnostic({
110
- code: 'frontend.watch.pipeline.success',
111
- kind: 'watch-daemon',
112
- stage: 'pipeline',
113
- severity: 'info',
114
- message,
115
- data
116
- });
117
- }
118
-
119
- export function serializeSummary(
120
- summary: JavaScriptBuildSummary,
121
- changedFile: string | undefined,
122
- skipped: boolean
123
- ): Record<string, unknown> {
124
- const data: Record<string, unknown> = {
125
- pages: summary.pagesBuilt
126
- };
127
-
128
- if (changedFile) {
129
- data.changedFile = changedFile;
130
- }
131
-
132
- if (summary.warnings.length > 0) {
133
- data.warnings = summary.warnings;
134
- }
135
-
136
- if (skipped) {
137
- data.skipped = true;
138
- }
139
-
140
- if (summary.modules.length > 0) {
141
- data.modules = summary.modules.map(asset => asset.url);
142
- }
143
-
144
- if (summary.requiresReload) {
145
- data.requiresReload = true;
146
- }
147
-
148
- if (summary.fallbackReasons.length > 0) {
149
- data.fallbackReasons = summary.fallbackReasons;
150
- }
151
-
152
- return data;
153
- }
154
-
155
- export function emitJavaScriptFailure(error: unknown, changedFile?: string): void {
156
- let message = 'JavaScript rebuild failed.';
157
- let severity: DiagnosticSeverity = 'error';
158
- const data: Record<string, unknown> = changedFile ? { changedFile } : {};
159
-
160
- if (error instanceof JavaScriptBuildError) {
161
- message = `JavaScript rebuild failed for page '${error.pageName}'.`;
162
- if (error.details.length > 0) {
163
- data.errors = error.details;
164
- }
165
- } else if (error instanceof Error) {
166
- message = `JavaScript rebuild failed: ${error.message}`;
167
- }
168
-
169
- emitDiagnostic({
170
- code: 'frontend.watch.javascript.build.failure',
171
- kind: 'watch-daemon',
172
- stage: 'javascript',
173
- severity,
174
- message,
175
- data: Object.keys(data).length > 0 ? data : undefined
176
- });
177
- }
178
-
179
- export class JavaScriptBuildError extends Error {
180
- public readonly pageName: string;
181
- public readonly details: readonly SerializedMessage[];
182
-
183
- public constructor(pageName: string, cause: unknown) {
184
- const message = cause instanceof Error ? cause.message : String(cause);
185
- super(message);
186
- this.pageName = pageName;
187
- this.details = isBuildFailure(cause) ? serializeMessages(cause.errors ?? []) : [];
188
- }
189
- }
190
-
191
- function serializeHotUpdate(hotUpdate: HotUpdateDetails, relativeChange?: string): Record<string, unknown> {
192
- const data: Record<string, unknown> = {
193
- requiresReload: hotUpdate.requiresReload,
194
- modules: hotUpdate.modules.map(asset => serializeHotAsset(asset)),
195
- styles: hotUpdate.styles.map(asset => serializeHotAsset(asset))
196
- };
197
-
198
- if (relativeChange) {
199
- data.changedFile = relativeChange;
200
- } else if (hotUpdate.changedFile) {
201
- data.changedFile = hotUpdate.changedFile;
202
- }
203
-
204
- if (hotUpdate.fallbackReasons.length > 0) {
205
- data.fallbackReasons = hotUpdate.fallbackReasons;
206
- }
207
-
208
- if (hotUpdate.stats) {
209
- data.stats = {
210
- hotUpdates: hotUpdate.stats.hotUpdates,
211
- reloadFallbacks: hotUpdate.stats.reloadFallbacks
212
- };
213
- }
214
-
215
- return data;
216
- }
217
-
218
- function serializeHotAsset(asset: HotAsset): Record<string, string> {
219
- return {
220
- type: asset.type,
221
- path: asset.path,
222
- relativePath: asset.relativePath,
223
- url: asset.url
224
- };
225
- }
226
-
227
- function isBuildFailure(error: unknown): error is BuildFailure {
228
- if (typeof error !== 'object' || error === null) {
229
- return false;
230
- }
231
-
232
- const candidate = error as BuildFailure;
233
- return Array.isArray(candidate.errors) && candidate.errors.every(isEsbuildMessage);
234
- }
235
-
236
- function isEsbuildMessage(candidate: unknown): candidate is Message {
237
- if (typeof candidate !== 'object' || candidate === null) {
238
- return false;
239
- }
240
-
241
- return typeof (candidate as Message).text === 'string';
242
- }