@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.
Files changed (138) hide show
  1. package/README.md +124 -60
  2. package/dist/assets/imageOptimizer.js +10 -15
  3. package/dist/assets/precompression.js +1 -1
  4. package/dist/builders/contentBuilder.js +102 -90
  5. package/dist/builders/cssBuilder.js +25 -19
  6. package/dist/builders/htmlBuilder.js +57 -42
  7. package/dist/builders/index.js +1 -1
  8. package/dist/builders/jsBuilder.js +219 -76
  9. package/dist/builders/staticAssetsBuilder.js +27 -9
  10. package/dist/builders/types.d.ts +1 -0
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +6 -30
  13. package/dist/config/manifest.js +7 -6
  14. package/dist/config/paths.js +2 -2
  15. package/dist/config/schema.d.ts +8 -0
  16. package/dist/config/schema.js +7 -6
  17. package/dist/config/setup.js +1 -1
  18. package/dist/config/workspace.js +11 -9
  19. package/dist/core/constants.d.ts +1 -1
  20. package/dist/core/constants.js +5 -5
  21. package/dist/core/diagnostics.js +1 -1
  22. package/dist/core/pages.js +4 -4
  23. package/dist/hooks.js +3 -3
  24. package/dist/html/criticalCss.js +6 -3
  25. package/dist/html/htmlSecurity.d.ts +6 -1
  26. package/dist/html/htmlSecurity.js +28 -14
  27. package/dist/html/lazyLoad.js +1 -1
  28. package/dist/html/pageScaffold.js +1 -1
  29. package/dist/html/resourceHints.js +5 -2
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +2 -0
  32. package/dist/inspect.d.ts +2 -0
  33. package/dist/inspect.js +110 -0
  34. package/dist/modes/ssg/metadata.js +4 -4
  35. package/dist/modes/ssg/routing.js +2 -5
  36. package/dist/modes/ssg/seo.js +5 -5
  37. package/dist/modes/ssg/views.js +17 -11
  38. package/dist/operations.js +18 -10
  39. package/dist/pipeline.d.ts +1 -0
  40. package/dist/pipeline.js +6 -1
  41. package/dist/provider.js +28 -24
  42. package/dist/runtime/boundary.d.ts +28 -0
  43. package/dist/runtime/boundary.js +247 -0
  44. package/dist/runtime/index.d.ts +1 -0
  45. package/dist/runtime/index.js +1 -0
  46. package/dist/types.d.ts +52 -0
  47. package/dist/utils/fs.d.ts +11 -10
  48. package/dist/utils/fs.js +48 -20
  49. package/dist/utils/glob.d.ts +8 -0
  50. package/dist/utils/glob.js +21 -0
  51. package/dist/utils/hash.js +1 -2
  52. package/dist/utils/pagePaths.js +2 -2
  53. package/package.json +19 -14
  54. package/scripts/publish.sh +2 -94
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/assets/assetManifest.ts +39 -29
  57. package/src/assets/imageOptimizer.ts +91 -82
  58. package/src/assets/precompression.ts +22 -16
  59. package/src/builders/contentBuilder.ts +1224 -1149
  60. package/src/builders/cssBuilder.ts +466 -417
  61. package/src/builders/htmlBuilder.ts +511 -448
  62. package/src/builders/index.ts +7 -7
  63. package/src/builders/jsBuilder.ts +538 -280
  64. package/src/builders/staticAssetsBuilder.ts +166 -135
  65. package/src/builders/types.ts +7 -6
  66. package/src/cli.ts +66 -90
  67. package/src/config/manifest.ts +16 -14
  68. package/src/config/paths.ts +5 -5
  69. package/src/config/schema.ts +38 -37
  70. package/src/config/setup.ts +7 -7
  71. package/src/config/workspace.ts +118 -116
  72. package/src/config/workspaceManifest.ts +14 -14
  73. package/src/core/constants.ts +62 -62
  74. package/src/core/diagnostics.ts +26 -26
  75. package/src/core/pages.ts +19 -19
  76. package/src/hooks.ts +128 -118
  77. package/src/html/criticalCss.ts +84 -77
  78. package/src/html/htmlSecurity.ts +107 -66
  79. package/src/html/lazyLoad.ts +22 -19
  80. package/src/html/pageScaffold.ts +37 -28
  81. package/src/html/resourceHints.ts +83 -74
  82. package/src/index.ts +2 -0
  83. package/src/inspect.ts +158 -0
  84. package/src/modes/ssg/metadata.ts +53 -51
  85. package/src/modes/ssg/routing.ts +177 -177
  86. package/src/modes/ssg/seo.ts +208 -200
  87. package/src/modes/ssg/validation.ts +31 -25
  88. package/src/modes/ssg/views.ts +257 -238
  89. package/src/operations.ts +105 -95
  90. package/src/pipeline.ts +81 -69
  91. package/src/provider.ts +184 -176
  92. package/src/runtime/boundary.ts +325 -0
  93. package/src/runtime/index.ts +1 -0
  94. package/src/types.ts +107 -48
  95. package/src/utils/changedFile.ts +22 -22
  96. package/src/utils/fs.ts +73 -26
  97. package/src/utils/glob.ts +38 -0
  98. package/src/utils/hash.ts +2 -4
  99. package/src/utils/pagePaths.ts +35 -23
  100. package/src/utils/pathMatch.ts +26 -23
  101. package/tests/add-page-defaults.test.js +44 -39
  102. package/tests/bundlerParity.test.js +252 -0
  103. package/tests/cli.contract.test.js +13 -0
  104. package/tests/content-pages.test.js +108 -13
  105. package/tests/css-app-imports.test.js +22 -11
  106. package/tests/css-page-imports.test.js +26 -13
  107. package/tests/diagnostics.test.js +39 -36
  108. package/tests/features.test.js +48 -43
  109. package/tests/hooks.test.js +58 -42
  110. package/tests/htmlSecurity.test.js +66 -0
  111. package/tests/inspect.test.js +148 -0
  112. package/tests/provider.integration.test.js +71 -20
  113. package/tests/runtime.test.js +493 -0
  114. package/tests/ssg-defaults.test.js +284 -177
  115. package/tests/ssg-guardrails.test.js +51 -51
  116. package/tsconfig.json +3 -10
  117. package/dist/watch/frontendFiles.d.ts +0 -3
  118. package/dist/watch/frontendFiles.js +0 -25
  119. package/dist/watch/hotUpdateTracker.d.ts +0 -51
  120. package/dist/watch/hotUpdateTracker.js +0 -205
  121. package/dist/watch/pipelineHelpers.d.ts +0 -26
  122. package/dist/watch/pipelineHelpers.js +0 -177
  123. package/dist/watch/types.d.ts +0 -27
  124. package/dist/watch/types.js +0 -1
  125. package/dist/watch/watchCoordinator.d.ts +0 -36
  126. package/dist/watch/watchCoordinator.js +0 -551
  127. package/dist/watch/watchDaemon.d.ts +0 -17
  128. package/dist/watch/watchDaemon.js +0 -127
  129. package/dist/watch/watchReporter.d.ts +0 -21
  130. package/dist/watch/watchReporter.js +0 -64
  131. package/scripts/smoke.mjs +0 -35
  132. package/src/watch/frontendFiles.ts +0 -32
  133. package/src/watch/hotUpdateTracker.ts +0 -285
  134. package/src/watch/pipelineHelpers.ts +0 -242
  135. package/src/watch/types.ts +0 -23
  136. package/src/watch/watchCoordinator.ts +0 -666
  137. package/src/watch/watchDaemon.ts +0 -144
  138. 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 loadProviderOrSkip(t) {
8
+ async function loadFrontendModuleOrSkip(t) {
9
9
  try {
10
- const mod = await import('../dist/index.js');
11
- return mod.frontendProvider;
10
+ return await import('../dist/index.js');
12
11
  } catch (err) {
13
- console.warn('[frontend-tests] Skipping provider integration: optional dependency unavailable:', err?.message ?? err);
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(path.join(pageDir, 'index.html'), '<head></head><main><section>Home</section></main>', 'utf8');
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 frontendProvider = await loadProviderOrSkip(t);
57
- if (!frontendProvider) return;
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({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false });
100
+ await frontendProvider.build({
101
+ workspaceRoot: workspace,
102
+ env: { WEBSTIR_MODULE_MODE: 'build' },
103
+ incremental: false,
104
+ });
62
105
 
63
- const htmlPath = path.join(workspace, 'build', 'frontend', 'pages', 'docs', 'readme', 'index.html');
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(nav.some((entry) => entry.path === '/docs/readme/'), 'expected docs-nav.json to include /docs/readme/');
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('[frontend-tests] Skipping provider integration: optional dependency unavailable:', err?.message ?? err);
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
- '@layer reset, base;',
36
- '@import "./styles/base.css";'
37
- ].join('\n'),
38
- 'utf8'
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({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false });
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('[frontend-tests] Skipping provider integration: optional dependency unavailable:', err?.message ?? err);
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(path.join(pageDir, 'index.html'), '<head></head><main><section>Home</section></main>', 'utf8');
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({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false });
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(path.join(workspace, 'build', 'frontend', 'pages', 'home', 'partials', 'colors.css')),
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
- const originalLog = console.log;
8
- const originalWarn = console.warn;
9
- const logs = [];
10
- const warnings = [];
7
+ const originalLog = console.log;
8
+ const originalWarn = console.warn;
9
+ const logs = [];
10
+ const warnings = [];
11
11
 
12
- console.log = (message) => {
13
- logs.push(message);
14
- };
15
- console.warn = (message) => {
16
- warnings.push(message);
17
- };
12
+ console.log = (message) => {
13
+ logs.push(message);
14
+ };
15
+ console.warn = (message) => {
16
+ warnings.push(message);
17
+ };
18
18
 
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
- }
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
- assert.equal(warnings.length, 1);
34
- assert.match(warnings[0], /\[webstir-frontend]\[frontend\.test\.warning\] Sample diagnostic for testing\./);
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
- assert.equal(logs.length, 1);
37
- const structuredLine = logs[0];
38
- assert.ok(structuredLine.startsWith(STRUCTURED_DIAGNOSTIC_PREFIX));
39
+ assert.equal(logs.length, 1);
40
+ const structuredLine = logs[0];
41
+ assert.ok(structuredLine.startsWith(STRUCTURED_DIAGNOSTIC_PREFIX));
39
42
 
40
- const payload = JSON.parse(structuredLine.slice(STRUCTURED_DIAGNOSTIC_PREFIX.length));
41
- assert.equal(payload.type, 'diagnostic');
42
- assert.equal(payload.code, 'frontend.test.warning');
43
- assert.equal(payload.kind, 'test');
44
- assert.equal(payload.stage, 'unit');
45
- assert.equal(payload.severity, 'warning');
46
- assert.equal(payload.message, 'Sample diagnostic for testing.');
47
- assert.deepEqual(payload.data, { flag: true });
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
  });
@@ -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
- 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
- };
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
- const workspace = await createWorkspace();
28
- t.after(workspace.cleanup);
27
+ const workspace = await createWorkspace();
28
+ t.after(workspace.cleanup);
29
29
 
30
- const config = buildConfig(workspace.workspaceRoot);
31
- assert.equal(config.features.htmlSecurity, true);
32
- assert.equal(config.features.imageOptimization, true);
33
- assert.equal(config.features.precompression, true);
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
- const workspace = await createWorkspace({
38
- features: {
39
- htmlSecurity: false,
40
- precompression: false
41
- }
42
- });
43
- t.after(workspace.cleanup);
44
-
45
- const config = buildConfig(workspace.workspaceRoot);
46
- assert.equal(config.features.htmlSecurity, false);
47
- assert.equal(config.features.precompression, false);
48
- assert.equal(config.features.imageOptimization, true);
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
- const workspace = await createWorkspace({
53
- htmlSecurity: false,
54
- imageOptimization: false,
55
- precompression: true
56
- });
57
- t.after(workspace.cleanup);
58
-
59
- const config = buildConfig(workspace.workspaceRoot);
60
- assert.equal(config.features.htmlSecurity, false);
61
- assert.equal(config.features.imageOptimization, false);
62
- assert.equal(config.features.precompression, true);
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
  });
@@ -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('[frontend-tests] Skipping hooks test: optional dependency unavailable:', err?.message ?? err);
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
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-hooks-'));
20
- const workspaceRoot = path.join(tempRoot, 'workspace');
21
- const appDir = path.join(workspaceRoot, 'src', 'frontend', 'app');
22
- const pageDir = path.join(workspaceRoot, 'src', 'frontend', 'pages', 'home');
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
- await fs.mkdir(appDir, { recursive: true });
25
- await fs.mkdir(pageDir, { recursive: true });
27
+ await fs.mkdir(appDir, { recursive: true });
28
+ await fs.mkdir(pageDir, { recursive: true });
26
29
 
27
- await fs.writeFile(path.join(appDir, 'app.html'), '<!DOCTYPE html><html><head></head><body><main></main></body></html>', 'utf8');
28
- await fs.writeFile(path.join(pageDir, 'index.html'), '<head></head><main><section>Home</section></main>', 'utf8');
29
- await fs.writeFile(path.join(pageDir, 'index.ts'), 'console.log("home");', 'utf8');
30
- await fs.writeFile(path.join(pageDir, 'index.css'), 'body { color: blue; }', 'utf8');
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
- const packageJson = {
33
- name: 'webstir-hooks-fixture',
34
- version: '0.0.0',
35
- private: true,
36
- type: 'module'
37
- };
38
- await fs.writeFile(path.join(workspaceRoot, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf8');
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
- 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`;
41
- await fs.writeFile(path.join(workspaceRoot, 'webstir.config.js'), hookConfig, 'utf8');
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
- return {
44
- workspaceRoot,
45
- cleanup: () => fs.rm(tempRoot, { recursive: true, force: true })
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
- const runBuild = await loadRunBuildOrSkip(t);
51
- if (!runBuild) return; // skip
52
- const workspace = await createWorkspaceWithHooks();
53
- t.after(workspace.cleanup);
65
+ const runBuild = await loadRunBuildOrSkip(t);
66
+ if (!runBuild) return; // skip
67
+ const workspace = await createWorkspaceWithHooks();
68
+ t.after(workspace.cleanup);
54
69
 
55
- await runBuild({ workspaceRoot: workspace.workspaceRoot });
70
+ await runBuild({ workspaceRoot: workspace.workspaceRoot });
56
71
 
57
- const logPath = path.join(workspace.workspaceRoot, 'hook-log.json');
58
- const raw = await fs.readFile(logPath, 'utf8');
59
- const entries = raw.trim().split('\n').map((line) => JSON.parse(line));
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
- assert.equal(entries.length, 4);
62
- assert.deepEqual(entries.map((entry) => entry.event), [
63
- 'pipeline-before',
64
- 'javascript-before',
65
- 'javascript-after',
66
- 'pipeline-after'
67
- ]);
68
- assert(entries.every((entry) => entry.mode === 'build'));
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
  });