@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
|
@@ -5,12 +5,14 @@ import fssync from 'node:fs';
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
|
-
async function
|
|
8
|
+
async function loadFrontendModuleOrSkip(t) {
|
|
9
9
|
try {
|
|
10
|
-
|
|
11
|
-
return mod.frontendProvider;
|
|
10
|
+
return await import('../dist/index.js');
|
|
12
11
|
} catch (err) {
|
|
13
|
-
console.warn(
|
|
12
|
+
console.warn(
|
|
13
|
+
'[frontend-tests] Skipping provider integration: optional dependency unavailable:',
|
|
14
|
+
err?.message ?? err,
|
|
15
|
+
);
|
|
14
16
|
t?.diagnostic?.('skip: missing optional dependency');
|
|
15
17
|
return null;
|
|
16
18
|
}
|
|
@@ -28,10 +30,29 @@ async function createWorkspaceWithContent() {
|
|
|
28
30
|
await fs.writeFile(
|
|
29
31
|
path.join(appDir, 'app.html'),
|
|
30
32
|
'<!DOCTYPE html><html><head><title>My Site</title></head><body><main></main></body></html>',
|
|
31
|
-
'utf8'
|
|
33
|
+
'utf8',
|
|
32
34
|
);
|
|
33
35
|
await fs.writeFile(path.join(appDir, 'app.css'), 'body{font-family:sans-serif;}', 'utf8');
|
|
34
|
-
await fs.writeFile(
|
|
36
|
+
await fs.writeFile(
|
|
37
|
+
path.join(pageDir, 'index.html'),
|
|
38
|
+
'<head></head><main><section>Home</section></main>',
|
|
39
|
+
'utf8',
|
|
40
|
+
);
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.join(root, 'package.json'),
|
|
43
|
+
JSON.stringify(
|
|
44
|
+
{
|
|
45
|
+
name: 'content-test',
|
|
46
|
+
private: true,
|
|
47
|
+
webstir: {
|
|
48
|
+
mode: 'ssg',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
null,
|
|
52
|
+
2,
|
|
53
|
+
),
|
|
54
|
+
'utf8',
|
|
55
|
+
);
|
|
35
56
|
|
|
36
57
|
await fs.writeFile(
|
|
37
58
|
path.join(contentDir, 'readme.md'),
|
|
@@ -44,23 +65,53 @@ async function createWorkspaceWithContent() {
|
|
|
44
65
|
'',
|
|
45
66
|
'# Content pipeline',
|
|
46
67
|
'',
|
|
47
|
-
'Hello from markdown.'
|
|
68
|
+
'Hello from markdown.',
|
|
48
69
|
].join('\n'),
|
|
49
|
-
'utf8'
|
|
70
|
+
'utf8',
|
|
71
|
+
);
|
|
72
|
+
await fs.writeFile(
|
|
73
|
+
path.join(contentDir, '_sidebar.json'),
|
|
74
|
+
JSON.stringify(
|
|
75
|
+
{
|
|
76
|
+
pages: [
|
|
77
|
+
{
|
|
78
|
+
path: '/docs/readme/',
|
|
79
|
+
title: 'Guide v1',
|
|
80
|
+
order: 1,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
null,
|
|
85
|
+
2,
|
|
86
|
+
),
|
|
87
|
+
'utf8',
|
|
50
88
|
);
|
|
51
89
|
|
|
52
90
|
return root;
|
|
53
91
|
}
|
|
54
92
|
|
|
55
93
|
test('content builder strips frontmatter and injects app styles', async (t) => {
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
94
|
+
const frontend = await loadFrontendModuleOrSkip(t);
|
|
95
|
+
if (!frontend) return;
|
|
96
|
+
const { frontendProvider } = frontend;
|
|
58
97
|
const workspace = await createWorkspaceWithContent();
|
|
59
98
|
|
|
60
99
|
try {
|
|
61
|
-
await frontendProvider.build({
|
|
100
|
+
await frontendProvider.build({
|
|
101
|
+
workspaceRoot: workspace,
|
|
102
|
+
env: { WEBSTIR_MODULE_MODE: 'build' },
|
|
103
|
+
incremental: false,
|
|
104
|
+
});
|
|
62
105
|
|
|
63
|
-
const htmlPath = path.join(
|
|
106
|
+
const htmlPath = path.join(
|
|
107
|
+
workspace,
|
|
108
|
+
'build',
|
|
109
|
+
'frontend',
|
|
110
|
+
'pages',
|
|
111
|
+
'docs',
|
|
112
|
+
'readme',
|
|
113
|
+
'index.html',
|
|
114
|
+
);
|
|
64
115
|
assert.equal(fssync.existsSync(htmlPath), true, `expected ${htmlPath}`);
|
|
65
116
|
|
|
66
117
|
const html = await fs.readFile(htmlPath, 'utf8');
|
|
@@ -74,7 +125,51 @@ test('content builder strips frontmatter and injects app styles', async (t) => {
|
|
|
74
125
|
|
|
75
126
|
const nav = JSON.parse(await fs.readFile(navPath, 'utf8'));
|
|
76
127
|
assert.ok(Array.isArray(nav) && nav.length > 0, 'expected docs-nav.json to contain entries');
|
|
77
|
-
assert.ok(
|
|
128
|
+
assert.ok(
|
|
129
|
+
nav.some((entry) => entry.path === '/docs/readme/'),
|
|
130
|
+
'expected docs-nav.json to include /docs/readme/',
|
|
131
|
+
);
|
|
132
|
+
} finally {
|
|
133
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('content rebuild updates docs-nav when _sidebar.json changes', async (t) => {
|
|
138
|
+
const frontend = await loadFrontendModuleOrSkip(t);
|
|
139
|
+
if (!frontend) return;
|
|
140
|
+
const { runBuild, runRebuild } = frontend;
|
|
141
|
+
const workspace = await createWorkspaceWithContent();
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await runBuild({ workspaceRoot: workspace });
|
|
145
|
+
|
|
146
|
+
const navPath = path.join(workspace, 'build', 'frontend', 'docs-nav.json');
|
|
147
|
+
let nav = JSON.parse(await fs.readFile(navPath, 'utf8'));
|
|
148
|
+
assert.equal(nav[0]?.title, 'Guide v1');
|
|
149
|
+
|
|
150
|
+
const sidebarPath = path.join(workspace, 'src', 'frontend', 'content', '_sidebar.json');
|
|
151
|
+
await fs.writeFile(
|
|
152
|
+
sidebarPath,
|
|
153
|
+
JSON.stringify(
|
|
154
|
+
{
|
|
155
|
+
pages: [
|
|
156
|
+
{
|
|
157
|
+
path: '/docs/readme/',
|
|
158
|
+
title: 'Guide v2',
|
|
159
|
+
order: 1,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
null,
|
|
164
|
+
2,
|
|
165
|
+
),
|
|
166
|
+
'utf8',
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
await runRebuild({ workspaceRoot: workspace, changedFile: sidebarPath });
|
|
170
|
+
|
|
171
|
+
nav = JSON.parse(await fs.readFile(navPath, 'utf8'));
|
|
172
|
+
assert.equal(nav[0]?.title, 'Guide v2');
|
|
78
173
|
} finally {
|
|
79
174
|
await fs.rm(workspace, { recursive: true, force: true });
|
|
80
175
|
}
|
|
@@ -10,7 +10,10 @@ async function loadProviderOrSkip(t) {
|
|
|
10
10
|
const mod = await import('../dist/index.js');
|
|
11
11
|
return mod.frontendProvider;
|
|
12
12
|
} catch (err) {
|
|
13
|
-
console.warn(
|
|
13
|
+
console.warn(
|
|
14
|
+
'[frontend-tests] Skipping provider integration: optional dependency unavailable:',
|
|
15
|
+
err?.message ?? err,
|
|
16
|
+
);
|
|
14
17
|
t?.diagnostic?.('skip: missing optional dependency');
|
|
15
18
|
return null;
|
|
16
19
|
}
|
|
@@ -27,18 +30,23 @@ async function createWorkspace() {
|
|
|
27
30
|
await fs.writeFile(
|
|
28
31
|
path.join(appDir, 'app.html'),
|
|
29
32
|
'<!DOCTYPE html><html><head><title>App</title></head><body><main></main></body></html>',
|
|
30
|
-
'utf8'
|
|
33
|
+
'utf8',
|
|
31
34
|
);
|
|
32
35
|
await fs.writeFile(
|
|
33
36
|
path.join(appDir, 'app.css'),
|
|
34
|
-
[
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'
|
|
37
|
+
['@layer reset, base;', '@import "./styles/base.css";'].join('\n'),
|
|
38
|
+
'utf8',
|
|
39
|
+
);
|
|
40
|
+
await fs.writeFile(
|
|
41
|
+
path.join(stylesDir, 'base.css'),
|
|
42
|
+
'@layer base { body { background: blue; } }',
|
|
43
|
+
'utf8',
|
|
44
|
+
);
|
|
45
|
+
await fs.writeFile(
|
|
46
|
+
path.join(pageDir, 'index.html'),
|
|
47
|
+
'<head></head><main><section>Home</section></main>',
|
|
48
|
+
'utf8',
|
|
39
49
|
);
|
|
40
|
-
await fs.writeFile(path.join(stylesDir, 'base.css'), '@layer base { body { background: blue; } }', 'utf8');
|
|
41
|
-
await fs.writeFile(path.join(pageDir, 'index.html'), '<head></head><main><section>Home</section></main>', 'utf8');
|
|
42
50
|
await fs.writeFile(path.join(pageDir, 'index.css'), '@import "@app/app.css";', 'utf8');
|
|
43
51
|
|
|
44
52
|
return root;
|
|
@@ -50,7 +58,11 @@ test('development app.css import URLs include a cache-busting version', async (t
|
|
|
50
58
|
const workspace = await createWorkspace();
|
|
51
59
|
|
|
52
60
|
try {
|
|
53
|
-
await frontendProvider.build({
|
|
61
|
+
await frontendProvider.build({
|
|
62
|
+
workspaceRoot: workspace,
|
|
63
|
+
env: { WEBSTIR_MODULE_MODE: 'build' },
|
|
64
|
+
incremental: false,
|
|
65
|
+
});
|
|
54
66
|
|
|
55
67
|
const appCssPath = path.join(workspace, 'build', 'frontend', 'app', 'app.css');
|
|
56
68
|
assert.equal(fssync.existsSync(appCssPath), true, `expected ${appCssPath}`);
|
|
@@ -61,4 +73,3 @@ test('development app.css import URLs include a cache-busting version', async (t
|
|
|
61
73
|
await fs.rm(workspace, { recursive: true, force: true });
|
|
62
74
|
}
|
|
63
75
|
});
|
|
64
|
-
|
|
@@ -10,7 +10,10 @@ async function loadProviderOrSkip(t) {
|
|
|
10
10
|
const mod = await import('../dist/index.js');
|
|
11
11
|
return mod.frontendProvider;
|
|
12
12
|
} catch (err) {
|
|
13
|
-
console.warn(
|
|
13
|
+
console.warn(
|
|
14
|
+
'[frontend-tests] Skipping provider integration: optional dependency unavailable:',
|
|
15
|
+
err?.message ?? err,
|
|
16
|
+
);
|
|
14
17
|
t?.diagnostic?.('skip: missing optional dependency');
|
|
15
18
|
return null;
|
|
16
19
|
}
|
|
@@ -27,40 +30,44 @@ async function createWorkspace() {
|
|
|
27
30
|
await fs.writeFile(
|
|
28
31
|
path.join(appDir, 'app.html'),
|
|
29
32
|
'<!DOCTYPE html><html><head><title>App</title></head><body><main></main></body></html>',
|
|
30
|
-
'utf8'
|
|
33
|
+
'utf8',
|
|
31
34
|
);
|
|
32
35
|
await fs.writeFile(path.join(appDir, 'app.css'), '', 'utf8');
|
|
33
|
-
await fs.writeFile(
|
|
36
|
+
await fs.writeFile(
|
|
37
|
+
path.join(pageDir, 'index.html'),
|
|
38
|
+
'<head></head><main><section>Home</section></main>',
|
|
39
|
+
'utf8',
|
|
40
|
+
);
|
|
34
41
|
|
|
35
42
|
await fs.writeFile(
|
|
36
43
|
path.join(pageDir, 'index.css'),
|
|
37
44
|
[
|
|
38
45
|
'@layer overrides { .home { color: red; } }',
|
|
39
46
|
'@import "./layout.css";',
|
|
40
|
-
'@import url("./partials/colors.css");'
|
|
47
|
+
'@import url("./partials/colors.css");',
|
|
41
48
|
].join('\n'),
|
|
42
|
-
'utf8'
|
|
49
|
+
'utf8',
|
|
43
50
|
);
|
|
44
51
|
|
|
45
52
|
await fs.writeFile(
|
|
46
53
|
path.join(pageDir, 'layout.css'),
|
|
47
54
|
[
|
|
48
55
|
'@layer overrides { .layout { display: grid; } }',
|
|
49
|
-
'@import "./partials/typography.css";'
|
|
56
|
+
'@import "./partials/typography.css";',
|
|
50
57
|
].join('\n'),
|
|
51
|
-
'utf8'
|
|
58
|
+
'utf8',
|
|
52
59
|
);
|
|
53
60
|
|
|
54
61
|
await fs.writeFile(
|
|
55
62
|
path.join(partialsDir, 'colors.css'),
|
|
56
63
|
'@layer overrides { .colors { color: blue; } }',
|
|
57
|
-
'utf8'
|
|
64
|
+
'utf8',
|
|
58
65
|
);
|
|
59
66
|
|
|
60
67
|
await fs.writeFile(
|
|
61
68
|
path.join(partialsDir, 'typography.css'),
|
|
62
69
|
'@layer overrides { .type { font-weight: 700; } }',
|
|
63
|
-
'utf8'
|
|
70
|
+
'utf8',
|
|
64
71
|
);
|
|
65
72
|
|
|
66
73
|
return root;
|
|
@@ -72,7 +79,11 @@ test('build inlines page-local CSS @import files', async (t) => {
|
|
|
72
79
|
const workspace = await createWorkspace();
|
|
73
80
|
|
|
74
81
|
try {
|
|
75
|
-
await frontendProvider.build({
|
|
82
|
+
await frontendProvider.build({
|
|
83
|
+
workspaceRoot: workspace,
|
|
84
|
+
env: { WEBSTIR_MODULE_MODE: 'build' },
|
|
85
|
+
incremental: false,
|
|
86
|
+
});
|
|
76
87
|
|
|
77
88
|
const cssPath = path.join(workspace, 'build', 'frontend', 'pages', 'home', 'index.css');
|
|
78
89
|
assert.equal(fssync.existsSync(cssPath), true, `expected ${cssPath}`);
|
|
@@ -80,12 +91,14 @@ test('build inlines page-local CSS @import files', async (t) => {
|
|
|
80
91
|
assert.equal(
|
|
81
92
|
fssync.existsSync(path.join(workspace, 'build', 'frontend', 'pages', 'home', 'layout.css')),
|
|
82
93
|
true,
|
|
83
|
-
'expected imported layout.css copied for dev server'
|
|
94
|
+
'expected imported layout.css copied for dev server',
|
|
84
95
|
);
|
|
85
96
|
assert.equal(
|
|
86
|
-
fssync.existsSync(
|
|
97
|
+
fssync.existsSync(
|
|
98
|
+
path.join(workspace, 'build', 'frontend', 'pages', 'home', 'partials', 'colors.css'),
|
|
99
|
+
),
|
|
87
100
|
true,
|
|
88
|
-
'expected imported nested css copied for dev server'
|
|
101
|
+
'expected imported nested css copied for dev server',
|
|
89
102
|
);
|
|
90
103
|
|
|
91
104
|
const css = await fs.readFile(cssPath, 'utf8');
|
|
@@ -4,45 +4,48 @@ import assert from 'node:assert/strict';
|
|
|
4
4
|
import { emitDiagnostic, STRUCTURED_DIAGNOSTIC_PREFIX } from '../dist/core/index.js';
|
|
5
5
|
|
|
6
6
|
test('emitDiagnostic emits human-readable and structured output', () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const originalLog = console.log;
|
|
8
|
+
const originalWarn = console.warn;
|
|
9
|
+
const logs = [];
|
|
10
|
+
const warnings = [];
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
console.log = (message) => {
|
|
13
|
+
logs.push(message);
|
|
14
|
+
};
|
|
15
|
+
console.warn = (message) => {
|
|
16
|
+
warnings.push(message);
|
|
17
|
+
};
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
try {
|
|
20
|
+
emitDiagnostic({
|
|
21
|
+
code: 'frontend.test.warning',
|
|
22
|
+
kind: 'test',
|
|
23
|
+
stage: 'unit',
|
|
24
|
+
severity: 'warning',
|
|
25
|
+
message: 'Sample diagnostic for testing.',
|
|
26
|
+
data: { flag: true },
|
|
27
|
+
});
|
|
28
|
+
} finally {
|
|
29
|
+
console.log = originalLog;
|
|
30
|
+
console.warn = originalWarn;
|
|
31
|
+
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
assert.equal(warnings.length, 1);
|
|
34
|
+
assert.match(
|
|
35
|
+
warnings[0],
|
|
36
|
+
/\[webstir-frontend]\[frontend\.test\.warning\] Sample diagnostic for testing\./,
|
|
37
|
+
);
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
assert.equal(logs.length, 1);
|
|
40
|
+
const structuredLine = logs[0];
|
|
41
|
+
assert.ok(structuredLine.startsWith(STRUCTURED_DIAGNOSTIC_PREFIX));
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
const payload = JSON.parse(structuredLine.slice(STRUCTURED_DIAGNOSTIC_PREFIX.length));
|
|
44
|
+
assert.equal(payload.type, 'diagnostic');
|
|
45
|
+
assert.equal(payload.code, 'frontend.test.warning');
|
|
46
|
+
assert.equal(payload.kind, 'test');
|
|
47
|
+
assert.equal(payload.stage, 'unit');
|
|
48
|
+
assert.equal(payload.severity, 'warning');
|
|
49
|
+
assert.equal(payload.message, 'Sample diagnostic for testing.');
|
|
50
|
+
assert.deepEqual(payload.data, { flag: true });
|
|
48
51
|
});
|
package/tests/features.test.js
CHANGED
|
@@ -7,57 +7,62 @@ import os from 'node:os';
|
|
|
7
7
|
import { buildConfig } from '../dist/config/workspace.js';
|
|
8
8
|
|
|
9
9
|
async function createWorkspace(frontendConfig) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-'));
|
|
11
|
+
const workspaceRoot = path.join(tempRoot, 'workspace');
|
|
12
|
+
const frontendRoot = path.join(workspaceRoot, 'src', 'frontend');
|
|
13
|
+
await fs.mkdir(frontendRoot, { recursive: true });
|
|
14
|
+
|
|
15
|
+
if (frontendConfig !== undefined) {
|
|
16
|
+
const configPath = path.join(frontendRoot, 'frontend.config.json');
|
|
17
|
+
await fs.writeFile(configPath, JSON.stringify(frontendConfig, null, 2), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
workspaceRoot,
|
|
22
|
+
cleanup: () => fs.rm(tempRoot, { recursive: true, force: true }),
|
|
23
|
+
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
test('buildConfig returns defaults when frontend.config.json is absent', async (t) => {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const workspace = await createWorkspace();
|
|
28
|
+
t.after(workspace.cleanup);
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const config = buildConfig(workspace.workspaceRoot);
|
|
31
|
+
assert.equal(config.features.htmlSecurity, true);
|
|
32
|
+
assert.equal(config.features.externalResourceIntegrity, false);
|
|
33
|
+
assert.equal(config.features.imageOptimization, true);
|
|
34
|
+
assert.equal(config.features.precompression, true);
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
test('buildConfig applies overrides from nested features key', async (t) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
const workspace = await createWorkspace({
|
|
39
|
+
features: {
|
|
40
|
+
htmlSecurity: false,
|
|
41
|
+
externalResourceIntegrity: true,
|
|
42
|
+
precompression: false,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
t.after(workspace.cleanup);
|
|
46
|
+
|
|
47
|
+
const config = buildConfig(workspace.workspaceRoot);
|
|
48
|
+
assert.equal(config.features.htmlSecurity, false);
|
|
49
|
+
assert.equal(config.features.externalResourceIntegrity, true);
|
|
50
|
+
assert.equal(config.features.precompression, false);
|
|
51
|
+
assert.equal(config.features.imageOptimization, true);
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
test('buildConfig accepts top-level feature flags', async (t) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
const workspace = await createWorkspace({
|
|
56
|
+
htmlSecurity: false,
|
|
57
|
+
externalResourceIntegrity: true,
|
|
58
|
+
imageOptimization: false,
|
|
59
|
+
precompression: true,
|
|
60
|
+
});
|
|
61
|
+
t.after(workspace.cleanup);
|
|
62
|
+
|
|
63
|
+
const config = buildConfig(workspace.workspaceRoot);
|
|
64
|
+
assert.equal(config.features.htmlSecurity, false);
|
|
65
|
+
assert.equal(config.features.externalResourceIntegrity, true);
|
|
66
|
+
assert.equal(config.features.imageOptimization, false);
|
|
67
|
+
assert.equal(config.features.precompression, true);
|
|
63
68
|
});
|
package/tests/hooks.test.js
CHANGED
|
@@ -9,63 +9,79 @@ async function loadRunBuildOrSkip(t) {
|
|
|
9
9
|
const mod = await import('../dist/operations.js');
|
|
10
10
|
return mod.runBuild;
|
|
11
11
|
} catch (err) {
|
|
12
|
-
console.warn(
|
|
12
|
+
console.warn(
|
|
13
|
+
'[frontend-tests] Skipping hooks test: optional dependency unavailable:',
|
|
14
|
+
err?.message ?? err,
|
|
15
|
+
);
|
|
13
16
|
t?.diagnostic?.('skip: missing optional dependency');
|
|
14
17
|
return null;
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
async function createWorkspaceWithHooks() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-hooks-'));
|
|
23
|
+
const workspaceRoot = path.join(tempRoot, 'workspace');
|
|
24
|
+
const appDir = path.join(workspaceRoot, 'src', 'frontend', 'app');
|
|
25
|
+
const pageDir = path.join(workspaceRoot, 'src', 'frontend', 'pages', 'home');
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
28
|
+
await fs.mkdir(pageDir, { recursive: true });
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
await fs.writeFile(
|
|
31
|
+
path.join(appDir, 'app.html'),
|
|
32
|
+
'<!DOCTYPE html><html><head></head><body><main></main></body></html>',
|
|
33
|
+
'utf8',
|
|
34
|
+
);
|
|
35
|
+
await fs.writeFile(
|
|
36
|
+
path.join(pageDir, 'index.html'),
|
|
37
|
+
'<head></head><main><section>Home</section></main>',
|
|
38
|
+
'utf8',
|
|
39
|
+
);
|
|
40
|
+
await fs.writeFile(path.join(pageDir, 'index.ts'), 'console.log("home");', 'utf8');
|
|
41
|
+
await fs.writeFile(path.join(pageDir, 'index.css'), 'body { color: blue; }', 'utf8');
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
const packageJson = {
|
|
44
|
+
name: 'webstir-hooks-fixture',
|
|
45
|
+
version: '0.0.0',
|
|
46
|
+
private: true,
|
|
47
|
+
type: 'module',
|
|
48
|
+
};
|
|
49
|
+
await fs.writeFile(
|
|
50
|
+
path.join(workspaceRoot, 'package.json'),
|
|
51
|
+
JSON.stringify(packageJson, null, 2),
|
|
52
|
+
'utf8',
|
|
53
|
+
);
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
const hookConfig = `import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nasync function record(event, context) {\n const logPath = path.join(context.workspaceRoot, 'hook-log.json');\n const payload = JSON.stringify({ event, mode: context.mode, builder: context.builderName ?? null });\n await fs.appendFile(logPath, payload + '\\n', 'utf8');\n}\n\nexport default {\n hooks: {\n pipeline: {\n beforeAll: (context) => record('pipeline-before', context),\n afterAll: (context) => record('pipeline-after', context)\n },\n builders: {\n javascript: {\n before: (context) => record('javascript-before', context),\n after: (context) => record('javascript-after', context)\n }\n }\n }\n};\n`;
|
|
56
|
+
await fs.writeFile(path.join(workspaceRoot, 'webstir.config.js'), hookConfig, 'utf8');
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
return {
|
|
59
|
+
workspaceRoot,
|
|
60
|
+
cleanup: () => fs.rm(tempRoot, { recursive: true, force: true }),
|
|
61
|
+
};
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
test('pipeline hooks execute in order', async (t) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
const runBuild = await loadRunBuildOrSkip(t);
|
|
66
|
+
if (!runBuild) return; // skip
|
|
67
|
+
const workspace = await createWorkspaceWithHooks();
|
|
68
|
+
t.after(workspace.cleanup);
|
|
54
69
|
|
|
55
|
-
|
|
70
|
+
await runBuild({ workspaceRoot: workspace.workspaceRoot });
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
const logPath = path.join(workspace.workspaceRoot, 'hook-log.json');
|
|
73
|
+
const raw = await fs.readFile(logPath, 'utf8');
|
|
74
|
+
const entries = raw
|
|
75
|
+
.trim()
|
|
76
|
+
.split('\n')
|
|
77
|
+
.map((line) => JSON.parse(line));
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
assert.equal(entries[1].builder, 'javascript');
|
|
70
|
-
assert.equal(entries[2].builder, 'javascript');
|
|
79
|
+
assert.equal(entries.length, 4);
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
entries.map((entry) => entry.event),
|
|
82
|
+
['pipeline-before', 'javascript-before', 'javascript-after', 'pipeline-after'],
|
|
83
|
+
);
|
|
84
|
+
assert(entries.every((entry) => entry.mode === 'build'));
|
|
85
|
+
assert.equal(entries[1].builder, 'javascript');
|
|
86
|
+
assert.equal(entries[2].builder, 'javascript');
|
|
71
87
|
});
|