@webstir-io/webstir-frontend 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/assets/assetManifest.d.ts +16 -0
  4. package/dist/assets/assetManifest.js +31 -0
  5. package/dist/assets/imageOptimizer.d.ts +6 -0
  6. package/dist/assets/imageOptimizer.js +93 -0
  7. package/dist/assets/precompression.d.ts +1 -0
  8. package/dist/assets/precompression.js +21 -0
  9. package/dist/builders/contentBuilder.d.ts +2 -0
  10. package/dist/builders/contentBuilder.js +1052 -0
  11. package/dist/builders/cssBuilder.d.ts +2 -0
  12. package/dist/builders/cssBuilder.js +439 -0
  13. package/dist/builders/htmlBuilder.d.ts +2 -0
  14. package/dist/builders/htmlBuilder.js +430 -0
  15. package/dist/builders/index.d.ts +2 -0
  16. package/dist/builders/index.js +14 -0
  17. package/dist/builders/jsBuilder.d.ts +2 -0
  18. package/dist/builders/jsBuilder.js +300 -0
  19. package/dist/builders/staticAssetsBuilder.d.ts +2 -0
  20. package/dist/builders/staticAssetsBuilder.js +158 -0
  21. package/dist/builders/types.d.ts +12 -0
  22. package/dist/builders/types.js +1 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +105 -0
  25. package/dist/config/manifest.d.ts +7 -0
  26. package/dist/config/manifest.js +17 -0
  27. package/dist/config/paths.d.ts +3 -0
  28. package/dist/config/paths.js +11 -0
  29. package/dist/config/schema.d.ts +413 -0
  30. package/dist/config/schema.js +44 -0
  31. package/dist/config/setup.d.ts +2 -0
  32. package/dist/config/setup.js +12 -0
  33. package/dist/config/workspace.d.ts +2 -0
  34. package/dist/config/workspace.js +131 -0
  35. package/dist/config/workspaceManifest.d.ts +23 -0
  36. package/dist/config/workspaceManifest.js +1 -0
  37. package/dist/core/constants.d.ts +70 -0
  38. package/dist/core/constants.js +70 -0
  39. package/dist/core/diagnostics.d.ts +15 -0
  40. package/dist/core/diagnostics.js +21 -0
  41. package/dist/core/index.d.ts +3 -0
  42. package/dist/core/index.js +3 -0
  43. package/dist/core/pages.d.ts +6 -0
  44. package/dist/core/pages.js +23 -0
  45. package/dist/hooks.d.ts +19 -0
  46. package/dist/hooks.js +115 -0
  47. package/dist/html/criticalCss.d.ts +4 -0
  48. package/dist/html/criticalCss.js +192 -0
  49. package/dist/html/htmlSecurity.d.ts +5 -0
  50. package/dist/html/htmlSecurity.js +73 -0
  51. package/dist/html/lazyLoad.d.ts +6 -0
  52. package/dist/html/lazyLoad.js +21 -0
  53. package/dist/html/pageScaffold.d.ts +10 -0
  54. package/dist/html/pageScaffold.js +51 -0
  55. package/dist/html/resourceHints.d.ts +7 -0
  56. package/dist/html/resourceHints.js +64 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +5 -0
  59. package/dist/modes/ssg/index.d.ts +4 -0
  60. package/dist/modes/ssg/index.js +4 -0
  61. package/dist/modes/ssg/metadata.d.ts +5 -0
  62. package/dist/modes/ssg/metadata.js +50 -0
  63. package/dist/modes/ssg/routing.d.ts +2 -0
  64. package/dist/modes/ssg/routing.js +186 -0
  65. package/dist/modes/ssg/seo.d.ts +4 -0
  66. package/dist/modes/ssg/seo.js +208 -0
  67. package/dist/modes/ssg/validation.d.ts +3 -0
  68. package/dist/modes/ssg/validation.js +27 -0
  69. package/dist/modes/ssg/views.d.ts +2 -0
  70. package/dist/modes/ssg/views.js +236 -0
  71. package/dist/operations.d.ts +5 -0
  72. package/dist/operations.js +102 -0
  73. package/dist/pipeline.d.ts +7 -0
  74. package/dist/pipeline.js +71 -0
  75. package/dist/provider.d.ts +2 -0
  76. package/dist/provider.js +176 -0
  77. package/dist/types.d.ts +61 -0
  78. package/dist/types.js +1 -0
  79. package/dist/utils/changedFile.d.ts +8 -0
  80. package/dist/utils/changedFile.js +26 -0
  81. package/dist/utils/fs.d.ts +11 -0
  82. package/dist/utils/fs.js +39 -0
  83. package/dist/utils/hash.d.ts +1 -0
  84. package/dist/utils/hash.js +5 -0
  85. package/dist/utils/pagePaths.d.ts +5 -0
  86. package/dist/utils/pagePaths.js +36 -0
  87. package/dist/utils/pathMatch.d.ts +3 -0
  88. package/dist/utils/pathMatch.js +29 -0
  89. package/dist/watch/frontendFiles.d.ts +3 -0
  90. package/dist/watch/frontendFiles.js +25 -0
  91. package/dist/watch/hotUpdateTracker.d.ts +51 -0
  92. package/dist/watch/hotUpdateTracker.js +205 -0
  93. package/dist/watch/pipelineHelpers.d.ts +26 -0
  94. package/dist/watch/pipelineHelpers.js +177 -0
  95. package/dist/watch/types.d.ts +27 -0
  96. package/dist/watch/types.js +1 -0
  97. package/dist/watch/watchCoordinator.d.ts +36 -0
  98. package/dist/watch/watchCoordinator.js +551 -0
  99. package/dist/watch/watchDaemon.d.ts +17 -0
  100. package/dist/watch/watchDaemon.js +127 -0
  101. package/dist/watch/watchReporter.d.ts +21 -0
  102. package/dist/watch/watchReporter.js +64 -0
  103. package/package.json +92 -0
  104. package/scripts/publish.sh +101 -0
  105. package/scripts/smoke.mjs +35 -0
  106. package/scripts/update-contract.sh +121 -0
  107. package/src/assets/assetManifest.ts +51 -0
  108. package/src/assets/imageOptimizer.ts +112 -0
  109. package/src/assets/precompression.ts +25 -0
  110. package/src/builders/contentBuilder.ts +1400 -0
  111. package/src/builders/cssBuilder.ts +552 -0
  112. package/src/builders/htmlBuilder.ts +540 -0
  113. package/src/builders/index.ts +16 -0
  114. package/src/builders/jsBuilder.ts +358 -0
  115. package/src/builders/staticAssetsBuilder.ts +174 -0
  116. package/src/builders/types.ts +15 -0
  117. package/src/cli.ts +108 -0
  118. package/src/config/manifest.ts +24 -0
  119. package/src/config/paths.ts +14 -0
  120. package/src/config/schema.ts +49 -0
  121. package/src/config/setup.ts +14 -0
  122. package/src/config/workspace.ts +150 -0
  123. package/src/config/workspaceManifest.ts +27 -0
  124. package/src/core/constants.ts +73 -0
  125. package/src/core/diagnostics.ts +40 -0
  126. package/src/core/index.ts +3 -0
  127. package/src/core/pages.ts +31 -0
  128. package/src/hooks.ts +175 -0
  129. package/src/html/criticalCss.ts +214 -0
  130. package/src/html/htmlSecurity.ts +86 -0
  131. package/src/html/lazyLoad.ts +30 -0
  132. package/src/html/pageScaffold.ts +70 -0
  133. package/src/html/resourceHints.ts +91 -0
  134. package/src/index.ts +5 -0
  135. package/src/modes/ssg/index.ts +4 -0
  136. package/src/modes/ssg/metadata.ts +63 -0
  137. package/src/modes/ssg/routing.ts +230 -0
  138. package/src/modes/ssg/seo.ts +261 -0
  139. package/src/modes/ssg/validation.ts +37 -0
  140. package/src/modes/ssg/views.ts +309 -0
  141. package/src/operations.ts +138 -0
  142. package/src/pipeline.ts +88 -0
  143. package/src/provider.ts +249 -0
  144. package/src/types.ts +67 -0
  145. package/src/utils/changedFile.ts +39 -0
  146. package/src/utils/fs.ts +48 -0
  147. package/src/utils/hash.ts +6 -0
  148. package/src/utils/pagePaths.ts +43 -0
  149. package/src/utils/pathMatch.ts +36 -0
  150. package/src/watch/frontendFiles.ts +32 -0
  151. package/src/watch/hotUpdateTracker.ts +285 -0
  152. package/src/watch/pipelineHelpers.ts +242 -0
  153. package/src/watch/types.ts +23 -0
  154. package/src/watch/watchCoordinator.ts +666 -0
  155. package/src/watch/watchDaemon.ts +144 -0
  156. package/src/watch/watchReporter.ts +98 -0
  157. package/tests/add-page-defaults.test.js +64 -0
  158. package/tests/content-pages.test.js +81 -0
  159. package/tests/css-app-imports.test.js +64 -0
  160. package/tests/css-page-imports.test.js +100 -0
  161. package/tests/diagnostics.test.js +48 -0
  162. package/tests/features.test.js +63 -0
  163. package/tests/hooks.test.js +71 -0
  164. package/tests/provider.integration.test.js +137 -0
  165. package/tests/ssg-defaults.test.js +201 -0
  166. package/tests/ssg-guardrails.test.js +69 -0
  167. package/tsconfig.json +27 -0
@@ -0,0 +1,249 @@
1
+ import path from 'node:path';
2
+ import { readdir } from 'node:fs/promises';
3
+ import fs from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+
6
+ import { glob } from 'glob';
7
+ import type {
8
+ ModuleAsset,
9
+ ModuleArtifact,
10
+ ModuleBuildOptions,
11
+ ModuleBuildResult,
12
+ ModuleDiagnostic,
13
+ ModuleProvider,
14
+ ResolvedModuleWorkspace
15
+ } from '@webstir-io/module-contract';
16
+
17
+ import { runPipeline } from './pipeline.js';
18
+ import type { PipelineMode } from './pipeline.js';
19
+ import { prepareWorkspaceConfig } from './config/setup.js';
20
+ import type { FrontendConfig } from './types.js';
21
+ import { FOLDERS } from './core/constants.js';
22
+ import { pathExists, readJson, remove } from './utils/fs.js';
23
+ import { applySsgRouting, assertNoSsgRoutes, generateSsgViewData } from './modes/ssg/index.js';
24
+
25
+ interface PackageJson {
26
+ readonly name: string;
27
+ readonly version: string;
28
+ readonly engines?: {
29
+ readonly node?: string;
30
+ };
31
+ }
32
+
33
+ const require = createRequire(import.meta.url);
34
+ const pkg = require('../package.json') as PackageJson;
35
+
36
+ function resolveWorkspacePaths(workspaceRoot: string): ResolvedModuleWorkspace {
37
+ return {
38
+ sourceRoot: path.join(workspaceRoot, 'src', 'frontend'),
39
+ buildRoot: path.join(workspaceRoot, 'build', 'frontend'),
40
+ testsRoot: path.join(workspaceRoot, 'src', 'frontend', 'tests')
41
+ };
42
+ }
43
+
44
+ async function buildModule(options: ModuleBuildOptions): Promise<ModuleBuildResult> {
45
+ const config = await prepareWorkspaceConfig(options.workspaceRoot);
46
+ const mode = normalizeMode(options.env?.WEBSTIR_MODULE_MODE);
47
+ const workspaceMode = await readWorkspaceMode(options.workspaceRoot);
48
+ const frontendMode = normalizeFrontendMode(options.env?.WEBSTIR_FRONTEND_MODE);
49
+ const shouldRunSsgPublish =
50
+ mode === 'publish' && (frontendMode === 'ssg' || (frontendMode === undefined && workspaceMode.mode === 'ssg'));
51
+ const publishConfig = shouldRunSsgPublish ? applySsgPublishLayout(config) : config;
52
+
53
+ if (shouldRunSsgPublish) {
54
+ await assertNoSsgRoutes(config.paths.workspace);
55
+ }
56
+ await runPipeline(publishConfig, mode, { changedFile: undefined, enable: workspaceMode.enable });
57
+
58
+ if (shouldRunSsgPublish) {
59
+ await generateSsgViewData(publishConfig);
60
+ await applySsgRouting(publishConfig);
61
+ await removeLegacyPagesFolder(publishConfig);
62
+ }
63
+
64
+ const artifacts = await collectArtifacts(config);
65
+ const manifest = createManifest(config, artifacts, workspaceMode.mode, workspaceMode.isSsg);
66
+
67
+ return {
68
+ artifacts,
69
+ manifest
70
+ };
71
+ }
72
+
73
+ function applySsgPublishLayout(config: FrontendConfig): FrontendConfig {
74
+ const distFrontend = config.paths.dist.frontend;
75
+ const distPages = distFrontend;
76
+ const distContent = path.join(distFrontend, 'docs');
77
+
78
+ return {
79
+ ...config,
80
+ paths: {
81
+ ...config.paths,
82
+ dist: {
83
+ ...config.paths.dist,
84
+ pages: distPages,
85
+ content: distContent
86
+ }
87
+ }
88
+ };
89
+ }
90
+
91
+ async function removeLegacyPagesFolder(config: FrontendConfig): Promise<void> {
92
+ const legacyPagesRoot = path.join(config.paths.dist.frontend, FOLDERS.pages);
93
+ if (legacyPagesRoot === config.paths.dist.pages) {
94
+ return;
95
+ }
96
+
97
+ if (!(await pathExists(legacyPagesRoot))) {
98
+ return;
99
+ }
100
+
101
+ const entries = await readdir(legacyPagesRoot);
102
+ if (entries.length > 0) {
103
+ return;
104
+ }
105
+
106
+ await remove(legacyPagesRoot);
107
+ }
108
+
109
+ function normalizeMode(rawMode: unknown): PipelineMode {
110
+ if (typeof rawMode !== 'string') {
111
+ return 'build';
112
+ }
113
+
114
+ return rawMode.toLowerCase() === 'publish' ? 'publish' : 'build';
115
+ }
116
+
117
+ async function getScaffoldAssets(): Promise<readonly ModuleAsset[]> {
118
+ return [];
119
+ }
120
+
121
+ async function collectArtifacts(config: FrontendConfig): Promise<ModuleArtifact[]> {
122
+ const buildRoot = config.paths.build.frontend;
123
+ const matches = await glob('**/*', {
124
+ cwd: buildRoot,
125
+ nodir: true,
126
+ dot: false
127
+ });
128
+
129
+ return matches.map<ModuleArtifact>((relative) => {
130
+ const absolutePath = path.join(buildRoot, relative);
131
+ const ext = path.extname(relative).toLowerCase();
132
+ const artifactType = ext === '.js' || ext === '.mjs' ? 'bundle' : 'asset';
133
+
134
+ return {
135
+ path: absolutePath,
136
+ type: artifactType
137
+ };
138
+ });
139
+ }
140
+
141
+ interface WorkspaceEnableFlags {
142
+ readonly spa?: boolean;
143
+ readonly clientNav?: boolean;
144
+ readonly backend?: boolean;
145
+ readonly search?: boolean;
146
+ }
147
+
148
+ interface WorkspacePackageJson {
149
+ readonly webstir?: {
150
+ readonly mode?: string;
151
+ readonly enable?: WorkspaceEnableFlags;
152
+ readonly moduleManifest?: {
153
+ readonly views?: ReadonlyArray<{
154
+ readonly renderMode?: string;
155
+ }>;
156
+ };
157
+ };
158
+ }
159
+
160
+ function createManifest(
161
+ config: FrontendConfig,
162
+ assets: readonly ModuleArtifact[],
163
+ workspaceMode?: string,
164
+ isSsgWorkspace?: boolean
165
+ ) {
166
+ const entryPoints: string[] = [];
167
+ const staticAssets: string[] = [];
168
+ const diagnostics: ModuleDiagnostic[] = [];
169
+
170
+ const normalizedMode = workspaceMode?.toLowerCase();
171
+ const isSsg = isSsgWorkspace || normalizedMode === 'ssg';
172
+
173
+ for (const asset of assets) {
174
+ const relativePath = path.relative(config.paths.build.frontend, asset.path);
175
+ const ext = path.extname(relativePath).toLowerCase();
176
+
177
+ if (ext === '.js' || ext === '.mjs') {
178
+ entryPoints.push(relativePath);
179
+ } else if (ext) {
180
+ staticAssets.push(relativePath);
181
+ }
182
+ }
183
+
184
+ if (entryPoints.length === 0) {
185
+ const fallback = path.join(config.paths.build.app, 'index.js');
186
+ if (fs.existsSync(fallback)) {
187
+ entryPoints.push(path.relative(config.paths.build.frontend, fallback));
188
+ } else if (!isSsg) {
189
+ diagnostics.push({
190
+ severity: 'warn',
191
+ message: 'No JavaScript entry points found under build/frontend.'
192
+ });
193
+ }
194
+ }
195
+
196
+ return {
197
+ entryPoints,
198
+ staticAssets,
199
+ diagnostics
200
+ };
201
+ }
202
+
203
+ async function readWorkspaceMode(workspaceRoot: string): Promise<{ mode?: string; isSsg: boolean; enable?: WorkspaceEnableFlags }> {
204
+ const pkgPath = path.join(workspaceRoot, 'package.json');
205
+ const pkg = await readJson<WorkspacePackageJson>(pkgPath);
206
+ const mode = pkg?.webstir?.mode;
207
+ const normalizedMode = typeof mode === 'string' ? mode.toLowerCase() : undefined;
208
+ const views = pkg?.webstir?.moduleManifest?.views;
209
+ const hasSsgView = Array.isArray(views) && views.some(view => view.renderMode?.toLowerCase() === 'ssg');
210
+ return {
211
+ mode,
212
+ isSsg: normalizedMode === 'ssg' || hasSsgView,
213
+ enable: pkg?.webstir?.enable
214
+ };
215
+ }
216
+
217
+ function normalizeFrontendMode(value: unknown): 'bundle' | 'ssg' | undefined {
218
+ if (typeof value !== 'string') {
219
+ return undefined;
220
+ }
221
+
222
+ const normalized = value.trim().toLowerCase();
223
+ return normalized === 'ssg'
224
+ ? 'ssg'
225
+ : normalized === 'bundle'
226
+ ? 'bundle'
227
+ : undefined;
228
+ }
229
+
230
+ export const frontendProvider: ModuleProvider = {
231
+ metadata: {
232
+ id: pkg.name ?? '@webstir-io/webstir-frontend',
233
+ kind: 'frontend',
234
+ version: pkg.version ?? '0.0.0',
235
+ compatibility: {
236
+ minCliVersion: '0.1.0',
237
+ nodeRange: pkg.engines?.node ?? '>=20.18.1'
238
+ }
239
+ },
240
+ resolveWorkspace(options) {
241
+ return resolveWorkspacePaths(options.workspaceRoot);
242
+ },
243
+ async build(options) {
244
+ return await buildModule(options);
245
+ },
246
+ async getScaffoldAssets() {
247
+ return await getScaffoldAssets();
248
+ }
249
+ };
package/src/types.ts ADDED
@@ -0,0 +1,67 @@
1
+ export type FrontendPublishMode = 'bundle' | 'ssg';
2
+
3
+ export interface FrontendCommandOptions {
4
+ readonly workspaceRoot: string;
5
+ readonly changedFile?: string;
6
+ readonly watch?: boolean;
7
+ readonly publishMode?: FrontendPublishMode;
8
+ }
9
+
10
+ export interface FrontendConfig {
11
+ readonly version: 1;
12
+ readonly paths: FrontendPathConfig;
13
+ readonly features: FrontendFeatureFlags;
14
+ }
15
+
16
+ export interface EnableFlags {
17
+ readonly spa?: boolean;
18
+ readonly clientNav?: boolean;
19
+ readonly backend?: boolean;
20
+ readonly search?: boolean;
21
+ readonly contentNav?: boolean;
22
+ }
23
+
24
+ export interface FrontendPathConfig {
25
+ readonly workspace: string;
26
+ readonly src: {
27
+ readonly root: string;
28
+ readonly frontend: string;
29
+ readonly app: string;
30
+ readonly pages: string;
31
+ readonly content: string;
32
+ readonly images: string;
33
+ readonly fonts: string;
34
+ readonly media: string;
35
+ };
36
+ readonly build: {
37
+ readonly root: string;
38
+ readonly frontend: string;
39
+ readonly app: string;
40
+ readonly pages: string;
41
+ readonly content: string;
42
+ readonly images: string;
43
+ readonly fonts: string;
44
+ readonly media: string;
45
+ };
46
+ readonly dist: {
47
+ readonly root: string;
48
+ readonly frontend: string;
49
+ readonly app: string;
50
+ readonly pages: string;
51
+ readonly content: string;
52
+ readonly images: string;
53
+ readonly fonts: string;
54
+ readonly media: string;
55
+ };
56
+ }
57
+
58
+ export interface FrontendFeatureFlags {
59
+ readonly htmlSecurity: boolean;
60
+ readonly imageOptimization: boolean;
61
+ readonly precompression: boolean;
62
+ }
63
+
64
+ export interface AddPageCommandOptions extends FrontendCommandOptions {
65
+ readonly pageName: string;
66
+ readonly ssg?: boolean;
67
+ }
@@ -0,0 +1,39 @@
1
+ import path from 'node:path';
2
+ import type { BuilderContext } from '../builders/types.js';
3
+
4
+ interface Rule {
5
+ readonly directory: string;
6
+ readonly extensions?: readonly string[];
7
+ }
8
+
9
+ export function shouldProcess(context: BuilderContext, rules: readonly Rule[]): boolean {
10
+ const changed = context.changedFile;
11
+ if (!changed) {
12
+ return true;
13
+ }
14
+
15
+ const normalizedChanged = path.resolve(changed);
16
+
17
+ for (const rule of rules) {
18
+ const normalizedDir = path.resolve(rule.directory);
19
+ if (!isPathInside(normalizedChanged, normalizedDir)) {
20
+ continue;
21
+ }
22
+
23
+ if (!rule.extensions || rule.extensions.length === 0) {
24
+ return true;
25
+ }
26
+
27
+ const extension = path.extname(normalizedChanged).toLowerCase();
28
+ if (rule.extensions.includes(extension)) {
29
+ return true;
30
+ }
31
+ }
32
+
33
+ return false;
34
+ }
35
+
36
+ export function isPathInside(target: string, directory: string): boolean {
37
+ const relative = path.relative(directory, target);
38
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
39
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'fs-extra';
2
+
3
+ export async function ensureDir(path: string): Promise<void> {
4
+ await fs.ensureDir(path);
5
+ }
6
+
7
+ export async function emptyDir(path: string): Promise<void> {
8
+ await fs.emptyDir(path);
9
+ }
10
+
11
+ export async function remove(path: string): Promise<void> {
12
+ await fs.remove(path);
13
+ }
14
+
15
+ export async function copy(source: string, destination: string): Promise<void> {
16
+ await fs.copy(source, destination, { overwrite: true, errorOnExist: false });
17
+ }
18
+
19
+ export async function pathExists(path: string): Promise<boolean> {
20
+ return fs.pathExists(path);
21
+ }
22
+
23
+ export async function stat(path: string): Promise<fs.Stats> {
24
+ return fs.stat(path);
25
+ }
26
+
27
+ export async function readJson<T>(path: string): Promise<T | null> {
28
+ try {
29
+ return await fs.readJson(path);
30
+ } catch (error) {
31
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
32
+ return null;
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+
38
+ export async function writeJson(path: string, data: unknown): Promise<void> {
39
+ await fs.writeJson(path, data, { spaces: 2 });
40
+ }
41
+
42
+ export async function readFile(path: string): Promise<string> {
43
+ return fs.readFile(path, 'utf8');
44
+ }
45
+
46
+ export async function writeFile(path: string, contents: string): Promise<void> {
47
+ await fs.outputFile(path, contents, 'utf8');
48
+ }
@@ -0,0 +1,6 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export function hashContent(content: string, length = 8): string {
4
+ const hash = createHash('sha256').update(content).digest('hex');
5
+ return hash.slice(0, length);
6
+ }
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import { FOLDERS, FILES } from '../core/constants.js';
3
+
4
+ export function resolvePagesUrlPrefix(frontendRoot: string, pagesRoot: string): string {
5
+ const relative = path.relative(frontendRoot, pagesRoot).replace(/\\/g, '/');
6
+ if (!relative || relative === '.' || relative.startsWith('..')) {
7
+ return '';
8
+ }
9
+ return `/${trimSlashes(relative)}`;
10
+ }
11
+
12
+ export function isRootPagesLayout(frontendRoot: string, pagesRoot: string): boolean {
13
+ return resolvePagesUrlPrefix(frontendRoot, pagesRoot) === '';
14
+ }
15
+
16
+ export function resolvePageAssetUrl(pagesUrlPrefix: string, pageName: string, fileName: string): string {
17
+ return joinUrl(pagesUrlPrefix, pageName, fileName);
18
+ }
19
+
20
+ export function resolvePageHtmlUrl(pagesUrlPrefix: string, pageName: string, useRootIndex: boolean): string {
21
+ if (useRootIndex && pageName === FOLDERS.home) {
22
+ return `/${FILES.indexHtml}`;
23
+ }
24
+ return joinUrl(pagesUrlPrefix, pageName, FILES.indexHtml);
25
+ }
26
+
27
+ export function resolvePageHtmlDir(pagesRoot: string, pageName: string, useRootIndex: boolean): string {
28
+ if (useRootIndex && pageName === FOLDERS.home) {
29
+ return pagesRoot;
30
+ }
31
+ return path.join(pagesRoot, pageName);
32
+ }
33
+
34
+ function joinUrl(...segments: string[]): string {
35
+ const cleaned = segments
36
+ .map(segment => trimSlashes(segment))
37
+ .filter(segment => segment.length > 0);
38
+ return `/${cleaned.join('/')}`;
39
+ }
40
+
41
+ function trimSlashes(value: string): string {
42
+ return value.replace(/^\/+|\/+$/g, '');
43
+ }
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+
3
+ export function isInsideDirectory(filePath: string, directory: string): boolean {
4
+ const resolvedFile = path.resolve(filePath);
5
+ const resolvedDirectory = path.resolve(directory);
6
+ const relative = path.relative(resolvedDirectory, resolvedFile);
7
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
8
+ }
9
+
10
+ export function findPageFromChangedFile(changedFile: string | undefined, pagesRoot: string): string | null {
11
+ if (!changedFile) {
12
+ return null;
13
+ }
14
+
15
+ const resolvedChanged = path.resolve(changedFile);
16
+ const resolvedPagesRoot = path.resolve(pagesRoot);
17
+ if (!isInsideDirectory(resolvedChanged, resolvedPagesRoot)) {
18
+ return null;
19
+ }
20
+
21
+ const relative = path.relative(resolvedPagesRoot, resolvedChanged);
22
+ const segments = relative.split(path.sep);
23
+ return segments.length > 0 && segments[0] ? segments[0] : null;
24
+ }
25
+
26
+ export function relativePathWithin(filePath: string | undefined, directory: string): string | null {
27
+ if (!filePath) {
28
+ return null;
29
+ }
30
+
31
+ if (!isInsideDirectory(filePath, directory)) {
32
+ return null;
33
+ }
34
+
35
+ return path.relative(path.resolve(directory), path.resolve(filePath));
36
+ }
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { ensureDir, pathExists, copy } from '../utils/fs.js';
3
+ import { FILES, EXTENSIONS } from '../core/constants.js';
4
+ import type { EnableFlags, FrontendConfig } from '../types.js';
5
+
6
+ export async function resolveEntryPoint(pageDirectory: string): Promise<string | null> {
7
+ const candidates = [`${FILES.index}${EXTENSIONS.ts}`, `${FILES.index}.tsx`, `${FILES.index}${EXTENSIONS.js}`, `${FILES.index}.jsx`];
8
+
9
+ for (const candidate of candidates) {
10
+ const file = path.join(pageDirectory, candidate);
11
+ if (await pathExists(file)) {
12
+ return file;
13
+ }
14
+ }
15
+
16
+ return null;
17
+ }
18
+
19
+ export async function copyRefreshScript(config: FrontendConfig, enable?: EnableFlags): Promise<void> {
20
+ const runtimeScripts: string[] = [FILES.refreshJs, FILES.hmrJs];
21
+
22
+ for (const scriptName of runtimeScripts) {
23
+ const source = path.join(config.paths.src.app, scriptName);
24
+ if (!(await pathExists(source))) {
25
+ continue;
26
+ }
27
+
28
+ const destination = path.join(config.paths.build.frontend, scriptName);
29
+ await ensureDir(path.dirname(destination));
30
+ await copy(source, destination);
31
+ }
32
+ }