@webstir-io/webstir-frontend 0.1.40 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -60
- package/dist/assets/imageOptimizer.js +10 -15
- package/dist/assets/precompression.js +1 -1
- package/dist/builders/contentBuilder.js +102 -90
- package/dist/builders/cssBuilder.js +25 -19
- package/dist/builders/htmlBuilder.js +57 -42
- package/dist/builders/index.js +1 -1
- package/dist/builders/jsBuilder.js +219 -76
- package/dist/builders/staticAssetsBuilder.js +27 -9
- package/dist/builders/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -30
- package/dist/config/manifest.js +7 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -6
- package/dist/config/setup.js +1 -1
- package/dist/config/workspace.js +11 -9
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +5 -5
- package/dist/core/diagnostics.js +1 -1
- package/dist/core/pages.js +4 -4
- package/dist/hooks.js +3 -3
- package/dist/html/criticalCss.js +6 -3
- package/dist/html/htmlSecurity.d.ts +6 -1
- package/dist/html/htmlSecurity.js +28 -14
- package/dist/html/lazyLoad.js +1 -1
- package/dist/html/pageScaffold.js +1 -1
- package/dist/html/resourceHints.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +110 -0
- package/dist/modes/ssg/metadata.js +4 -4
- package/dist/modes/ssg/routing.js +2 -5
- package/dist/modes/ssg/seo.js +5 -5
- package/dist/modes/ssg/views.js +17 -11
- package/dist/operations.js +18 -10
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.js +6 -1
- package/dist/provider.js +28 -24
- package/dist/runtime/boundary.d.ts +28 -0
- package/dist/runtime/boundary.js +247 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils/fs.d.ts +11 -10
- package/dist/utils/fs.js +48 -20
- package/dist/utils/glob.d.ts +8 -0
- package/dist/utils/glob.js +21 -0
- package/dist/utils/hash.js +1 -2
- package/dist/utils/pagePaths.js +2 -2
- package/package.json +19 -14
- package/scripts/publish.sh +2 -94
- package/scripts/update-contract.sh +12 -10
- package/src/assets/assetManifest.ts +39 -29
- package/src/assets/imageOptimizer.ts +91 -82
- package/src/assets/precompression.ts +22 -16
- package/src/builders/contentBuilder.ts +1224 -1149
- package/src/builders/cssBuilder.ts +466 -417
- package/src/builders/htmlBuilder.ts +511 -448
- package/src/builders/index.ts +7 -7
- package/src/builders/jsBuilder.ts +538 -280
- package/src/builders/staticAssetsBuilder.ts +166 -135
- package/src/builders/types.ts +7 -6
- package/src/cli.ts +66 -90
- package/src/config/manifest.ts +16 -14
- package/src/config/paths.ts +5 -5
- package/src/config/schema.ts +38 -37
- package/src/config/setup.ts +7 -7
- package/src/config/workspace.ts +118 -116
- package/src/config/workspaceManifest.ts +14 -14
- package/src/core/constants.ts +62 -62
- package/src/core/diagnostics.ts +26 -26
- package/src/core/pages.ts +19 -19
- package/src/hooks.ts +128 -118
- package/src/html/criticalCss.ts +84 -77
- package/src/html/htmlSecurity.ts +107 -66
- package/src/html/lazyLoad.ts +22 -19
- package/src/html/pageScaffold.ts +37 -28
- package/src/html/resourceHints.ts +83 -74
- package/src/index.ts +2 -0
- package/src/inspect.ts +158 -0
- package/src/modes/ssg/metadata.ts +53 -51
- package/src/modes/ssg/routing.ts +177 -177
- package/src/modes/ssg/seo.ts +208 -200
- package/src/modes/ssg/validation.ts +31 -25
- package/src/modes/ssg/views.ts +257 -238
- package/src/operations.ts +105 -95
- package/src/pipeline.ts +81 -69
- package/src/provider.ts +184 -176
- package/src/runtime/boundary.ts +325 -0
- package/src/runtime/index.ts +1 -0
- package/src/types.ts +107 -48
- package/src/utils/changedFile.ts +22 -22
- package/src/utils/fs.ts +73 -26
- package/src/utils/glob.ts +38 -0
- package/src/utils/hash.ts +2 -4
- package/src/utils/pagePaths.ts +35 -23
- package/src/utils/pathMatch.ts +26 -23
- package/tests/add-page-defaults.test.js +44 -39
- package/tests/bundlerParity.test.js +252 -0
- package/tests/cli.contract.test.js +13 -0
- package/tests/content-pages.test.js +108 -13
- package/tests/css-app-imports.test.js +22 -11
- package/tests/css-page-imports.test.js +26 -13
- package/tests/diagnostics.test.js +39 -36
- package/tests/features.test.js +48 -43
- package/tests/hooks.test.js +58 -42
- package/tests/htmlSecurity.test.js +66 -0
- package/tests/inspect.test.js +148 -0
- package/tests/provider.integration.test.js +71 -20
- package/tests/runtime.test.js +493 -0
- package/tests/ssg-defaults.test.js +284 -177
- package/tests/ssg-guardrails.test.js +51 -51
- package/tsconfig.json +3 -10
- package/dist/watch/frontendFiles.d.ts +0 -3
- package/dist/watch/frontendFiles.js +0 -25
- package/dist/watch/hotUpdateTracker.d.ts +0 -51
- package/dist/watch/hotUpdateTracker.js +0 -205
- package/dist/watch/pipelineHelpers.d.ts +0 -26
- package/dist/watch/pipelineHelpers.js +0 -177
- package/dist/watch/types.d.ts +0 -27
- package/dist/watch/types.js +0 -1
- package/dist/watch/watchCoordinator.d.ts +0 -36
- package/dist/watch/watchCoordinator.js +0 -551
- package/dist/watch/watchDaemon.d.ts +0 -17
- package/dist/watch/watchDaemon.js +0 -127
- package/dist/watch/watchReporter.d.ts +0 -21
- package/dist/watch/watchReporter.js +0 -64
- package/scripts/smoke.mjs +0 -35
- package/src/watch/frontendFiles.ts +0 -32
- package/src/watch/hotUpdateTracker.ts +0 -285
- package/src/watch/pipelineHelpers.ts +0 -242
- package/src/watch/types.ts +0 -23
- package/src/watch/watchCoordinator.ts +0 -666
- package/src/watch/watchDaemon.ts +0 -144
- package/src/watch/watchReporter.ts +0 -98
|
@@ -1,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
|
-
}
|