@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
@@ -6,304 +6,323 @@ import { FOLDERS } from '../../core/constants.js';
6
6
  import type { WorkspaceModuleView, WorkspacePackageJson } from '../../config/workspaceManifest.js';
7
7
 
8
8
  interface ViewDefinitionLike {
9
- readonly name?: string;
10
- readonly path?: string;
11
- readonly renderMode?: 'ssg' | 'ssr' | 'spa';
12
- readonly staticPaths?: readonly string[];
9
+ readonly name?: string;
10
+ readonly path?: string;
11
+ readonly renderMode?: 'ssg' | 'ssr' | 'spa';
12
+ readonly staticPaths?: readonly string[];
13
13
  }
14
14
 
15
15
  interface ViewSpecLike {
16
- readonly definition?: ViewDefinitionLike;
17
- readonly load?: (context: any) => unknown | Promise<unknown>;
16
+ readonly definition?: ViewDefinitionLike;
17
+ readonly load?: (context: unknown) => unknown | Promise<unknown>;
18
18
  }
19
19
 
20
20
  interface ModuleDefinitionLike {
21
- readonly views?: readonly ViewSpecLike[];
21
+ readonly views?: readonly ViewSpecLike[];
22
22
  }
23
23
 
24
24
  interface ViewDataEntry {
25
- readonly viewName: string;
26
- readonly path: string;
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- readonly data: any;
25
+ readonly viewName: string;
26
+ readonly path: string;
27
+ readonly data: unknown;
29
28
  }
30
29
 
31
30
  export async function generateSsgViewData(config: FrontendConfig): Promise<void> {
32
- const workspaceRoot = config.paths.workspace;
33
- const pkgPath = path.join(workspaceRoot, 'package.json');
34
- const pkg = await readJson<WorkspacePackageJson>(pkgPath);
35
- const moduleConfig = pkg?.webstir?.moduleManifest;
36
- const viewMetadata = moduleConfig?.views ?? [];
37
- const workspaceMode = pkg?.webstir?.mode;
38
- const isSsgWorkspace = typeof workspaceMode === 'string' && workspaceMode.toLowerCase() === 'ssg';
39
-
40
- const moduleDefinition = await loadBackendModuleDefinition(workspaceRoot);
41
- if (!moduleDefinition?.views || moduleDefinition.views.length === 0) {
42
- return;
31
+ const workspaceRoot = config.paths.workspace;
32
+ const pkgPath = path.join(workspaceRoot, 'package.json');
33
+ const pkg = await readJson<WorkspacePackageJson>(pkgPath);
34
+ const moduleConfig = pkg?.webstir?.moduleManifest;
35
+ const viewMetadata = moduleConfig?.views ?? [];
36
+ const workspaceMode = pkg?.webstir?.mode;
37
+ const isSsgWorkspace = typeof workspaceMode === 'string' && workspaceMode.toLowerCase() === 'ssg';
38
+
39
+ const moduleDefinition = await loadBackendModuleDefinition(workspaceRoot);
40
+ if (!moduleDefinition?.views || moduleDefinition.views.length === 0) {
41
+ return;
42
+ }
43
+
44
+ const perPageData = new Map<string, ViewDataEntry[]>();
45
+
46
+ for (const spec of moduleDefinition.views) {
47
+ const definition = spec.definition ?? {};
48
+ const viewName = definition.name ?? '';
49
+ const viewPathTemplate = definition.path ?? '';
50
+ const meta = findViewMetadata(viewMetadata, viewName, viewPathTemplate);
51
+ const renderMode =
52
+ meta?.renderMode ?? definition.renderMode ?? (isSsgWorkspace ? 'ssg' : undefined);
53
+ if (renderMode !== 'ssg') {
54
+ continue;
43
55
  }
44
56
 
45
- const perPageData = new Map<string, ViewDataEntry[]>();
46
-
47
- for (const spec of moduleDefinition.views) {
48
- const definition = spec.definition ?? {};
49
- const viewName = definition.name ?? '';
50
- const viewPathTemplate = definition.path ?? '';
51
- const meta = findViewMetadata(viewMetadata, viewName, viewPathTemplate);
52
- const renderMode = meta?.renderMode ?? definition.renderMode ?? (isSsgWorkspace ? 'ssg' : undefined);
53
- if (renderMode !== 'ssg') {
54
- continue;
55
- }
56
-
57
- const staticPaths = getEffectiveStaticPaths(meta, definition, isSsgWorkspace);
58
- if (!spec.load || !Array.isArray(staticPaths) || staticPaths.length === 0) {
59
- continue;
60
- }
61
-
62
- for (const rawPath of staticPaths) {
63
- if (typeof rawPath !== 'string' || rawPath.length === 0) {
64
- continue;
65
- }
66
-
67
- const normalizedPath = normalizePath(rawPath);
68
- const params = deriveRouteParams(viewPathTemplate, normalizedPath);
69
- if (!params) {
70
- continue;
71
- }
72
-
73
- const ssrContext = createMinimalSsrContext(normalizedPath, params);
74
-
75
- let data: unknown;
76
- try {
77
- data = await spec.load(ssrContext);
78
- } catch {
79
- // Best-effort only; skip paths that fail to load.
80
- continue;
81
- }
82
-
83
- const pageName = normalizedPath === '/' ? FOLDERS.home : firstPathSegment(normalizedPath) ?? FOLDERS.home;
84
- const entries = perPageData.get(pageName) ?? [];
85
- entries.push({
86
- viewName: viewName || viewPathTemplate || normalizedPath,
87
- path: normalizedPath,
88
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
89
- data
90
- });
91
- perPageData.set(pageName, entries);
92
- }
57
+ const staticPaths = getEffectiveStaticPaths(meta, definition, isSsgWorkspace);
58
+ if (!spec.load || !Array.isArray(staticPaths) || staticPaths.length === 0) {
59
+ continue;
93
60
  }
94
61
 
95
- if (perPageData.size === 0) {
96
- return;
62
+ for (const rawPath of staticPaths) {
63
+ if (typeof rawPath !== 'string' || rawPath.length === 0) {
64
+ continue;
65
+ }
66
+
67
+ const normalizedPath = normalizePath(rawPath);
68
+ const params = deriveRouteParams(viewPathTemplate, normalizedPath);
69
+ if (!params) {
70
+ continue;
71
+ }
72
+
73
+ const ssrContext = createMinimalSsrContext(normalizedPath, params);
74
+
75
+ let data: unknown;
76
+ try {
77
+ data = await spec.load(ssrContext);
78
+ } catch (error) {
79
+ const viewLabel = viewName || viewPathTemplate || normalizedPath;
80
+ throw new Error(
81
+ `[webstir-frontend] failed to load SSG view data for "${viewLabel}" at ${normalizedPath}: ${formatErrorMessage(error)}`,
82
+ );
83
+ }
84
+
85
+ const pageName =
86
+ normalizedPath === '/' ? FOLDERS.home : (firstPathSegment(normalizedPath) ?? FOLDERS.home);
87
+ const entries = perPageData.get(pageName) ?? [];
88
+ entries.push({
89
+ viewName: viewName || viewPathTemplate || normalizedPath,
90
+ path: normalizedPath,
91
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
92
+ data,
93
+ });
94
+ perPageData.set(pageName, entries);
97
95
  }
96
+ }
98
97
 
99
- const pagesRoot = config.paths.dist.pages;
98
+ if (perPageData.size === 0) {
99
+ return;
100
+ }
100
101
 
101
- for (const [pageName, entries] of perPageData.entries()) {
102
- const pageDir = path.join(pagesRoot, pageName);
103
- if (!(await pathExists(pageDir))) {
104
- continue;
105
- }
102
+ const pagesRoot = config.paths.dist.pages;
106
103
 
107
- const dataPath = path.join(pageDir, 'view-data.json');
108
- await ensureDir(pageDir);
109
- await writeJson(dataPath, entries);
104
+ for (const [pageName, entries] of perPageData.entries()) {
105
+ const pageDir = path.join(pagesRoot, pageName);
106
+ if (!(await pathExists(pageDir))) {
107
+ continue;
110
108
  }
109
+
110
+ const dataPath = path.join(pageDir, 'view-data.json');
111
+ await ensureDir(pageDir);
112
+ await writeJson(dataPath, entries);
113
+ }
111
114
  }
112
115
 
113
116
  function findViewMetadata(
114
- views: readonly WorkspaceModuleView[],
115
- name: string,
116
- templatePath: string
117
+ views: readonly WorkspaceModuleView[],
118
+ name: string,
119
+ templatePath: string,
117
120
  ): WorkspaceModuleView | undefined {
118
- return (
119
- views.find((view) => (view.name && view.name === name) || (view.path && view.path === templatePath)) ??
120
- views.find((view) => view.path === templatePath) ??
121
- views.find((view) => view.name === name)
122
- );
121
+ return (
122
+ views.find(
123
+ (view) => (view.name && view.name === name) || (view.path && view.path === templatePath),
124
+ ) ??
125
+ views.find((view) => view.path === templatePath) ??
126
+ views.find((view) => view.name === name)
127
+ );
123
128
  }
124
129
 
125
- async function loadBackendModuleDefinition(workspaceRoot: string): Promise<ModuleDefinitionLike | undefined> {
126
- const buildRoot = path.join(workspaceRoot, 'build', 'backend');
127
- const candidates = [
128
- path.join(buildRoot, 'module.js'),
129
- path.join(buildRoot, 'module.mjs'),
130
- path.join(buildRoot, 'module', 'index.js'),
131
- path.join(buildRoot, 'module', 'index.mjs')
132
- ];
133
-
134
- for (const fullPath of candidates) {
135
- if (!(await pathExists(fullPath))) {
136
- continue;
137
- }
138
-
139
- try {
140
- const url = `${pathToFileURL(fullPath).href}?t=${Date.now()}`;
141
- const imported = (await import(url)) as Record<string, unknown>;
142
- const candidate = extractModuleDefinition(imported);
143
- if (candidate) {
144
- return candidate;
145
- }
146
- } catch {
147
- // Best-effort only.
148
- }
130
+ async function loadBackendModuleDefinition(
131
+ workspaceRoot: string,
132
+ ): Promise<ModuleDefinitionLike | undefined> {
133
+ const buildRoot = path.join(workspaceRoot, 'build', 'backend');
134
+ const candidates = [
135
+ path.join(buildRoot, 'module.js'),
136
+ path.join(buildRoot, 'module.mjs'),
137
+ path.join(buildRoot, 'module', 'index.js'),
138
+ path.join(buildRoot, 'module', 'index.mjs'),
139
+ ];
140
+
141
+ for (const fullPath of candidates) {
142
+ if (!(await pathExists(fullPath))) {
143
+ continue;
149
144
  }
150
145
 
151
- return undefined;
146
+ try {
147
+ const url = `${pathToFileURL(fullPath).href}?t=${Date.now()}`;
148
+ const imported = (await import(url)) as Record<string, unknown>;
149
+ const candidate = extractModuleDefinition(imported);
150
+ if (candidate) {
151
+ return candidate;
152
+ }
153
+ } catch (error) {
154
+ throw new Error(
155
+ `[webstir-frontend] failed to import backend module definition from ${fullPath}: ${formatErrorMessage(error)}`,
156
+ );
157
+ }
158
+ }
159
+
160
+ return undefined;
152
161
  }
153
162
 
154
- function extractModuleDefinition(exports: Record<string, unknown>): ModuleDefinitionLike | undefined {
155
- const keys = ['module', 'moduleDefinition', 'default', 'backendModule'];
156
- for (const key of keys) {
157
- if (key in exports) {
158
- const value = exports[key as keyof typeof exports];
159
- if (value && typeof value === 'object') {
160
- return value as ModuleDefinitionLike;
161
- }
162
- }
163
+ function extractModuleDefinition(
164
+ exports: Record<string, unknown>,
165
+ ): ModuleDefinitionLike | undefined {
166
+ const keys = ['module', 'moduleDefinition', 'default', 'backendModule'];
167
+ for (const key of keys) {
168
+ if (key in exports) {
169
+ const value = exports[key as keyof typeof exports];
170
+ if (value && typeof value === 'object') {
171
+ return value as ModuleDefinitionLike;
172
+ }
163
173
  }
164
- return undefined;
174
+ }
175
+ return undefined;
165
176
  }
166
177
 
167
178
  function normalizePath(value: string): string {
168
- let s = value.trim();
169
- if (!s.startsWith('/')) {
170
- s = `/${s}`;
171
- }
172
- if (s.length > 1 && s.endsWith('/')) {
173
- s = s.slice(0, -1);
174
- }
175
- return s;
179
+ let s = value.trim();
180
+ if (!s.startsWith('/')) {
181
+ s = `/${s}`;
182
+ }
183
+ if (s.length > 1 && s.endsWith('/')) {
184
+ s = s.slice(0, -1);
185
+ }
186
+ return s;
187
+ }
188
+
189
+ function formatErrorMessage(error: unknown): string {
190
+ if (error instanceof Error && error.message) {
191
+ return error.message;
192
+ }
193
+
194
+ return String(error);
176
195
  }
177
196
 
178
197
  function firstPathSegment(pathname: string): string | undefined {
179
- const [, segment] = pathname.split('/');
180
- if (!segment) {
181
- return undefined;
182
- }
183
- return segment;
198
+ const [, segment] = pathname.split('/');
199
+ if (!segment) {
200
+ return undefined;
201
+ }
202
+ return segment;
184
203
  }
185
204
 
186
205
  function deriveRouteParams(template: string, actual: string): Record<string, string> | null {
187
- if (!template || !actual) {
188
- return {};
189
- }
206
+ if (!template || !actual) {
207
+ return {};
208
+ }
190
209
 
191
- const templateSegments = template.split('/').filter(Boolean);
192
- const actualSegments = actual.split('/').filter(Boolean);
210
+ const templateSegments = template.split('/').filter(Boolean);
211
+ const actualSegments = actual.split('/').filter(Boolean);
193
212
 
194
- if (templateSegments.length !== actualSegments.length) {
195
- return null;
196
- }
213
+ if (templateSegments.length !== actualSegments.length) {
214
+ return null;
215
+ }
197
216
 
198
- const params: Record<string, string> = {};
199
-
200
- for (let i = 0; i < templateSegments.length; i++) {
201
- const templateSegment = templateSegments[i];
202
- const actualSegment = actualSegments[i];
203
-
204
- if (templateSegment.startsWith(':')) {
205
- const key = templateSegment.slice(1);
206
- if (!key) {
207
- return null;
208
- }
209
- params[key] = decodeURIComponent(actualSegment);
210
- } else if (templateSegment !== actualSegment) {
211
- return null;
212
- }
217
+ const params: Record<string, string> = {};
218
+
219
+ for (let i = 0; i < templateSegments.length; i++) {
220
+ const templateSegment = templateSegments[i];
221
+ const actualSegment = actualSegments[i];
222
+
223
+ if (templateSegment.startsWith(':')) {
224
+ const key = templateSegment.slice(1);
225
+ if (!key) {
226
+ return null;
227
+ }
228
+ params[key] = decodeURIComponent(actualSegment);
229
+ } else if (templateSegment !== actualSegment) {
230
+ return null;
213
231
  }
232
+ }
214
233
 
215
- return params;
234
+ return params;
216
235
  }
217
236
 
218
237
  function createMinimalSsrContext(pathname: string, params: Record<string, string>): unknown {
219
- const url = new URL(`http://localhost${pathname}`);
220
-
221
- const envAccessor = {
222
- get(name: string): string | undefined {
223
- return process.env[name];
224
- },
225
- require(name: string): string {
226
- const value = process.env[name];
227
- if (value === undefined) {
228
- throw new Error(`Missing required env variable ${name} for SSG view rendering.`);
229
- }
230
- return value;
231
- },
232
- entries(): Record<string, string | undefined> {
233
- return process.env as Record<string, string | undefined>;
234
- }
235
- };
236
-
237
- const logger = {
238
- level: 'info',
239
- log(_level: string, _message: string, _metadata?: Record<string, unknown>): void {
240
- // no-op for SSG
241
- },
242
- debug(_message: string, _metadata?: Record<string, unknown>): void {
243
- // no-op for SSG
244
- },
245
- info(_message: string, _metadata?: Record<string, unknown>): void {
246
- // no-op for SSG
247
- },
248
- warn(_message: string, _metadata?: Record<string, unknown>): void {
249
- // no-op for SSG
250
- },
251
- error(_message: string, _metadata?: Record<string, unknown>): void {
252
- // no-op for SSG
253
- },
254
- with(_bindings: Record<string, unknown>) {
255
- return this;
256
- }
257
- };
258
-
259
- return {
260
- url,
261
- params,
262
- cookies: {},
263
- headers: {},
264
- auth: undefined,
265
- session: null,
266
- env: envAccessor,
267
- logger,
268
- now: () => new Date()
269
- };
238
+ const url = new URL(`http://localhost${pathname}`);
239
+
240
+ const envAccessor = {
241
+ get(name: string): string | undefined {
242
+ return process.env[name];
243
+ },
244
+ require(name: string): string {
245
+ const value = process.env[name];
246
+ if (value === undefined) {
247
+ throw new Error(`Missing required env variable ${name} for SSG view rendering.`);
248
+ }
249
+ return value;
250
+ },
251
+ entries(): Record<string, string | undefined> {
252
+ return process.env as Record<string, string | undefined>;
253
+ },
254
+ };
255
+
256
+ const logger = {
257
+ level: 'info',
258
+ log(_level: string, _message: string, _metadata?: Record<string, unknown>): void {
259
+ // no-op for SSG
260
+ },
261
+ debug(_message: string, _metadata?: Record<string, unknown>): void {
262
+ // no-op for SSG
263
+ },
264
+ info(_message: string, _metadata?: Record<string, unknown>): void {
265
+ // no-op for SSG
266
+ },
267
+ warn(_message: string, _metadata?: Record<string, unknown>): void {
268
+ // no-op for SSG
269
+ },
270
+ error(_message: string, _metadata?: Record<string, unknown>): void {
271
+ // no-op for SSG
272
+ },
273
+ with(_bindings: Record<string, unknown>) {
274
+ return this;
275
+ },
276
+ };
277
+
278
+ return {
279
+ url,
280
+ params,
281
+ cookies: {},
282
+ headers: {},
283
+ auth: undefined,
284
+ session: null,
285
+ env: envAccessor,
286
+ logger,
287
+ now: () => new Date(),
288
+ };
270
289
  }
271
290
 
272
291
  function getEffectiveStaticPaths(
273
- meta: WorkspaceModuleView | undefined,
274
- definition: ViewDefinitionLike,
275
- isSsgWorkspace: boolean
292
+ meta: WorkspaceModuleView | undefined,
293
+ definition: ViewDefinitionLike,
294
+ isSsgWorkspace: boolean,
276
295
  ): readonly string[] {
277
- const explicit = meta?.staticPaths ?? definition.staticPaths ?? [];
278
- if (Array.isArray(explicit) && explicit.length > 0) {
279
- return explicit;
280
- }
296
+ const explicit = meta?.staticPaths ?? definition.staticPaths ?? [];
297
+ if (Array.isArray(explicit) && explicit.length > 0) {
298
+ return explicit;
299
+ }
281
300
 
282
- if (!isSsgWorkspace) {
283
- return [];
284
- }
301
+ if (!isSsgWorkspace) {
302
+ return [];
303
+ }
285
304
 
286
- const candidate = meta?.path ?? definition.path ?? '';
287
- if (!isDefaultStaticPathCandidate(candidate)) {
288
- return [];
289
- }
305
+ const candidate = meta?.path ?? definition.path ?? '';
306
+ if (!isDefaultStaticPathCandidate(candidate)) {
307
+ return [];
308
+ }
290
309
 
291
- return [candidate];
310
+ return [candidate];
292
311
  }
293
312
 
294
313
  function isDefaultStaticPathCandidate(template: string): boolean {
295
- if (typeof template !== 'string') {
296
- return false;
297
- }
314
+ if (typeof template !== 'string') {
315
+ return false;
316
+ }
298
317
 
299
- const trimmed = template.trim();
300
- if (!trimmed.startsWith('/')) {
301
- return false;
302
- }
318
+ const trimmed = template.trim();
319
+ if (!trimmed.startsWith('/')) {
320
+ return false;
321
+ }
303
322
 
304
- if (trimmed.includes(':') || trimmed.includes('*')) {
305
- return false;
306
- }
323
+ if (trimmed.includes(':') || trimmed.includes('*')) {
324
+ return false;
325
+ }
307
326
 
308
- return true;
327
+ return true;
309
328
  }