@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,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const frontendPathSchema = z.object({
|
|
4
|
+
workspace: z.string(),
|
|
5
|
+
src: z.object({
|
|
6
|
+
root: z.string(),
|
|
7
|
+
frontend: z.string(),
|
|
8
|
+
app: z.string(),
|
|
9
|
+
pages: z.string(),
|
|
10
|
+
content: z.string(),
|
|
11
|
+
images: z.string(),
|
|
12
|
+
fonts: z.string(),
|
|
13
|
+
media: z.string()
|
|
14
|
+
}),
|
|
15
|
+
build: z.object({
|
|
16
|
+
root: z.string(),
|
|
17
|
+
frontend: z.string(),
|
|
18
|
+
app: z.string(),
|
|
19
|
+
pages: z.string(),
|
|
20
|
+
content: z.string(),
|
|
21
|
+
images: z.string(),
|
|
22
|
+
fonts: z.string(),
|
|
23
|
+
media: z.string()
|
|
24
|
+
}),
|
|
25
|
+
dist: z.object({
|
|
26
|
+
root: z.string(),
|
|
27
|
+
frontend: z.string(),
|
|
28
|
+
app: z.string(),
|
|
29
|
+
pages: z.string(),
|
|
30
|
+
content: z.string(),
|
|
31
|
+
images: z.string(),
|
|
32
|
+
fonts: z.string(),
|
|
33
|
+
media: z.string()
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const frontendFeatureFlagsSchema = z.object({
|
|
38
|
+
htmlSecurity: z.boolean().default(true),
|
|
39
|
+
imageOptimization: z.boolean().default(true),
|
|
40
|
+
precompression: z.boolean().default(true)
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const frontendConfigSchema = z.object({
|
|
44
|
+
version: z.literal(1),
|
|
45
|
+
paths: frontendPathSchema,
|
|
46
|
+
features: frontendFeatureFlagsSchema
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type FrontendConfigInput = z.infer<typeof frontendConfigSchema>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { writeConfigManifest } from './manifest.js';
|
|
2
|
+
import { buildConfig } from './workspace.js';
|
|
3
|
+
import { ensureWebstirDirectory, resolveManifestPath } from './paths.js';
|
|
4
|
+
import type { FrontendConfig } from '../types.js';
|
|
5
|
+
|
|
6
|
+
export async function prepareWorkspaceConfig(workspaceRoot: string): Promise<FrontendConfig> {
|
|
7
|
+
const config = buildConfig(workspaceRoot);
|
|
8
|
+
await ensureWebstirDirectory(workspaceRoot);
|
|
9
|
+
await writeConfigManifest({
|
|
10
|
+
outputPath: resolveManifestPath(workspaceRoot),
|
|
11
|
+
data: config
|
|
12
|
+
});
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { FrontendConfig, FrontendFeatureFlags } from '../types.js';
|
|
4
|
+
import { FOLDERS } from '../core/constants.js';
|
|
5
|
+
import { frontendFeatureFlagsSchema } from './schema.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_FEATURE_FLAGS: FrontendFeatureFlags = {
|
|
8
|
+
htmlSecurity: true,
|
|
9
|
+
imageOptimization: true,
|
|
10
|
+
precompression: true
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function buildConfig(workspaceRoot: string): FrontendConfig {
|
|
14
|
+
const srcRoot = path.join(workspaceRoot, FOLDERS.src);
|
|
15
|
+
const frontendRoot = path.join(srcRoot, FOLDERS.frontend);
|
|
16
|
+
const buildRoot = path.join(workspaceRoot, FOLDERS.build);
|
|
17
|
+
const distRoot = path.join(workspaceRoot, FOLDERS.dist);
|
|
18
|
+
|
|
19
|
+
const buildFrontend = path.join(buildRoot, FOLDERS.frontend);
|
|
20
|
+
const distFrontend = path.join(distRoot, FOLDERS.frontend);
|
|
21
|
+
const srcContentRoot = resolveContentRoot(workspaceRoot, frontendRoot);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
version: 1,
|
|
25
|
+
paths: {
|
|
26
|
+
workspace: workspaceRoot,
|
|
27
|
+
src: {
|
|
28
|
+
root: srcRoot,
|
|
29
|
+
frontend: frontendRoot,
|
|
30
|
+
app: path.join(frontendRoot, FOLDERS.app),
|
|
31
|
+
pages: path.join(frontendRoot, FOLDERS.pages),
|
|
32
|
+
content: srcContentRoot,
|
|
33
|
+
images: path.join(frontendRoot, FOLDERS.images),
|
|
34
|
+
fonts: path.join(frontendRoot, FOLDERS.fonts),
|
|
35
|
+
media: path.join(frontendRoot, FOLDERS.media)
|
|
36
|
+
},
|
|
37
|
+
build: {
|
|
38
|
+
root: buildRoot,
|
|
39
|
+
frontend: buildFrontend,
|
|
40
|
+
app: path.join(buildFrontend, FOLDERS.app),
|
|
41
|
+
pages: path.join(buildFrontend, FOLDERS.pages),
|
|
42
|
+
content: path.join(buildFrontend, FOLDERS.pages, 'docs'),
|
|
43
|
+
images: path.join(buildFrontend, FOLDERS.images),
|
|
44
|
+
fonts: path.join(buildFrontend, FOLDERS.fonts),
|
|
45
|
+
media: path.join(buildFrontend, FOLDERS.media)
|
|
46
|
+
},
|
|
47
|
+
dist: {
|
|
48
|
+
root: distRoot,
|
|
49
|
+
frontend: distFrontend,
|
|
50
|
+
app: path.join(distFrontend, FOLDERS.app),
|
|
51
|
+
pages: path.join(distFrontend, FOLDERS.pages),
|
|
52
|
+
content: path.join(distFrontend, FOLDERS.pages, 'docs'),
|
|
53
|
+
images: path.join(distFrontend, FOLDERS.images),
|
|
54
|
+
fonts: path.join(distFrontend, FOLDERS.fonts),
|
|
55
|
+
media: path.join(distFrontend, FOLDERS.media)
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
features: loadFeatureFlags(frontendRoot)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveContentRoot(workspaceRoot: string, frontendRoot: string): string {
|
|
63
|
+
const defaultContentRoot = path.join(frontendRoot, 'content');
|
|
64
|
+
const configPath = path.join(frontendRoot, 'frontend.config.json');
|
|
65
|
+
if (!fs.existsSync(configPath)) {
|
|
66
|
+
return defaultContentRoot;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
71
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
72
|
+
const override = extractContentRoot(parsed);
|
|
73
|
+
|
|
74
|
+
if (override === undefined) {
|
|
75
|
+
return defaultContentRoot;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof override !== 'string') {
|
|
79
|
+
throw new Error('Expected contentRoot to be a string when specified.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const trimmed = override.trim();
|
|
83
|
+
if (!trimmed) {
|
|
84
|
+
return defaultContentRoot;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (path.isAbsolute(trimmed)) {
|
|
88
|
+
return trimmed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return path.join(workspaceRoot, trimmed);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
throw new Error(`Failed to read frontend content root from ${configPath}: ${message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractContentRoot(value: unknown): unknown {
|
|
99
|
+
if (!value || typeof value !== 'object') {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const container = value as Record<string, unknown>;
|
|
104
|
+
|
|
105
|
+
if ('paths' in container && container.paths && typeof container.paths === 'object') {
|
|
106
|
+
const pathsContainer = container.paths as Record<string, unknown>;
|
|
107
|
+
if ('contentRoot' in pathsContainer) {
|
|
108
|
+
return pathsContainer.contentRoot;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if ('contentRoot' in container) {
|
|
113
|
+
return container.contentRoot;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function loadFeatureFlags(frontendRoot: string): FrontendFeatureFlags {
|
|
120
|
+
const configPath = path.join(frontendRoot, 'frontend.config.json');
|
|
121
|
+
if (!fs.existsSync(configPath)) {
|
|
122
|
+
return DEFAULT_FEATURE_FLAGS;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
127
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
128
|
+
const overridesSource = extractOverrideSource(parsed);
|
|
129
|
+
const overrides = frontendFeatureFlagsSchema.parse(overridesSource);
|
|
130
|
+
return {
|
|
131
|
+
htmlSecurity: overrides.htmlSecurity,
|
|
132
|
+
imageOptimization: overrides.imageOptimization,
|
|
133
|
+
precompression: overrides.precompression
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
throw new Error(`Failed to read frontend feature flags from ${configPath}: ${message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractOverrideSource(value: unknown): Record<string, unknown> {
|
|
142
|
+
if (value && typeof value === 'object' && 'features' in (value as Record<string, unknown>)) {
|
|
143
|
+
const container = (value as Record<string, unknown>).features;
|
|
144
|
+
if (container && typeof container === 'object') {
|
|
145
|
+
return container as Record<string, unknown>;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
|
150
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type RenderMode = 'ssg' | 'ssr' | 'spa';
|
|
2
|
+
|
|
3
|
+
export interface WorkspaceModuleView {
|
|
4
|
+
readonly name?: string;
|
|
5
|
+
readonly path?: string;
|
|
6
|
+
readonly renderMode?: RenderMode;
|
|
7
|
+
readonly staticPaths?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WorkspaceModuleRouteGuard {
|
|
11
|
+
readonly renderMode?: unknown;
|
|
12
|
+
readonly staticPaths?: unknown;
|
|
13
|
+
readonly ssg?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WorkspaceModuleConfig {
|
|
17
|
+
readonly views?: readonly WorkspaceModuleView[];
|
|
18
|
+
readonly routes?: readonly WorkspaceModuleRouteGuard[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkspacePackageJson {
|
|
22
|
+
readonly name?: string;
|
|
23
|
+
readonly webstir?: {
|
|
24
|
+
readonly mode?: string;
|
|
25
|
+
readonly moduleManifest?: WorkspaceModuleConfig;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const FOLDERS = {
|
|
2
|
+
src: 'src',
|
|
3
|
+
build: 'build',
|
|
4
|
+
dist: 'dist',
|
|
5
|
+
webstir: '.webstir',
|
|
6
|
+
tests: 'tests',
|
|
7
|
+
frontend: 'frontend',
|
|
8
|
+
backend: 'backend',
|
|
9
|
+
shared: 'shared',
|
|
10
|
+
types: 'types',
|
|
11
|
+
app: 'app',
|
|
12
|
+
pages: 'pages',
|
|
13
|
+
styles: 'styles',
|
|
14
|
+
scripts: 'scripts',
|
|
15
|
+
images: 'images',
|
|
16
|
+
fonts: 'fonts',
|
|
17
|
+
media: 'media',
|
|
18
|
+
chunks: 'chunks',
|
|
19
|
+
home: 'home',
|
|
20
|
+
nodeModules: 'node_modules',
|
|
21
|
+
seed: 'seed',
|
|
22
|
+
demo: 'demo',
|
|
23
|
+
temp: 'temp'
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export const FILES = {
|
|
27
|
+
packageJson: 'package.json',
|
|
28
|
+
packageLockJson: 'package-lock.json',
|
|
29
|
+
tsBuildInfo: '.tsbuildinfo',
|
|
30
|
+
baseTsConfigJson: 'base.tsconfig.json',
|
|
31
|
+
manifestJson: 'manifest.json',
|
|
32
|
+
test: '.test',
|
|
33
|
+
index: 'index',
|
|
34
|
+
indexHtml: 'index.html',
|
|
35
|
+
refreshJs: 'refresh.js',
|
|
36
|
+
hmrJs: 'hmr.js',
|
|
37
|
+
robotsTxt: 'robots.txt'
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
export const EXTENSIONS = {
|
|
41
|
+
html: '.html',
|
|
42
|
+
css: '.css',
|
|
43
|
+
br: '.br',
|
|
44
|
+
gz: '.gz',
|
|
45
|
+
dts: '.d.ts',
|
|
46
|
+
ts: '.ts',
|
|
47
|
+
js: '.js',
|
|
48
|
+
map: '.map',
|
|
49
|
+
png: '.png',
|
|
50
|
+
jpg: '.jpg',
|
|
51
|
+
jpeg: '.jpeg',
|
|
52
|
+
gif: '.gif',
|
|
53
|
+
svg: '.svg',
|
|
54
|
+
webp: '.webp',
|
|
55
|
+
avif: '.avif',
|
|
56
|
+
ico: '.ico',
|
|
57
|
+
woff: '.woff',
|
|
58
|
+
woff2: '.woff2',
|
|
59
|
+
ttf: '.ttf',
|
|
60
|
+
otf: '.otf',
|
|
61
|
+
eot: '.eot',
|
|
62
|
+
mp3: '.mp3',
|
|
63
|
+
m4a: '.m4a',
|
|
64
|
+
wav: '.wav',
|
|
65
|
+
ogg: '.ogg',
|
|
66
|
+
mp4: '.mp4',
|
|
67
|
+
webm: '.webm',
|
|
68
|
+
mov: '.mov'
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
71
|
+
export const FILE_NAMES = {
|
|
72
|
+
htmlAppTemplate: 'app.html'
|
|
73
|
+
} as const;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type DiagnosticSeverity = 'info' | 'warning' | 'error';
|
|
2
|
+
|
|
3
|
+
export interface DiagnosticEvent {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly kind: string;
|
|
6
|
+
readonly stage: string;
|
|
7
|
+
readonly severity: DiagnosticSeverity;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly data?: Record<string, unknown>;
|
|
10
|
+
readonly suggestion?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DiagnosticPayload extends DiagnosticEvent {
|
|
14
|
+
readonly type: 'diagnostic';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const STRUCTURED_DIAGNOSTIC_PREFIX = 'WEBSTIR_DIAGNOSTIC ';
|
|
18
|
+
|
|
19
|
+
export function emitDiagnostic(event: DiagnosticEvent): void {
|
|
20
|
+
const payload: DiagnosticPayload = {
|
|
21
|
+
type: 'diagnostic',
|
|
22
|
+
...event
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const logMessage = `[webstir-frontend][${event.code}] ${event.message}`;
|
|
26
|
+
switch (event.severity) {
|
|
27
|
+
case 'error':
|
|
28
|
+
console.error(logMessage);
|
|
29
|
+
break;
|
|
30
|
+
case 'warning':
|
|
31
|
+
console.warn(logMessage);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
console.info(logMessage);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const serialized = JSON.stringify(payload);
|
|
39
|
+
console.log(`${STRUCTURED_DIAGNOSTIC_PREFIX}${serialized}`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { glob } from 'glob';
|
|
3
|
+
import { pathExists } from '../utils/fs.js';
|
|
4
|
+
|
|
5
|
+
export interface PageInfo {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly directory: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getPages(root: string): Promise<PageInfo[]> {
|
|
11
|
+
const directories = await getPageDirectories(root);
|
|
12
|
+
return directories.map((entry) => ({
|
|
13
|
+
name: entry.name,
|
|
14
|
+
directory: entry.directory
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getPageDirectories(root: string): Promise<PageInfo[]> {
|
|
19
|
+
if (!(await pathExists(root))) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const entries = await glob('*/', { cwd: root, absolute: false, withFileTypes: false });
|
|
24
|
+
return entries.map((entry) => {
|
|
25
|
+
const name = entry.endsWith('/') ? entry.slice(0, -1) : entry;
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
directory: path.join(root, name)
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import type { FrontendConfig } from './types.js';
|
|
5
|
+
import type { PipelineMode } from './pipeline.js';
|
|
6
|
+
|
|
7
|
+
const CONFIG_CANDIDATES = ['webstir.config.mjs', 'webstir.config.js', 'webstir.config.cjs'];
|
|
8
|
+
|
|
9
|
+
export interface HookContext {
|
|
10
|
+
readonly config: FrontendConfig;
|
|
11
|
+
readonly mode: PipelineMode;
|
|
12
|
+
readonly workspaceRoot: string;
|
|
13
|
+
readonly builderName?: string;
|
|
14
|
+
readonly changedFile?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type HookHandler = (context: HookContext) => unknown | Promise<unknown>;
|
|
18
|
+
|
|
19
|
+
export interface ResolvedHooks {
|
|
20
|
+
readonly pipelineBefore: HookHandler[];
|
|
21
|
+
readonly pipelineAfter: HookHandler[];
|
|
22
|
+
readonly builderBefore: Map<string, HookHandler[]>;
|
|
23
|
+
readonly builderAfter: Map<string, HookHandler[]>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RawHooks {
|
|
27
|
+
readonly pipeline?: RawPipelineHooks;
|
|
28
|
+
readonly builders?: Record<string, RawBuilderHooks | HookHandler | HookHandler[]>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface RawPipelineHooks {
|
|
32
|
+
readonly beforeAll?: HookHandler | HookHandler[];
|
|
33
|
+
readonly afterAll?: HookHandler | HookHandler[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RawBuilderHooks {
|
|
37
|
+
readonly before?: HookHandler | HookHandler[];
|
|
38
|
+
readonly after?: HookHandler | HookHandler[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const EMPTY_HOOKS: ResolvedHooks = {
|
|
42
|
+
pipelineBefore: [],
|
|
43
|
+
pipelineAfter: [],
|
|
44
|
+
builderBefore: new Map(),
|
|
45
|
+
builderAfter: new Map()
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export async function loadHooks(workspaceRoot: string, cacheBust: boolean): Promise<ResolvedHooks> {
|
|
49
|
+
const configPath = findConfigPath(workspaceRoot);
|
|
50
|
+
if (!configPath) {
|
|
51
|
+
return EMPTY_HOOKS;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let moduleConfig: unknown;
|
|
55
|
+
try {
|
|
56
|
+
const fileUrl = pathToFileURL(configPath).href;
|
|
57
|
+
const url = cacheBust ? `${fileUrl}?update=${Date.now()}` : fileUrl;
|
|
58
|
+
moduleConfig = await import(url);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
throw new Error(`Failed to load hooks from ${configPath}: ${message}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const exported = (moduleConfig as Record<string, unknown>)?.default ?? moduleConfig;
|
|
65
|
+
if (!exported || typeof exported !== 'object') {
|
|
66
|
+
return EMPTY_HOOKS;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rawHooks = (exported as Record<string, unknown>).hooks ?? exported;
|
|
70
|
+
if (!rawHooks || typeof rawHooks !== 'object') {
|
|
71
|
+
return EMPTY_HOOKS;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return normalizeHooks(rawHooks as RawHooks, configPath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createHookContext(
|
|
78
|
+
config: FrontendConfig,
|
|
79
|
+
mode: PipelineMode,
|
|
80
|
+
changedFile: string | undefined,
|
|
81
|
+
builderName?: string
|
|
82
|
+
): HookContext {
|
|
83
|
+
return {
|
|
84
|
+
config,
|
|
85
|
+
mode,
|
|
86
|
+
workspaceRoot: config.paths.workspace,
|
|
87
|
+
builderName,
|
|
88
|
+
changedFile
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function executeHooks(label: string, handlers: HookHandler[], context: HookContext): Promise<void> {
|
|
93
|
+
for (const handler of handlers) {
|
|
94
|
+
try {
|
|
95
|
+
await handler(context);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw wrapHookError(label, error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function wrapHookError(label: string, error: unknown): Error {
|
|
103
|
+
if (error instanceof Error) {
|
|
104
|
+
error.message = `[hook:${label}] ${error.message}`;
|
|
105
|
+
return error;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return new Error(`[hook:${label}] ${String(error)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeHooks(raw: RawHooks, configPath: string): ResolvedHooks {
|
|
112
|
+
const pipeline = raw.pipeline ?? {};
|
|
113
|
+
const pipelineBefore = normalizeHandlerSet(pipeline.beforeAll, `${configPath} pipeline.beforeAll`);
|
|
114
|
+
const pipelineAfter = normalizeHandlerSet(pipeline.afterAll, `${configPath} pipeline.afterAll`);
|
|
115
|
+
|
|
116
|
+
const builderBefore = new Map<string, HookHandler[]>();
|
|
117
|
+
const builderAfter = new Map<string, HookHandler[]>();
|
|
118
|
+
|
|
119
|
+
const builders = raw.builders ?? {};
|
|
120
|
+
for (const [name, value] of Object.entries(builders)) {
|
|
121
|
+
if (typeof value === 'function' || Array.isArray(value)) {
|
|
122
|
+
builderBefore.set(name, normalizeHandlerSet(value, `${configPath} builders.${name}`));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!value || typeof value !== 'object') {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const before = normalizeHandlerSet(value.before, `${configPath} builders.${name}.before`);
|
|
131
|
+
const after = normalizeHandlerSet(value.after, `${configPath} builders.${name}.after`);
|
|
132
|
+
if (before.length > 0) {
|
|
133
|
+
builderBefore.set(name, before);
|
|
134
|
+
}
|
|
135
|
+
if (after.length > 0) {
|
|
136
|
+
builderAfter.set(name, after);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
pipelineBefore,
|
|
142
|
+
pipelineAfter,
|
|
143
|
+
builderBefore,
|
|
144
|
+
builderAfter
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeHandlerSet(value: HookHandler | HookHandler[] | undefined, label: string): HookHandler[] {
|
|
149
|
+
if (value === undefined) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const handlers = Array.isArray(value) ? value : [value];
|
|
154
|
+
const normalized: HookHandler[] = [];
|
|
155
|
+
|
|
156
|
+
for (const handler of handlers) {
|
|
157
|
+
if (typeof handler !== 'function') {
|
|
158
|
+
throw new Error(`Invalid hook handler in ${label}; expected function`);
|
|
159
|
+
}
|
|
160
|
+
normalized.push(handler);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return normalized;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function findConfigPath(workspaceRoot: string): string | undefined {
|
|
167
|
+
for (const candidate of CONFIG_CANDIDATES) {
|
|
168
|
+
const fullPath = path.join(workspaceRoot, candidate);
|
|
169
|
+
if (fs.existsSync(fullPath)) {
|
|
170
|
+
return fullPath;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|