@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.
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/assets/assetManifest.d.ts +16 -0
- package/dist/assets/assetManifest.js +31 -0
- package/dist/assets/imageOptimizer.d.ts +6 -0
- package/dist/assets/imageOptimizer.js +93 -0
- package/dist/assets/precompression.d.ts +1 -0
- package/dist/assets/precompression.js +21 -0
- package/dist/builders/contentBuilder.d.ts +2 -0
- package/dist/builders/contentBuilder.js +1052 -0
- package/dist/builders/cssBuilder.d.ts +2 -0
- package/dist/builders/cssBuilder.js +439 -0
- package/dist/builders/htmlBuilder.d.ts +2 -0
- package/dist/builders/htmlBuilder.js +430 -0
- package/dist/builders/index.d.ts +2 -0
- package/dist/builders/index.js +14 -0
- package/dist/builders/jsBuilder.d.ts +2 -0
- package/dist/builders/jsBuilder.js +300 -0
- package/dist/builders/staticAssetsBuilder.d.ts +2 -0
- package/dist/builders/staticAssetsBuilder.js +158 -0
- package/dist/builders/types.d.ts +12 -0
- package/dist/builders/types.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +105 -0
- package/dist/config/manifest.d.ts +7 -0
- package/dist/config/manifest.js +17 -0
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.js +11 -0
- package/dist/config/schema.d.ts +413 -0
- package/dist/config/schema.js +44 -0
- package/dist/config/setup.d.ts +2 -0
- package/dist/config/setup.js +12 -0
- package/dist/config/workspace.d.ts +2 -0
- package/dist/config/workspace.js +131 -0
- package/dist/config/workspaceManifest.d.ts +23 -0
- package/dist/config/workspaceManifest.js +1 -0
- package/dist/core/constants.d.ts +70 -0
- package/dist/core/constants.js +70 -0
- package/dist/core/diagnostics.d.ts +15 -0
- package/dist/core/diagnostics.js +21 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/pages.d.ts +6 -0
- package/dist/core/pages.js +23 -0
- package/dist/hooks.d.ts +19 -0
- package/dist/hooks.js +115 -0
- package/dist/html/criticalCss.d.ts +4 -0
- package/dist/html/criticalCss.js +192 -0
- package/dist/html/htmlSecurity.d.ts +5 -0
- package/dist/html/htmlSecurity.js +73 -0
- package/dist/html/lazyLoad.d.ts +6 -0
- package/dist/html/lazyLoad.js +21 -0
- package/dist/html/pageScaffold.d.ts +10 -0
- package/dist/html/pageScaffold.js +51 -0
- package/dist/html/resourceHints.d.ts +7 -0
- package/dist/html/resourceHints.js +64 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/modes/ssg/index.d.ts +4 -0
- package/dist/modes/ssg/index.js +4 -0
- package/dist/modes/ssg/metadata.d.ts +5 -0
- package/dist/modes/ssg/metadata.js +50 -0
- package/dist/modes/ssg/routing.d.ts +2 -0
- package/dist/modes/ssg/routing.js +186 -0
- package/dist/modes/ssg/seo.d.ts +4 -0
- package/dist/modes/ssg/seo.js +208 -0
- package/dist/modes/ssg/validation.d.ts +3 -0
- package/dist/modes/ssg/validation.js +27 -0
- package/dist/modes/ssg/views.d.ts +2 -0
- package/dist/modes/ssg/views.js +236 -0
- package/dist/operations.d.ts +5 -0
- package/dist/operations.js +102 -0
- package/dist/pipeline.d.ts +7 -0
- package/dist/pipeline.js +71 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +176 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/dist/utils/changedFile.d.ts +8 -0
- package/dist/utils/changedFile.js +26 -0
- package/dist/utils/fs.d.ts +11 -0
- package/dist/utils/fs.js +39 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +5 -0
- package/dist/utils/pagePaths.d.ts +5 -0
- package/dist/utils/pagePaths.js +36 -0
- package/dist/utils/pathMatch.d.ts +3 -0
- package/dist/utils/pathMatch.js +29 -0
- package/dist/watch/frontendFiles.d.ts +3 -0
- package/dist/watch/frontendFiles.js +25 -0
- package/dist/watch/hotUpdateTracker.d.ts +51 -0
- package/dist/watch/hotUpdateTracker.js +205 -0
- package/dist/watch/pipelineHelpers.d.ts +26 -0
- package/dist/watch/pipelineHelpers.js +177 -0
- package/dist/watch/types.d.ts +27 -0
- package/dist/watch/types.js +1 -0
- package/dist/watch/watchCoordinator.d.ts +36 -0
- package/dist/watch/watchCoordinator.js +551 -0
- package/dist/watch/watchDaemon.d.ts +17 -0
- package/dist/watch/watchDaemon.js +127 -0
- package/dist/watch/watchReporter.d.ts +21 -0
- package/dist/watch/watchReporter.js +64 -0
- package/package.json +92 -0
- package/scripts/publish.sh +101 -0
- package/scripts/smoke.mjs +35 -0
- package/scripts/update-contract.sh +121 -0
- package/src/assets/assetManifest.ts +51 -0
- package/src/assets/imageOptimizer.ts +112 -0
- package/src/assets/precompression.ts +25 -0
- package/src/builders/contentBuilder.ts +1400 -0
- package/src/builders/cssBuilder.ts +552 -0
- package/src/builders/htmlBuilder.ts +540 -0
- package/src/builders/index.ts +16 -0
- package/src/builders/jsBuilder.ts +358 -0
- package/src/builders/staticAssetsBuilder.ts +174 -0
- package/src/builders/types.ts +15 -0
- package/src/cli.ts +108 -0
- package/src/config/manifest.ts +24 -0
- package/src/config/paths.ts +14 -0
- package/src/config/schema.ts +49 -0
- package/src/config/setup.ts +14 -0
- package/src/config/workspace.ts +150 -0
- package/src/config/workspaceManifest.ts +27 -0
- package/src/core/constants.ts +73 -0
- package/src/core/diagnostics.ts +40 -0
- package/src/core/index.ts +3 -0
- package/src/core/pages.ts +31 -0
- package/src/hooks.ts +175 -0
- package/src/html/criticalCss.ts +214 -0
- package/src/html/htmlSecurity.ts +86 -0
- package/src/html/lazyLoad.ts +30 -0
- package/src/html/pageScaffold.ts +70 -0
- package/src/html/resourceHints.ts +91 -0
- package/src/index.ts +5 -0
- package/src/modes/ssg/index.ts +4 -0
- package/src/modes/ssg/metadata.ts +63 -0
- package/src/modes/ssg/routing.ts +230 -0
- package/src/modes/ssg/seo.ts +261 -0
- package/src/modes/ssg/validation.ts +37 -0
- package/src/modes/ssg/views.ts +309 -0
- package/src/operations.ts +138 -0
- package/src/pipeline.ts +88 -0
- package/src/provider.ts +249 -0
- package/src/types.ts +67 -0
- package/src/utils/changedFile.ts +39 -0
- package/src/utils/fs.ts +48 -0
- package/src/utils/hash.ts +6 -0
- package/src/utils/pagePaths.ts +43 -0
- package/src/utils/pathMatch.ts +36 -0
- package/src/watch/frontendFiles.ts +32 -0
- package/src/watch/hotUpdateTracker.ts +285 -0
- package/src/watch/pipelineHelpers.ts +242 -0
- package/src/watch/types.ts +23 -0
- package/src/watch/watchCoordinator.ts +666 -0
- package/src/watch/watchDaemon.ts +144 -0
- package/src/watch/watchReporter.ts +98 -0
- package/tests/add-page-defaults.test.js +64 -0
- package/tests/content-pages.test.js +81 -0
- package/tests/css-app-imports.test.js +64 -0
- package/tests/css-page-imports.test.js +100 -0
- package/tests/diagnostics.test.js +48 -0
- package/tests/features.test.js +63 -0
- package/tests/hooks.test.js +71 -0
- package/tests/provider.integration.test.js +137 -0
- package/tests/ssg-defaults.test.js +201 -0
- package/tests/ssg-guardrails.test.js +69 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type WatchDaemonCommand = {
|
|
2
|
+
readonly type: 'start';
|
|
3
|
+
} | {
|
|
4
|
+
readonly type: 'change';
|
|
5
|
+
readonly path: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly type: 'reload';
|
|
8
|
+
} | {
|
|
9
|
+
readonly type: 'shutdown';
|
|
10
|
+
} | {
|
|
11
|
+
readonly type: 'ping';
|
|
12
|
+
readonly id?: string;
|
|
13
|
+
};
|
|
14
|
+
export interface WatchDaemonOptions {
|
|
15
|
+
readonly workspaceRoot: string;
|
|
16
|
+
readonly autoStart?: boolean;
|
|
17
|
+
readonly verbose?: boolean;
|
|
18
|
+
readonly hmrVerbose?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface WatchCoordinatorOptions {
|
|
21
|
+
readonly workspaceRoot: string;
|
|
22
|
+
readonly verbose?: boolean;
|
|
23
|
+
readonly hmrVerbose?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface WatchChangeIntent {
|
|
26
|
+
readonly path?: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { WatchChangeIntent, WatchCoordinatorOptions } from './types.js';
|
|
2
|
+
export declare class WatchCoordinator {
|
|
3
|
+
private readonly workspaceRoot;
|
|
4
|
+
private readonly jsContexts;
|
|
5
|
+
private readonly verbose;
|
|
6
|
+
private readonly hmrVerbose;
|
|
7
|
+
private readonly reporter;
|
|
8
|
+
private readonly hotUpdateTracker;
|
|
9
|
+
private readonly hmrTotals;
|
|
10
|
+
private config?;
|
|
11
|
+
private isSsgWorkspace;
|
|
12
|
+
private enable?;
|
|
13
|
+
private isStopping;
|
|
14
|
+
private queue;
|
|
15
|
+
constructor(options: WatchCoordinatorOptions);
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
reload(): Promise<void>;
|
|
18
|
+
handleChange(intent: WatchChangeIntent): Promise<void>;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
private enqueue;
|
|
21
|
+
private refreshJavaScriptContexts;
|
|
22
|
+
private ensureJavaScriptContext;
|
|
23
|
+
private runFullBuildCycle;
|
|
24
|
+
private runAdditionalBuilders;
|
|
25
|
+
private getRelativeChange;
|
|
26
|
+
private runJavaScriptBuild;
|
|
27
|
+
private executeJavaScriptBuild;
|
|
28
|
+
private readWorkspaceSettings;
|
|
29
|
+
private resolveTargetPages;
|
|
30
|
+
private resolveChangedFile;
|
|
31
|
+
private emitHotUpdateFallback;
|
|
32
|
+
private recordHotUpdateOutcome;
|
|
33
|
+
private combineFallbackReasons;
|
|
34
|
+
private requireConfig;
|
|
35
|
+
private logUnexpectedError;
|
|
36
|
+
}
|
|
@@ -0,0 +1,551 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|