@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
@@ -0,0 +1,66 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { load } from 'cheerio';
4
+
5
+ import { addSubresourceIntegrity } from '../dist/html/htmlSecurity.js';
6
+
7
+ test('addSubresourceIntegrity skips external fetches by default', async () => {
8
+ const document = load(`
9
+ <html>
10
+ <head>
11
+ <script src="https://cdn.example.com/app.js"></script>
12
+ <link rel="stylesheet" href="https://cdn.example.com/app.css" />
13
+ </head>
14
+ </html>
15
+ `);
16
+
17
+ let calls = 0;
18
+ const result = await addSubresourceIntegrity(document, {
19
+ fetcher: async () => {
20
+ calls += 1;
21
+ throw new Error('fetch should not be called when external fetch is disabled');
22
+ },
23
+ });
24
+
25
+ assert.equal(calls, 0);
26
+ assert.deepEqual(result.failures, []);
27
+ assert.deepEqual(result.skippedExternalResources, [
28
+ 'https://cdn.example.com/app.js',
29
+ 'https://cdn.example.com/app.css',
30
+ ]);
31
+ assert.equal(document('script').attr('integrity'), undefined);
32
+ assert.equal(document('link').attr('integrity'), undefined);
33
+ });
34
+
35
+ test('addSubresourceIntegrity can opt in to fetching external resources', async () => {
36
+ const document = load(`
37
+ <html>
38
+ <head>
39
+ <script src="https://cdn.example.com/app.js"></script>
40
+ <link rel="stylesheet" href="https://cdn.example.com/app.css" />
41
+ </head>
42
+ </html>
43
+ `);
44
+
45
+ let calls = 0;
46
+ const result = await addSubresourceIntegrity(document, {
47
+ allowExternalFetch: true,
48
+ fetcher: async () => {
49
+ calls += 1;
50
+ return {
51
+ ok: true,
52
+ async arrayBuffer() {
53
+ return Buffer.from('console.log("webstir");');
54
+ },
55
+ };
56
+ },
57
+ });
58
+
59
+ assert.equal(calls, 2);
60
+ assert.deepEqual(result.failures, []);
61
+ assert.deepEqual(result.skippedExternalResources, []);
62
+ assert.match(String(document('script').attr('integrity')), /^sha384-/);
63
+ assert.match(String(document('link').attr('integrity')), /^sha384-/);
64
+ assert.equal(document('script').attr('crossorigin'), 'anonymous');
65
+ assert.equal(document('link').attr('crossorigin'), 'anonymous');
66
+ });
@@ -0,0 +1,148 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import fssync from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+
8
+ import { inspectFrontendWorkspace } from '../dist/index.js';
9
+
10
+ async function createWorkspace({ frontendConfig, pkg, withFrontend = true } = {}) {
11
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-inspect-'));
12
+ const frontendRoot = path.join(root, 'src', 'frontend');
13
+
14
+ if (withFrontend) {
15
+ await fs.mkdir(frontendRoot, { recursive: true });
16
+ }
17
+
18
+ await fs.writeFile(
19
+ path.join(root, 'package.json'),
20
+ JSON.stringify(
21
+ pkg ?? {
22
+ name: 'inspect-workspace',
23
+ version: '1.0.0',
24
+ },
25
+ null,
26
+ 2,
27
+ ),
28
+ 'utf8',
29
+ );
30
+
31
+ if (frontendConfig !== undefined) {
32
+ await fs.writeFile(
33
+ path.join(frontendRoot, 'frontend.config.json'),
34
+ JSON.stringify(frontendConfig, null, 2),
35
+ 'utf8',
36
+ );
37
+ }
38
+
39
+ return {
40
+ root,
41
+ cleanup: () => fs.rm(root, { recursive: true, force: true }),
42
+ };
43
+ }
44
+
45
+ test('inspectFrontendWorkspace resolves shallow workspace facts without building', async (t) => {
46
+ const workspace = await createWorkspace({
47
+ pkg: {
48
+ name: 'inspect-workspace',
49
+ version: '1.0.0',
50
+ webstir: {
51
+ enable: {
52
+ clientNav: true,
53
+ contentNav: true,
54
+ unknownFlag: true,
55
+ },
56
+ },
57
+ },
58
+ frontendConfig: {
59
+ paths: {
60
+ contentRoot: 'docs/content',
61
+ },
62
+ },
63
+ });
64
+
65
+ t.after(workspace.cleanup);
66
+
67
+ const appRoot = path.join(workspace.root, 'src', 'frontend', 'app');
68
+ const pageRoot = path.join(workspace.root, 'src', 'frontend', 'pages', 'home');
69
+ const contentRoot = path.join(workspace.root, 'docs', 'content');
70
+
71
+ await fs.mkdir(appRoot, { recursive: true });
72
+ await fs.mkdir(pageRoot, { recursive: true });
73
+ await fs.mkdir(contentRoot, { recursive: true });
74
+
75
+ await fs.writeFile(path.join(appRoot, 'app.html'), '<!doctype html><html></html>', 'utf8');
76
+ await fs.writeFile(path.join(appRoot, 'app.css'), 'body{}', 'utf8');
77
+ await fs.writeFile(path.join(appRoot, 'app.ts'), 'export {};\n', 'utf8');
78
+ await fs.writeFile(path.join(pageRoot, 'index.html'), '<head></head><main></main>', 'utf8');
79
+ await fs.writeFile(path.join(pageRoot, 'index.css'), 'main{}', 'utf8');
80
+ await fs.writeFile(path.join(pageRoot, 'index.ts'), 'export {};\n', 'utf8');
81
+ await fs.writeFile(path.join(contentRoot, '_sidebar.json'), '{}', 'utf8');
82
+
83
+ const result = await inspectFrontendWorkspace(workspace.root);
84
+
85
+ assert.equal(result.config.paths.src.content, contentRoot);
86
+ assert.equal(result.packageJson.exists, true);
87
+ assert.deepEqual(result.packageJson.enable.raw, {
88
+ clientNav: true,
89
+ contentNav: true,
90
+ unknownFlag: true,
91
+ });
92
+ assert.deepEqual(result.packageJson.enable.known, {
93
+ spa: false,
94
+ clientNav: true,
95
+ backend: false,
96
+ search: false,
97
+ contentNav: true,
98
+ });
99
+
100
+ assert.equal(result.appShell.exists, true);
101
+ assert.equal(result.appShell.templateExists, true);
102
+ assert.equal(result.appShell.stylesheetExists, true);
103
+ assert.equal(result.appShell.scriptExists, true);
104
+
105
+ assert.deepEqual(
106
+ result.pages.map((page) => ({
107
+ name: page.name,
108
+ htmlExists: page.htmlExists,
109
+ stylesheetExists: page.stylesheetExists,
110
+ scriptExists: page.scriptExists,
111
+ })),
112
+ [
113
+ {
114
+ name: 'home',
115
+ htmlExists: true,
116
+ stylesheetExists: true,
117
+ scriptExists: true,
118
+ },
119
+ ],
120
+ );
121
+
122
+ assert.equal(result.content.exists, true);
123
+ assert.equal(result.content.sidebarOverrideExists, true);
124
+
125
+ assert.equal(fssync.existsSync(path.join(workspace.root, 'build')), false);
126
+ assert.equal(fssync.existsSync(path.join(workspace.root, '.webstir')), false);
127
+ });
128
+
129
+ test('inspectFrontendWorkspace reports absent frontend facts cleanly', async (t) => {
130
+ const workspace = await createWorkspace();
131
+ t.after(workspace.cleanup);
132
+
133
+ const result = await inspectFrontendWorkspace(workspace.root);
134
+
135
+ assert.equal(result.packageJson.exists, true);
136
+ assert.equal(result.packageJson.enable.raw, undefined);
137
+ assert.deepEqual(result.packageJson.enable.known, {
138
+ spa: false,
139
+ clientNav: false,
140
+ backend: false,
141
+ search: false,
142
+ contentNav: false,
143
+ });
144
+ assert.equal(result.appShell.exists, false);
145
+ assert.equal(result.pages.length, 0);
146
+ assert.equal(result.content.exists, false);
147
+ assert.equal(result.content.sidebarOverrideExists, false);
148
+ });
@@ -11,7 +11,10 @@ async function loadProviderOrSkip(t) {
11
11
  return mod.frontendProvider;
12
12
  } catch (err) {
13
13
  // Likely due to optional native deps like sharp; skip gracefully.
14
- console.warn('[frontend-tests] Skipping provider integration: optional dependency unavailable:', err?.message ?? err);
14
+ console.warn(
15
+ '[frontend-tests] Skipping provider integration: optional dependency unavailable:',
16
+ err?.message ?? err,
17
+ );
15
18
  t?.diagnostic?.('skip: missing optional dependency');
16
19
  return null;
17
20
  }
@@ -25,8 +28,16 @@ async function createWorkspace() {
25
28
  await fs.mkdir(pageDir, { recursive: true });
26
29
 
27
30
  // Minimal app template and page fragment
28
- await fs.writeFile(path.join(appDir, 'app.html'), '<!DOCTYPE html><html><head><title>App</title></head><body><main></main></body></html>', 'utf8');
29
- await fs.writeFile(path.join(pageDir, 'index.html'), '<head></head><main><section>Home</section></main>', 'utf8');
31
+ await fs.writeFile(
32
+ path.join(appDir, 'app.html'),
33
+ '<!DOCTYPE html><html><head><title>App</title></head><body><main></main></body></html>',
34
+ 'utf8',
35
+ );
36
+ await fs.writeFile(
37
+ path.join(pageDir, 'index.html'),
38
+ '<head></head><main><section>Home</section></main>',
39
+ 'utf8',
40
+ );
30
41
  await fs.writeFile(path.join(pageDir, 'index.ts'), 'console.log("home");', 'utf8');
31
42
 
32
43
  return root;
@@ -47,16 +58,28 @@ async function createWorkspaceWithClientNav() {
47
58
  webstir: {
48
59
  mode: 'ssg',
49
60
  enable: {
50
- clientNav: true
51
- }
52
- }
61
+ clientNav: true,
62
+ },
63
+ },
53
64
  };
54
65
  await fs.writeFile(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');
55
66
 
56
- await fs.writeFile(path.join(appDir, 'app.html'), '<!DOCTYPE html><html><head><title>App</title></head><body><main></main></body></html>', 'utf8');
57
- await fs.writeFile(path.join(appDir, 'app.ts'), 'import "./scripts/features/client-nav.js";', 'utf8');
67
+ await fs.writeFile(
68
+ path.join(appDir, 'app.html'),
69
+ '<!DOCTYPE html><html><head><title>App</title></head><body><main></main></body></html>',
70
+ 'utf8',
71
+ );
72
+ await fs.writeFile(
73
+ path.join(appDir, 'app.ts'),
74
+ 'import "./scripts/features/client-nav.js";',
75
+ 'utf8',
76
+ );
58
77
  await fs.writeFile(path.join(featureDir, 'client-nav.ts'), 'export {};', 'utf8');
59
- await fs.writeFile(path.join(pageDir, 'index.html'), '<head></head><main><section>Home</section></main>', 'utf8');
78
+ await fs.writeFile(
79
+ path.join(pageDir, 'index.html'),
80
+ '<head></head><main><section>Home</section></main>',
81
+ 'utf8',
82
+ );
60
83
 
61
84
  return root;
62
85
  }
@@ -85,8 +108,16 @@ test('frontend provider publish produces dist assets and preserves entry in mani
85
108
  const workspace = await createWorkspace();
86
109
 
87
110
  // Run build first so manifest has entries from build/frontend
88
- await frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false });
89
- const publishResult = await frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'publish' }, incremental: false });
111
+ await frontendProvider.build({
112
+ workspaceRoot: workspace,
113
+ env: { WEBSTIR_MODULE_MODE: 'build' },
114
+ incremental: false,
115
+ });
116
+ const publishResult = await frontendProvider.build({
117
+ workspaceRoot: workspace,
118
+ env: { WEBSTIR_MODULE_MODE: 'publish' },
119
+ incremental: false,
120
+ });
90
121
 
91
122
  // Dist should contain a hashed JS file
92
123
  const distPageDir = path.join(workspace, 'dist', 'frontend', 'pages', 'home');
@@ -102,13 +133,28 @@ test('enable.clientNav uses feature module (no legacy helper injection)', async
102
133
  if (!frontendProvider) return; // skip
103
134
  const workspace = await createWorkspaceWithClientNav();
104
135
 
105
- await frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false });
106
- await frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'publish' }, incremental: false });
136
+ await frontendProvider.build({
137
+ workspaceRoot: workspace,
138
+ env: { WEBSTIR_MODULE_MODE: 'build' },
139
+ incremental: false,
140
+ });
141
+ await frontendProvider.build({
142
+ workspaceRoot: workspace,
143
+ env: { WEBSTIR_MODULE_MODE: 'publish' },
144
+ incremental: false,
145
+ });
107
146
 
108
147
  const distClientNav = path.join(workspace, 'dist', 'frontend', 'clientNav.js');
109
- assert.equal(fssync.existsSync(distClientNav), false, 'did not expect dist/frontend/clientNav.js');
148
+ assert.equal(
149
+ fssync.existsSync(distClientNav),
150
+ false,
151
+ 'did not expect dist/frontend/clientNav.js',
152
+ );
110
153
 
111
- const distHtml = await fs.readFile(path.join(workspace, 'dist', 'frontend', 'index.html'), 'utf8');
154
+ const distHtml = await fs.readFile(
155
+ path.join(workspace, 'dist', 'frontend', 'index.html'),
156
+ 'utf8',
157
+ );
112
158
  assert.ok(!distHtml.includes('clientNav.js'), 'did not expect client-nav script injected');
113
159
  assert.ok(!distHtml.includes('index.js'), 'should not inject page index.js when none exists');
114
160
  });
@@ -124,14 +170,19 @@ test('enable.clientNav without feature module fails fast', async (t) => {
124
170
  webstir: {
125
171
  mode: 'ssg',
126
172
  enable: {
127
- clientNav: true
128
- }
129
- }
173
+ clientNav: true,
174
+ },
175
+ },
130
176
  };
131
177
  await fs.writeFile(path.join(workspace, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');
132
178
 
133
179
  await assert.rejects(
134
- () => frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false }),
135
- /Enabled feature module\(s\) missing: client-nav/
180
+ () =>
181
+ frontendProvider.build({
182
+ workspaceRoot: workspace,
183
+ env: { WEBSTIR_MODULE_MODE: 'build' },
184
+ incremental: false,
185
+ }),
186
+ /Enabled feature module\(s\) missing: client-nav/,
136
187
  );
137
188
  });