@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.
Files changed (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/assets/assetManifest.d.ts +16 -0
  4. package/dist/assets/assetManifest.js +31 -0
  5. package/dist/assets/imageOptimizer.d.ts +6 -0
  6. package/dist/assets/imageOptimizer.js +93 -0
  7. package/dist/assets/precompression.d.ts +1 -0
  8. package/dist/assets/precompression.js +21 -0
  9. package/dist/builders/contentBuilder.d.ts +2 -0
  10. package/dist/builders/contentBuilder.js +1052 -0
  11. package/dist/builders/cssBuilder.d.ts +2 -0
  12. package/dist/builders/cssBuilder.js +439 -0
  13. package/dist/builders/htmlBuilder.d.ts +2 -0
  14. package/dist/builders/htmlBuilder.js +430 -0
  15. package/dist/builders/index.d.ts +2 -0
  16. package/dist/builders/index.js +14 -0
  17. package/dist/builders/jsBuilder.d.ts +2 -0
  18. package/dist/builders/jsBuilder.js +300 -0
  19. package/dist/builders/staticAssetsBuilder.d.ts +2 -0
  20. package/dist/builders/staticAssetsBuilder.js +158 -0
  21. package/dist/builders/types.d.ts +12 -0
  22. package/dist/builders/types.js +1 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +105 -0
  25. package/dist/config/manifest.d.ts +7 -0
  26. package/dist/config/manifest.js +17 -0
  27. package/dist/config/paths.d.ts +3 -0
  28. package/dist/config/paths.js +11 -0
  29. package/dist/config/schema.d.ts +413 -0
  30. package/dist/config/schema.js +44 -0
  31. package/dist/config/setup.d.ts +2 -0
  32. package/dist/config/setup.js +12 -0
  33. package/dist/config/workspace.d.ts +2 -0
  34. package/dist/config/workspace.js +131 -0
  35. package/dist/config/workspaceManifest.d.ts +23 -0
  36. package/dist/config/workspaceManifest.js +1 -0
  37. package/dist/core/constants.d.ts +70 -0
  38. package/dist/core/constants.js +70 -0
  39. package/dist/core/diagnostics.d.ts +15 -0
  40. package/dist/core/diagnostics.js +21 -0
  41. package/dist/core/index.d.ts +3 -0
  42. package/dist/core/index.js +3 -0
  43. package/dist/core/pages.d.ts +6 -0
  44. package/dist/core/pages.js +23 -0
  45. package/dist/hooks.d.ts +19 -0
  46. package/dist/hooks.js +115 -0
  47. package/dist/html/criticalCss.d.ts +4 -0
  48. package/dist/html/criticalCss.js +192 -0
  49. package/dist/html/htmlSecurity.d.ts +5 -0
  50. package/dist/html/htmlSecurity.js +73 -0
  51. package/dist/html/lazyLoad.d.ts +6 -0
  52. package/dist/html/lazyLoad.js +21 -0
  53. package/dist/html/pageScaffold.d.ts +10 -0
  54. package/dist/html/pageScaffold.js +51 -0
  55. package/dist/html/resourceHints.d.ts +7 -0
  56. package/dist/html/resourceHints.js +64 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +5 -0
  59. package/dist/modes/ssg/index.d.ts +4 -0
  60. package/dist/modes/ssg/index.js +4 -0
  61. package/dist/modes/ssg/metadata.d.ts +5 -0
  62. package/dist/modes/ssg/metadata.js +50 -0
  63. package/dist/modes/ssg/routing.d.ts +2 -0
  64. package/dist/modes/ssg/routing.js +186 -0
  65. package/dist/modes/ssg/seo.d.ts +4 -0
  66. package/dist/modes/ssg/seo.js +208 -0
  67. package/dist/modes/ssg/validation.d.ts +3 -0
  68. package/dist/modes/ssg/validation.js +27 -0
  69. package/dist/modes/ssg/views.d.ts +2 -0
  70. package/dist/modes/ssg/views.js +236 -0
  71. package/dist/operations.d.ts +5 -0
  72. package/dist/operations.js +102 -0
  73. package/dist/pipeline.d.ts +7 -0
  74. package/dist/pipeline.js +71 -0
  75. package/dist/provider.d.ts +2 -0
  76. package/dist/provider.js +176 -0
  77. package/dist/types.d.ts +61 -0
  78. package/dist/types.js +1 -0
  79. package/dist/utils/changedFile.d.ts +8 -0
  80. package/dist/utils/changedFile.js +26 -0
  81. package/dist/utils/fs.d.ts +11 -0
  82. package/dist/utils/fs.js +39 -0
  83. package/dist/utils/hash.d.ts +1 -0
  84. package/dist/utils/hash.js +5 -0
  85. package/dist/utils/pagePaths.d.ts +5 -0
  86. package/dist/utils/pagePaths.js +36 -0
  87. package/dist/utils/pathMatch.d.ts +3 -0
  88. package/dist/utils/pathMatch.js +29 -0
  89. package/dist/watch/frontendFiles.d.ts +3 -0
  90. package/dist/watch/frontendFiles.js +25 -0
  91. package/dist/watch/hotUpdateTracker.d.ts +51 -0
  92. package/dist/watch/hotUpdateTracker.js +205 -0
  93. package/dist/watch/pipelineHelpers.d.ts +26 -0
  94. package/dist/watch/pipelineHelpers.js +177 -0
  95. package/dist/watch/types.d.ts +27 -0
  96. package/dist/watch/types.js +1 -0
  97. package/dist/watch/watchCoordinator.d.ts +36 -0
  98. package/dist/watch/watchCoordinator.js +551 -0
  99. package/dist/watch/watchDaemon.d.ts +17 -0
  100. package/dist/watch/watchDaemon.js +127 -0
  101. package/dist/watch/watchReporter.d.ts +21 -0
  102. package/dist/watch/watchReporter.js +64 -0
  103. package/package.json +92 -0
  104. package/scripts/publish.sh +101 -0
  105. package/scripts/smoke.mjs +35 -0
  106. package/scripts/update-contract.sh +121 -0
  107. package/src/assets/assetManifest.ts +51 -0
  108. package/src/assets/imageOptimizer.ts +112 -0
  109. package/src/assets/precompression.ts +25 -0
  110. package/src/builders/contentBuilder.ts +1400 -0
  111. package/src/builders/cssBuilder.ts +552 -0
  112. package/src/builders/htmlBuilder.ts +540 -0
  113. package/src/builders/index.ts +16 -0
  114. package/src/builders/jsBuilder.ts +358 -0
  115. package/src/builders/staticAssetsBuilder.ts +174 -0
  116. package/src/builders/types.ts +15 -0
  117. package/src/cli.ts +108 -0
  118. package/src/config/manifest.ts +24 -0
  119. package/src/config/paths.ts +14 -0
  120. package/src/config/schema.ts +49 -0
  121. package/src/config/setup.ts +14 -0
  122. package/src/config/workspace.ts +150 -0
  123. package/src/config/workspaceManifest.ts +27 -0
  124. package/src/core/constants.ts +73 -0
  125. package/src/core/diagnostics.ts +40 -0
  126. package/src/core/index.ts +3 -0
  127. package/src/core/pages.ts +31 -0
  128. package/src/hooks.ts +175 -0
  129. package/src/html/criticalCss.ts +214 -0
  130. package/src/html/htmlSecurity.ts +86 -0
  131. package/src/html/lazyLoad.ts +30 -0
  132. package/src/html/pageScaffold.ts +70 -0
  133. package/src/html/resourceHints.ts +91 -0
  134. package/src/index.ts +5 -0
  135. package/src/modes/ssg/index.ts +4 -0
  136. package/src/modes/ssg/metadata.ts +63 -0
  137. package/src/modes/ssg/routing.ts +230 -0
  138. package/src/modes/ssg/seo.ts +261 -0
  139. package/src/modes/ssg/validation.ts +37 -0
  140. package/src/modes/ssg/views.ts +309 -0
  141. package/src/operations.ts +138 -0
  142. package/src/pipeline.ts +88 -0
  143. package/src/provider.ts +249 -0
  144. package/src/types.ts +67 -0
  145. package/src/utils/changedFile.ts +39 -0
  146. package/src/utils/fs.ts +48 -0
  147. package/src/utils/hash.ts +6 -0
  148. package/src/utils/pagePaths.ts +43 -0
  149. package/src/utils/pathMatch.ts +36 -0
  150. package/src/watch/frontendFiles.ts +32 -0
  151. package/src/watch/hotUpdateTracker.ts +285 -0
  152. package/src/watch/pipelineHelpers.ts +242 -0
  153. package/src/watch/types.ts +23 -0
  154. package/src/watch/watchCoordinator.ts +666 -0
  155. package/src/watch/watchDaemon.ts +144 -0
  156. package/src/watch/watchReporter.ts +98 -0
  157. package/tests/add-page-defaults.test.js +64 -0
  158. package/tests/content-pages.test.js +81 -0
  159. package/tests/css-app-imports.test.js +64 -0
  160. package/tests/css-page-imports.test.js +100 -0
  161. package/tests/diagnostics.test.js +48 -0
  162. package/tests/features.test.js +63 -0
  163. package/tests/hooks.test.js +71 -0
  164. package/tests/provider.integration.test.js +137 -0
  165. package/tests/ssg-defaults.test.js +201 -0
  166. package/tests/ssg-guardrails.test.js +69 -0
  167. package/tsconfig.json +27 -0
@@ -0,0 +1,71 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+
7
+ async function loadRunBuildOrSkip(t) {
8
+ try {
9
+ const mod = await import('../dist/operations.js');
10
+ return mod.runBuild;
11
+ } catch (err) {
12
+ console.warn('[frontend-tests] Skipping hooks test: optional dependency unavailable:', err?.message ?? err);
13
+ t?.diagnostic?.('skip: missing optional dependency');
14
+ return null;
15
+ }
16
+ }
17
+
18
+ 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');
23
+
24
+ await fs.mkdir(appDir, { recursive: true });
25
+ await fs.mkdir(pageDir, { recursive: true });
26
+
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');
31
+
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');
39
+
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');
42
+
43
+ return {
44
+ workspaceRoot,
45
+ cleanup: () => fs.rm(tempRoot, { recursive: true, force: true })
46
+ };
47
+ }
48
+
49
+ 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);
54
+
55
+ await runBuild({ workspaceRoot: workspace.workspaceRoot });
56
+
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));
60
+
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');
71
+ });
@@ -0,0 +1,137 @@
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
+ async function loadProviderOrSkip(t) {
9
+ try {
10
+ const mod = await import('../dist/index.js');
11
+ return mod.frontendProvider;
12
+ } catch (err) {
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);
15
+ t?.diagnostic?.('skip: missing optional dependency');
16
+ return null;
17
+ }
18
+ }
19
+
20
+ async function createWorkspace() {
21
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-workspace-'));
22
+ const appDir = path.join(root, 'src', 'frontend', 'app');
23
+ const pageDir = path.join(root, 'src', 'frontend', 'pages', 'home');
24
+ await fs.mkdir(appDir, { recursive: true });
25
+ await fs.mkdir(pageDir, { recursive: true });
26
+
27
+ // 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');
30
+ await fs.writeFile(path.join(pageDir, 'index.ts'), 'console.log("home");', 'utf8');
31
+
32
+ return root;
33
+ }
34
+
35
+ async function createWorkspaceWithClientNav() {
36
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-workspace-'));
37
+ const appDir = path.join(root, 'src', 'frontend', 'app');
38
+ const featureDir = path.join(appDir, 'scripts', 'features');
39
+ const pageDir = path.join(root, 'src', 'frontend', 'pages', 'home');
40
+ await fs.mkdir(appDir, { recursive: true });
41
+ await fs.mkdir(featureDir, { recursive: true });
42
+ await fs.mkdir(pageDir, { recursive: true });
43
+
44
+ const pkg = {
45
+ name: 'webstir-project',
46
+ version: '1.0.0',
47
+ webstir: {
48
+ mode: 'ssg',
49
+ enable: {
50
+ clientNav: true
51
+ }
52
+ }
53
+ };
54
+ await fs.writeFile(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');
55
+
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');
58
+ 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');
60
+
61
+ return root;
62
+ }
63
+
64
+ test('frontend provider build emits JS bundle and manifest entry', async (t) => {
65
+ const frontendProvider = await loadProviderOrSkip(t);
66
+ if (!frontendProvider) return; // skip
67
+ const workspace = await createWorkspace();
68
+
69
+ const result = await frontendProvider.build({
70
+ workspaceRoot: workspace,
71
+ env: { WEBSTIR_MODULE_MODE: 'build' },
72
+ incremental: false,
73
+ });
74
+
75
+ const jsOut = path.join(workspace, 'build', 'frontend', 'pages', 'home', 'index.js');
76
+ assert.equal(fssync.existsSync(jsOut), true, 'expected build/frontend/pages/home/index.js');
77
+
78
+ assert.ok(Array.isArray(result.manifest.entryPoints));
79
+ assert.ok(result.manifest.entryPoints.some((e) => e.endsWith('pages/home/index.js')));
80
+ });
81
+
82
+ test('frontend provider publish produces dist assets and preserves entry in manifest', async (t) => {
83
+ const frontendProvider = await loadProviderOrSkip(t);
84
+ if (!frontendProvider) return; // skip
85
+ const workspace = await createWorkspace();
86
+
87
+ // 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 });
90
+
91
+ // Dist should contain a hashed JS file
92
+ const distPageDir = path.join(workspace, 'dist', 'frontend', 'pages', 'home');
93
+ const files = fssync.existsSync(distPageDir) ? fssync.readdirSync(distPageDir) : [];
94
+ assert.ok(files.some((f) => f.startsWith('index-') && f.endsWith('.js')));
95
+
96
+ // Manifest still reflects build/frontend entry points by design
97
+ assert.ok(publishResult.manifest.entryPoints.some((e) => e.endsWith('pages/home/index.js')));
98
+ });
99
+
100
+ test('enable.clientNav uses feature module (no legacy helper injection)', async (t) => {
101
+ const frontendProvider = await loadProviderOrSkip(t);
102
+ if (!frontendProvider) return; // skip
103
+ const workspace = await createWorkspaceWithClientNav();
104
+
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 });
107
+
108
+ const distClientNav = path.join(workspace, 'dist', 'frontend', 'clientNav.js');
109
+ assert.equal(fssync.existsSync(distClientNav), false, 'did not expect dist/frontend/clientNav.js');
110
+
111
+ const distHtml = await fs.readFile(path.join(workspace, 'dist', 'frontend', 'index.html'), 'utf8');
112
+ assert.ok(!distHtml.includes('clientNav.js'), 'did not expect client-nav script injected');
113
+ assert.ok(!distHtml.includes('index.js'), 'should not inject page index.js when none exists');
114
+ });
115
+
116
+ test('enable.clientNav without feature module fails fast', async (t) => {
117
+ const frontendProvider = await loadProviderOrSkip(t);
118
+ if (!frontendProvider) return; // skip
119
+ const workspace = await createWorkspace();
120
+
121
+ const pkg = {
122
+ name: 'webstir-project',
123
+ version: '1.0.0',
124
+ webstir: {
125
+ mode: 'ssg',
126
+ enable: {
127
+ clientNav: true
128
+ }
129
+ }
130
+ };
131
+ await fs.writeFile(path.join(workspace, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');
132
+
133
+ await assert.rejects(
134
+ () => frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false }),
135
+ /Enabled feature module\(s\) missing: client-nav/
136
+ );
137
+ });
@@ -0,0 +1,201 @@
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 { applySsgRouting, generateSsgViewData } from '../dist/modes/ssg/index.js';
9
+
10
+ async function createWorkspace() {
11
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-ssg-defaults-'));
12
+
13
+ const distFrontend = path.join(root, 'dist', 'frontend');
14
+ const distPages = path.join(distFrontend, 'pages');
15
+ await fs.mkdir(path.join(distPages, 'home'), { recursive: true });
16
+ await fs.mkdir(path.join(distPages, 'about'), { recursive: true });
17
+
18
+ await fs.writeFile(path.join(distPages, 'home', 'index.html'), '<!doctype html><main>home</main>', 'utf8');
19
+ await fs.writeFile(path.join(distPages, 'about', 'index.html'), '<!doctype html><main>about</main>', 'utf8');
20
+
21
+ const pkg = {
22
+ name: 'webstir-project',
23
+ version: '1.0.0',
24
+ webstir: {
25
+ mode: 'ssg',
26
+ moduleManifest: {
27
+ views: [
28
+ {
29
+ name: 'AboutView',
30
+ path: '/about',
31
+ staticPaths: ['/about', '/about/team']
32
+ }
33
+ ]
34
+ }
35
+ }
36
+ };
37
+
38
+ await fs.writeFile(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');
39
+
40
+ return root;
41
+ }
42
+
43
+ test('ssg workspace defaults views to renderMode=ssg when omitted', async () => {
44
+ const workspace = await createWorkspace();
45
+ const distFrontend = path.join(workspace, 'dist', 'frontend');
46
+ const distPages = path.join(distFrontend, 'pages');
47
+
48
+ try {
49
+ await applySsgRouting({
50
+ version: 1,
51
+ paths: {
52
+ workspace,
53
+ dist: {
54
+ root: path.join(workspace, 'dist'),
55
+ frontend: distFrontend,
56
+ app: path.join(distFrontend, 'app'),
57
+ pages: distPages,
58
+ content: path.join(distPages, 'docs'),
59
+ images: path.join(distFrontend, 'images'),
60
+ fonts: path.join(distFrontend, 'fonts'),
61
+ media: path.join(distFrontend, 'media')
62
+ },
63
+ build: {
64
+ root: path.join(workspace, 'build'),
65
+ frontend: path.join(workspace, 'build', 'frontend'),
66
+ app: path.join(workspace, 'build', 'frontend', 'app'),
67
+ pages: path.join(workspace, 'build', 'frontend', 'pages'),
68
+ content: path.join(workspace, 'build', 'frontend', 'pages', 'docs'),
69
+ images: path.join(workspace, 'build', 'frontend', 'images'),
70
+ fonts: path.join(workspace, 'build', 'frontend', 'fonts'),
71
+ media: path.join(workspace, 'build', 'frontend', 'media')
72
+ },
73
+ src: {
74
+ root: path.join(workspace, 'src'),
75
+ frontend: path.join(workspace, 'src', 'frontend'),
76
+ app: path.join(workspace, 'src', 'frontend', 'app'),
77
+ pages: path.join(workspace, 'src', 'frontend', 'pages'),
78
+ content: path.join(workspace, 'src', 'frontend', 'content'),
79
+ images: path.join(workspace, 'src', 'frontend', 'images'),
80
+ fonts: path.join(workspace, 'src', 'frontend', 'fonts'),
81
+ media: path.join(workspace, 'src', 'frontend', 'media')
82
+ }
83
+ },
84
+ features: {
85
+ htmlSecurity: true,
86
+ imageOptimization: true,
87
+ precompression: true
88
+ }
89
+ });
90
+
91
+ const nestedAlias = path.join(distFrontend, 'about', 'team', 'index.html');
92
+ assert.equal(fssync.existsSync(nestedAlias), true, `expected nested alias at ${nestedAlias}`);
93
+ } finally {
94
+ await fs.rm(workspace, { recursive: true, force: true });
95
+ }
96
+ });
97
+
98
+ test('ssg workspace defaults staticPaths to [path] for view data', async () => {
99
+ const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-ssg-default-paths-'));
100
+ const distFrontend = path.join(workspace, 'dist', 'frontend');
101
+ const distPages = path.join(distFrontend, 'pages');
102
+ const buildBackend = path.join(workspace, 'build', 'backend');
103
+
104
+ await fs.mkdir(path.join(distPages, 'about'), { recursive: true });
105
+ await fs.writeFile(path.join(distPages, 'about', 'index.html'), '<!doctype html><main>about</main>', 'utf8');
106
+
107
+ await fs.mkdir(buildBackend, { recursive: true });
108
+ await fs.writeFile(
109
+ path.join(buildBackend, 'module.mjs'),
110
+ [
111
+ "export const module = {",
112
+ " views: [",
113
+ " {",
114
+ " definition: { name: 'AboutView', path: '/about' },",
115
+ " load: async () => ({ title: 'about' })",
116
+ " }",
117
+ " ]",
118
+ "};"
119
+ ].join('\n'),
120
+ 'utf8'
121
+ );
122
+
123
+ await fs.writeFile(
124
+ path.join(workspace, 'package.json'),
125
+ JSON.stringify(
126
+ {
127
+ name: 'webstir-project',
128
+ version: '1.0.0',
129
+ webstir: {
130
+ mode: 'ssg',
131
+ moduleManifest: {
132
+ views: [
133
+ {
134
+ name: 'AboutView',
135
+ path: '/about'
136
+ }
137
+ ]
138
+ }
139
+ }
140
+ },
141
+ null,
142
+ 2
143
+ ),
144
+ 'utf8'
145
+ );
146
+
147
+ const config = {
148
+ version: 1,
149
+ paths: {
150
+ workspace,
151
+ dist: {
152
+ root: path.join(workspace, 'dist'),
153
+ frontend: distFrontend,
154
+ app: path.join(distFrontend, 'app'),
155
+ pages: distPages,
156
+ content: path.join(distPages, 'docs'),
157
+ images: path.join(distFrontend, 'images'),
158
+ fonts: path.join(distFrontend, 'fonts'),
159
+ media: path.join(distFrontend, 'media')
160
+ },
161
+ build: {
162
+ root: path.join(workspace, 'build'),
163
+ frontend: path.join(workspace, 'build', 'frontend'),
164
+ app: path.join(workspace, 'build', 'frontend', 'app'),
165
+ pages: path.join(workspace, 'build', 'frontend', 'pages'),
166
+ content: path.join(workspace, 'build', 'frontend', 'pages', 'docs'),
167
+ images: path.join(workspace, 'build', 'frontend', 'images'),
168
+ fonts: path.join(workspace, 'build', 'frontend', 'fonts'),
169
+ media: path.join(workspace, 'build', 'frontend', 'media')
170
+ },
171
+ src: {
172
+ root: path.join(workspace, 'src'),
173
+ frontend: path.join(workspace, 'src', 'frontend'),
174
+ app: path.join(workspace, 'src', 'frontend', 'app'),
175
+ pages: path.join(workspace, 'src', 'frontend', 'pages'),
176
+ content: path.join(workspace, 'src', 'frontend', 'content'),
177
+ images: path.join(workspace, 'src', 'frontend', 'images'),
178
+ fonts: path.join(workspace, 'src', 'frontend', 'fonts'),
179
+ media: path.join(workspace, 'src', 'frontend', 'media')
180
+ }
181
+ },
182
+ features: {
183
+ htmlSecurity: true,
184
+ imageOptimization: true,
185
+ precompression: true
186
+ }
187
+ };
188
+
189
+ try {
190
+ await generateSsgViewData(config);
191
+ const dataPath = path.join(distPages, 'about', 'view-data.json');
192
+ assert.equal(fssync.existsSync(dataPath), true, `expected view data at ${dataPath}`);
193
+ const raw = await fs.readFile(dataPath, 'utf8');
194
+ const parsed = JSON.parse(raw);
195
+ assert.equal(Array.isArray(parsed), true);
196
+ assert.equal(parsed[0]?.path, '/about');
197
+ assert.deepEqual(parsed[0]?.data, { title: 'about' });
198
+ } finally {
199
+ await fs.rm(workspace, { recursive: true, force: true });
200
+ }
201
+ });
@@ -0,0 +1,69 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ import { runPublish } from '../dist/index.js';
8
+
9
+ async function createWorkspace(pkg) {
10
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-ssg-guard-'));
11
+ await fs.writeFile(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');
12
+ return root;
13
+ }
14
+
15
+ test('ssg publish rejects route-level renderMode/staticPaths/ssg metadata', async () => {
16
+ const workspace = await createWorkspace({
17
+ name: 'webstir-project',
18
+ version: '1.0.0',
19
+ webstir: {
20
+ moduleManifest: {
21
+ routes: [
22
+ {
23
+ name: 'ApiRoute',
24
+ method: 'GET',
25
+ path: '/api/route',
26
+ renderMode: 'ssg'
27
+ }
28
+ ]
29
+ }
30
+ }
31
+ });
32
+
33
+ try {
34
+ await assert.rejects(
35
+ runPublish({ workspaceRoot: workspace, publishMode: 'ssg' }),
36
+ /SSG publish expects SSG metadata under `webstir\.moduleManifest\.views`/i
37
+ );
38
+ } finally {
39
+ await fs.rm(workspace, { recursive: true, force: true });
40
+ }
41
+ });
42
+
43
+ test('ssg publish rejects route-level staticPaths without renderMode', async () => {
44
+ const workspace = await createWorkspace({
45
+ name: 'webstir-project',
46
+ version: '1.0.0',
47
+ webstir: {
48
+ moduleManifest: {
49
+ routes: [
50
+ {
51
+ name: 'ApiRoute',
52
+ method: 'GET',
53
+ path: '/api/route',
54
+ staticPaths: ['/']
55
+ }
56
+ ]
57
+ }
58
+ }
59
+ });
60
+
61
+ try {
62
+ await assert.rejects(
63
+ runPublish({ workspaceRoot: workspace, publishMode: 'ssg' }),
64
+ /SSG publish expects SSG metadata under `webstir\.moduleManifest\.views`/i
65
+ );
66
+ } finally {
67
+ await fs.rm(workspace, { recursive: true, force: true });
68
+ }
69
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": true,
13
+ "declarationMap": false,
14
+ "sourceMap": false,
15
+ "types": [
16
+ "node"
17
+ ],
18
+ "resolveJsonModule": true
19
+ },
20
+ "include": [
21
+ "src/**/*"
22
+ ],
23
+ "exclude": [
24
+ "dist",
25
+ "node_modules"
26
+ ]
27
+ }