@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
@@ -8,194 +8,301 @@ import path from 'node:path';
8
8
  import { applySsgRouting, generateSsgViewData } from '../dist/modes/ssg/index.js';
9
9
 
10
10
  async function createWorkspace() {
11
- const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-ssg-defaults-'));
11
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-ssg-defaults-'));
12
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 });
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
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');
18
+ await fs.writeFile(
19
+ path.join(distPages, 'home', 'index.html'),
20
+ '<!doctype html><main>home</main>',
21
+ 'utf8',
22
+ );
23
+ await fs.writeFile(
24
+ path.join(distPages, 'about', 'index.html'),
25
+ '<!doctype html><main>about</main>',
26
+ 'utf8',
27
+ );
20
28
 
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;
29
+ const pkg = {
30
+ name: 'webstir-project',
31
+ version: '1.0.0',
32
+ webstir: {
33
+ mode: 'ssg',
34
+ moduleManifest: {
35
+ views: [
36
+ {
37
+ name: 'AboutView',
38
+ path: '/about',
39
+ staticPaths: ['/about', '/about/team'],
40
+ },
41
+ ],
42
+ },
43
+ },
44
+ };
45
+
46
+ await fs.writeFile(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');
47
+
48
+ return root;
49
+ }
50
+
51
+ function createFrontendConfig(workspace) {
52
+ const distFrontend = path.join(workspace, 'dist', 'frontend');
53
+ const distPages = path.join(distFrontend, 'pages');
54
+
55
+ return {
56
+ version: 1,
57
+ paths: {
58
+ workspace,
59
+ dist: {
60
+ root: path.join(workspace, 'dist'),
61
+ frontend: distFrontend,
62
+ app: path.join(distFrontend, 'app'),
63
+ pages: distPages,
64
+ content: path.join(distPages, 'docs'),
65
+ images: path.join(distFrontend, 'images'),
66
+ fonts: path.join(distFrontend, 'fonts'),
67
+ media: path.join(distFrontend, 'media'),
68
+ },
69
+ build: {
70
+ root: path.join(workspace, 'build'),
71
+ frontend: path.join(workspace, 'build', 'frontend'),
72
+ app: path.join(workspace, 'build', 'frontend', 'app'),
73
+ pages: path.join(workspace, 'build', 'frontend', 'pages'),
74
+ content: path.join(workspace, 'build', 'frontend', 'pages', 'docs'),
75
+ images: path.join(workspace, 'build', 'frontend', 'images'),
76
+ fonts: path.join(workspace, 'build', 'frontend', 'fonts'),
77
+ media: path.join(workspace, 'build', 'frontend', 'media'),
78
+ },
79
+ src: {
80
+ root: path.join(workspace, 'src'),
81
+ frontend: path.join(workspace, 'src', 'frontend'),
82
+ app: path.join(workspace, 'src', 'frontend', 'app'),
83
+ pages: path.join(workspace, 'src', 'frontend', 'pages'),
84
+ content: path.join(workspace, 'src', 'frontend', 'content'),
85
+ images: path.join(workspace, 'src', 'frontend', 'images'),
86
+ fonts: path.join(workspace, 'src', 'frontend', 'fonts'),
87
+ media: path.join(workspace, 'src', 'frontend', 'media'),
88
+ },
89
+ },
90
+ features: {
91
+ htmlSecurity: true,
92
+ externalResourceIntegrity: false,
93
+ imageOptimization: true,
94
+ precompression: true,
95
+ },
96
+ };
41
97
  }
42
98
 
43
99
  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
- }
100
+ const workspace = await createWorkspace();
101
+ const distFrontend = path.join(workspace, 'dist', 'frontend');
102
+ const distPages = path.join(distFrontend, 'pages');
103
+
104
+ try {
105
+ await applySsgRouting({
106
+ version: 1,
107
+ paths: {
108
+ workspace,
109
+ dist: {
110
+ root: path.join(workspace, 'dist'),
111
+ frontend: distFrontend,
112
+ app: path.join(distFrontend, 'app'),
113
+ pages: distPages,
114
+ content: path.join(distPages, 'docs'),
115
+ images: path.join(distFrontend, 'images'),
116
+ fonts: path.join(distFrontend, 'fonts'),
117
+ media: path.join(distFrontend, 'media'),
118
+ },
119
+ build: {
120
+ root: path.join(workspace, 'build'),
121
+ frontend: path.join(workspace, 'build', 'frontend'),
122
+ app: path.join(workspace, 'build', 'frontend', 'app'),
123
+ pages: path.join(workspace, 'build', 'frontend', 'pages'),
124
+ content: path.join(workspace, 'build', 'frontend', 'pages', 'docs'),
125
+ images: path.join(workspace, 'build', 'frontend', 'images'),
126
+ fonts: path.join(workspace, 'build', 'frontend', 'fonts'),
127
+ media: path.join(workspace, 'build', 'frontend', 'media'),
128
+ },
129
+ src: {
130
+ root: path.join(workspace, 'src'),
131
+ frontend: path.join(workspace, 'src', 'frontend'),
132
+ app: path.join(workspace, 'src', 'frontend', 'app'),
133
+ pages: path.join(workspace, 'src', 'frontend', 'pages'),
134
+ content: path.join(workspace, 'src', 'frontend', 'content'),
135
+ images: path.join(workspace, 'src', 'frontend', 'images'),
136
+ fonts: path.join(workspace, 'src', 'frontend', 'fonts'),
137
+ media: path.join(workspace, 'src', 'frontend', 'media'),
138
+ },
139
+ },
140
+ features: {
141
+ htmlSecurity: true,
142
+ externalResourceIntegrity: false,
143
+ imageOptimization: true,
144
+ precompression: true,
145
+ },
146
+ });
147
+
148
+ const nestedAlias = path.join(distFrontend, 'about', 'team', 'index.html');
149
+ assert.equal(fssync.existsSync(nestedAlias), true, `expected nested alias at ${nestedAlias}`);
150
+ } finally {
151
+ await fs.rm(workspace, { recursive: true, force: true });
152
+ }
96
153
  });
97
154
 
98
155
  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
- );
156
+ const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-ssg-default-paths-'));
157
+ const distFrontend = path.join(workspace, 'dist', 'frontend');
158
+ const distPages = path.join(distFrontend, 'pages');
159
+ const buildBackend = path.join(workspace, 'build', 'backend');
160
+
161
+ await fs.mkdir(path.join(distPages, 'about'), { recursive: true });
162
+ await fs.writeFile(
163
+ path.join(distPages, 'about', 'index.html'),
164
+ '<!doctype html><main>about</main>',
165
+ 'utf8',
166
+ );
122
167
 
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'
168
+ await fs.mkdir(buildBackend, { recursive: true });
169
+ await fs.writeFile(
170
+ path.join(buildBackend, 'module.mjs'),
171
+ [
172
+ 'export const module = {',
173
+ ' views: [',
174
+ ' {',
175
+ " definition: { name: 'AboutView', path: '/about' },",
176
+ " load: async () => ({ title: 'about' })",
177
+ ' }',
178
+ ' ]',
179
+ '};',
180
+ ].join('\n'),
181
+ 'utf8',
182
+ );
183
+
184
+ await fs.writeFile(
185
+ path.join(workspace, 'package.json'),
186
+ JSON.stringify(
187
+ {
188
+ name: 'webstir-project',
189
+ version: '1.0.0',
190
+ webstir: {
191
+ mode: 'ssg',
192
+ moduleManifest: {
193
+ views: [
194
+ {
195
+ name: 'AboutView',
196
+ path: '/about',
197
+ },
198
+ ],
199
+ },
200
+ },
201
+ },
202
+ null,
203
+ 2,
204
+ ),
205
+ 'utf8',
206
+ );
207
+
208
+ const config = createFrontendConfig(workspace);
209
+
210
+ try {
211
+ await generateSsgViewData(config);
212
+ const dataPath = path.join(distPages, 'about', 'view-data.json');
213
+ assert.equal(fssync.existsSync(dataPath), true, `expected view data at ${dataPath}`);
214
+ const raw = await fs.readFile(dataPath, 'utf8');
215
+ const parsed = JSON.parse(raw);
216
+ assert.equal(Array.isArray(parsed), true);
217
+ assert.equal(parsed[0]?.path, '/about');
218
+ assert.deepEqual(parsed[0]?.data, { title: 'about' });
219
+ } finally {
220
+ await fs.rm(workspace, { recursive: true, force: true });
221
+ }
222
+ });
223
+
224
+ test('ssg view-data generation fails when a backend module candidate cannot be imported', async () => {
225
+ const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-ssg-import-fail-'));
226
+ const distPages = path.join(workspace, 'dist', 'frontend', 'pages');
227
+ const buildBackend = path.join(workspace, 'build', 'backend');
228
+
229
+ await fs.mkdir(path.join(distPages, 'about'), { recursive: true });
230
+ await fs.writeFile(
231
+ path.join(distPages, 'about', 'index.html'),
232
+ '<!doctype html><main>about</main>',
233
+ );
234
+ await fs.mkdir(buildBackend, { recursive: true });
235
+ await fs.writeFile(path.join(buildBackend, 'module.mjs'), 'export const module = ;\n', 'utf8');
236
+ await fs.writeFile(
237
+ path.join(workspace, 'package.json'),
238
+ JSON.stringify(
239
+ { name: 'webstir-project', version: '1.0.0', webstir: { mode: 'ssg' } },
240
+ null,
241
+ 2,
242
+ ),
243
+ 'utf8',
244
+ );
245
+
246
+ try {
247
+ await assert.rejects(
248
+ generateSsgViewData(createFrontendConfig(workspace)),
249
+ /failed to import backend module definition/,
145
250
  );
251
+ } finally {
252
+ await fs.rm(workspace, { recursive: true, force: true });
253
+ }
254
+ });
146
255
 
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
- }
256
+ test('ssg view-data generation fails when a view loader throws', async () => {
257
+ const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-ssg-load-fail-'));
258
+ const distPages = path.join(workspace, 'dist', 'frontend', 'pages');
259
+ const buildBackend = path.join(workspace, 'build', 'backend');
260
+
261
+ await fs.mkdir(path.join(distPages, 'about'), { recursive: true });
262
+ await fs.writeFile(
263
+ path.join(distPages, 'about', 'index.html'),
264
+ '<!doctype html><main>about</main>',
265
+ );
266
+ await fs.mkdir(buildBackend, { recursive: true });
267
+ await fs.writeFile(
268
+ path.join(buildBackend, 'module.mjs'),
269
+ [
270
+ 'export const module = {',
271
+ ' views: [',
272
+ ' {',
273
+ " definition: { name: 'AboutView', path: '/about', renderMode: 'ssg', staticPaths: ['/about'] },",
274
+ " load: async () => { throw new Error('boom'); }",
275
+ ' }',
276
+ ' ]',
277
+ '};',
278
+ ].join('\n'),
279
+ 'utf8',
280
+ );
281
+ await fs.writeFile(
282
+ path.join(workspace, 'package.json'),
283
+ JSON.stringify(
284
+ {
285
+ name: 'webstir-project',
286
+ version: '1.0.0',
287
+ webstir: {
288
+ mode: 'ssg',
289
+ moduleManifest: {
290
+ views: [{ name: 'AboutView', path: '/about', staticPaths: ['/about'] }],
291
+ },
181
292
  },
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
- }
293
+ },
294
+ null,
295
+ 2,
296
+ ),
297
+ 'utf8',
298
+ );
299
+
300
+ try {
301
+ await assert.rejects(
302
+ generateSsgViewData(createFrontendConfig(workspace)),
303
+ /failed to load SSG view data/,
304
+ );
305
+ } finally {
306
+ await fs.rm(workspace, { recursive: true, force: true });
307
+ }
201
308
  });
@@ -7,63 +7,63 @@ import path from 'node:path';
7
7
  import { runPublish } from '../dist/index.js';
8
8
 
9
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;
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
13
  }
14
14
 
15
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
- });
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
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
- }
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
41
  });
42
42
 
43
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
- });
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
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
- }
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
69
  });
package/tsconfig.json CHANGED
@@ -12,16 +12,9 @@
12
12
  "declaration": true,
13
13
  "declarationMap": false,
14
14
  "sourceMap": false,
15
- "types": [
16
- "node"
17
- ],
15
+ "types": ["bun", "node"],
18
16
  "resolveJsonModule": true
19
17
  },
20
- "include": [
21
- "src/**/*"
22
- ],
23
- "exclude": [
24
- "dist",
25
- "node_modules"
26
- ]
18
+ "include": ["src/**/*"],
19
+ "exclude": ["dist", "node_modules"]
27
20
  }
@@ -1,3 +0,0 @@
1
- import type { EnableFlags, FrontendConfig } from '../types.js';
2
- export declare function resolveEntryPoint(pageDirectory: string): Promise<string | null>;
3
- export declare function copyRefreshScript(config: FrontendConfig, enable?: EnableFlags): Promise<void>;
@@ -1,25 +0,0 @@
1
- import path from 'node:path';
2
- import { ensureDir, pathExists, copy } from '../utils/fs.js';
3
- import { FILES, EXTENSIONS } from '../core/constants.js';
4
- export async function resolveEntryPoint(pageDirectory) {
5
- const candidates = [`${FILES.index}${EXTENSIONS.ts}`, `${FILES.index}.tsx`, `${FILES.index}${EXTENSIONS.js}`, `${FILES.index}.jsx`];
6
- for (const candidate of candidates) {
7
- const file = path.join(pageDirectory, candidate);
8
- if (await pathExists(file)) {
9
- return file;
10
- }
11
- }
12
- return null;
13
- }
14
- export async function copyRefreshScript(config, enable) {
15
- const runtimeScripts = [FILES.refreshJs, FILES.hmrJs];
16
- for (const scriptName of runtimeScripts) {
17
- const source = path.join(config.paths.src.app, scriptName);
18
- if (!(await pathExists(source))) {
19
- continue;
20
- }
21
- const destination = path.join(config.paths.build.frontend, scriptName);
22
- await ensureDir(path.dirname(destination));
23
- await copy(source, destination);
24
- }
25
- }