@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
@@ -7,168 +7,199 @@ import { optimizeImages } from '../assets/imageOptimizer.js';
7
7
  import { relativePathWithin } from '../utils/pathMatch.js';
8
8
 
9
9
  const IMAGE_EXTENSIONS = [
10
- EXTENSIONS.png,
11
- EXTENSIONS.jpg,
12
- EXTENSIONS.jpeg,
13
- EXTENSIONS.gif,
14
- EXTENSIONS.svg,
15
- EXTENSIONS.webp,
16
- EXTENSIONS.ico
10
+ EXTENSIONS.png,
11
+ EXTENSIONS.jpg,
12
+ EXTENSIONS.jpeg,
13
+ EXTENSIONS.gif,
14
+ EXTENSIONS.svg,
15
+ EXTENSIONS.webp,
16
+ EXTENSIONS.ico,
17
17
  ] as const;
18
18
 
19
19
  const FONT_EXTENSIONS = [
20
- EXTENSIONS.woff,
21
- EXTENSIONS.woff2,
22
- EXTENSIONS.ttf,
23
- EXTENSIONS.otf,
24
- EXTENSIONS.eot
20
+ EXTENSIONS.woff,
21
+ EXTENSIONS.woff2,
22
+ EXTENSIONS.ttf,
23
+ EXTENSIONS.otf,
24
+ EXTENSIONS.eot,
25
25
  ] as const;
26
26
 
27
27
  const MEDIA_EXTENSIONS = [
28
- EXTENSIONS.mp3,
29
- EXTENSIONS.m4a,
30
- EXTENSIONS.wav,
31
- EXTENSIONS.ogg,
32
- EXTENSIONS.mp4,
33
- EXTENSIONS.webm,
34
- EXTENSIONS.mov
28
+ EXTENSIONS.mp3,
29
+ EXTENSIONS.m4a,
30
+ EXTENSIONS.wav,
31
+ EXTENSIONS.ogg,
32
+ EXTENSIONS.mp4,
33
+ EXTENSIONS.webm,
34
+ EXTENSIONS.mov,
35
35
  ] as const;
36
36
 
37
37
  const ALLOW_ALL_ROBOTS = 'User-agent: *\nAllow: /\n';
38
38
 
39
39
  export function createStaticAssetsBuilder(context: BuilderContext): Builder {
40
- return {
41
- name: 'static-assets',
42
- async build(): Promise<void> {
43
- await copyStaticAssets(context, false);
44
- },
45
- async publish(): Promise<void> {
46
- await copyStaticAssets(context, true);
47
- }
48
- };
40
+ return {
41
+ name: 'static-assets',
42
+ async build(): Promise<void> {
43
+ await copyStaticAssets(context, false);
44
+ },
45
+ async publish(): Promise<void> {
46
+ await copyStaticAssets(context, true);
47
+ },
48
+ };
49
49
  }
50
50
 
51
51
  async function copyStaticAssets(context: BuilderContext, isProduction: boolean): Promise<void> {
52
- const { config } = context;
53
- if (!shouldProcess(context, [
54
- { directory: config.paths.src.images, extensions: IMAGE_EXTENSIONS },
55
- { directory: config.paths.src.fonts, extensions: FONT_EXTENSIONS },
56
- { directory: config.paths.src.media, extensions: MEDIA_EXTENSIONS }
57
- ])) {
58
- return;
52
+ const { config } = context;
53
+ if (
54
+ !shouldProcess(context, [
55
+ { directory: config.paths.src.images, extensions: IMAGE_EXTENSIONS },
56
+ { directory: config.paths.src.fonts, extensions: FONT_EXTENSIONS },
57
+ { directory: config.paths.src.media, extensions: MEDIA_EXTENSIONS },
58
+ ])
59
+ ) {
60
+ return;
61
+ }
62
+
63
+ const targets = [
64
+ {
65
+ source: config.paths.src.images,
66
+ build: config.paths.build.frontend,
67
+ dist: config.paths.dist.frontend,
68
+ folder: FOLDERS.images,
69
+ extensions: IMAGE_EXTENSIONS,
70
+ },
71
+ {
72
+ source: config.paths.src.fonts,
73
+ build: config.paths.build.frontend,
74
+ dist: config.paths.dist.frontend,
75
+ folder: FOLDERS.fonts,
76
+ extensions: FONT_EXTENSIONS,
77
+ },
78
+ {
79
+ source: config.paths.src.media,
80
+ build: config.paths.build.frontend,
81
+ dist: config.paths.dist.frontend,
82
+ folder: FOLDERS.media,
83
+ extensions: MEDIA_EXTENSIONS,
84
+ },
85
+ ];
86
+
87
+ for (const target of targets) {
88
+ if (!(await pathExists(target.source))) {
89
+ continue;
59
90
  }
60
91
 
61
- const targets = [
62
- { source: config.paths.src.images, build: config.paths.build.frontend, dist: config.paths.dist.frontend, folder: FOLDERS.images, extensions: IMAGE_EXTENSIONS },
63
- { source: config.paths.src.fonts, build: config.paths.build.frontend, dist: config.paths.dist.frontend, folder: FOLDERS.fonts, extensions: FONT_EXTENSIONS },
64
- { source: config.paths.src.media, build: config.paths.build.frontend, dist: config.paths.dist.frontend, folder: FOLDERS.media, extensions: MEDIA_EXTENSIONS }
65
- ];
66
-
67
- for (const target of targets) {
68
- if (!(await pathExists(target.source))) {
69
- continue;
92
+ const changedRelative = relativePathWithin(context.changedFile, target.source);
93
+ const buildDestination = path.join(target.build, target.folder);
94
+
95
+ if (!context.changedFile || !changedRelative) {
96
+ await emptyDir(buildDestination);
97
+ await copy(target.source, buildDestination);
98
+
99
+ if (isProduction) {
100
+ const distDestination = path.join(target.dist, target.folder);
101
+ if (target.folder === FOLDERS.images) {
102
+ if (config.features.imageOptimization) {
103
+ await optimizeImages(buildDestination, distDestination);
104
+ } else {
105
+ await emptyDir(distDestination);
106
+ await copy(buildDestination, distDestination);
107
+ }
108
+ } else {
109
+ await emptyDir(distDestination);
110
+ await copy(buildDestination, distDestination);
70
111
  }
112
+ }
113
+ continue;
114
+ }
71
115
 
72
- const changedRelative = relativePathWithin(context.changedFile, target.source);
73
- const buildDestination = path.join(target.build, target.folder);
74
-
75
- if (!context.changedFile || !changedRelative) {
76
- await emptyDir(buildDestination);
77
- await copy(target.source, buildDestination);
78
-
79
- if (isProduction) {
80
- const distDestination = path.join(target.dist, target.folder);
81
- if (target.folder === FOLDERS.images) {
82
- if (config.features.imageOptimization) {
83
- await optimizeImages(buildDestination, distDestination);
84
- } else {
85
- await emptyDir(distDestination);
86
- await copy(buildDestination, distDestination);
87
- }
88
- } else {
89
- await emptyDir(distDestination);
90
- await copy(buildDestination, distDestination);
91
- }
92
- }
93
- continue;
94
- }
116
+ await copySingleAsset(target.source, buildDestination, changedRelative);
95
117
 
96
- await copySingleAsset(target.source, buildDestination, changedRelative);
97
-
98
- if (isProduction) {
99
- const distDestination = path.join(target.dist, target.folder);
100
- if (target.folder === FOLDERS.images) {
101
- if (config.features.imageOptimization) {
102
- await optimizeImages(buildDestination, distDestination, [changedRelative]);
103
- } else {
104
- await syncImageWithoutOptimization(buildDestination, distDestination, changedRelative);
105
- }
106
- } else {
107
- const sourcePath = path.join(target.source, changedRelative);
108
- const destPath = path.join(distDestination, changedRelative);
109
- if (await pathExists(sourcePath)) {
110
- await ensureDir(path.dirname(destPath));
111
- await copy(sourcePath, destPath);
112
- } else {
113
- await remove(destPath).catch(() => undefined);
114
- }
115
- }
118
+ if (isProduction) {
119
+ const distDestination = path.join(target.dist, target.folder);
120
+ if (target.folder === FOLDERS.images) {
121
+ if (config.features.imageOptimization) {
122
+ await optimizeImages(buildDestination, distDestination, [changedRelative]);
123
+ } else {
124
+ await syncImageWithoutOptimization(buildDestination, distDestination, changedRelative);
116
125
  }
126
+ } else {
127
+ const sourcePath = path.join(target.source, changedRelative);
128
+ const destPath = path.join(distDestination, changedRelative);
129
+ if (await pathExists(sourcePath)) {
130
+ await ensureDir(path.dirname(destPath));
131
+ await copy(sourcePath, destPath);
132
+ } else {
133
+ await remove(destPath).catch(() => undefined);
134
+ }
135
+ }
117
136
  }
137
+ }
118
138
 
119
- await syncRobotsTxt(config, isProduction);
139
+ await syncRobotsTxt(config, isProduction);
120
140
  }
121
141
 
122
- async function copySingleAsset(sourceRoot: string, buildRoot: string, relativePath: string): Promise<void> {
123
- const sourcePath = path.join(sourceRoot, relativePath);
124
- const destinationPath = path.join(buildRoot, relativePath);
125
-
126
- if (await pathExists(sourcePath)) {
127
- await ensureDir(path.dirname(destinationPath));
128
- await copy(sourcePath, destinationPath);
129
- } else {
130
- await remove(destinationPath).catch(() => undefined);
131
- }
142
+ async function copySingleAsset(
143
+ sourceRoot: string,
144
+ buildRoot: string,
145
+ relativePath: string,
146
+ ): Promise<void> {
147
+ const sourcePath = path.join(sourceRoot, relativePath);
148
+ const destinationPath = path.join(buildRoot, relativePath);
149
+
150
+ if (await pathExists(sourcePath)) {
151
+ await ensureDir(path.dirname(destinationPath));
152
+ await copy(sourcePath, destinationPath);
153
+ } else {
154
+ await remove(destinationPath).catch(() => undefined);
155
+ }
132
156
  }
133
157
 
134
- async function syncImageWithoutOptimization(buildRoot: string, distRoot: string, relativePath: string): Promise<void> {
135
- const sourcePath = path.join(buildRoot, relativePath);
136
- const destinationPath = path.join(distRoot, relativePath);
137
-
138
- if (await pathExists(sourcePath)) {
139
- await ensureDir(path.dirname(destinationPath));
140
- await copy(sourcePath, destinationPath);
141
- } else {
142
- await remove(destinationPath).catch(() => undefined);
143
- }
144
-
145
- await Promise.all([
146
- remove(`${destinationPath}${EXTENSIONS.webp}`).catch(() => undefined),
147
- remove(`${destinationPath}${EXTENSIONS.avif}`).catch(() => undefined)
148
- ]);
158
+ async function syncImageWithoutOptimization(
159
+ buildRoot: string,
160
+ distRoot: string,
161
+ relativePath: string,
162
+ ): Promise<void> {
163
+ const sourcePath = path.join(buildRoot, relativePath);
164
+ const destinationPath = path.join(distRoot, relativePath);
165
+
166
+ if (await pathExists(sourcePath)) {
167
+ await ensureDir(path.dirname(destinationPath));
168
+ await copy(sourcePath, destinationPath);
169
+ } else {
170
+ await remove(destinationPath).catch(() => undefined);
171
+ }
172
+
173
+ await Promise.all([
174
+ remove(`${destinationPath}${EXTENSIONS.webp}`).catch(() => undefined),
175
+ remove(`${destinationPath}${EXTENSIONS.avif}`).catch(() => undefined),
176
+ ]);
149
177
  }
150
178
 
151
- async function syncRobotsTxt(config: BuilderContext['config'], isProduction: boolean): Promise<void> {
152
- const sourcePath = path.join(config.paths.src.frontend, FILES.robotsTxt);
153
- const buildPath = path.join(config.paths.build.frontend, FILES.robotsTxt);
154
-
155
- if (await pathExists(sourcePath)) {
156
- await ensureDir(path.dirname(buildPath));
157
- await copy(sourcePath, buildPath);
158
-
159
- if (isProduction) {
160
- const distPath = path.join(config.paths.dist.frontend, FILES.robotsTxt);
161
- await ensureDir(path.dirname(distPath));
162
- await copy(sourcePath, distPath);
163
- }
164
- } else {
165
- await ensureDir(path.dirname(buildPath));
166
- await writeFile(buildPath, ALLOW_ALL_ROBOTS);
167
-
168
- if (isProduction) {
169
- const distPath = path.join(config.paths.dist.frontend, FILES.robotsTxt);
170
- await ensureDir(path.dirname(distPath));
171
- await writeFile(distPath, ALLOW_ALL_ROBOTS);
172
- }
179
+ async function syncRobotsTxt(
180
+ config: BuilderContext['config'],
181
+ isProduction: boolean,
182
+ ): Promise<void> {
183
+ const sourcePath = path.join(config.paths.src.frontend, FILES.robotsTxt);
184
+ const buildPath = path.join(config.paths.build.frontend, FILES.robotsTxt);
185
+
186
+ if (await pathExists(sourcePath)) {
187
+ await ensureDir(path.dirname(buildPath));
188
+ await copy(sourcePath, buildPath);
189
+
190
+ if (isProduction) {
191
+ const distPath = path.join(config.paths.dist.frontend, FILES.robotsTxt);
192
+ await ensureDir(path.dirname(distPath));
193
+ await copy(sourcePath, distPath);
194
+ }
195
+ } else {
196
+ await ensureDir(path.dirname(buildPath));
197
+ await writeFile(buildPath, ALLOW_ALL_ROBOTS);
198
+
199
+ if (isProduction) {
200
+ const distPath = path.join(config.paths.dist.frontend, FILES.robotsTxt);
201
+ await ensureDir(path.dirname(distPath));
202
+ await writeFile(distPath, ALLOW_ALL_ROBOTS);
173
203
  }
204
+ }
174
205
  }
@@ -1,15 +1,16 @@
1
1
  import type { EnableFlags, FrontendConfig } from '../types.js';
2
2
 
3
3
  export interface BuilderContext {
4
- readonly config: FrontendConfig;
5
- readonly changedFile?: string;
6
- readonly enable?: EnableFlags;
4
+ readonly config: FrontendConfig;
5
+ readonly changedFile?: string;
6
+ readonly enable?: EnableFlags;
7
+ readonly env?: Record<string, string | undefined>;
7
8
  }
8
9
 
9
10
  export interface Builder {
10
- readonly name: string;
11
- build(context: BuilderContext): Promise<void>;
12
- publish(context: BuilderContext): Promise<void>;
11
+ readonly name: string;
12
+ build(context: BuilderContext): Promise<void>;
13
+ publish(context: BuilderContext): Promise<void>;
13
14
  }
14
15
 
15
16
  export type BuilderFactory = (context: BuilderContext) => Builder;
package/src/cli.ts CHANGED
@@ -1,108 +1,84 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  import { Command } from 'commander';
3
3
  import { runAddPage, runBuild, runPublish, runRebuild } from './operations.js';
4
- import { WatchDaemon } from './watch/watchDaemon.js';
5
4
 
6
5
  const program = new Command();
7
6
 
8
- program
9
- .name('webstir-frontend')
10
- .description('Webstir frontend build orchestrator');
11
-
12
- program
13
- .command('build')
14
- .description('Build frontend assets for development workflows')
15
- .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
16
- .option('-c, --changed-file <path>', 'Optional path filter for incremental builds')
17
- .action(async (cmd) => {
18
- try {
19
- await runBuild({
20
- workspaceRoot: cmd.workspace,
21
- changedFile: cmd.changedFile ?? undefined
22
- });
23
- } catch (error) {
24
- handleError(error);
25
- }
26
- });
7
+ program.name('webstir-frontend').description('Webstir frontend build orchestrator');
27
8
 
28
9
  program
29
- .command('publish')
30
- .description('Build production assets into the dist directory')
31
- .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
32
- .option('-m, --mode <mode>', 'Publish mode: bundle or ssg', 'bundle')
33
- .action(async (cmd) => {
34
- try {
35
- await runPublish({
36
- workspaceRoot: cmd.workspace,
37
- publishMode: cmd.mode === 'ssg' ? 'ssg' : 'bundle'
38
- });
39
- } catch (error) {
40
- handleError(error);
41
- }
42
- });
10
+ .command('build')
11
+ .description('Build frontend assets for development workflows')
12
+ .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
13
+ .option('-c, --changed-file <path>', 'Optional path filter for incremental builds')
14
+ .action(async (cmd) => {
15
+ try {
16
+ await runBuild({
17
+ workspaceRoot: cmd.workspace,
18
+ changedFile: cmd.changedFile ?? undefined,
19
+ });
20
+ } catch (error) {
21
+ handleError(error);
22
+ }
23
+ });
43
24
 
44
25
  program
45
- .command('rebuild')
46
- .description('Rebuild frontend assets in response to file changes')
47
- .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
48
- .requiredOption('-c, --changed-file <path>', 'Path to the changed file triggering the rebuild')
49
- .action(async (cmd) => {
50
- try {
51
- await runRebuild({
52
- workspaceRoot: cmd.workspace,
53
- changedFile: cmd.changedFile ?? undefined
54
- });
55
- } catch (error) {
56
- handleError(error);
57
- }
58
- });
26
+ .command('publish')
27
+ .description('Build production assets into the dist directory')
28
+ .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
29
+ .option('-m, --mode <mode>', 'Publish mode: bundle or ssg', 'bundle')
30
+ .action(async (cmd) => {
31
+ try {
32
+ await runPublish({
33
+ workspaceRoot: cmd.workspace,
34
+ publishMode: cmd.mode === 'ssg' ? 'ssg' : 'bundle',
35
+ });
36
+ } catch (error) {
37
+ handleError(error);
38
+ }
39
+ });
59
40
 
60
41
  program
61
- .command('add-page <name>')
62
- .description('Scaffold a new frontend page (HTML/CSS/TS)')
63
- .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
64
- .option('-m, --mode <mode>', 'Page mode: standard or ssg (defaults to ssg when webstir.mode=ssg)')
65
- .action(async (name, cmd) => {
66
- try {
67
- const rawMode = typeof cmd.mode === 'string' ? cmd.mode.toLowerCase() : undefined;
68
- await runAddPage({
69
- workspaceRoot: cmd.workspace,
70
- pageName: name,
71
- ssg: rawMode === 'ssg' ? true : rawMode === 'standard' ? false : undefined
72
- });
73
- } catch (error) {
74
- handleError(error);
75
- }
76
- });
42
+ .command('rebuild')
43
+ .description('Rebuild frontend assets in response to file changes')
44
+ .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
45
+ .requiredOption('-c, --changed-file <path>', 'Path to the changed file triggering the rebuild')
46
+ .action(async (cmd) => {
47
+ try {
48
+ await runRebuild({
49
+ workspaceRoot: cmd.workspace,
50
+ changedFile: cmd.changedFile ?? undefined,
51
+ });
52
+ } catch (error) {
53
+ handleError(error);
54
+ }
55
+ });
77
56
 
78
57
  program
79
- .command('watch-daemon')
80
- .description('Run the persistent frontend watch daemon')
81
- .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
82
- .option('--no-auto-start', 'Defer startup until a start command is received')
83
- .option('-v, --verbose', 'Enable verbose watch diagnostics')
84
- .option('--hmr-verbose', 'Log detailed hot-update diagnostics')
85
- .action(async (cmd) => {
86
- try {
87
- const daemon = new WatchDaemon({
88
- workspaceRoot: cmd.workspace,
89
- autoStart: cmd.autoStart,
90
- verbose: cmd.verbose === true,
91
- hmrVerbose: cmd.hmrVerbose === true
92
- });
93
- await daemon.run();
94
- } catch (error) {
95
- handleError(error);
96
- }
97
- });
58
+ .command('add-page <name>')
59
+ .description('Scaffold a new frontend page (HTML/CSS/TS)')
60
+ .requiredOption('-w, --workspace <path>', 'Absolute path to the workspace root')
61
+ .option('-m, --mode <mode>', 'Page mode: standard or ssg (defaults to ssg when webstir.mode=ssg)')
62
+ .action(async (name, cmd) => {
63
+ try {
64
+ const rawMode = typeof cmd.mode === 'string' ? cmd.mode.toLowerCase() : undefined;
65
+ await runAddPage({
66
+ workspaceRoot: cmd.workspace,
67
+ pageName: name,
68
+ ssg: rawMode === 'ssg' ? true : rawMode === 'standard' ? false : undefined,
69
+ });
70
+ } catch (error) {
71
+ handleError(error);
72
+ }
73
+ });
98
74
 
99
75
  program.parseAsync(process.argv).catch(handleError);
100
76
 
101
77
  function handleError(error: unknown): void {
102
- if (error instanceof Error) {
103
- console.error(error.message);
104
- } else {
105
- console.error('Unknown error', error);
106
- }
107
- process.exitCode = 1;
78
+ if (error instanceof Error) {
79
+ console.error(error.message);
80
+ } else {
81
+ console.error('Unknown error', error);
82
+ }
83
+ process.exitCode = 1;
108
84
  }
@@ -1,24 +1,26 @@
1
- import { promises as fs } from 'fs';
2
- import path from 'path';
1
+ import { rename } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
3
4
  import { frontendConfigSchema, type FrontendConfigInput } from './schema.js';
5
+ import { ensureDir, readFile, writeFile } from '../utils/fs.js';
4
6
 
5
7
  export interface WriteManifestOptions {
6
- readonly outputPath: string;
7
- readonly data: FrontendConfigInput;
8
+ readonly outputPath: string;
9
+ readonly data: FrontendConfigInput;
8
10
  }
9
11
 
10
12
  export async function writeConfigManifest(options: WriteManifestOptions): Promise<void> {
11
- const parsed = frontendConfigSchema.parse(options.data);
12
- const directory = path.dirname(options.outputPath);
13
- await fs.mkdir(directory, { recursive: true });
14
- const serialized = JSON.stringify(parsed, undefined, 2);
15
- const tempPath = path.join(directory, `.webstir-frontend-${process.pid}-${Date.now()}.tmp`);
16
- await fs.writeFile(tempPath, serialized, 'utf8');
17
- await fs.rename(tempPath, options.outputPath);
13
+ const parsed = frontendConfigSchema.parse(options.data);
14
+ const directory = path.dirname(options.outputPath);
15
+ await ensureDir(directory);
16
+ const serialized = JSON.stringify(parsed, undefined, 2);
17
+ const tempPath = path.join(directory, `.webstir-frontend-${process.pid}-${Date.now()}.tmp`);
18
+ await writeFile(tempPath, serialized);
19
+ await rename(tempPath, options.outputPath);
18
20
  }
19
21
 
20
22
  export async function readConfigManifest(manifestPath: string): Promise<FrontendConfigInput> {
21
- const json = await fs.readFile(manifestPath, 'utf8');
22
- const parsed = JSON.parse(json) as unknown;
23
- return frontendConfigSchema.parse(parsed);
23
+ const json = await readFile(manifestPath);
24
+ const parsed = JSON.parse(json) as unknown;
25
+ return frontendConfigSchema.parse(parsed);
24
26
  }
@@ -1,14 +1,14 @@
1
- import path from 'path';
2
- import { promises as fs } from 'fs';
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
3
  import { FOLDERS } from '../core/constants.js';
4
4
 
5
5
  export const FRONTEND_MANIFEST_FILENAME = 'frontend-manifest.json';
6
6
 
7
7
  export function resolveManifestPath(workspaceRoot: string): string {
8
- return path.join(workspaceRoot, FOLDERS.webstir, FRONTEND_MANIFEST_FILENAME);
8
+ return path.join(workspaceRoot, FOLDERS.webstir, FRONTEND_MANIFEST_FILENAME);
9
9
  }
10
10
 
11
11
  export async function ensureWebstirDirectory(workspaceRoot: string): Promise<void> {
12
- const webstirPath = path.join(workspaceRoot, FOLDERS.webstir);
13
- await fs.mkdir(webstirPath, { recursive: true });
12
+ const webstirPath = path.join(workspaceRoot, FOLDERS.webstir);
13
+ await fs.mkdir(webstirPath, { recursive: true });
14
14
  }