@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
package/src/inspect.ts ADDED
@@ -0,0 +1,158 @@
1
+ import path from 'node:path';
2
+
3
+ import { FILES, FILE_NAMES } from './core/constants.js';
4
+ import { getPageDirectories } from './core/pages.js';
5
+ import { buildConfig } from './config/workspace.js';
6
+ import type {
7
+ FrontendWorkspaceAppShellInspect,
8
+ FrontendWorkspaceContentInspect,
9
+ FrontendWorkspaceInspectResult,
10
+ FrontendWorkspaceKnownEnableFlags,
11
+ FrontendWorkspacePackageInspect,
12
+ FrontendWorkspacePageInspect,
13
+ } from './types.js';
14
+ import { pathExists, readJson } from './utils/fs.js';
15
+
16
+ interface WorkspacePackageJson {
17
+ readonly webstir?: {
18
+ readonly mode?: string;
19
+ readonly enable?: Record<string, unknown>;
20
+ };
21
+ }
22
+
23
+ const PAGE_SCRIPT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] as const;
24
+
25
+ export async function inspectFrontendWorkspace(
26
+ workspaceRoot: string,
27
+ ): Promise<FrontendWorkspaceInspectResult> {
28
+ const config = buildConfig(workspaceRoot);
29
+ const packageJson = await readWorkspacePackageInspect(workspaceRoot);
30
+ const appShell = await inspectAppShell(config.paths.src.app);
31
+ const pages = await inspectPages(config.paths.src.pages);
32
+ const content = await inspectContent(config.paths.src.content);
33
+
34
+ return {
35
+ workspaceRoot,
36
+ config,
37
+ packageJson,
38
+ appShell,
39
+ pages,
40
+ content,
41
+ };
42
+ }
43
+
44
+ async function readWorkspacePackageInspect(
45
+ workspaceRoot: string,
46
+ ): Promise<FrontendWorkspacePackageInspect> {
47
+ const packagePath = path.join(workspaceRoot, FILES.packageJson);
48
+ const pkg = await readJson<WorkspacePackageJson>(packagePath);
49
+ const enable = pkg?.webstir?.enable;
50
+
51
+ return {
52
+ path: packagePath,
53
+ exists: pkg !== null,
54
+ mode: pkg?.webstir?.mode,
55
+ enable: {
56
+ raw: enable,
57
+ known: normalizeKnownEnableFlags(enable),
58
+ },
59
+ };
60
+ }
61
+
62
+ async function inspectAppShell(appRoot: string): Promise<FrontendWorkspaceAppShellInspect> {
63
+ const templatePath = path.join(appRoot, FILE_NAMES.htmlAppTemplate);
64
+ const stylesheetPath = path.join(appRoot, 'app.css');
65
+ const scriptPath = await resolveFirstExistingPath(appRoot, 'app', PAGE_SCRIPT_EXTENSIONS);
66
+
67
+ const [exists, templateExists, stylesheetExists, scriptExists] = await Promise.all([
68
+ pathExists(appRoot),
69
+ pathExists(templatePath),
70
+ pathExists(stylesheetPath),
71
+ pathExists(scriptPath),
72
+ ]);
73
+
74
+ return {
75
+ root: appRoot,
76
+ exists,
77
+ templatePath,
78
+ templateExists,
79
+ stylesheetPath,
80
+ stylesheetExists,
81
+ scriptPath,
82
+ scriptExists,
83
+ };
84
+ }
85
+
86
+ async function inspectPages(pagesRoot: string): Promise<readonly FrontendWorkspacePageInspect[]> {
87
+ const pages = await getPageDirectories(pagesRoot);
88
+ return await Promise.all(
89
+ pages.map(async (page) => {
90
+ const htmlPath = path.join(page.directory, FILES.indexHtml);
91
+ const stylesheetPath = path.join(page.directory, `${FILES.index}.css`);
92
+ const scriptPath = await resolveFirstExistingPath(
93
+ page.directory,
94
+ FILES.index,
95
+ PAGE_SCRIPT_EXTENSIONS,
96
+ );
97
+
98
+ const [htmlExists, stylesheetExists, scriptExists] = await Promise.all([
99
+ pathExists(htmlPath),
100
+ pathExists(stylesheetPath),
101
+ pathExists(scriptPath),
102
+ ]);
103
+
104
+ return {
105
+ name: page.name,
106
+ directory: page.directory,
107
+ htmlPath,
108
+ htmlExists,
109
+ stylesheetPath,
110
+ stylesheetExists,
111
+ scriptPath,
112
+ scriptExists,
113
+ };
114
+ }),
115
+ );
116
+ }
117
+
118
+ async function inspectContent(contentRoot: string): Promise<FrontendWorkspaceContentInspect> {
119
+ const sidebarOverridePath = path.join(contentRoot, '_sidebar.json');
120
+ const [exists, sidebarOverrideExists] = await Promise.all([
121
+ pathExists(contentRoot),
122
+ pathExists(sidebarOverridePath),
123
+ ]);
124
+
125
+ return {
126
+ root: contentRoot,
127
+ exists,
128
+ sidebarOverridePath,
129
+ sidebarOverrideExists,
130
+ };
131
+ }
132
+
133
+ async function resolveFirstExistingPath(
134
+ root: string,
135
+ baseName: string,
136
+ extensions: readonly string[],
137
+ ): Promise<string> {
138
+ for (const extension of extensions) {
139
+ const candidate = path.join(root, `${baseName}${extension}`);
140
+ if (await pathExists(candidate)) {
141
+ return candidate;
142
+ }
143
+ }
144
+
145
+ return path.join(root, `${baseName}${extensions[0] ?? ''}`);
146
+ }
147
+
148
+ function normalizeKnownEnableFlags(
149
+ value: Record<string, unknown> | undefined,
150
+ ): FrontendWorkspaceKnownEnableFlags {
151
+ return {
152
+ spa: value?.spa === true,
153
+ clientNav: value?.clientNav === true,
154
+ backend: value?.backend === true,
155
+ search: value?.search === true,
156
+ contentNav: value?.contentNav === true,
157
+ };
158
+ }
@@ -3,61 +3,63 @@ import { readJson, writeJson } from '../../utils/fs.js';
3
3
  import type { WorkspaceModuleView, WorkspacePackageJson } from '../../config/workspaceManifest.js';
4
4
 
5
5
  export interface SsgViewMetadataOptions {
6
- readonly workspaceRoot: string;
7
- readonly pageName: string;
6
+ readonly workspaceRoot: string;
7
+ readonly pageName: string;
8
8
  }
9
9
 
10
10
  export async function ensureSsgViewMetadataForPage(options: SsgViewMetadataOptions): Promise<void> {
11
- const pkgPath = path.join(options.workspaceRoot, 'package.json');
12
- const pkg = (await readJson<WorkspacePackageJson>(pkgPath)) ?? {};
13
- const workspaceMode = pkg.webstir?.mode;
14
- const isSsgWorkspace = typeof workspaceMode === 'string' && workspaceMode.toLowerCase() === 'ssg';
15
- if (isSsgWorkspace) {
16
- return;
17
- }
18
-
19
- const webstir = pkg.webstir ?? {};
20
- const moduleConfig = webstir.moduleManifest ?? {};
21
- const existingViews = Array.isArray(moduleConfig.views) ? [...moduleConfig.views] : [];
22
-
23
- const pageName = options.pageName;
24
- const isHome = pageName === 'home';
25
- const viewName = `${capitalize(pageName)}View`;
26
- const viewPath = isHome ? '/' : `/${pageName}`;
27
-
28
- const existingIndex = existingViews.findIndex((view) => view?.name === viewName || view?.path === viewPath);
29
- const existing = existingIndex >= 0 ? (existingViews[existingIndex] ?? {}) : {};
30
- const nextView: WorkspaceModuleView = {
31
- ...existing,
32
- name: viewName,
33
- path: viewPath,
34
- renderMode: 'ssg',
35
- staticPaths: [viewPath]
36
- };
37
-
38
- if (existingIndex >= 0) {
39
- existingViews[existingIndex] = nextView;
40
- } else {
41
- existingViews.push(nextView);
42
- }
43
-
44
- const nextPkg: WorkspacePackageJson = {
45
- ...pkg,
46
- webstir: {
47
- ...webstir,
48
- moduleManifest: {
49
- ...moduleConfig,
50
- views: existingViews
51
- }
52
- }
53
- };
54
-
55
- await writeJson(pkgPath, nextPkg);
11
+ const pkgPath = path.join(options.workspaceRoot, 'package.json');
12
+ const pkg = (await readJson<WorkspacePackageJson>(pkgPath)) ?? {};
13
+ const workspaceMode = pkg.webstir?.mode;
14
+ const isSsgWorkspace = typeof workspaceMode === 'string' && workspaceMode.toLowerCase() === 'ssg';
15
+ if (isSsgWorkspace) {
16
+ return;
17
+ }
18
+
19
+ const webstir = pkg.webstir ?? {};
20
+ const moduleConfig = webstir.moduleManifest ?? {};
21
+ const existingViews = Array.isArray(moduleConfig.views) ? [...moduleConfig.views] : [];
22
+
23
+ const pageName = options.pageName;
24
+ const isHome = pageName === 'home';
25
+ const viewName = `${capitalize(pageName)}View`;
26
+ const viewPath = isHome ? '/' : `/${pageName}`;
27
+
28
+ const existingIndex = existingViews.findIndex(
29
+ (view) => view?.name === viewName || view?.path === viewPath,
30
+ );
31
+ const existing = existingIndex >= 0 ? (existingViews[existingIndex] ?? {}) : {};
32
+ const nextView: WorkspaceModuleView = {
33
+ ...existing,
34
+ name: viewName,
35
+ path: viewPath,
36
+ renderMode: 'ssg',
37
+ staticPaths: [viewPath],
38
+ };
39
+
40
+ if (existingIndex >= 0) {
41
+ existingViews[existingIndex] = nextView;
42
+ } else {
43
+ existingViews.push(nextView);
44
+ }
45
+
46
+ const nextPkg: WorkspacePackageJson = {
47
+ ...pkg,
48
+ webstir: {
49
+ ...webstir,
50
+ moduleManifest: {
51
+ ...moduleConfig,
52
+ views: existingViews,
53
+ },
54
+ },
55
+ };
56
+
57
+ await writeJson(pkgPath, nextPkg);
56
58
  }
57
59
 
58
60
  function capitalize(value: string): string {
59
- if (!value) {
60
- return value;
61
- }
62
- return value.charAt(0).toUpperCase() + value.slice(1);
61
+ if (!value) {
62
+ return value;
63
+ }
64
+ return value.charAt(0).toUpperCase() + value.slice(1);
63
65
  }
@@ -1,230 +1,230 @@
1
1
  import path from 'node:path';
2
- import { glob } from 'glob';
3
2
  import { FOLDERS, FILES } from '../../core/constants.js';
4
3
  import type { FrontendConfig } from '../../types.js';
5
4
  import { copy, ensureDir, pathExists, readJson } from '../../utils/fs.js';
5
+ import { scanGlob } from '../../utils/glob.js';
6
6
  import { getPageDirectories } from '../../core/pages.js';
7
7
  import { assertNoSsgRoutesInModuleConfig } from './validation.js';
8
8
  import type { WorkspaceModuleView, WorkspacePackageJson } from '../../config/workspaceManifest.js';
9
9
  import { runSsgSeo } from './seo.js';
10
10
 
11
11
  export async function applySsgRouting(config: FrontendConfig): Promise<void> {
12
- const distRoot = config.paths.dist.frontend;
13
- const distPagesRoot = config.paths.dist.pages;
14
- const isRootLayout = path.resolve(distRoot) === path.resolve(distPagesRoot);
12
+ const distRoot = config.paths.dist.frontend;
13
+ const distPagesRoot = config.paths.dist.pages;
14
+ const isRootLayout = path.resolve(distRoot) === path.resolve(distPagesRoot);
15
15
 
16
- const pages = await getPageDirectories(distPagesRoot);
17
- const pageIndexMap = new Map<string, string>();
18
- const rootIndexPath = path.join(distRoot, FILES.indexHtml);
16
+ const pages = await getPageDirectories(distPagesRoot);
17
+ const pageIndexMap = new Map<string, string>();
18
+ const rootIndexPath = path.join(distRoot, FILES.indexHtml);
19
19
 
20
- for (const page of pages) {
21
- const sourceIndex = path.join(page.directory, FILES.indexHtml);
22
- if (!(await pathExists(sourceIndex))) {
23
- continue;
24
- }
25
-
26
- pageIndexMap.set(page.name, sourceIndex);
20
+ for (const page of pages) {
21
+ const sourceIndex = path.join(page.directory, FILES.indexHtml);
22
+ if (!(await pathExists(sourceIndex))) {
23
+ continue;
24
+ }
27
25
 
28
- if (isRootLayout) {
29
- continue;
30
- }
26
+ pageIndexMap.set(page.name, sourceIndex);
31
27
 
32
- // For each page, create a /<page>/index.html alias to its main HTML file when available.
33
- const targetDir = path.join(distRoot, page.name);
34
- await ensureDir(targetDir);
35
- const targetIndex = path.join(targetDir, FILES.indexHtml);
36
- await copy(sourceIndex, targetIndex);
28
+ if (isRootLayout) {
29
+ continue;
37
30
  }
38
31
 
39
- if (isRootLayout) {
40
- if (await pathExists(rootIndexPath)) {
41
- pageIndexMap.set(FOLDERS.home, rootIndexPath);
42
- }
43
- } else {
44
- // Ensure a root index.html that aliases the home page when present.
45
- const homeIndexPath = path.join(distPagesRoot, FOLDERS.home, FILES.indexHtml);
46
- if (await pathExists(homeIndexPath)) {
47
- await ensureDir(path.dirname(rootIndexPath));
48
- await copy(homeIndexPath, rootIndexPath);
49
- }
32
+ // For each page, create a /<page>/index.html alias to its main HTML file when available.
33
+ const targetDir = path.join(distRoot, page.name);
34
+ await ensureDir(targetDir);
35
+ const targetIndex = path.join(targetDir, FILES.indexHtml);
36
+ await copy(sourceIndex, targetIndex);
37
+ }
50
38
 
51
- await applyDocsContentAliases(distRoot, distPagesRoot);
39
+ if (isRootLayout) {
40
+ if (await pathExists(rootIndexPath)) {
41
+ pageIndexMap.set(FOLDERS.home, rootIndexPath);
42
+ }
43
+ } else {
44
+ // Ensure a root index.html that aliases the home page when present.
45
+ const homeIndexPath = path.join(distPagesRoot, FOLDERS.home, FILES.indexHtml);
46
+ if (await pathExists(homeIndexPath)) {
47
+ await ensureDir(path.dirname(rootIndexPath));
48
+ await copy(homeIndexPath, rootIndexPath);
52
49
  }
53
50
 
54
- await applyStaticPathAliases(config, distRoot, distPagesRoot, pageIndexMap);
51
+ await applyDocsContentAliases(distRoot, distPagesRoot);
52
+ }
55
53
 
56
- const siteUrl = await resolveWorkspaceSiteUrl(config.paths.workspace);
57
- await runSsgSeo(distRoot, { siteUrl });
54
+ await applyStaticPathAliases(config, distRoot, distPagesRoot, pageIndexMap);
55
+
56
+ const siteUrl = await resolveWorkspaceSiteUrl(config.paths.workspace);
57
+ await runSsgSeo(distRoot, { siteUrl });
58
58
  }
59
59
 
60
60
  async function applyDocsContentAliases(distRoot: string, distPagesRoot: string): Promise<void> {
61
- if (path.resolve(distRoot) === path.resolve(distPagesRoot)) {
62
- return;
63
- }
61
+ if (path.resolve(distRoot) === path.resolve(distPagesRoot)) {
62
+ return;
63
+ }
64
64
 
65
- const docsRoot = path.join(distPagesRoot, 'docs');
66
- if (!(await pathExists(docsRoot))) {
67
- return;
68
- }
65
+ const docsRoot = path.join(distPagesRoot, 'docs');
66
+ if (!(await pathExists(docsRoot))) {
67
+ return;
68
+ }
69
69
 
70
- const indexes = await glob('docs/**/index.html', {
71
- cwd: distPagesRoot,
72
- nodir: true
73
- });
74
-
75
- for (const relativeIndex of indexes) {
76
- const sourceIndex = path.join(distPagesRoot, relativeIndex);
77
- if (!(await pathExists(sourceIndex))) {
78
- continue;
79
- }
70
+ const indexes = await scanGlob('docs/**/index.html', { cwd: distPagesRoot });
80
71
 
81
- const targetIndex = path.join(distRoot, relativeIndex);
82
- await ensureDir(path.dirname(targetIndex));
83
- await copy(sourceIndex, targetIndex);
72
+ for (const relativeIndex of indexes) {
73
+ const sourceIndex = path.join(distPagesRoot, relativeIndex);
74
+ if (!(await pathExists(sourceIndex))) {
75
+ continue;
84
76
  }
77
+
78
+ const targetIndex = path.join(distRoot, relativeIndex);
79
+ await ensureDir(path.dirname(targetIndex));
80
+ await copy(sourceIndex, targetIndex);
81
+ }
85
82
  }
86
83
 
87
84
  async function resolveWorkspaceSiteUrl(workspaceRoot: string): Promise<string | undefined> {
88
- const fromEnv = process.env.WEBSTIR_SITE_URL?.trim();
89
- if (fromEnv) {
90
- return fromEnv;
91
- }
92
-
93
- const pkgPath = path.join(workspaceRoot, 'package.json');
94
- const pkg = await readJson<Record<string, unknown>>(pkgPath);
95
- const webstir = pkg?.webstir;
96
- if (!webstir || typeof webstir !== 'object') {
97
- return undefined;
98
- }
99
-
100
- const candidate = (webstir as Record<string, unknown>).siteUrl;
101
- return typeof candidate === 'string' && candidate.trim() ? candidate.trim() : undefined;
85
+ const fromEnv = process.env.WEBSTIR_SITE_URL?.trim();
86
+ if (fromEnv) {
87
+ return fromEnv;
88
+ }
89
+
90
+ const pkgPath = path.join(workspaceRoot, 'package.json');
91
+ const pkg = await readJson<Record<string, unknown>>(pkgPath);
92
+ const webstir = pkg?.webstir;
93
+ if (!webstir || typeof webstir !== 'object') {
94
+ return undefined;
95
+ }
96
+
97
+ const candidate = (webstir as Record<string, unknown>).siteUrl;
98
+ return typeof candidate === 'string' && candidate.trim() ? candidate.trim() : undefined;
102
99
  }
103
100
 
104
101
  async function applyStaticPathAliases(
105
- config: FrontendConfig,
106
- distRoot: string,
107
- distPagesRoot: string,
108
- pageIndexMap: Map<string, string>
102
+ config: FrontendConfig,
103
+ distRoot: string,
104
+ distPagesRoot: string,
105
+ pageIndexMap: Map<string, string>,
109
106
  ): Promise<void> {
110
- if (pageIndexMap.size === 0) {
111
- return;
112
- }
113
-
114
- const workspaceRoot = config.paths.workspace;
115
- const pkgPath = path.join(workspaceRoot, 'package.json');
116
- const pkg = await readJson<WorkspacePackageJson>(pkgPath);
117
- const workspaceMode = pkg?.webstir?.mode;
118
- const isSsgWorkspace = typeof workspaceMode === 'string' && workspaceMode.toLowerCase() === 'ssg';
119
- const moduleConfig = pkg?.webstir?.moduleManifest;
120
- assertNoSsgRoutesInModuleConfig(moduleConfig);
121
-
122
- const views = moduleConfig?.views ?? [];
123
- if (views.length === 0) {
124
- return;
125
- }
126
-
127
- for (const view of views) {
128
- const renderMode = view.renderMode ?? (isSsgWorkspace ? 'ssg' : undefined);
129
- if (renderMode !== 'ssg') {
107
+ if (pageIndexMap.size === 0) {
108
+ return;
109
+ }
110
+
111
+ const workspaceRoot = config.paths.workspace;
112
+ const pkgPath = path.join(workspaceRoot, 'package.json');
113
+ const pkg = await readJson<WorkspacePackageJson>(pkgPath);
114
+ const workspaceMode = pkg?.webstir?.mode;
115
+ const isSsgWorkspace = typeof workspaceMode === 'string' && workspaceMode.toLowerCase() === 'ssg';
116
+ const moduleConfig = pkg?.webstir?.moduleManifest;
117
+ assertNoSsgRoutesInModuleConfig(moduleConfig);
118
+
119
+ const views = moduleConfig?.views ?? [];
120
+ if (views.length === 0) {
121
+ return;
122
+ }
123
+
124
+ for (const view of views) {
125
+ const renderMode = view.renderMode ?? (isSsgWorkspace ? 'ssg' : undefined);
126
+ if (renderMode !== 'ssg') {
127
+ continue;
128
+ }
129
+
130
+ const paths = getEffectiveStaticPaths(view, isSsgWorkspace);
131
+ for (const raw of paths) {
132
+ if (typeof raw !== 'string' || raw.length === 0) {
133
+ continue;
134
+ }
135
+
136
+ const normalized = normalizeStaticPath(raw);
137
+ let sourceIndex: string | undefined;
138
+
139
+ if (normalized === '/') {
140
+ sourceIndex = pageIndexMap.get(FOLDERS.home);
141
+ } else {
142
+ const relativePath = normalized.replace(/^\/+/, '');
143
+ const candidate = path.join(distPagesRoot, relativePath, FILES.indexHtml);
144
+ if (await pathExists(candidate)) {
145
+ sourceIndex = candidate;
146
+ } else {
147
+ const pageName = firstPathSegment(normalized);
148
+ if (!pageName) {
130
149
  continue;
150
+ }
151
+ sourceIndex = pageIndexMap.get(pageName);
131
152
  }
153
+ }
132
154
 
133
- const paths = getEffectiveStaticPaths(view, isSsgWorkspace);
134
- for (const raw of paths) {
135
- if (typeof raw !== 'string' || raw.length === 0) {
136
- continue;
137
- }
138
-
139
- const normalized = normalizeStaticPath(raw);
140
- let sourceIndex: string | undefined;
141
-
142
- if (normalized === '/') {
143
- sourceIndex = pageIndexMap.get(FOLDERS.home);
144
- } else {
145
- const relativePath = normalized.replace(/^\/+/, '');
146
- const candidate = path.join(distPagesRoot, relativePath, FILES.indexHtml);
147
- if (await pathExists(candidate)) {
148
- sourceIndex = candidate;
149
- } else {
150
- const pageName = firstPathSegment(normalized);
151
- if (!pageName) {
152
- continue;
153
- }
154
- sourceIndex = pageIndexMap.get(pageName);
155
- }
156
- }
157
-
158
- if (!sourceIndex) {
159
- continue;
160
- }
161
-
162
- const targetIndex =
163
- normalized === '/'
164
- ? path.join(distRoot, FILES.indexHtml)
165
- : path.join(distRoot, normalized.replace(/^\/+/, ''), FILES.indexHtml);
166
-
167
- if (path.resolve(sourceIndex) === path.resolve(targetIndex)) {
168
- continue;
169
- }
170
-
171
- await ensureDir(path.dirname(targetIndex));
172
- await copy(sourceIndex, targetIndex);
173
- }
155
+ if (!sourceIndex) {
156
+ continue;
157
+ }
158
+
159
+ const targetIndex =
160
+ normalized === '/'
161
+ ? path.join(distRoot, FILES.indexHtml)
162
+ : path.join(distRoot, normalized.replace(/^\/+/, ''), FILES.indexHtml);
163
+
164
+ if (path.resolve(sourceIndex) === path.resolve(targetIndex)) {
165
+ continue;
166
+ }
167
+
168
+ await ensureDir(path.dirname(targetIndex));
169
+ await copy(sourceIndex, targetIndex);
174
170
  }
171
+ }
175
172
  }
176
173
 
177
174
  function normalizeStaticPath(value: string): string {
178
- let s = value.trim();
179
- if (!s.startsWith('/')) {
180
- s = `/${s}`;
181
- }
182
- if (s.length > 1 && s.endsWith('/')) {
183
- s = s.slice(0, -1);
184
- }
185
- return s;
175
+ let s = value.trim();
176
+ if (!s.startsWith('/')) {
177
+ s = `/${s}`;
178
+ }
179
+ if (s.length > 1 && s.endsWith('/')) {
180
+ s = s.slice(0, -1);
181
+ }
182
+ return s;
186
183
  }
187
184
 
188
185
  function firstPathSegment(pathname: string): string | undefined {
189
- const [, segment] = pathname.split('/');
190
- if (!segment) {
191
- return undefined;
192
- }
193
- return segment;
186
+ const [, segment] = pathname.split('/');
187
+ if (!segment) {
188
+ return undefined;
189
+ }
190
+ return segment;
194
191
  }
195
192
 
196
- function getEffectiveStaticPaths(view: WorkspaceModuleView, isSsgWorkspace: boolean): readonly string[] {
197
- const explicitPaths = view.staticPaths ?? [];
198
- if (explicitPaths.length > 0) {
199
- return explicitPaths;
200
- }
201
-
202
- if (!isSsgWorkspace) {
203
- return [];
204
- }
205
-
206
- const candidate = view.path ?? '';
207
- if (!isDefaultStaticPathCandidate(candidate)) {
208
- return [];
209
- }
210
-
211
- return [candidate];
193
+ function getEffectiveStaticPaths(
194
+ view: WorkspaceModuleView,
195
+ isSsgWorkspace: boolean,
196
+ ): readonly string[] {
197
+ const explicitPaths = view.staticPaths ?? [];
198
+ if (explicitPaths.length > 0) {
199
+ return explicitPaths;
200
+ }
201
+
202
+ if (!isSsgWorkspace) {
203
+ return [];
204
+ }
205
+
206
+ const candidate = view.path ?? '';
207
+ if (!isDefaultStaticPathCandidate(candidate)) {
208
+ return [];
209
+ }
210
+
211
+ return [candidate];
212
212
  }
213
213
 
214
214
  function isDefaultStaticPathCandidate(template: string): boolean {
215
- if (typeof template !== 'string') {
216
- return false;
217
- }
215
+ if (typeof template !== 'string') {
216
+ return false;
217
+ }
218
218
 
219
- const trimmed = template.trim();
220
- if (!trimmed.startsWith('/')) {
221
- return false;
222
- }
219
+ const trimmed = template.trim();
220
+ if (!trimmed.startsWith('/')) {
221
+ return false;
222
+ }
223
223
 
224
- // Avoid treating parameterized or wildcard templates as a single concrete path.
225
- if (trimmed.includes(':') || trimmed.includes('*')) {
226
- return false;
227
- }
224
+ // Avoid treating parameterized or wildcard templates as a single concrete path.
225
+ if (trimmed.includes(':') || trimmed.includes('*')) {
226
+ return false;
227
+ }
228
228
 
229
- return true;
229
+ return true;
230
230
  }