@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,26 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function shouldProcess(context, rules) {
|
|
3
|
+
const changed = context.changedFile;
|
|
4
|
+
if (!changed) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
const normalizedChanged = path.resolve(changed);
|
|
8
|
+
for (const rule of rules) {
|
|
9
|
+
const normalizedDir = path.resolve(rule.directory);
|
|
10
|
+
if (!isPathInside(normalizedChanged, normalizedDir)) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (!rule.extensions || rule.extensions.length === 0) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const extension = path.extname(normalizedChanged).toLowerCase();
|
|
17
|
+
if (rule.extensions.includes(extension)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
export function isPathInside(target, directory) {
|
|
24
|
+
const relative = path.relative(directory, target);
|
|
25
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
export declare function ensureDir(path: string): Promise<void>;
|
|
3
|
+
export declare function emptyDir(path: string): Promise<void>;
|
|
4
|
+
export declare function remove(path: string): Promise<void>;
|
|
5
|
+
export declare function copy(source: string, destination: string): Promise<void>;
|
|
6
|
+
export declare function pathExists(path: string): Promise<boolean>;
|
|
7
|
+
export declare function stat(path: string): Promise<fs.Stats>;
|
|
8
|
+
export declare function readJson<T>(path: string): Promise<T | null>;
|
|
9
|
+
export declare function writeJson(path: string, data: unknown): Promise<void>;
|
|
10
|
+
export declare function readFile(path: string): Promise<string>;
|
|
11
|
+
export declare function writeFile(path: string, contents: string): Promise<void>;
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
export async function ensureDir(path) {
|
|
3
|
+
await fs.ensureDir(path);
|
|
4
|
+
}
|
|
5
|
+
export async function emptyDir(path) {
|
|
6
|
+
await fs.emptyDir(path);
|
|
7
|
+
}
|
|
8
|
+
export async function remove(path) {
|
|
9
|
+
await fs.remove(path);
|
|
10
|
+
}
|
|
11
|
+
export async function copy(source, destination) {
|
|
12
|
+
await fs.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
13
|
+
}
|
|
14
|
+
export async function pathExists(path) {
|
|
15
|
+
return fs.pathExists(path);
|
|
16
|
+
}
|
|
17
|
+
export async function stat(path) {
|
|
18
|
+
return fs.stat(path);
|
|
19
|
+
}
|
|
20
|
+
export async function readJson(path) {
|
|
21
|
+
try {
|
|
22
|
+
return await fs.readJson(path);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code === 'ENOENT') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function writeJson(path, data) {
|
|
32
|
+
await fs.writeJson(path, data, { spaces: 2 });
|
|
33
|
+
}
|
|
34
|
+
export async function readFile(path) {
|
|
35
|
+
return fs.readFile(path, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
export async function writeFile(path, contents) {
|
|
38
|
+
await fs.outputFile(path, contents, 'utf8');
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function hashContent(content: string, length?: number): string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function resolvePagesUrlPrefix(frontendRoot: string, pagesRoot: string): string;
|
|
2
|
+
export declare function isRootPagesLayout(frontendRoot: string, pagesRoot: string): boolean;
|
|
3
|
+
export declare function resolvePageAssetUrl(pagesUrlPrefix: string, pageName: string, fileName: string): string;
|
|
4
|
+
export declare function resolvePageHtmlUrl(pagesUrlPrefix: string, pageName: string, useRootIndex: boolean): string;
|
|
5
|
+
export declare function resolvePageHtmlDir(pagesRoot: string, pageName: string, useRootIndex: boolean): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { FOLDERS, FILES } from '../core/constants.js';
|
|
3
|
+
export function resolvePagesUrlPrefix(frontendRoot, pagesRoot) {
|
|
4
|
+
const relative = path.relative(frontendRoot, pagesRoot).replace(/\\/g, '/');
|
|
5
|
+
if (!relative || relative === '.' || relative.startsWith('..')) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
return `/${trimSlashes(relative)}`;
|
|
9
|
+
}
|
|
10
|
+
export function isRootPagesLayout(frontendRoot, pagesRoot) {
|
|
11
|
+
return resolvePagesUrlPrefix(frontendRoot, pagesRoot) === '';
|
|
12
|
+
}
|
|
13
|
+
export function resolvePageAssetUrl(pagesUrlPrefix, pageName, fileName) {
|
|
14
|
+
return joinUrl(pagesUrlPrefix, pageName, fileName);
|
|
15
|
+
}
|
|
16
|
+
export function resolvePageHtmlUrl(pagesUrlPrefix, pageName, useRootIndex) {
|
|
17
|
+
if (useRootIndex && pageName === FOLDERS.home) {
|
|
18
|
+
return `/${FILES.indexHtml}`;
|
|
19
|
+
}
|
|
20
|
+
return joinUrl(pagesUrlPrefix, pageName, FILES.indexHtml);
|
|
21
|
+
}
|
|
22
|
+
export function resolvePageHtmlDir(pagesRoot, pageName, useRootIndex) {
|
|
23
|
+
if (useRootIndex && pageName === FOLDERS.home) {
|
|
24
|
+
return pagesRoot;
|
|
25
|
+
}
|
|
26
|
+
return path.join(pagesRoot, pageName);
|
|
27
|
+
}
|
|
28
|
+
function joinUrl(...segments) {
|
|
29
|
+
const cleaned = segments
|
|
30
|
+
.map(segment => trimSlashes(segment))
|
|
31
|
+
.filter(segment => segment.length > 0);
|
|
32
|
+
return `/${cleaned.join('/')}`;
|
|
33
|
+
}
|
|
34
|
+
function trimSlashes(value) {
|
|
35
|
+
return value.replace(/^\/+|\/+$/g, '');
|
|
36
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function isInsideDirectory(filePath: string, directory: string): boolean;
|
|
2
|
+
export declare function findPageFromChangedFile(changedFile: string | undefined, pagesRoot: string): string | null;
|
|
3
|
+
export declare function relativePathWithin(filePath: string | undefined, directory: string): string | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function isInsideDirectory(filePath, directory) {
|
|
3
|
+
const resolvedFile = path.resolve(filePath);
|
|
4
|
+
const resolvedDirectory = path.resolve(directory);
|
|
5
|
+
const relative = path.relative(resolvedDirectory, resolvedFile);
|
|
6
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
7
|
+
}
|
|
8
|
+
export function findPageFromChangedFile(changedFile, pagesRoot) {
|
|
9
|
+
if (!changedFile) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const resolvedChanged = path.resolve(changedFile);
|
|
13
|
+
const resolvedPagesRoot = path.resolve(pagesRoot);
|
|
14
|
+
if (!isInsideDirectory(resolvedChanged, resolvedPagesRoot)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const relative = path.relative(resolvedPagesRoot, resolvedChanged);
|
|
18
|
+
const segments = relative.split(path.sep);
|
|
19
|
+
return segments.length > 0 && segments[0] ? segments[0] : null;
|
|
20
|
+
}
|
|
21
|
+
export function relativePathWithin(filePath, directory) {
|
|
22
|
+
if (!filePath) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!isInsideDirectory(filePath, directory)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return path.relative(path.resolve(directory), path.resolve(filePath));
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureDir, pathExists, copy } from '../utils/fs.js';
|
|
3
|
+
import { FILES, EXTENSIONS } from '../core/constants.js';
|
|
4
|
+
export async function resolveEntryPoint(pageDirectory) {
|
|
5
|
+
const candidates = [`${FILES.index}${EXTENSIONS.ts}`, `${FILES.index}.tsx`, `${FILES.index}${EXTENSIONS.js}`, `${FILES.index}.jsx`];
|
|
6
|
+
for (const candidate of candidates) {
|
|
7
|
+
const file = path.join(pageDirectory, candidate);
|
|
8
|
+
if (await pathExists(file)) {
|
|
9
|
+
return file;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export async function copyRefreshScript(config, enable) {
|
|
15
|
+
const runtimeScripts = [FILES.refreshJs, FILES.hmrJs];
|
|
16
|
+
for (const scriptName of runtimeScripts) {
|
|
17
|
+
const source = path.join(config.paths.src.app, scriptName);
|
|
18
|
+
if (!(await pathExists(source))) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const destination = path.join(config.paths.build.frontend, scriptName);
|
|
22
|
+
await ensureDir(path.dirname(destination));
|
|
23
|
+
await copy(source, destination);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BuildResult } from 'esbuild';
|
|
2
|
+
import type { FrontendConfig } from '../types.js';
|
|
3
|
+
import type { BuilderContext } from '../builders/types.js';
|
|
4
|
+
export interface HotAsset {
|
|
5
|
+
readonly type: 'js' | 'css';
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly relativePath: string;
|
|
8
|
+
readonly url: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HotUpdateDetails {
|
|
11
|
+
readonly modules: readonly HotAsset[];
|
|
12
|
+
readonly styles: readonly HotAsset[];
|
|
13
|
+
readonly requiresReload: boolean;
|
|
14
|
+
readonly fallbackReasons: readonly string[];
|
|
15
|
+
readonly changedFile?: string;
|
|
16
|
+
readonly stats?: HotUpdateStats;
|
|
17
|
+
}
|
|
18
|
+
export interface HotUpdateStats {
|
|
19
|
+
readonly hotUpdates: number;
|
|
20
|
+
readonly reloadFallbacks: number;
|
|
21
|
+
}
|
|
22
|
+
interface ProcessJavaScriptResult {
|
|
23
|
+
readonly modules: readonly HotAsset[];
|
|
24
|
+
readonly requiresReload: boolean;
|
|
25
|
+
readonly fallbackReasons: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
interface CollectCssResult {
|
|
28
|
+
readonly styles: readonly HotAsset[];
|
|
29
|
+
readonly requiresReload: boolean;
|
|
30
|
+
readonly fallbackReasons: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
interface HotUpdateTrackerOptions {
|
|
33
|
+
readonly workspaceRoot: string;
|
|
34
|
+
}
|
|
35
|
+
export declare class HotUpdateTracker {
|
|
36
|
+
private readonly workspaceRoot;
|
|
37
|
+
private readonly pageOutputHashes;
|
|
38
|
+
private readonly assetFingerprints;
|
|
39
|
+
constructor(options: HotUpdateTrackerOptions);
|
|
40
|
+
reset(): void;
|
|
41
|
+
removePage(pageName: string): void;
|
|
42
|
+
processJavaScriptResult(pageName: string, result: BuildResult, config: FrontendConfig): Promise<ProcessJavaScriptResult>;
|
|
43
|
+
collectCssChanges(context: BuilderContext, pageNames: readonly string[]): Promise<CollectCssResult>;
|
|
44
|
+
private computeAssetFingerprint;
|
|
45
|
+
private getPageCssOutputPath;
|
|
46
|
+
private getAppCssOutputPath;
|
|
47
|
+
private createHotAsset;
|
|
48
|
+
private resolveOutputPath;
|
|
49
|
+
private toWebPath;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { FOLDERS, FILES, EXTENSIONS } from '../core/constants.js';
|
|
5
|
+
import { emitDiagnostic } from '../core/diagnostics.js';
|
|
6
|
+
import { pathExists } from '../utils/fs.js';
|
|
7
|
+
import { isPathInside } from '../utils/changedFile.js';
|
|
8
|
+
import { findPageFromChangedFile } from '../utils/pathMatch.js';
|
|
9
|
+
export class HotUpdateTracker {
|
|
10
|
+
workspaceRoot;
|
|
11
|
+
pageOutputHashes = new Map();
|
|
12
|
+
assetFingerprints = new Map();
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.workspaceRoot = options.workspaceRoot;
|
|
15
|
+
}
|
|
16
|
+
reset() {
|
|
17
|
+
this.pageOutputHashes.clear();
|
|
18
|
+
this.assetFingerprints.clear();
|
|
19
|
+
}
|
|
20
|
+
removePage(pageName) {
|
|
21
|
+
this.pageOutputHashes.delete(pageName);
|
|
22
|
+
}
|
|
23
|
+
async processJavaScriptResult(pageName, result, config) {
|
|
24
|
+
const modules = [];
|
|
25
|
+
let requiresReload = false;
|
|
26
|
+
const fallbackReasons = [];
|
|
27
|
+
const metafile = result.metafile;
|
|
28
|
+
if (!metafile) {
|
|
29
|
+
fallbackReasons.push('javascript.metafile.missing');
|
|
30
|
+
return { modules, requiresReload: true, fallbackReasons };
|
|
31
|
+
}
|
|
32
|
+
const buildRoot = config.paths.build.frontend;
|
|
33
|
+
const currentOutputs = new Set();
|
|
34
|
+
const previousOutputs = this.pageOutputHashes.get(pageName) ?? new Map();
|
|
35
|
+
for (const outputPath of Object.keys(metafile.outputs)) {
|
|
36
|
+
const extension = path.extname(outputPath).toLowerCase();
|
|
37
|
+
if (extension !== EXTENSIONS.js && extension !== '.mjs') {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const absoluteOutput = this.resolveOutputPath(outputPath);
|
|
41
|
+
currentOutputs.add(absoluteOutput);
|
|
42
|
+
const fingerprint = await this.computeAssetFingerprint(absoluteOutput, buildRoot, 'js');
|
|
43
|
+
if (!fingerprint) {
|
|
44
|
+
if (this.assetFingerprints.has(absoluteOutput)) {
|
|
45
|
+
this.assetFingerprints.delete(absoluteOutput);
|
|
46
|
+
}
|
|
47
|
+
if (previousOutputs.has(absoluteOutput)) {
|
|
48
|
+
previousOutputs.delete(absoluteOutput);
|
|
49
|
+
requiresReload = true;
|
|
50
|
+
fallbackReasons.push('javascript.output.missing');
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (fingerprint.requiresReload) {
|
|
55
|
+
requiresReload = true;
|
|
56
|
+
fallbackReasons.push('javascript.fingerprint.error');
|
|
57
|
+
}
|
|
58
|
+
if (fingerprint.changed) {
|
|
59
|
+
modules.push(fingerprint.asset);
|
|
60
|
+
}
|
|
61
|
+
if (fingerprint.hash) {
|
|
62
|
+
previousOutputs.set(absoluteOutput, { hash: fingerprint.hash });
|
|
63
|
+
}
|
|
64
|
+
else if (previousOutputs.has(absoluteOutput)) {
|
|
65
|
+
previousOutputs.delete(absoluteOutput);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const known of Array.from(previousOutputs.keys())) {
|
|
69
|
+
if (!currentOutputs.has(known)) {
|
|
70
|
+
previousOutputs.delete(known);
|
|
71
|
+
requiresReload = true;
|
|
72
|
+
fallbackReasons.push('javascript.output.removed');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.pageOutputHashes.set(pageName, previousOutputs);
|
|
76
|
+
return { modules, requiresReload, fallbackReasons: uniqueReasons(fallbackReasons) };
|
|
77
|
+
}
|
|
78
|
+
async collectCssChanges(context, pageNames) {
|
|
79
|
+
const { config, changedFile } = context;
|
|
80
|
+
const buildRoot = config.paths.build.frontend;
|
|
81
|
+
const candidates = new Set();
|
|
82
|
+
if (!changedFile) {
|
|
83
|
+
for (const page of pageNames) {
|
|
84
|
+
candidates.add(this.getPageCssOutputPath(config, page));
|
|
85
|
+
}
|
|
86
|
+
candidates.add(this.getAppCssOutputPath(config));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const normalized = path.resolve(changedFile);
|
|
90
|
+
const extension = path.extname(normalized).toLowerCase();
|
|
91
|
+
if (extension === EXTENSIONS.css) {
|
|
92
|
+
if (isPathInside(normalized, config.paths.src.app)) {
|
|
93
|
+
for (const page of pageNames) {
|
|
94
|
+
candidates.add(this.getPageCssOutputPath(config, page));
|
|
95
|
+
}
|
|
96
|
+
candidates.add(this.getAppCssOutputPath(config));
|
|
97
|
+
}
|
|
98
|
+
else if (isPathInside(normalized, config.paths.src.pages)) {
|
|
99
|
+
const page = findPageFromChangedFile(normalized, config.paths.src.pages);
|
|
100
|
+
if (page) {
|
|
101
|
+
candidates.add(this.getPageCssOutputPath(config, page));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (isPathInside(normalized, config.paths.src.frontend)) {
|
|
105
|
+
for (const page of pageNames) {
|
|
106
|
+
candidates.add(this.getPageCssOutputPath(config, page));
|
|
107
|
+
}
|
|
108
|
+
candidates.add(this.getAppCssOutputPath(config));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (candidates.size === 0 && !changedFile) {
|
|
113
|
+
candidates.add(this.getAppCssOutputPath(config));
|
|
114
|
+
for (const page of pageNames) {
|
|
115
|
+
candidates.add(this.getPageCssOutputPath(config, page));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const styles = [];
|
|
119
|
+
let requiresReload = !changedFile;
|
|
120
|
+
const fallbackReasons = [];
|
|
121
|
+
if (!changedFile) {
|
|
122
|
+
fallbackReasons.push('css.full-rebuild');
|
|
123
|
+
}
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
const fingerprint = await this.computeAssetFingerprint(candidate, buildRoot, 'css');
|
|
126
|
+
if (!fingerprint) {
|
|
127
|
+
if (this.assetFingerprints.has(path.resolve(candidate))) {
|
|
128
|
+
this.assetFingerprints.delete(path.resolve(candidate));
|
|
129
|
+
requiresReload = true;
|
|
130
|
+
fallbackReasons.push('css.asset.missing');
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (fingerprint.requiresReload) {
|
|
135
|
+
requiresReload = true;
|
|
136
|
+
fallbackReasons.push('css.fingerprint.error');
|
|
137
|
+
}
|
|
138
|
+
if (fingerprint.changed) {
|
|
139
|
+
styles.push(fingerprint.asset);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { styles, requiresReload, fallbackReasons: uniqueReasons(fallbackReasons) };
|
|
143
|
+
}
|
|
144
|
+
async computeAssetFingerprint(filePath, buildRoot, type) {
|
|
145
|
+
const absolutePath = path.resolve(filePath);
|
|
146
|
+
if (!(await pathExists(absolutePath))) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const contents = await readFile(absolutePath);
|
|
151
|
+
const hash = createHash('sha1').update(contents).digest('hex');
|
|
152
|
+
const previous = this.assetFingerprints.get(absolutePath);
|
|
153
|
+
const changed = previous !== hash;
|
|
154
|
+
this.assetFingerprints.set(absolutePath, hash);
|
|
155
|
+
return {
|
|
156
|
+
asset: this.createHotAsset(absolutePath, buildRoot, type),
|
|
157
|
+
changed,
|
|
158
|
+
requiresReload: false,
|
|
159
|
+
hash
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
emitDiagnostic({
|
|
164
|
+
code: 'frontend.watch.unexpected',
|
|
165
|
+
kind: 'watch-daemon',
|
|
166
|
+
stage: 'css-fingerprint',
|
|
167
|
+
severity: 'error',
|
|
168
|
+
message: `Failed to fingerprint asset '${absolutePath}': ${error instanceof Error ? error.message : String(error)}`
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
asset: this.createHotAsset(absolutePath, buildRoot, type),
|
|
172
|
+
changed: false,
|
|
173
|
+
requiresReload: true
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getPageCssOutputPath(config, pageName) {
|
|
178
|
+
return path.join(config.paths.build.frontend, FOLDERS.pages, pageName, `${FILES.index}${EXTENSIONS.css}`);
|
|
179
|
+
}
|
|
180
|
+
getAppCssOutputPath(config) {
|
|
181
|
+
return path.join(config.paths.build.frontend, FOLDERS.app, 'app.css');
|
|
182
|
+
}
|
|
183
|
+
createHotAsset(filePath, buildRoot, type) {
|
|
184
|
+
const relativePath = path.relative(buildRoot, filePath);
|
|
185
|
+
const webPath = this.toWebPath(relativePath);
|
|
186
|
+
return {
|
|
187
|
+
type,
|
|
188
|
+
path: filePath,
|
|
189
|
+
relativePath,
|
|
190
|
+
url: webPath.startsWith('/') ? webPath : `/${webPath}`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
resolveOutputPath(outputPath) {
|
|
194
|
+
if (path.isAbsolute(outputPath)) {
|
|
195
|
+
return outputPath;
|
|
196
|
+
}
|
|
197
|
+
return path.resolve(this.workspaceRoot, outputPath);
|
|
198
|
+
}
|
|
199
|
+
toWebPath(relativePath) {
|
|
200
|
+
return relativePath.split(path.sep).join('/') || '';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function uniqueReasons(reasons) {
|
|
204
|
+
return Array.from(new Set(reasons.filter(Boolean)));
|
|
205
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Builder, BuilderContext } from '../builders/types.js';
|
|
2
|
+
import { WatchReporter, type SerializedMessage } from './watchReporter.js';
|
|
3
|
+
import type { HotAsset, HotUpdateDetails } from './hotUpdateTracker.js';
|
|
4
|
+
export interface JavaScriptBuildSummary {
|
|
5
|
+
readonly pagesBuilt: readonly string[];
|
|
6
|
+
readonly warnings: readonly SerializedMessage[];
|
|
7
|
+
readonly modules: readonly HotAsset[];
|
|
8
|
+
readonly requiresReload: boolean;
|
|
9
|
+
readonly fallbackReasons: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export interface AdditionalBuildResult {
|
|
12
|
+
readonly succeeded: boolean;
|
|
13
|
+
readonly assets: readonly string[];
|
|
14
|
+
readonly styles: readonly HotAsset[];
|
|
15
|
+
readonly requiresReload: boolean;
|
|
16
|
+
readonly fallbackReasons: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function runBuilderWithDiagnostics(builder: Builder, reporter: WatchReporter, context: BuilderContext, changedFile: string | undefined, relativeChange: string | undefined): Promise<boolean>;
|
|
19
|
+
export declare function emitPipelineSuccess(summary: JavaScriptBuildSummary, assetsResult: AdditionalBuildResult, changedFile: string | undefined, relativeChange: string | undefined, hotUpdate: HotUpdateDetails): void;
|
|
20
|
+
export declare function serializeSummary(summary: JavaScriptBuildSummary, changedFile: string | undefined, skipped: boolean): Record<string, unknown>;
|
|
21
|
+
export declare function emitJavaScriptFailure(error: unknown, changedFile?: string): void;
|
|
22
|
+
export declare class JavaScriptBuildError extends Error {
|
|
23
|
+
readonly pageName: string;
|
|
24
|
+
readonly details: readonly SerializedMessage[];
|
|
25
|
+
constructor(pageName: string, cause: unknown);
|
|
26
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { emitDiagnostic } from '../core/diagnostics.js';
|
|
2
|
+
import { serializeMessages } from './watchReporter.js';
|
|
3
|
+
const BUILDER_DISPLAY_NAMES = {
|
|
4
|
+
css: 'CSS',
|
|
5
|
+
html: 'HTML',
|
|
6
|
+
'static-assets': 'Static assets'
|
|
7
|
+
};
|
|
8
|
+
export async function runBuilderWithDiagnostics(builder, reporter, context, changedFile, relativeChange) {
|
|
9
|
+
const displayName = BUILDER_DISPLAY_NAMES[builder.name] ?? builder.name;
|
|
10
|
+
const messageContext = relativeChange ? ` (${relativeChange})` : '';
|
|
11
|
+
reporter.emitVerbose({
|
|
12
|
+
code: `frontend.watch.${builder.name}.build.start`,
|
|
13
|
+
kind: 'watch-daemon',
|
|
14
|
+
stage: builder.name,
|
|
15
|
+
severity: 'info',
|
|
16
|
+
message: `Starting ${displayName} rebuild${messageContext}.`,
|
|
17
|
+
data: changedFile ? { changedFile, builder: builder.name } : { builder: builder.name }
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
await builder.build(context);
|
|
21
|
+
reporter.emitVerbose({
|
|
22
|
+
code: `frontend.watch.${builder.name}.build.success`,
|
|
23
|
+
kind: 'watch-daemon',
|
|
24
|
+
stage: builder.name,
|
|
25
|
+
severity: 'info',
|
|
26
|
+
message: `${displayName} rebuild completed${messageContext}.`,
|
|
27
|
+
data: changedFile ? { changedFile, builder: builder.name } : { builder: builder.name }
|
|
28
|
+
});
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const details = { builder: builder.name };
|
|
33
|
+
if (changedFile) {
|
|
34
|
+
details.changedFile = changedFile;
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
details.error = error.message;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
details.error = String(error);
|
|
41
|
+
}
|
|
42
|
+
emitDiagnostic({
|
|
43
|
+
code: `frontend.watch.${builder.name}.build.failure`,
|
|
44
|
+
kind: 'watch-daemon',
|
|
45
|
+
stage: builder.name,
|
|
46
|
+
severity: 'error',
|
|
47
|
+
message: `${displayName} rebuild failed${messageContext}.`,
|
|
48
|
+
data: details
|
|
49
|
+
});
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function emitPipelineSuccess(summary, assetsResult, changedFile, relativeChange, hotUpdate) {
|
|
54
|
+
const message = `Frontend rebuild pipeline completed${relativeChange ? ` (${relativeChange})` : ''}.`;
|
|
55
|
+
const data = {
|
|
56
|
+
pages: summary.pagesBuilt,
|
|
57
|
+
assets: assetsResult.assets,
|
|
58
|
+
hotUpdate: serializeHotUpdate(hotUpdate, relativeChange)
|
|
59
|
+
};
|
|
60
|
+
if (relativeChange) {
|
|
61
|
+
data.changedFile = relativeChange;
|
|
62
|
+
}
|
|
63
|
+
else if (changedFile) {
|
|
64
|
+
data.changedFile = changedFile;
|
|
65
|
+
}
|
|
66
|
+
if (summary.warnings.length > 0) {
|
|
67
|
+
data.javascriptWarnings = summary.warnings;
|
|
68
|
+
}
|
|
69
|
+
emitDiagnostic({
|
|
70
|
+
code: 'frontend.watch.pipeline.success',
|
|
71
|
+
kind: 'watch-daemon',
|
|
72
|
+
stage: 'pipeline',
|
|
73
|
+
severity: 'info',
|
|
74
|
+
message,
|
|
75
|
+
data
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function serializeSummary(summary, changedFile, skipped) {
|
|
79
|
+
const data = {
|
|
80
|
+
pages: summary.pagesBuilt
|
|
81
|
+
};
|
|
82
|
+
if (changedFile) {
|
|
83
|
+
data.changedFile = changedFile;
|
|
84
|
+
}
|
|
85
|
+
if (summary.warnings.length > 0) {
|
|
86
|
+
data.warnings = summary.warnings;
|
|
87
|
+
}
|
|
88
|
+
if (skipped) {
|
|
89
|
+
data.skipped = true;
|
|
90
|
+
}
|
|
91
|
+
if (summary.modules.length > 0) {
|
|
92
|
+
data.modules = summary.modules.map(asset => asset.url);
|
|
93
|
+
}
|
|
94
|
+
if (summary.requiresReload) {
|
|
95
|
+
data.requiresReload = true;
|
|
96
|
+
}
|
|
97
|
+
if (summary.fallbackReasons.length > 0) {
|
|
98
|
+
data.fallbackReasons = summary.fallbackReasons;
|
|
99
|
+
}
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
102
|
+
export function emitJavaScriptFailure(error, changedFile) {
|
|
103
|
+
let message = 'JavaScript rebuild failed.';
|
|
104
|
+
let severity = 'error';
|
|
105
|
+
const data = changedFile ? { changedFile } : {};
|
|
106
|
+
if (error instanceof JavaScriptBuildError) {
|
|
107
|
+
message = `JavaScript rebuild failed for page '${error.pageName}'.`;
|
|
108
|
+
if (error.details.length > 0) {
|
|
109
|
+
data.errors = error.details;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (error instanceof Error) {
|
|
113
|
+
message = `JavaScript rebuild failed: ${error.message}`;
|
|
114
|
+
}
|
|
115
|
+
emitDiagnostic({
|
|
116
|
+
code: 'frontend.watch.javascript.build.failure',
|
|
117
|
+
kind: 'watch-daemon',
|
|
118
|
+
stage: 'javascript',
|
|
119
|
+
severity,
|
|
120
|
+
message,
|
|
121
|
+
data: Object.keys(data).length > 0 ? data : undefined
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export class JavaScriptBuildError extends Error {
|
|
125
|
+
pageName;
|
|
126
|
+
details;
|
|
127
|
+
constructor(pageName, cause) {
|
|
128
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
129
|
+
super(message);
|
|
130
|
+
this.pageName = pageName;
|
|
131
|
+
this.details = isBuildFailure(cause) ? serializeMessages(cause.errors ?? []) : [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function serializeHotUpdate(hotUpdate, relativeChange) {
|
|
135
|
+
const data = {
|
|
136
|
+
requiresReload: hotUpdate.requiresReload,
|
|
137
|
+
modules: hotUpdate.modules.map(asset => serializeHotAsset(asset)),
|
|
138
|
+
styles: hotUpdate.styles.map(asset => serializeHotAsset(asset))
|
|
139
|
+
};
|
|
140
|
+
if (relativeChange) {
|
|
141
|
+
data.changedFile = relativeChange;
|
|
142
|
+
}
|
|
143
|
+
else if (hotUpdate.changedFile) {
|
|
144
|
+
data.changedFile = hotUpdate.changedFile;
|
|
145
|
+
}
|
|
146
|
+
if (hotUpdate.fallbackReasons.length > 0) {
|
|
147
|
+
data.fallbackReasons = hotUpdate.fallbackReasons;
|
|
148
|
+
}
|
|
149
|
+
if (hotUpdate.stats) {
|
|
150
|
+
data.stats = {
|
|
151
|
+
hotUpdates: hotUpdate.stats.hotUpdates,
|
|
152
|
+
reloadFallbacks: hotUpdate.stats.reloadFallbacks
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
function serializeHotAsset(asset) {
|
|
158
|
+
return {
|
|
159
|
+
type: asset.type,
|
|
160
|
+
path: asset.path,
|
|
161
|
+
relativePath: asset.relativePath,
|
|
162
|
+
url: asset.url
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function isBuildFailure(error) {
|
|
166
|
+
if (typeof error !== 'object' || error === null) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const candidate = error;
|
|
170
|
+
return Array.isArray(candidate.errors) && candidate.errors.every(isEsbuildMessage);
|
|
171
|
+
}
|
|
172
|
+
function isEsbuildMessage(candidate) {
|
|
173
|
+
if (typeof candidate !== 'object' || candidate === null) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return typeof candidate.text === 'string';
|
|
177
|
+
}
|