@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.
- package/README.md +124 -60
- package/dist/assets/imageOptimizer.js +10 -15
- package/dist/assets/precompression.js +1 -1
- package/dist/builders/contentBuilder.js +102 -90
- package/dist/builders/cssBuilder.js +25 -19
- package/dist/builders/htmlBuilder.js +57 -42
- package/dist/builders/index.js +1 -1
- package/dist/builders/jsBuilder.js +219 -76
- package/dist/builders/staticAssetsBuilder.js +27 -9
- package/dist/builders/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -30
- package/dist/config/manifest.js +7 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -6
- package/dist/config/setup.js +1 -1
- package/dist/config/workspace.js +11 -9
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +5 -5
- package/dist/core/diagnostics.js +1 -1
- package/dist/core/pages.js +4 -4
- package/dist/hooks.js +3 -3
- package/dist/html/criticalCss.js +6 -3
- package/dist/html/htmlSecurity.d.ts +6 -1
- package/dist/html/htmlSecurity.js +28 -14
- package/dist/html/lazyLoad.js +1 -1
- package/dist/html/pageScaffold.js +1 -1
- package/dist/html/resourceHints.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +110 -0
- package/dist/modes/ssg/metadata.js +4 -4
- package/dist/modes/ssg/routing.js +2 -5
- package/dist/modes/ssg/seo.js +5 -5
- package/dist/modes/ssg/views.js +17 -11
- package/dist/operations.js +18 -10
- package/dist/pipeline.d.ts +1 -0
- package/dist/pipeline.js +6 -1
- package/dist/provider.js +28 -24
- package/dist/runtime/boundary.d.ts +28 -0
- package/dist/runtime/boundary.js +247 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/utils/fs.d.ts +11 -10
- package/dist/utils/fs.js +48 -20
- package/dist/utils/glob.d.ts +8 -0
- package/dist/utils/glob.js +21 -0
- package/dist/utils/hash.js +1 -2
- package/dist/utils/pagePaths.js +2 -2
- package/package.json +19 -14
- package/scripts/publish.sh +2 -94
- package/scripts/update-contract.sh +12 -10
- package/src/assets/assetManifest.ts +39 -29
- package/src/assets/imageOptimizer.ts +91 -82
- package/src/assets/precompression.ts +22 -16
- package/src/builders/contentBuilder.ts +1224 -1149
- package/src/builders/cssBuilder.ts +466 -417
- package/src/builders/htmlBuilder.ts +511 -448
- package/src/builders/index.ts +7 -7
- package/src/builders/jsBuilder.ts +538 -280
- package/src/builders/staticAssetsBuilder.ts +166 -135
- package/src/builders/types.ts +7 -6
- package/src/cli.ts +66 -90
- package/src/config/manifest.ts +16 -14
- package/src/config/paths.ts +5 -5
- package/src/config/schema.ts +38 -37
- package/src/config/setup.ts +7 -7
- package/src/config/workspace.ts +118 -116
- package/src/config/workspaceManifest.ts +14 -14
- package/src/core/constants.ts +62 -62
- package/src/core/diagnostics.ts +26 -26
- package/src/core/pages.ts +19 -19
- package/src/hooks.ts +128 -118
- package/src/html/criticalCss.ts +84 -77
- package/src/html/htmlSecurity.ts +107 -66
- package/src/html/lazyLoad.ts +22 -19
- package/src/html/pageScaffold.ts +37 -28
- package/src/html/resourceHints.ts +83 -74
- package/src/index.ts +2 -0
- package/src/inspect.ts +158 -0
- package/src/modes/ssg/metadata.ts +53 -51
- package/src/modes/ssg/routing.ts +177 -177
- package/src/modes/ssg/seo.ts +208 -200
- package/src/modes/ssg/validation.ts +31 -25
- package/src/modes/ssg/views.ts +257 -238
- package/src/operations.ts +105 -95
- package/src/pipeline.ts +81 -69
- package/src/provider.ts +184 -176
- package/src/runtime/boundary.ts +325 -0
- package/src/runtime/index.ts +1 -0
- package/src/types.ts +107 -48
- package/src/utils/changedFile.ts +22 -22
- package/src/utils/fs.ts +73 -26
- package/src/utils/glob.ts +38 -0
- package/src/utils/hash.ts +2 -4
- package/src/utils/pagePaths.ts +35 -23
- package/src/utils/pathMatch.ts +26 -23
- package/tests/add-page-defaults.test.js +44 -39
- package/tests/bundlerParity.test.js +252 -0
- package/tests/cli.contract.test.js +13 -0
- package/tests/content-pages.test.js +108 -13
- package/tests/css-app-imports.test.js +22 -11
- package/tests/css-page-imports.test.js +26 -13
- package/tests/diagnostics.test.js +39 -36
- package/tests/features.test.js +48 -43
- package/tests/hooks.test.js +58 -42
- package/tests/htmlSecurity.test.js +66 -0
- package/tests/inspect.test.js +148 -0
- package/tests/provider.integration.test.js +71 -20
- package/tests/runtime.test.js +493 -0
- package/tests/ssg-defaults.test.js +284 -177
- package/tests/ssg-guardrails.test.js +51 -51
- package/tsconfig.json +3 -10
- package/dist/watch/frontendFiles.d.ts +0 -3
- package/dist/watch/frontendFiles.js +0 -25
- package/dist/watch/hotUpdateTracker.d.ts +0 -51
- package/dist/watch/hotUpdateTracker.js +0 -205
- package/dist/watch/pipelineHelpers.d.ts +0 -26
- package/dist/watch/pipelineHelpers.js +0 -177
- package/dist/watch/types.d.ts +0 -27
- package/dist/watch/types.js +0 -1
- package/dist/watch/watchCoordinator.d.ts +0 -36
- package/dist/watch/watchCoordinator.js +0 -551
- package/dist/watch/watchDaemon.d.ts +0 -17
- package/dist/watch/watchDaemon.js +0 -127
- package/dist/watch/watchReporter.d.ts +0 -21
- package/dist/watch/watchReporter.js +0 -64
- package/scripts/smoke.mjs +0 -35
- package/src/watch/frontendFiles.ts +0 -32
- package/src/watch/hotUpdateTracker.ts +0 -285
- package/src/watch/pipelineHelpers.ts +0 -242
- package/src/watch/types.ts +0 -23
- package/src/watch/watchCoordinator.ts +0 -666
- package/src/watch/watchDaemon.ts +0 -144
- 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
|
-
|
|
7
|
-
|
|
6
|
+
readonly workspaceRoot: string;
|
|
7
|
+
readonly pageName: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export async function ensureSsgViewMetadataForPage(options: SsgViewMetadataOptions): Promise<void> {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
if (!value) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
63
65
|
}
|
package/src/modes/ssg/routing.ts
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const pages = await getPageDirectories(distPagesRoot);
|
|
17
|
+
const pageIndexMap = new Map<string, string>();
|
|
18
|
+
const rootIndexPath = path.join(distRoot, FILES.indexHtml);
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
26
|
+
pageIndexMap.set(page.name, sourceIndex);
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
51
|
+
await applyDocsContentAliases(distRoot, distPagesRoot);
|
|
52
|
+
}
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
if (path.resolve(distRoot) === path.resolve(distPagesRoot)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
const docsRoot = path.join(distPagesRoot, 'docs');
|
|
66
|
+
if (!(await pathExists(docsRoot))) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
69
|
|
|
70
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
config: FrontendConfig,
|
|
103
|
+
distRoot: string,
|
|
104
|
+
distPagesRoot: string,
|
|
105
|
+
pageIndexMap: Map<string, string>,
|
|
109
106
|
): Promise<void> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
186
|
+
const [, segment] = pathname.split('/');
|
|
187
|
+
if (!segment) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
return segment;
|
|
194
191
|
}
|
|
195
192
|
|
|
196
|
-
function getEffectiveStaticPaths(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return [
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
if (typeof template !== 'string') {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
const trimmed = template.trim();
|
|
220
|
+
if (!trimmed.startsWith('/')) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
+
return true;
|
|
230
230
|
}
|