@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
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
readonly version: 1;
|
|
12
|
+
readonly paths: FrontendPathConfig;
|
|
13
|
+
readonly features: FrontendFeatureFlags;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface EnableFlags {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
}
|
package/src/utils/changedFile.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const changed = context.changedFile;
|
|
11
|
+
if (!changed) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const normalizedChanged = path.resolve(changed);
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if (!rule.extensions || rule.extensions.length === 0) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
27
|
+
const extension = path.extname(normalizedChanged).toLowerCase();
|
|
28
|
+
if (rule.extensions.includes(extension)) {
|
|
29
|
+
return true;
|
|
31
30
|
}
|
|
31
|
+
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
return false;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function isPathInside(target: string, directory: string): boolean {
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
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(
|
|
8
|
-
|
|
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(
|
|
12
|
-
|
|
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
|
-
|
|
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(
|
|
20
|
-
|
|
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(
|
|
24
|
-
|
|
65
|
+
export async function stat(targetPath: string): Promise<Stats> {
|
|
66
|
+
return await statFs(targetPath);
|
|
25
67
|
}
|
|
26
68
|
|
|
27
|
-
export async function readJson<T>(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
39
|
-
|
|
84
|
+
export async function readFile(targetPath: string): Promise<string> {
|
|
85
|
+
return await getBunRuntime().file(targetPath).text();
|
|
40
86
|
}
|
|
41
87
|
|
|
42
|
-
export async function
|
|
43
|
-
|
|
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(
|
|
47
|
-
|
|
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
|
}
|