@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,551 +0,0 @@
1
- import path from 'node:path';
2
- import { performance } from 'node:perf_hooks';
3
- import { context as createEsbuildContext } from 'esbuild';
4
- import { FOLDERS, FILES, FILE_NAMES, EXTENSIONS } from '../core/constants.js';
5
- import { getPages } from '../core/pages.js';
6
- import { emitDiagnostic } from '../core/diagnostics.js';
7
- import { prepareWorkspaceConfig } from '../config/setup.js';
8
- import { ensureDir, readJson } from '../utils/fs.js';
9
- import { shouldProcess, isPathInside } from '../utils/changedFile.js';
10
- import { findPageFromChangedFile } from '../utils/pathMatch.js';
11
- import { createCssBuilder } from '../builders/cssBuilder.js';
12
- import { createHtmlBuilder } from '../builders/htmlBuilder.js';
13
- import { createStaticAssetsBuilder } from '../builders/staticAssetsBuilder.js';
14
- import { WatchReporter, serializeMessages } from './watchReporter.js';
15
- import { HotUpdateTracker } from './hotUpdateTracker.js';
16
- import { runBuilderWithDiagnostics, emitPipelineSuccess, serializeSummary, emitJavaScriptFailure, JavaScriptBuildError } from './pipelineHelpers.js';
17
- import { resolveEntryPoint, copyRefreshScript } from './frontendFiles.js';
18
- const JAVASCRIPT_EXTENSIONS = [EXTENSIONS.ts, EXTENSIONS.js, '.tsx', '.jsx'];
19
- export class WatchCoordinator {
20
- workspaceRoot;
21
- jsContexts = new Map();
22
- verbose;
23
- hmrVerbose;
24
- reporter;
25
- hotUpdateTracker;
26
- hmrTotals = { hotUpdates: 0, reloadFallbacks: 0 };
27
- config;
28
- isSsgWorkspace = false;
29
- enable;
30
- isStopping = false;
31
- queue = Promise.resolve();
32
- constructor(options) {
33
- this.workspaceRoot = options.workspaceRoot;
34
- this.verbose = options.verbose ?? false;
35
- this.hmrVerbose = options.hmrVerbose ?? false;
36
- this.reporter = new WatchReporter({ verbose: this.verbose });
37
- this.hotUpdateTracker = new HotUpdateTracker({ workspaceRoot: this.workspaceRoot });
38
- }
39
- async start() {
40
- if (this.config) {
41
- return;
42
- }
43
- this.reporter.emitVerbose({
44
- code: 'frontend.watch.starting',
45
- kind: 'watch-daemon',
46
- stage: 'startup',
47
- severity: 'info',
48
- message: 'Starting frontend watch daemon...'
49
- });
50
- this.config = await prepareWorkspaceConfig(this.workspaceRoot);
51
- const workspaceSettings = await this.readWorkspaceSettings(this.workspaceRoot);
52
- this.isSsgWorkspace = workspaceSettings.isSsg;
53
- this.enable = workspaceSettings.enable;
54
- await this.refreshJavaScriptContexts();
55
- const pipelineReady = await this.runFullBuildCycle();
56
- if (pipelineReady) {
57
- this.reporter.emitVerbose({
58
- code: 'frontend.watch.ready',
59
- kind: 'watch-daemon',
60
- stage: 'startup',
61
- severity: 'info',
62
- message: 'Frontend watch daemon is ready.'
63
- });
64
- }
65
- }
66
- async reload() {
67
- await this.enqueue(async () => {
68
- if (!this.config) {
69
- await this.start();
70
- return;
71
- }
72
- this.reporter.emitVerbose({
73
- code: 'frontend.watch.reload',
74
- kind: 'watch-daemon',
75
- stage: 'startup',
76
- severity: 'info',
77
- message: 'Reloading frontend watch contexts...'
78
- });
79
- await this.refreshJavaScriptContexts();
80
- const workspaceSettings = await this.readWorkspaceSettings(this.workspaceRoot);
81
- this.isSsgWorkspace = workspaceSettings.isSsg;
82
- this.enable = workspaceSettings.enable;
83
- const pipelineSucceeded = await this.runFullBuildCycle();
84
- if (pipelineSucceeded) {
85
- this.reporter.emitVerbose({
86
- code: 'frontend.watch.reload.complete',
87
- kind: 'watch-daemon',
88
- stage: 'startup',
89
- severity: 'info',
90
- message: 'Frontend watch contexts reloaded.'
91
- });
92
- }
93
- });
94
- }
95
- async handleChange(intent) {
96
- await this.enqueue(async () => {
97
- if (!this.config) {
98
- await this.start();
99
- }
100
- const resolvedChange = this.resolveChangedFile(intent.path);
101
- if (resolvedChange && path.resolve(resolvedChange) === path.resolve(this.workspaceRoot, FILES.packageJson)) {
102
- const workspaceSettings = await this.readWorkspaceSettings(this.workspaceRoot);
103
- this.isSsgWorkspace = workspaceSettings.isSsg;
104
- this.enable = workspaceSettings.enable;
105
- }
106
- await this.runFullBuildCycle(resolvedChange);
107
- });
108
- }
109
- async stop() {
110
- if (this.isStopping) {
111
- return;
112
- }
113
- this.isStopping = true;
114
- await this.enqueue(async () => {
115
- for (const entry of this.jsContexts.values()) {
116
- await entry.context.dispose();
117
- }
118
- this.jsContexts.clear();
119
- this.hotUpdateTracker.reset();
120
- this.config = undefined;
121
- });
122
- this.isStopping = false;
123
- this.reporter.emitVerbose({
124
- code: 'frontend.watch.stopped',
125
- kind: 'watch-daemon',
126
- stage: 'shutdown',
127
- severity: 'info',
128
- message: 'Frontend watch daemon stopped.'
129
- });
130
- }
131
- async enqueue(task) {
132
- const runTask = async () => {
133
- try {
134
- await task();
135
- }
136
- catch (error) {
137
- this.logUnexpectedError('queue-task', error);
138
- }
139
- };
140
- this.queue = this.queue.then(runTask, runTask);
141
- await this.queue;
142
- }
143
- async refreshJavaScriptContexts() {
144
- const config = this.requireConfig();
145
- const pages = await getPages(config.paths.src.pages);
146
- const observed = new Set();
147
- for (const page of pages) {
148
- observed.add(page.name);
149
- await this.ensureJavaScriptContext(config, page);
150
- }
151
- for (const existing of Array.from(this.jsContexts.keys())) {
152
- if (!observed.has(existing)) {
153
- const context = this.jsContexts.get(existing);
154
- if (context) {
155
- await context.context.dispose();
156
- }
157
- this.jsContexts.delete(existing);
158
- this.hotUpdateTracker.removePage(existing);
159
- this.reporter.emitVerbose({
160
- code: 'frontend.watch.javascript.context.removed',
161
- kind: 'watch-daemon',
162
- stage: 'javascript',
163
- severity: 'info',
164
- message: `Removed watch context for page '${existing}'.`
165
- });
166
- }
167
- }
168
- }
169
- async ensureJavaScriptContext(config, page) {
170
- const entryPoint = await resolveEntryPoint(page.directory);
171
- if (!entryPoint) {
172
- if (!this.isSsgWorkspace) {
173
- emitDiagnostic({
174
- code: 'frontend.watch.javascript.entry.missing',
175
- kind: 'watch-daemon',
176
- stage: 'javascript',
177
- severity: 'warning',
178
- message: `No JavaScript entry point found for page '${page.name}'.`
179
- });
180
- }
181
- else if (this.verbose) {
182
- emitDiagnostic({
183
- code: 'frontend.watch.javascript.entry.missing',
184
- kind: 'watch-daemon',
185
- stage: 'javascript',
186
- severity: 'info',
187
- message: `No JavaScript entry point found for page '${page.name}' (ssg workspace).`
188
- });
189
- }
190
- if (this.jsContexts.has(page.name)) {
191
- const existing = this.jsContexts.get(page.name);
192
- if (existing) {
193
- await existing.context.dispose();
194
- }
195
- this.jsContexts.delete(page.name);
196
- this.hotUpdateTracker.removePage(page.name);
197
- }
198
- return;
199
- }
200
- const existing = this.jsContexts.get(page.name);
201
- if (existing && path.resolve(existing.entryPoint) === path.resolve(entryPoint)) {
202
- return;
203
- }
204
- if (existing) {
205
- await existing.context.dispose();
206
- this.jsContexts.delete(page.name);
207
- this.hotUpdateTracker.removePage(page.name);
208
- }
209
- const outputDir = path.join(config.paths.build.frontend, FOLDERS.pages, page.name);
210
- await ensureDir(outputDir);
211
- const context = await createEsbuildContext({
212
- entryPoints: [entryPoint],
213
- bundle: true,
214
- format: 'esm',
215
- target: 'es2020',
216
- platform: 'browser',
217
- sourcemap: true,
218
- outfile: path.join(outputDir, `${FILES.index}${EXTENSIONS.js}`),
219
- logLevel: 'silent',
220
- metafile: true
221
- });
222
- this.jsContexts.set(page.name, {
223
- name: page.name,
224
- entryPoint,
225
- context
226
- });
227
- this.reporter.emitVerbose({
228
- code: 'frontend.watch.javascript.context.created',
229
- kind: 'watch-daemon',
230
- stage: 'javascript',
231
- severity: 'info',
232
- message: `Created watch context for page '${page.name}'.`
233
- });
234
- }
235
- async runFullBuildCycle(changedFile) {
236
- const summary = await this.runJavaScriptBuild(changedFile);
237
- if (!summary) {
238
- return false;
239
- }
240
- const assetsResult = await this.runAdditionalBuilders(changedFile);
241
- if (!assetsResult.succeeded) {
242
- return false;
243
- }
244
- const requiresReload = !changedFile || summary.requiresReload || assetsResult.requiresReload;
245
- const fallbackReasons = this.combineFallbackReasons(summary.fallbackReasons, assetsResult.fallbackReasons);
246
- const relativeChange = this.getRelativeChange(changedFile);
247
- const baseHotUpdate = {
248
- modules: summary.modules,
249
- styles: assetsResult.styles,
250
- requiresReload,
251
- fallbackReasons,
252
- changedFile
253
- };
254
- const stats = this.recordHotUpdateOutcome(changedFile, relativeChange, baseHotUpdate);
255
- const hotUpdate = stats
256
- ? {
257
- ...baseHotUpdate,
258
- stats
259
- }
260
- : baseHotUpdate;
261
- if (changedFile && requiresReload) {
262
- this.emitHotUpdateFallback(relativeChange ?? changedFile, hotUpdate);
263
- }
264
- emitPipelineSuccess(summary, assetsResult, changedFile, relativeChange, hotUpdate);
265
- return true;
266
- }
267
- async runAdditionalBuilders(changedFile) {
268
- const config = this.requireConfig();
269
- const context = { config, changedFile, enable: this.enable };
270
- const builders = [
271
- createCssBuilder(context),
272
- createHtmlBuilder(context),
273
- createStaticAssetsBuilder(context)
274
- ];
275
- const executed = [];
276
- const styles = [];
277
- let succeeded = true;
278
- let requiresReload = false;
279
- const pageNames = Array.from(this.jsContexts.keys());
280
- const relativeChange = this.getRelativeChange(changedFile);
281
- const fallbackReasons = [];
282
- const normalizedChange = changedFile ? path.resolve(changedFile) : undefined;
283
- const appTemplatePath = path.resolve(config.paths.src.app, FILE_NAMES.htmlAppTemplate);
284
- const isHtmlChange = Boolean(normalizedChange && ((path.extname(normalizedChange).toLowerCase() === EXTENSIONS.html
285
- && (isPathInside(normalizedChange, config.paths.src.pages) || isPathInside(normalizedChange, config.paths.src.app)))
286
- || normalizedChange === appTemplatePath));
287
- const staticAssetDirectories = [
288
- config.paths.src.images,
289
- config.paths.src.fonts,
290
- config.paths.src.media
291
- ].filter((directory) => Boolean(directory)).map((directory) => path.resolve(directory));
292
- const robotsPath = path.resolve(config.paths.src.frontend, FILES.robotsTxt);
293
- const isStaticAssetChange = Boolean(normalizedChange && (staticAssetDirectories.some(directory => isPathInside(normalizedChange, directory))
294
- || normalizedChange === robotsPath));
295
- for (const builder of builders) {
296
- executed.push(builder.name);
297
- const builderSucceeded = await runBuilderWithDiagnostics(builder, this.reporter, context, changedFile, relativeChange);
298
- if (!builderSucceeded) {
299
- succeeded = false;
300
- break;
301
- }
302
- if (builder.name === 'css') {
303
- const cssResult = await this.hotUpdateTracker.collectCssChanges(context, pageNames);
304
- styles.push(...cssResult.styles);
305
- if (cssResult.requiresReload) {
306
- requiresReload = true;
307
- }
308
- fallbackReasons.push(...cssResult.fallbackReasons);
309
- }
310
- if (builder.name === 'html') {
311
- if (!changedFile || isHtmlChange) {
312
- requiresReload = true;
313
- fallbackReasons.push('builder.html.reload');
314
- }
315
- }
316
- if (builder.name === 'static-assets') {
317
- if (!changedFile || isStaticAssetChange) {
318
- requiresReload = true;
319
- fallbackReasons.push('builder.static-assets.reload');
320
- }
321
- }
322
- }
323
- return {
324
- succeeded,
325
- assets: executed,
326
- styles,
327
- requiresReload,
328
- fallbackReasons: this.combineFallbackReasons([], fallbackReasons)
329
- };
330
- }
331
- getRelativeChange(changedFile) {
332
- if (!changedFile) {
333
- return undefined;
334
- }
335
- return path.relative(this.workspaceRoot, changedFile);
336
- }
337
- async runJavaScriptBuild(changedFile) {
338
- const config = this.requireConfig();
339
- const context = { config, changedFile, enable: this.enable };
340
- const shouldRun = shouldProcess(context, [
341
- {
342
- directory: config.paths.src.frontend,
343
- extensions: JAVASCRIPT_EXTENSIONS
344
- },
345
- {
346
- directory: config.paths.src.pages,
347
- extensions: JAVASCRIPT_EXTENSIONS
348
- }
349
- ]);
350
- const relativeChange = this.getRelativeChange(changedFile);
351
- if (shouldRun) {
352
- this.reporter.emitVerbose({
353
- code: 'frontend.watch.javascript.build.start',
354
- kind: 'watch-daemon',
355
- stage: 'javascript',
356
- severity: 'info',
357
- message: `Starting JavaScript rebuild${relativeChange ? ` (${relativeChange})` : ''}.`,
358
- data: changedFile ? { changedFile } : undefined
359
- });
360
- }
361
- try {
362
- const summary = shouldRun
363
- ? await this.executeJavaScriptBuild(changedFile)
364
- : { pagesBuilt: [], warnings: [], modules: [], requiresReload: false, fallbackReasons: [] };
365
- const skipped = !shouldRun;
366
- const message = skipped
367
- ? `JavaScript rebuild not required${relativeChange ? ` (${relativeChange})` : ''}.`
368
- : `JavaScript rebuild completed (${summary.pagesBuilt.length} page(s))${relativeChange ? ` (${relativeChange})` : ''}.`;
369
- this.reporter.emitVerbose({
370
- code: 'frontend.watch.javascript.build.success',
371
- kind: 'watch-daemon',
372
- stage: 'javascript',
373
- severity: 'info',
374
- message,
375
- data: serializeSummary(summary, changedFile, skipped)
376
- });
377
- return summary;
378
- }
379
- catch (error) {
380
- emitJavaScriptFailure(error, changedFile);
381
- return null;
382
- }
383
- }
384
- async executeJavaScriptBuild(changedFile) {
385
- const config = this.requireConfig();
386
- const targetPages = this.resolveTargetPages(changedFile);
387
- if (targetPages.length === 0) {
388
- return { pagesBuilt: [], warnings: [], modules: [], requiresReload: false, fallbackReasons: [] };
389
- }
390
- const warnings = [];
391
- const builtPages = [];
392
- const modules = [];
393
- let requiresReload = false;
394
- const fallbackReasons = [];
395
- for (const pageName of targetPages) {
396
- const pageContext = this.jsContexts.get(pageName);
397
- if (!pageContext) {
398
- continue;
399
- }
400
- try {
401
- const start = performance.now();
402
- const result = await pageContext.context.rebuild();
403
- const duration = performance.now() - start;
404
- builtPages.push(pageName);
405
- warnings.push(...serializeMessages(result.warnings ?? []));
406
- this.reporter.emitJavaScriptStats(pageName, result, duration);
407
- const outputDetails = await this.hotUpdateTracker.processJavaScriptResult(pageName, result, config);
408
- modules.push(...outputDetails.modules);
409
- if (outputDetails.requiresReload) {
410
- requiresReload = true;
411
- }
412
- fallbackReasons.push(...outputDetails.fallbackReasons);
413
- }
414
- catch (error) {
415
- throw new JavaScriptBuildError(pageName, error);
416
- }
417
- }
418
- if (builtPages.length > 0) {
419
- await copyRefreshScript(this.requireConfig(), this.enable);
420
- }
421
- return {
422
- pagesBuilt: builtPages,
423
- warnings,
424
- modules,
425
- requiresReload,
426
- fallbackReasons: this.combineFallbackReasons([], fallbackReasons)
427
- };
428
- }
429
- async readWorkspaceSettings(workspaceRoot) {
430
- const pkgPath = path.join(workspaceRoot, FILES.packageJson);
431
- const pkg = await readJson(pkgPath);
432
- if (!pkg?.webstir) {
433
- return { isSsg: false, enable: undefined };
434
- }
435
- const stringMode = pkg.webstir.mode;
436
- if (typeof stringMode === 'string' && stringMode.toLowerCase() === 'ssg') {
437
- return { isSsg: true, enable: pkg.webstir.enable };
438
- }
439
- const views = pkg.webstir.moduleManifest?.views;
440
- const hasSsgView = Array.isArray(views) && views.some(view => view.renderMode?.toLowerCase() === 'ssg');
441
- return { isSsg: hasSsgView, enable: pkg.webstir.enable };
442
- }
443
- resolveTargetPages(changedFile) {
444
- if (!changedFile) {
445
- return Array.from(this.jsContexts.keys());
446
- }
447
- const config = this.requireConfig();
448
- const targetPage = findPageFromChangedFile(changedFile, config.paths.src.pages);
449
- if (targetPage && this.jsContexts.has(targetPage)) {
450
- return [targetPage];
451
- }
452
- return Array.from(this.jsContexts.keys());
453
- }
454
- resolveChangedFile(changedFile) {
455
- if (!changedFile) {
456
- return undefined;
457
- }
458
- if (path.isAbsolute(changedFile)) {
459
- return changedFile;
460
- }
461
- return path.resolve(this.workspaceRoot, changedFile);
462
- }
463
- emitHotUpdateFallback(changedFile, hotUpdate) {
464
- if (hotUpdate.fallbackReasons.length === 0) {
465
- return;
466
- }
467
- emitDiagnostic({
468
- code: 'frontend.watch.pipeline.hmrfallback',
469
- kind: 'watch-daemon',
470
- stage: 'pipeline',
471
- severity: 'info',
472
- message: `Hot update fallback triggered for '${changedFile}' (${hotUpdate.fallbackReasons.join(', ')}).`,
473
- data: {
474
- changedFile,
475
- reasons: hotUpdate.fallbackReasons,
476
- modules: hotUpdate.modules.map(asset => asset.url),
477
- styles: hotUpdate.styles.map(asset => asset.url)
478
- }
479
- });
480
- }
481
- recordHotUpdateOutcome(changedFile, relativeChange, hotUpdate) {
482
- if (!changedFile) {
483
- return undefined;
484
- }
485
- if (hotUpdate.requiresReload) {
486
- this.hmrTotals.reloadFallbacks += 1;
487
- }
488
- else {
489
- this.hmrTotals.hotUpdates += 1;
490
- }
491
- const snapshot = {
492
- hotUpdates: this.hmrTotals.hotUpdates,
493
- reloadFallbacks: this.hmrTotals.reloadFallbacks
494
- };
495
- if (hotUpdate.requiresReload && hotUpdate.fallbackReasons.length > 0) {
496
- this.reporter.emitVerbose({
497
- code: 'frontend.watch.hmr.fallback.detail',
498
- kind: 'watch-daemon',
499
- stage: 'pipeline',
500
- severity: 'info',
501
- message: `Hot update declined for '${relativeChange ?? changedFile}'.`,
502
- data: {
503
- changedFile: relativeChange ?? changedFile,
504
- fallbackReasons: hotUpdate.fallbackReasons
505
- }
506
- });
507
- }
508
- if (this.hmrVerbose) {
509
- const identifier = relativeChange ?? changedFile;
510
- const modules = hotUpdate.modules.map(asset => asset.relativePath);
511
- const styles = hotUpdate.styles.map(asset => asset.relativePath);
512
- emitDiagnostic({
513
- code: 'frontend.watch.hmr.summary',
514
- kind: 'watch-daemon',
515
- stage: 'pipeline',
516
- severity: 'info',
517
- message: hotUpdate.requiresReload
518
- ? `HMR fallback required for '${identifier}'.`
519
- : `Hot update applied for '${identifier}'.`,
520
- data: {
521
- changedFile: identifier,
522
- requiresReload: hotUpdate.requiresReload,
523
- fallbackReasons: hotUpdate.fallbackReasons,
524
- modules,
525
- styles,
526
- totals: snapshot
527
- }
528
- });
529
- }
530
- return snapshot;
531
- }
532
- combineFallbackReasons(first, second) {
533
- return Array.from(new Set([...first, ...second].filter(Boolean)));
534
- }
535
- requireConfig() {
536
- if (!this.config) {
537
- throw new Error('Watch coordinator not initialized.');
538
- }
539
- return this.config;
540
- }
541
- logUnexpectedError(stage, error) {
542
- const message = error instanceof Error ? error.message : String(error);
543
- emitDiagnostic({
544
- code: 'frontend.watch.unexpected',
545
- kind: 'watch-daemon',
546
- stage,
547
- severity: 'error',
548
- message: `Unexpected watch daemon error: ${message}`
549
- });
550
- }
551
- }
@@ -1,17 +0,0 @@
1
- import type { WatchDaemonOptions } from './types.js';
2
- export declare class WatchDaemon {
3
- private readonly coordinator;
4
- private readonly options;
5
- private readonly shutdownPromise;
6
- private resolveShutdown;
7
- private commandQueue;
8
- private isShuttingDown;
9
- private rl?;
10
- constructor(options: WatchDaemonOptions);
11
- run(): Promise<void>;
12
- private setupCommandLoop;
13
- private setupSignalHandlers;
14
- private processLine;
15
- private handleCommand;
16
- private shutdown;
17
- }
@@ -1,127 +0,0 @@
1
- import process from 'node:process';
2
- import { createInterface } from 'node:readline';
3
- import { emitDiagnostic } from '../core/diagnostics.js';
4
- import { WatchCoordinator } from './watchCoordinator.js';
5
- export class WatchDaemon {
6
- coordinator;
7
- options;
8
- shutdownPromise;
9
- resolveShutdown = null;
10
- commandQueue = Promise.resolve();
11
- isShuttingDown = false;
12
- rl;
13
- constructor(options) {
14
- this.options = options;
15
- this.coordinator = new WatchCoordinator({
16
- workspaceRoot: options.workspaceRoot,
17
- verbose: options.verbose ?? false,
18
- hmrVerbose: options.hmrVerbose ?? false
19
- });
20
- this.shutdownPromise = new Promise((resolve) => {
21
- this.resolveShutdown = resolve;
22
- });
23
- }
24
- async run() {
25
- if (this.options.autoStart !== false) {
26
- await this.coordinator.start();
27
- }
28
- this.setupSignalHandlers();
29
- this.setupCommandLoop();
30
- await this.shutdownPromise;
31
- }
32
- setupCommandLoop() {
33
- if (process.stdin.isTTY) {
34
- process.stdin.setRawMode(false);
35
- }
36
- process.stdin.setEncoding('utf8');
37
- this.rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
38
- this.rl.on('line', (line) => this.processLine(line));
39
- this.rl.on('close', () => {
40
- void this.shutdown();
41
- });
42
- }
43
- setupSignalHandlers() {
44
- const shutdown = () => {
45
- void this.shutdown();
46
- };
47
- process.on('SIGINT', shutdown);
48
- process.on('SIGTERM', shutdown);
49
- }
50
- processLine(rawLine) {
51
- const line = rawLine.trim();
52
- if (line.length === 0) {
53
- return;
54
- }
55
- let command = null;
56
- try {
57
- command = JSON.parse(line);
58
- }
59
- catch (error) {
60
- emitDiagnostic({
61
- code: 'frontend.watch.command.invalid',
62
- kind: 'watch-daemon',
63
- stage: 'command',
64
- severity: 'warning',
65
- message: `Discarding invalid command payload: ${String(error)}`
66
- });
67
- return;
68
- }
69
- this.commandQueue = this.commandQueue.then(() => this.handleCommand(command)).catch((error) => {
70
- emitDiagnostic({
71
- code: 'frontend.watch.command.failure',
72
- kind: 'watch-daemon',
73
- stage: 'command',
74
- severity: 'error',
75
- message: `Command handling failed: ${error instanceof Error ? error.message : String(error)}`
76
- });
77
- });
78
- }
79
- async handleCommand(command) {
80
- switch (command.type) {
81
- case 'start':
82
- await this.coordinator.start();
83
- return;
84
- case 'reload':
85
- await this.coordinator.reload();
86
- return;
87
- case 'change':
88
- await this.coordinator.handleChange({ path: command.path });
89
- return;
90
- case 'shutdown':
91
- await this.shutdown();
92
- return;
93
- case 'ping':
94
- emitDiagnostic({
95
- code: 'frontend.watch.pong',
96
- kind: 'watch-daemon',
97
- stage: 'command',
98
- severity: 'info',
99
- message: 'Watch daemon heartbeat acknowledged.',
100
- data: command.id ? { id: command.id } : undefined
101
- });
102
- return;
103
- default:
104
- emitDiagnostic({
105
- code: 'frontend.watch.command.unknown',
106
- kind: 'watch-daemon',
107
- stage: 'command',
108
- severity: 'warning',
109
- message: `Unknown watch daemon command: ${command.type}`
110
- });
111
- return;
112
- }
113
- }
114
- async shutdown() {
115
- if (this.isShuttingDown) {
116
- return;
117
- }
118
- this.isShuttingDown = true;
119
- if (this.rl) {
120
- this.rl.close();
121
- this.rl = undefined;
122
- }
123
- await this.coordinator.stop();
124
- this.resolveShutdown?.();
125
- this.resolveShutdown = null;
126
- }
127
- }