@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
@@ -0,0 +1,325 @@
1
+ export type CleanupHandler = () => void | Promise<void>;
2
+
3
+ export interface CleanupScope {
4
+ add(cleanup: CleanupHandler): void;
5
+ dispose(): Promise<void>;
6
+ }
7
+
8
+ export interface ManagedObserver {
9
+ disconnect(): void;
10
+ }
11
+
12
+ export interface BoundaryScope extends CleanupScope {
13
+ mountChild<TState>(boundary: Boundary<TState>, root: Element): Promise<Boundary<TState>>;
14
+ }
15
+
16
+ export interface BoundarySpec<TState = void> {
17
+ mount(root: Element, scope: BoundaryScope): TState | Promise<TState>;
18
+ unmount?(state: TState, scope: BoundaryScope): void | Promise<void>;
19
+ snapshotState?(state: TState): unknown | Promise<unknown>;
20
+ restoreState?(root: Element, scope: BoundaryScope, state: unknown): TState | Promise<TState>;
21
+ }
22
+
23
+ export interface Boundary<TState = void> {
24
+ mount(root: Element): Promise<TState>;
25
+ unmount(): Promise<void>;
26
+ }
27
+
28
+ export function createCleanupScope(): CleanupScope {
29
+ const cleanups: CleanupHandler[] = [];
30
+ let disposed = false;
31
+ let disposePromise: Promise<void> | null = null;
32
+
33
+ return {
34
+ add(cleanup: CleanupHandler): void {
35
+ if (disposed) {
36
+ throw new Error('Cleanup scope has already been disposed.');
37
+ }
38
+
39
+ cleanups.push(cleanup);
40
+ },
41
+ dispose(): Promise<void> {
42
+ if (disposePromise) {
43
+ return disposePromise;
44
+ }
45
+
46
+ disposed = true;
47
+ disposePromise = (async () => {
48
+ let firstError: unknown;
49
+
50
+ for (let index = cleanups.length - 1; index >= 0; index -= 1) {
51
+ const cleanup = cleanups[index];
52
+ if (!cleanup) {
53
+ continue;
54
+ }
55
+
56
+ try {
57
+ await cleanup();
58
+ } catch (error) {
59
+ if (firstError === undefined) {
60
+ firstError = error;
61
+ }
62
+ }
63
+ }
64
+
65
+ cleanups.length = 0;
66
+
67
+ if (firstError !== undefined) {
68
+ throw firstError;
69
+ }
70
+ })();
71
+
72
+ return disposePromise;
73
+ },
74
+ };
75
+ }
76
+
77
+ export function listen(
78
+ scope: CleanupScope,
79
+ target: EventTarget,
80
+ type: string,
81
+ listener: EventListenerOrEventListenerObject,
82
+ options?: boolean | AddEventListenerOptions,
83
+ ): void {
84
+ target.addEventListener(type, listener, options);
85
+ scope.add(() => {
86
+ target.removeEventListener(type, listener, options);
87
+ });
88
+ }
89
+
90
+ export function scheduleTimeout(
91
+ scope: CleanupScope,
92
+ callback: TimerHandler,
93
+ delay = 0,
94
+ ...args: unknown[]
95
+ ): Parameters<typeof clearTimeout>[0] {
96
+ const handle = setTimeout(callback, delay, ...args) as Parameters<typeof clearTimeout>[0];
97
+ scope.add(() => {
98
+ clearTimeout(handle);
99
+ });
100
+ return handle;
101
+ }
102
+
103
+ export function scheduleInterval(
104
+ scope: CleanupScope,
105
+ callback: TimerHandler,
106
+ delay = 0,
107
+ ...args: unknown[]
108
+ ): Parameters<typeof clearInterval>[0] {
109
+ const handle = setInterval(callback, delay, ...args) as Parameters<typeof clearInterval>[0];
110
+ scope.add(() => {
111
+ clearInterval(handle);
112
+ });
113
+ return handle;
114
+ }
115
+
116
+ export function trackObserver<TObserver extends ManagedObserver>(
117
+ scope: CleanupScope,
118
+ observer: TObserver,
119
+ ): TObserver {
120
+ scope.add(() => {
121
+ observer.disconnect();
122
+ });
123
+ return observer;
124
+ }
125
+
126
+ export function createAbortController(scope: CleanupScope): AbortController {
127
+ const controller = new AbortController();
128
+ scope.add(() => {
129
+ controller.abort();
130
+ });
131
+ return controller;
132
+ }
133
+
134
+ function createBoundaryScope() {
135
+ const scope = createCleanupScope();
136
+ const children = new Set<Boundary<unknown>>();
137
+ let disposed = false;
138
+ let disposeChildrenPromise: Promise<void> | null = null;
139
+
140
+ return {
141
+ ...scope,
142
+ async mountChild<TState>(boundary: Boundary<TState>, root: Element): Promise<Boundary<TState>> {
143
+ if (disposed) {
144
+ throw new Error('Boundary scope has already been disposed.');
145
+ }
146
+
147
+ await boundary.mount(root);
148
+ children.delete(boundary);
149
+ children.add(boundary);
150
+ return boundary;
151
+ },
152
+ async disposeChildren(): Promise<void> {
153
+ if (disposeChildrenPromise) {
154
+ return disposeChildrenPromise;
155
+ }
156
+
157
+ disposed = true;
158
+ disposeChildrenPromise = (async () => {
159
+ let firstError: unknown;
160
+
161
+ const orderedChildren = Array.from(children);
162
+ for (let index = orderedChildren.length - 1; index >= 0; index -= 1) {
163
+ const child = orderedChildren[index];
164
+ if (!child) {
165
+ continue;
166
+ }
167
+
168
+ try {
169
+ await child.unmount();
170
+ } catch (error) {
171
+ if (firstError === undefined) {
172
+ firstError = error;
173
+ }
174
+ }
175
+ }
176
+
177
+ children.clear();
178
+
179
+ if (firstError !== undefined) {
180
+ throw firstError;
181
+ }
182
+ })();
183
+
184
+ return disposeChildrenPromise;
185
+ },
186
+ };
187
+ }
188
+
189
+ export function defineBoundary<TState = void>(spec: BoundarySpec<TState>): Boundary<TState> {
190
+ let currentRoot: Element | null = null;
191
+ let currentScope: BoundaryScope | null = null;
192
+ let currentChildScope: ReturnType<typeof createBoundaryScope> | null = null;
193
+ let currentState: TState | undefined;
194
+ let pendingHotState: unknown;
195
+ let hasPendingHotState = false;
196
+ let mountPromise: Promise<TState> | null = null;
197
+ let unmountPromise: Promise<void> | null = null;
198
+
199
+ const reset = (preserveHotState = false): void => {
200
+ currentRoot = null;
201
+ currentScope = null;
202
+ currentChildScope = null;
203
+ currentState = undefined;
204
+ mountPromise = null;
205
+ unmountPromise = null;
206
+ if (!preserveHotState) {
207
+ pendingHotState = undefined;
208
+ hasPendingHotState = false;
209
+ }
210
+ };
211
+
212
+ return {
213
+ async mount(root: Element): Promise<TState> {
214
+ if (currentScope || mountPromise || unmountPromise) {
215
+ throw new Error('Boundary is already mounted.');
216
+ }
217
+
218
+ currentRoot = root;
219
+ currentChildScope = createBoundaryScope();
220
+ currentScope = currentChildScope;
221
+ const scope = currentScope;
222
+ const hotState = hasPendingHotState ? pendingHotState : undefined;
223
+ pendingHotState = undefined;
224
+ hasPendingHotState = false;
225
+
226
+ mountPromise = (async () => {
227
+ try {
228
+ const state =
229
+ hotState !== undefined && spec.restoreState
230
+ ? await spec.restoreState(root, scope, hotState)
231
+ : await spec.mount(root, scope);
232
+ currentState = state;
233
+ return state;
234
+ } catch (error) {
235
+ await currentChildScope?.disposeChildren().catch(() => undefined);
236
+ await scope.dispose().catch(() => undefined);
237
+ reset();
238
+ throw error;
239
+ }
240
+ })();
241
+
242
+ return await mountPromise;
243
+ },
244
+ async unmount(): Promise<void> {
245
+ if (!currentScope) {
246
+ return;
247
+ }
248
+
249
+ if (unmountPromise) {
250
+ return await unmountPromise;
251
+ }
252
+
253
+ const scope = currentScope;
254
+ const childScope = currentChildScope;
255
+ const state = currentState as TState;
256
+ const mountTask = mountPromise;
257
+ const root = currentRoot;
258
+
259
+ unmountPromise = (async () => {
260
+ let firstError: unknown;
261
+ let capturedHotState: unknown;
262
+ let hasCapturedHotState = false;
263
+
264
+ if (mountTask) {
265
+ try {
266
+ await mountTask;
267
+ } catch (error) {
268
+ firstError = error;
269
+ }
270
+ }
271
+
272
+ if (!firstError && spec.snapshotState) {
273
+ try {
274
+ capturedHotState = await spec.snapshotState(state);
275
+ hasCapturedHotState = capturedHotState !== undefined;
276
+ } catch (error) {
277
+ firstError = error;
278
+ }
279
+ }
280
+
281
+ if (childScope) {
282
+ try {
283
+ await childScope.disposeChildren();
284
+ } catch (error) {
285
+ if (firstError === undefined) {
286
+ firstError = error;
287
+ }
288
+ }
289
+ }
290
+
291
+ if (!firstError && spec.unmount && root) {
292
+ try {
293
+ await spec.unmount(state, scope);
294
+ } catch (error) {
295
+ firstError = error;
296
+ }
297
+ }
298
+
299
+ try {
300
+ await scope.dispose();
301
+ } catch (error) {
302
+ if (firstError === undefined) {
303
+ firstError = error;
304
+ }
305
+ }
306
+
307
+ if (firstError === undefined && hasCapturedHotState) {
308
+ pendingHotState = capturedHotState;
309
+ hasPendingHotState = true;
310
+ } else {
311
+ pendingHotState = undefined;
312
+ hasPendingHotState = false;
313
+ }
314
+
315
+ reset(hasCapturedHotState && firstError === undefined);
316
+
317
+ if (firstError !== undefined) {
318
+ throw firstError;
319
+ }
320
+ })();
321
+
322
+ return await unmountPromise;
323
+ },
324
+ };
325
+ }
@@ -0,0 +1 @@
1
+ export * from './boundary.js';
package/src/types.ts CHANGED
@@ -1,67 +1,126 @@
1
1
  export type FrontendPublishMode = 'bundle' | 'ssg';
2
2
 
3
3
  export interface FrontendCommandOptions {
4
- readonly workspaceRoot: string;
5
- readonly changedFile?: string;
6
- readonly watch?: boolean;
7
- readonly publishMode?: FrontendPublishMode;
4
+ readonly workspaceRoot: string;
5
+ readonly changedFile?: string;
6
+ readonly watch?: boolean;
7
+ readonly publishMode?: FrontendPublishMode;
8
8
  }
9
9
 
10
10
  export interface FrontendConfig {
11
- readonly version: 1;
12
- readonly paths: FrontendPathConfig;
13
- readonly features: FrontendFeatureFlags;
11
+ readonly version: 1;
12
+ readonly paths: FrontendPathConfig;
13
+ readonly features: FrontendFeatureFlags;
14
14
  }
15
15
 
16
16
  export interface EnableFlags {
17
- readonly spa?: boolean;
18
- readonly clientNav?: boolean;
19
- readonly backend?: boolean;
20
- readonly search?: boolean;
21
- readonly contentNav?: boolean;
17
+ readonly spa?: boolean;
18
+ readonly clientNav?: boolean;
19
+ readonly backend?: boolean;
20
+ readonly search?: boolean;
21
+ readonly contentNav?: boolean;
22
22
  }
23
23
 
24
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
- };
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
56
  }
57
57
 
58
58
  export interface FrontendFeatureFlags {
59
- readonly htmlSecurity: boolean;
60
- readonly imageOptimization: boolean;
61
- readonly precompression: boolean;
59
+ readonly htmlSecurity: boolean;
60
+ readonly externalResourceIntegrity: boolean;
61
+ readonly imageOptimization: boolean;
62
+ readonly precompression: boolean;
62
63
  }
63
64
 
64
65
  export interface AddPageCommandOptions extends FrontendCommandOptions {
65
- readonly pageName: string;
66
- readonly ssg?: boolean;
66
+ readonly pageName: string;
67
+ readonly ssg?: boolean;
68
+ }
69
+
70
+ export interface FrontendWorkspaceKnownEnableFlags {
71
+ readonly spa: boolean;
72
+ readonly clientNav: boolean;
73
+ readonly backend: boolean;
74
+ readonly search: boolean;
75
+ readonly contentNav: boolean;
76
+ }
77
+
78
+ export interface FrontendWorkspaceEnableFlagsInspect {
79
+ readonly raw?: Record<string, unknown>;
80
+ readonly known: FrontendWorkspaceKnownEnableFlags;
81
+ }
82
+
83
+ export interface FrontendWorkspacePackageInspect {
84
+ readonly path: string;
85
+ readonly exists: boolean;
86
+ readonly mode?: string;
87
+ readonly enable: FrontendWorkspaceEnableFlagsInspect;
88
+ }
89
+
90
+ export interface FrontendWorkspaceAppShellInspect {
91
+ readonly root: string;
92
+ readonly exists: boolean;
93
+ readonly templatePath: string;
94
+ readonly templateExists: boolean;
95
+ readonly stylesheetPath: string;
96
+ readonly stylesheetExists: boolean;
97
+ readonly scriptPath: string;
98
+ readonly scriptExists: boolean;
99
+ }
100
+
101
+ export interface FrontendWorkspacePageInspect {
102
+ readonly name: string;
103
+ readonly directory: string;
104
+ readonly htmlPath: string;
105
+ readonly htmlExists: boolean;
106
+ readonly stylesheetPath: string;
107
+ readonly stylesheetExists: boolean;
108
+ readonly scriptPath: string;
109
+ readonly scriptExists: boolean;
110
+ }
111
+
112
+ export interface FrontendWorkspaceContentInspect {
113
+ readonly root: string;
114
+ readonly exists: boolean;
115
+ readonly sidebarOverridePath: string;
116
+ readonly sidebarOverrideExists: boolean;
117
+ }
118
+
119
+ export interface FrontendWorkspaceInspectResult {
120
+ readonly workspaceRoot: string;
121
+ readonly config: FrontendConfig;
122
+ readonly packageJson: FrontendWorkspacePackageInspect;
123
+ readonly appShell: FrontendWorkspaceAppShellInspect;
124
+ readonly pages: readonly FrontendWorkspacePageInspect[];
125
+ readonly content: FrontendWorkspaceContentInspect;
67
126
  }
@@ -2,38 +2,38 @@ import path from 'node:path';
2
2
  import type { BuilderContext } from '../builders/types.js';
3
3
 
4
4
  interface Rule {
5
- readonly directory: string;
6
- readonly extensions?: readonly string[];
5
+ readonly directory: string;
6
+ readonly extensions?: readonly string[];
7
7
  }
8
8
 
9
9
  export function shouldProcess(context: BuilderContext, rules: readonly Rule[]): boolean {
10
- const changed = context.changedFile;
11
- if (!changed) {
12
- return true;
13
- }
10
+ const changed = context.changedFile;
11
+ if (!changed) {
12
+ return true;
13
+ }
14
14
 
15
- const normalizedChanged = path.resolve(changed);
15
+ const normalizedChanged = path.resolve(changed);
16
16
 
17
- for (const rule of rules) {
18
- const normalizedDir = path.resolve(rule.directory);
19
- if (!isPathInside(normalizedChanged, normalizedDir)) {
20
- continue;
21
- }
17
+ for (const rule of rules) {
18
+ const normalizedDir = path.resolve(rule.directory);
19
+ if (!isPathInside(normalizedChanged, normalizedDir)) {
20
+ continue;
21
+ }
22
22
 
23
- if (!rule.extensions || rule.extensions.length === 0) {
24
- return true;
25
- }
23
+ if (!rule.extensions || rule.extensions.length === 0) {
24
+ return true;
25
+ }
26
26
 
27
- const extension = path.extname(normalizedChanged).toLowerCase();
28
- if (rule.extensions.includes(extension)) {
29
- return true;
30
- }
27
+ const extension = path.extname(normalizedChanged).toLowerCase();
28
+ if (rule.extensions.includes(extension)) {
29
+ return true;
31
30
  }
31
+ }
32
32
 
33
- return false;
33
+ return false;
34
34
  }
35
35
 
36
36
  export function isPathInside(target: string, directory: string): boolean {
37
- const relative = path.relative(directory, target);
38
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
37
+ const relative = path.relative(directory, target);
38
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
39
39
  }
package/src/utils/fs.ts CHANGED
@@ -1,48 +1,95 @@
1
- import fs from 'fs-extra';
1
+ import path from 'node:path';
2
+ import type { Stats } from 'node:fs';
3
+ import { lstat, mkdir, readdir, rm, stat as statFs } from 'node:fs/promises';
2
4
 
3
- export async function ensureDir(path: string): Promise<void> {
4
- await fs.ensureDir(path);
5
+ type BunFileLike = {
6
+ text(): Promise<string>;
7
+ arrayBuffer(): Promise<ArrayBuffer>;
8
+ };
9
+
10
+ interface BunLike {
11
+ file(path: string): BunFileLike;
12
+ write(path: string, data: string | ArrayBufferView | Blob | BunFileLike): Promise<number>;
13
+ }
14
+
15
+ function getBunRuntime(): BunLike {
16
+ const runtime = globalThis as typeof globalThis & { Bun?: BunLike };
17
+ if (typeof runtime.Bun?.file === 'function' && typeof runtime.Bun?.write === 'function') {
18
+ return runtime.Bun;
19
+ }
20
+
21
+ throw new Error('[webstir-frontend] Bun runtime is required for package-level IO.');
22
+ }
23
+
24
+ export async function ensureDir(targetPath: string): Promise<void> {
25
+ await mkdir(targetPath, { recursive: true });
5
26
  }
6
27
 
7
- export async function emptyDir(path: string): Promise<void> {
8
- await fs.emptyDir(path);
28
+ export async function emptyDir(targetPath: string): Promise<void> {
29
+ await rm(targetPath, { recursive: true, force: true });
30
+ await mkdir(targetPath, { recursive: true });
9
31
  }
10
32
 
11
- export async function remove(path: string): Promise<void> {
12
- await fs.remove(path);
33
+ export async function remove(targetPath: string): Promise<void> {
34
+ await rm(targetPath, { recursive: true, force: true });
13
35
  }
14
36
 
15
37
  export async function copy(source: string, destination: string): Promise<void> {
16
- await fs.copy(source, destination, { overwrite: true, errorOnExist: false });
38
+ const sourceInfo = await lstat(source);
39
+
40
+ if (sourceInfo.isDirectory()) {
41
+ await ensureDir(destination);
42
+ const entries = await readdir(source, { withFileTypes: true });
43
+ await Promise.all(
44
+ entries.map((entry) =>
45
+ copy(path.join(source, entry.name), path.join(destination, entry.name)),
46
+ ),
47
+ );
48
+ return;
49
+ }
50
+
51
+ await ensureDir(path.dirname(destination));
52
+ const bun = getBunRuntime();
53
+ await bun.write(destination, bun.file(source));
17
54
  }
18
55
 
19
- export async function pathExists(path: string): Promise<boolean> {
20
- return fs.pathExists(path);
56
+ export async function pathExists(targetPath: string): Promise<boolean> {
57
+ try {
58
+ await statFs(targetPath);
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
21
63
  }
22
64
 
23
- export async function stat(path: string): Promise<fs.Stats> {
24
- return fs.stat(path);
65
+ export async function stat(targetPath: string): Promise<Stats> {
66
+ return await statFs(targetPath);
25
67
  }
26
68
 
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;
69
+ export async function readJson<T>(targetPath: string): Promise<T | null> {
70
+ try {
71
+ return JSON.parse(await readFile(targetPath)) as T;
72
+ } catch (error) {
73
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
74
+ return null;
35
75
  }
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ export async function writeJson(targetPath: string, data: unknown): Promise<void> {
81
+ await writeFile(targetPath, JSON.stringify(data, undefined, 2));
36
82
  }
37
83
 
38
- export async function writeJson(path: string, data: unknown): Promise<void> {
39
- await fs.writeJson(path, data, { spaces: 2 });
84
+ export async function readFile(targetPath: string): Promise<string> {
85
+ return await getBunRuntime().file(targetPath).text();
40
86
  }
41
87
 
42
- export async function readFile(path: string): Promise<string> {
43
- return fs.readFile(path, 'utf8');
88
+ export async function readBinaryFile(targetPath: string): Promise<Uint8Array> {
89
+ return new Uint8Array(await getBunRuntime().file(targetPath).arrayBuffer());
44
90
  }
45
91
 
46
- export async function writeFile(path: string, contents: string): Promise<void> {
47
- await fs.outputFile(path, contents, 'utf8');
92
+ export async function writeFile(targetPath: string, contents: string): Promise<void> {
93
+ await ensureDir(path.dirname(targetPath));
94
+ await getBunRuntime().write(targetPath, contents);
48
95
  }