@webstir-io/webstir-frontend 0.1.40

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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/assets/assetManifest.d.ts +16 -0
  4. package/dist/assets/assetManifest.js +31 -0
  5. package/dist/assets/imageOptimizer.d.ts +6 -0
  6. package/dist/assets/imageOptimizer.js +93 -0
  7. package/dist/assets/precompression.d.ts +1 -0
  8. package/dist/assets/precompression.js +21 -0
  9. package/dist/builders/contentBuilder.d.ts +2 -0
  10. package/dist/builders/contentBuilder.js +1052 -0
  11. package/dist/builders/cssBuilder.d.ts +2 -0
  12. package/dist/builders/cssBuilder.js +439 -0
  13. package/dist/builders/htmlBuilder.d.ts +2 -0
  14. package/dist/builders/htmlBuilder.js +430 -0
  15. package/dist/builders/index.d.ts +2 -0
  16. package/dist/builders/index.js +14 -0
  17. package/dist/builders/jsBuilder.d.ts +2 -0
  18. package/dist/builders/jsBuilder.js +300 -0
  19. package/dist/builders/staticAssetsBuilder.d.ts +2 -0
  20. package/dist/builders/staticAssetsBuilder.js +158 -0
  21. package/dist/builders/types.d.ts +12 -0
  22. package/dist/builders/types.js +1 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +105 -0
  25. package/dist/config/manifest.d.ts +7 -0
  26. package/dist/config/manifest.js +17 -0
  27. package/dist/config/paths.d.ts +3 -0
  28. package/dist/config/paths.js +11 -0
  29. package/dist/config/schema.d.ts +413 -0
  30. package/dist/config/schema.js +44 -0
  31. package/dist/config/setup.d.ts +2 -0
  32. package/dist/config/setup.js +12 -0
  33. package/dist/config/workspace.d.ts +2 -0
  34. package/dist/config/workspace.js +131 -0
  35. package/dist/config/workspaceManifest.d.ts +23 -0
  36. package/dist/config/workspaceManifest.js +1 -0
  37. package/dist/core/constants.d.ts +70 -0
  38. package/dist/core/constants.js +70 -0
  39. package/dist/core/diagnostics.d.ts +15 -0
  40. package/dist/core/diagnostics.js +21 -0
  41. package/dist/core/index.d.ts +3 -0
  42. package/dist/core/index.js +3 -0
  43. package/dist/core/pages.d.ts +6 -0
  44. package/dist/core/pages.js +23 -0
  45. package/dist/hooks.d.ts +19 -0
  46. package/dist/hooks.js +115 -0
  47. package/dist/html/criticalCss.d.ts +4 -0
  48. package/dist/html/criticalCss.js +192 -0
  49. package/dist/html/htmlSecurity.d.ts +5 -0
  50. package/dist/html/htmlSecurity.js +73 -0
  51. package/dist/html/lazyLoad.d.ts +6 -0
  52. package/dist/html/lazyLoad.js +21 -0
  53. package/dist/html/pageScaffold.d.ts +10 -0
  54. package/dist/html/pageScaffold.js +51 -0
  55. package/dist/html/resourceHints.d.ts +7 -0
  56. package/dist/html/resourceHints.js +64 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +5 -0
  59. package/dist/modes/ssg/index.d.ts +4 -0
  60. package/dist/modes/ssg/index.js +4 -0
  61. package/dist/modes/ssg/metadata.d.ts +5 -0
  62. package/dist/modes/ssg/metadata.js +50 -0
  63. package/dist/modes/ssg/routing.d.ts +2 -0
  64. package/dist/modes/ssg/routing.js +186 -0
  65. package/dist/modes/ssg/seo.d.ts +4 -0
  66. package/dist/modes/ssg/seo.js +208 -0
  67. package/dist/modes/ssg/validation.d.ts +3 -0
  68. package/dist/modes/ssg/validation.js +27 -0
  69. package/dist/modes/ssg/views.d.ts +2 -0
  70. package/dist/modes/ssg/views.js +236 -0
  71. package/dist/operations.d.ts +5 -0
  72. package/dist/operations.js +102 -0
  73. package/dist/pipeline.d.ts +7 -0
  74. package/dist/pipeline.js +71 -0
  75. package/dist/provider.d.ts +2 -0
  76. package/dist/provider.js +176 -0
  77. package/dist/types.d.ts +61 -0
  78. package/dist/types.js +1 -0
  79. package/dist/utils/changedFile.d.ts +8 -0
  80. package/dist/utils/changedFile.js +26 -0
  81. package/dist/utils/fs.d.ts +11 -0
  82. package/dist/utils/fs.js +39 -0
  83. package/dist/utils/hash.d.ts +1 -0
  84. package/dist/utils/hash.js +5 -0
  85. package/dist/utils/pagePaths.d.ts +5 -0
  86. package/dist/utils/pagePaths.js +36 -0
  87. package/dist/utils/pathMatch.d.ts +3 -0
  88. package/dist/utils/pathMatch.js +29 -0
  89. package/dist/watch/frontendFiles.d.ts +3 -0
  90. package/dist/watch/frontendFiles.js +25 -0
  91. package/dist/watch/hotUpdateTracker.d.ts +51 -0
  92. package/dist/watch/hotUpdateTracker.js +205 -0
  93. package/dist/watch/pipelineHelpers.d.ts +26 -0
  94. package/dist/watch/pipelineHelpers.js +177 -0
  95. package/dist/watch/types.d.ts +27 -0
  96. package/dist/watch/types.js +1 -0
  97. package/dist/watch/watchCoordinator.d.ts +36 -0
  98. package/dist/watch/watchCoordinator.js +551 -0
  99. package/dist/watch/watchDaemon.d.ts +17 -0
  100. package/dist/watch/watchDaemon.js +127 -0
  101. package/dist/watch/watchReporter.d.ts +21 -0
  102. package/dist/watch/watchReporter.js +64 -0
  103. package/package.json +92 -0
  104. package/scripts/publish.sh +101 -0
  105. package/scripts/smoke.mjs +35 -0
  106. package/scripts/update-contract.sh +121 -0
  107. package/src/assets/assetManifest.ts +51 -0
  108. package/src/assets/imageOptimizer.ts +112 -0
  109. package/src/assets/precompression.ts +25 -0
  110. package/src/builders/contentBuilder.ts +1400 -0
  111. package/src/builders/cssBuilder.ts +552 -0
  112. package/src/builders/htmlBuilder.ts +540 -0
  113. package/src/builders/index.ts +16 -0
  114. package/src/builders/jsBuilder.ts +358 -0
  115. package/src/builders/staticAssetsBuilder.ts +174 -0
  116. package/src/builders/types.ts +15 -0
  117. package/src/cli.ts +108 -0
  118. package/src/config/manifest.ts +24 -0
  119. package/src/config/paths.ts +14 -0
  120. package/src/config/schema.ts +49 -0
  121. package/src/config/setup.ts +14 -0
  122. package/src/config/workspace.ts +150 -0
  123. package/src/config/workspaceManifest.ts +27 -0
  124. package/src/core/constants.ts +73 -0
  125. package/src/core/diagnostics.ts +40 -0
  126. package/src/core/index.ts +3 -0
  127. package/src/core/pages.ts +31 -0
  128. package/src/hooks.ts +175 -0
  129. package/src/html/criticalCss.ts +214 -0
  130. package/src/html/htmlSecurity.ts +86 -0
  131. package/src/html/lazyLoad.ts +30 -0
  132. package/src/html/pageScaffold.ts +70 -0
  133. package/src/html/resourceHints.ts +91 -0
  134. package/src/index.ts +5 -0
  135. package/src/modes/ssg/index.ts +4 -0
  136. package/src/modes/ssg/metadata.ts +63 -0
  137. package/src/modes/ssg/routing.ts +230 -0
  138. package/src/modes/ssg/seo.ts +261 -0
  139. package/src/modes/ssg/validation.ts +37 -0
  140. package/src/modes/ssg/views.ts +309 -0
  141. package/src/operations.ts +138 -0
  142. package/src/pipeline.ts +88 -0
  143. package/src/provider.ts +249 -0
  144. package/src/types.ts +67 -0
  145. package/src/utils/changedFile.ts +39 -0
  146. package/src/utils/fs.ts +48 -0
  147. package/src/utils/hash.ts +6 -0
  148. package/src/utils/pagePaths.ts +43 -0
  149. package/src/utils/pathMatch.ts +36 -0
  150. package/src/watch/frontendFiles.ts +32 -0
  151. package/src/watch/hotUpdateTracker.ts +285 -0
  152. package/src/watch/pipelineHelpers.ts +242 -0
  153. package/src/watch/types.ts +23 -0
  154. package/src/watch/watchCoordinator.ts +666 -0
  155. package/src/watch/watchDaemon.ts +144 -0
  156. package/src/watch/watchReporter.ts +98 -0
  157. package/tests/add-page-defaults.test.js +64 -0
  158. package/tests/content-pages.test.js +81 -0
  159. package/tests/css-app-imports.test.js +64 -0
  160. package/tests/css-page-imports.test.js +100 -0
  161. package/tests/diagnostics.test.js +48 -0
  162. package/tests/features.test.js +63 -0
  163. package/tests/hooks.test.js +71 -0
  164. package/tests/provider.integration.test.js +137 -0
  165. package/tests/ssg-defaults.test.js +201 -0
  166. package/tests/ssg-guardrails.test.js +69 -0
  167. package/tsconfig.json +27 -0
@@ -0,0 +1,285 @@
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
+ }
@@ -0,0 +1,242 @@
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
+ }
@@ -0,0 +1,23 @@
1
+ export type WatchDaemonCommand =
2
+ | { readonly type: 'start' }
3
+ | { readonly type: 'change'; readonly path: string }
4
+ | { readonly type: 'reload' }
5
+ | { readonly type: 'shutdown' }
6
+ | { readonly type: 'ping'; readonly id?: string };
7
+
8
+ export interface WatchDaemonOptions {
9
+ readonly workspaceRoot: string;
10
+ readonly autoStart?: boolean;
11
+ readonly verbose?: boolean;
12
+ readonly hmrVerbose?: boolean;
13
+ }
14
+
15
+ export interface WatchCoordinatorOptions {
16
+ readonly workspaceRoot: string;
17
+ readonly verbose?: boolean;
18
+ readonly hmrVerbose?: boolean;
19
+ }
20
+
21
+ export interface WatchChangeIntent {
22
+ readonly path?: string;
23
+ }