@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
package/src/core/pages.ts CHANGED
@@ -1,31 +1,31 @@
1
1
  import path from 'node:path';
2
- import { glob } from 'glob';
3
2
  import { pathExists } from '../utils/fs.js';
3
+ import { scanDirectories } from '../utils/glob.js';
4
4
 
5
5
  export interface PageInfo {
6
- readonly name: string;
7
- readonly directory: string;
6
+ readonly name: string;
7
+ readonly directory: string;
8
8
  }
9
9
 
10
10
  export async function getPages(root: string): Promise<PageInfo[]> {
11
- const directories = await getPageDirectories(root);
12
- return directories.map((entry) => ({
13
- name: entry.name,
14
- directory: entry.directory
15
- }));
11
+ const directories = await getPageDirectories(root);
12
+ return directories.map((entry) => ({
13
+ name: entry.name,
14
+ directory: entry.directory,
15
+ }));
16
16
  }
17
17
 
18
18
  export async function getPageDirectories(root: string): Promise<PageInfo[]> {
19
- if (!(await pathExists(root))) {
20
- return [];
21
- }
19
+ if (!(await pathExists(root))) {
20
+ return [];
21
+ }
22
22
 
23
- const entries = await glob('*/', { cwd: root, absolute: false, withFileTypes: false });
24
- return entries.map((entry) => {
25
- const name = entry.endsWith('/') ? entry.slice(0, -1) : entry;
26
- return {
27
- name,
28
- directory: path.join(root, name)
29
- };
30
- });
23
+ const entries = await scanDirectories('*/', { cwd: root, absolute: false });
24
+ return entries.map((entry) => {
25
+ const name = entry.endsWith('/') ? entry.slice(0, -1) : entry;
26
+ return {
27
+ name,
28
+ directory: path.join(root, name),
29
+ };
30
+ });
31
31
  }
package/src/hooks.ts CHANGED
@@ -7,169 +7,179 @@ import type { PipelineMode } from './pipeline.js';
7
7
  const CONFIG_CANDIDATES = ['webstir.config.mjs', 'webstir.config.js', 'webstir.config.cjs'];
8
8
 
9
9
  export interface HookContext {
10
- readonly config: FrontendConfig;
11
- readonly mode: PipelineMode;
12
- readonly workspaceRoot: string;
13
- readonly builderName?: string;
14
- readonly changedFile?: string;
10
+ readonly config: FrontendConfig;
11
+ readonly mode: PipelineMode;
12
+ readonly workspaceRoot: string;
13
+ readonly builderName?: string;
14
+ readonly changedFile?: string;
15
15
  }
16
16
 
17
17
  export type HookHandler = (context: HookContext) => unknown | Promise<unknown>;
18
18
 
19
19
  export interface ResolvedHooks {
20
- readonly pipelineBefore: HookHandler[];
21
- readonly pipelineAfter: HookHandler[];
22
- readonly builderBefore: Map<string, HookHandler[]>;
23
- readonly builderAfter: Map<string, HookHandler[]>;
20
+ readonly pipelineBefore: HookHandler[];
21
+ readonly pipelineAfter: HookHandler[];
22
+ readonly builderBefore: Map<string, HookHandler[]>;
23
+ readonly builderAfter: Map<string, HookHandler[]>;
24
24
  }
25
25
 
26
26
  interface RawHooks {
27
- readonly pipeline?: RawPipelineHooks;
28
- readonly builders?: Record<string, RawBuilderHooks | HookHandler | HookHandler[]>;
27
+ readonly pipeline?: RawPipelineHooks;
28
+ readonly builders?: Record<string, RawBuilderHooks | HookHandler | HookHandler[]>;
29
29
  }
30
30
 
31
31
  interface RawPipelineHooks {
32
- readonly beforeAll?: HookHandler | HookHandler[];
33
- readonly afterAll?: HookHandler | HookHandler[];
32
+ readonly beforeAll?: HookHandler | HookHandler[];
33
+ readonly afterAll?: HookHandler | HookHandler[];
34
34
  }
35
35
 
36
36
  interface RawBuilderHooks {
37
- readonly before?: HookHandler | HookHandler[];
38
- readonly after?: HookHandler | HookHandler[];
37
+ readonly before?: HookHandler | HookHandler[];
38
+ readonly after?: HookHandler | HookHandler[];
39
39
  }
40
40
 
41
41
  const EMPTY_HOOKS: ResolvedHooks = {
42
- pipelineBefore: [],
43
- pipelineAfter: [],
44
- builderBefore: new Map(),
45
- builderAfter: new Map()
42
+ pipelineBefore: [],
43
+ pipelineAfter: [],
44
+ builderBefore: new Map(),
45
+ builderAfter: new Map(),
46
46
  };
47
47
 
48
48
  export async function loadHooks(workspaceRoot: string, cacheBust: boolean): Promise<ResolvedHooks> {
49
- const configPath = findConfigPath(workspaceRoot);
50
- if (!configPath) {
51
- return EMPTY_HOOKS;
52
- }
53
-
54
- let moduleConfig: unknown;
55
- try {
56
- const fileUrl = pathToFileURL(configPath).href;
57
- const url = cacheBust ? `${fileUrl}?update=${Date.now()}` : fileUrl;
58
- moduleConfig = await import(url);
59
- } catch (error) {
60
- const message = error instanceof Error ? error.message : String(error);
61
- throw new Error(`Failed to load hooks from ${configPath}: ${message}`);
62
- }
63
-
64
- const exported = (moduleConfig as Record<string, unknown>)?.default ?? moduleConfig;
65
- if (!exported || typeof exported !== 'object') {
66
- return EMPTY_HOOKS;
67
- }
68
-
69
- const rawHooks = (exported as Record<string, unknown>).hooks ?? exported;
70
- if (!rawHooks || typeof rawHooks !== 'object') {
71
- return EMPTY_HOOKS;
72
- }
73
-
74
- return normalizeHooks(rawHooks as RawHooks, configPath);
49
+ const configPath = findConfigPath(workspaceRoot);
50
+ if (!configPath) {
51
+ return EMPTY_HOOKS;
52
+ }
53
+
54
+ let moduleConfig: unknown;
55
+ try {
56
+ const fileUrl = pathToFileURL(configPath).href;
57
+ const url = cacheBust ? `${fileUrl}?update=${Date.now()}` : fileUrl;
58
+ moduleConfig = await import(url);
59
+ } catch (error) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ throw new Error(`Failed to load hooks from ${configPath}: ${message}`);
62
+ }
63
+
64
+ const exported = (moduleConfig as Record<string, unknown>)?.default ?? moduleConfig;
65
+ if (!exported || typeof exported !== 'object') {
66
+ return EMPTY_HOOKS;
67
+ }
68
+
69
+ const rawHooks = (exported as Record<string, unknown>).hooks ?? exported;
70
+ if (!rawHooks || typeof rawHooks !== 'object') {
71
+ return EMPTY_HOOKS;
72
+ }
73
+
74
+ return normalizeHooks(rawHooks as RawHooks, configPath);
75
75
  }
76
76
 
77
77
  export function createHookContext(
78
- config: FrontendConfig,
79
- mode: PipelineMode,
80
- changedFile: string | undefined,
81
- builderName?: string
78
+ config: FrontendConfig,
79
+ mode: PipelineMode,
80
+ changedFile: string | undefined,
81
+ builderName?: string,
82
82
  ): HookContext {
83
- return {
84
- config,
85
- mode,
86
- workspaceRoot: config.paths.workspace,
87
- builderName,
88
- changedFile
89
- };
83
+ return {
84
+ config,
85
+ mode,
86
+ workspaceRoot: config.paths.workspace,
87
+ builderName,
88
+ changedFile,
89
+ };
90
90
  }
91
91
 
92
- export async function executeHooks(label: string, handlers: HookHandler[], context: HookContext): Promise<void> {
93
- for (const handler of handlers) {
94
- try {
95
- await handler(context);
96
- } catch (error) {
97
- throw wrapHookError(label, error);
98
- }
92
+ export async function executeHooks(
93
+ label: string,
94
+ handlers: HookHandler[],
95
+ context: HookContext,
96
+ ): Promise<void> {
97
+ for (const handler of handlers) {
98
+ try {
99
+ await handler(context);
100
+ } catch (error) {
101
+ throw wrapHookError(label, error);
99
102
  }
103
+ }
100
104
  }
101
105
 
102
106
  function wrapHookError(label: string, error: unknown): Error {
103
- if (error instanceof Error) {
104
- error.message = `[hook:${label}] ${error.message}`;
105
- return error;
106
- }
107
+ if (error instanceof Error) {
108
+ error.message = `[hook:${label}] ${error.message}`;
109
+ return error;
110
+ }
107
111
 
108
- return new Error(`[hook:${label}] ${String(error)}`);
112
+ return new Error(`[hook:${label}] ${String(error)}`);
109
113
  }
110
114
 
111
115
  function normalizeHooks(raw: RawHooks, configPath: string): ResolvedHooks {
112
- const pipeline = raw.pipeline ?? {};
113
- const pipelineBefore = normalizeHandlerSet(pipeline.beforeAll, `${configPath} pipeline.beforeAll`);
114
- const pipelineAfter = normalizeHandlerSet(pipeline.afterAll, `${configPath} pipeline.afterAll`);
115
-
116
- const builderBefore = new Map<string, HookHandler[]>();
117
- const builderAfter = new Map<string, HookHandler[]>();
118
-
119
- const builders = raw.builders ?? {};
120
- for (const [name, value] of Object.entries(builders)) {
121
- if (typeof value === 'function' || Array.isArray(value)) {
122
- builderBefore.set(name, normalizeHandlerSet(value, `${configPath} builders.${name}`));
123
- continue;
124
- }
125
-
126
- if (!value || typeof value !== 'object') {
127
- continue;
128
- }
129
-
130
- const before = normalizeHandlerSet(value.before, `${configPath} builders.${name}.before`);
131
- const after = normalizeHandlerSet(value.after, `${configPath} builders.${name}.after`);
132
- if (before.length > 0) {
133
- builderBefore.set(name, before);
134
- }
135
- if (after.length > 0) {
136
- builderAfter.set(name, after);
137
- }
116
+ const pipeline = raw.pipeline ?? {};
117
+ const pipelineBefore = normalizeHandlerSet(
118
+ pipeline.beforeAll,
119
+ `${configPath} pipeline.beforeAll`,
120
+ );
121
+ const pipelineAfter = normalizeHandlerSet(pipeline.afterAll, `${configPath} pipeline.afterAll`);
122
+
123
+ const builderBefore = new Map<string, HookHandler[]>();
124
+ const builderAfter = new Map<string, HookHandler[]>();
125
+
126
+ const builders = raw.builders ?? {};
127
+ for (const [name, value] of Object.entries(builders)) {
128
+ if (typeof value === 'function' || Array.isArray(value)) {
129
+ builderBefore.set(name, normalizeHandlerSet(value, `${configPath} builders.${name}`));
130
+ continue;
138
131
  }
139
132
 
140
- return {
141
- pipelineBefore,
142
- pipelineAfter,
143
- builderBefore,
144
- builderAfter
145
- };
146
- }
133
+ if (!value || typeof value !== 'object') {
134
+ continue;
135
+ }
147
136
 
148
- function normalizeHandlerSet(value: HookHandler | HookHandler[] | undefined, label: string): HookHandler[] {
149
- if (value === undefined) {
150
- return [];
137
+ const before = normalizeHandlerSet(value.before, `${configPath} builders.${name}.before`);
138
+ const after = normalizeHandlerSet(value.after, `${configPath} builders.${name}.after`);
139
+ if (before.length > 0) {
140
+ builderBefore.set(name, before);
141
+ }
142
+ if (after.length > 0) {
143
+ builderAfter.set(name, after);
151
144
  }
145
+ }
146
+
147
+ return {
148
+ pipelineBefore,
149
+ pipelineAfter,
150
+ builderBefore,
151
+ builderAfter,
152
+ };
153
+ }
154
+
155
+ function normalizeHandlerSet(
156
+ value: HookHandler | HookHandler[] | undefined,
157
+ label: string,
158
+ ): HookHandler[] {
159
+ if (value === undefined) {
160
+ return [];
161
+ }
152
162
 
153
- const handlers = Array.isArray(value) ? value : [value];
154
- const normalized: HookHandler[] = [];
163
+ const handlers = Array.isArray(value) ? value : [value];
164
+ const normalized: HookHandler[] = [];
155
165
 
156
- for (const handler of handlers) {
157
- if (typeof handler !== 'function') {
158
- throw new Error(`Invalid hook handler in ${label}; expected function`);
159
- }
160
- normalized.push(handler);
166
+ for (const handler of handlers) {
167
+ if (typeof handler !== 'function') {
168
+ throw new Error(`Invalid hook handler in ${label}; expected function`);
161
169
  }
170
+ normalized.push(handler);
171
+ }
162
172
 
163
- return normalized;
173
+ return normalized;
164
174
  }
165
175
 
166
176
  function findConfigPath(workspaceRoot: string): string | undefined {
167
- for (const candidate of CONFIG_CANDIDATES) {
168
- const fullPath = path.join(workspaceRoot, candidate);
169
- if (fs.existsSync(fullPath)) {
170
- return fullPath;
171
- }
177
+ for (const candidate of CONFIG_CANDIDATES) {
178
+ const fullPath = path.join(workspaceRoot, candidate);
179
+ if (fs.existsSync(fullPath)) {
180
+ return fullPath;
172
181
  }
182
+ }
173
183
 
174
- return undefined;
184
+ return undefined;
175
185
  }
@@ -6,13 +6,15 @@ import { pathExists, readFile, stat } from '../utils/fs.js';
6
6
  import { resolvePageAssetUrl } from '../utils/pagePaths.js';
7
7
 
8
8
  const INLINE_THRESHOLD_BYTES = 6 * 1024;
9
- const csso = ((cssoModule as unknown as { default?: typeof cssoModule }).default ?? cssoModule) as typeof cssoModule;
9
+ const csso = ((cssoModule as unknown as { default?: typeof cssoModule }).default ??
10
+ cssoModule) as typeof cssoModule;
10
11
 
11
12
  function minifyCriticalCss(css: string): string {
12
- return csso.minify(css).css;
13
+ return csso.minify(css).css;
13
14
  }
14
15
 
15
- const APP_SHELL_CRITICAL_CSS = minifyCriticalCss(`
16
+ const APP_SHELL_CRITICAL_CSS = minifyCriticalCss(
17
+ `
16
18
  @layer tokens {
17
19
  :root {
18
20
  --ws-header-control-size: 2.6rem;
@@ -83,8 +85,10 @@ const APP_SHELL_CRITICAL_CSS = minifyCriticalCss(`
83
85
  height: calc(var(--ws-header-sticky-offset) - 1px);
84
86
  }
85
87
  }
86
- `.trim());
87
- const DOCS_SHELL_CRITICAL_CSS = minifyCriticalCss(`
88
+ `.trim(),
89
+ );
90
+ const DOCS_SHELL_CRITICAL_CSS = minifyCriticalCss(
91
+ `
88
92
  @layer overrides {
89
93
  .docs-layout {
90
94
  --ws-docs-sidebar-width: clamp(14rem, 20vw, 18rem);
@@ -130,85 +134,88 @@ const DOCS_SHELL_CRITICAL_CSS = minifyCriticalCss(`
130
134
  }
131
135
  }
132
136
  }
133
- `.trim());
137
+ `.trim(),
138
+ );
134
139
 
135
140
  export async function inlineCriticalCss(
136
- document: CheerioAPI,
137
- pageName: string,
138
- pagesRoot: string,
139
- pagesUrlPrefix: string,
140
- cssFile?: string
141
+ document: CheerioAPI,
142
+ pageName: string,
143
+ pagesRoot: string,
144
+ pagesUrlPrefix: string,
145
+ cssFile?: string,
141
146
  ): Promise<void> {
142
- if (!cssFile) {
143
- return;
144
- }
145
-
146
- const cssPath = path.join(pagesRoot, pageName, cssFile);
147
- if (!(await pathExists(cssPath))) {
148
- return;
149
- }
150
-
151
- const info = await stat(cssPath).catch(() => null);
152
- if (!info || !info.isFile() || info.size > INLINE_THRESHOLD_BYTES) {
153
- return;
154
- }
155
-
156
- const cssContent = minifyCriticalCss(await readFile(cssPath));
157
- const head = document('head').first();
158
- if (head.length === 0) {
159
- return;
160
- }
161
-
162
- const href = resolvePageAssetUrl(pagesUrlPrefix, pageName, cssFile);
163
- document(`link[href="${href}"]`).remove();
164
-
165
- if (cssFile.endsWith(EXTENSIONS.css)) {
166
- document(`link[rel="preload"][href="${href}"]`).remove();
167
- }
168
-
169
- head.append(`\n<style data-critical>\n${cssContent}\n</style>\n`);
147
+ if (!cssFile) {
148
+ return;
149
+ }
150
+
151
+ const cssPath = path.join(pagesRoot, pageName, cssFile);
152
+ if (!(await pathExists(cssPath))) {
153
+ return;
154
+ }
155
+
156
+ const info = await stat(cssPath).catch(() => null);
157
+ if (!info || !info.isFile() || info.size > INLINE_THRESHOLD_BYTES) {
158
+ return;
159
+ }
160
+
161
+ const cssContent = minifyCriticalCss(await readFile(cssPath));
162
+ const head = document('head').first();
163
+ if (head.length === 0) {
164
+ return;
165
+ }
166
+
167
+ const href = resolvePageAssetUrl(pagesUrlPrefix, pageName, cssFile);
168
+ document(`link[href="${href}"]`).remove();
169
+
170
+ if (cssFile.endsWith(EXTENSIONS.css)) {
171
+ document(`link[rel="preload"][href="${href}"]`).remove();
172
+ }
173
+
174
+ head.append(`\n<style data-critical>\n${cssContent}\n</style>\n`);
170
175
  }
171
176
 
172
177
  export function ensureAppShellCriticalCss(document: CheerioAPI, appCssHref: string): void {
173
- const head = document('head').first();
174
- if (head.length === 0) {
175
- return;
176
- }
177
-
178
- const existing = head.find('style[data-critical="app"]').first();
179
- if (existing.length > 0) {
180
- return;
181
- }
182
-
183
- const stylesheet = document(`link[rel="stylesheet"][href="${appCssHref}"]`).first();
184
- const styleTag = `<style data-critical="app">\n${APP_SHELL_CRITICAL_CSS}\n</style>`;
185
- if (stylesheet.length > 0) {
186
- stylesheet.before(styleTag);
187
- } else {
188
- head.append(styleTag);
189
- }
178
+ const head = document('head').first();
179
+ if (head.length === 0) {
180
+ return;
181
+ }
182
+
183
+ const existing = head.find('style[data-critical="app"]').first();
184
+ if (existing.length > 0) {
185
+ return;
186
+ }
187
+
188
+ const stylesheet = document(`link[rel="stylesheet"][href="${appCssHref}"]`).first();
189
+ const styleTag = `<style data-critical="app">\n${APP_SHELL_CRITICAL_CSS}\n</style>`;
190
+ if (stylesheet.length > 0) {
191
+ stylesheet.before(styleTag);
192
+ } else {
193
+ head.append(styleTag);
194
+ }
190
195
  }
191
196
 
192
197
  export function ensureDocsShellCriticalCss(document: CheerioAPI): void {
193
- const head = document('head').first();
194
- if (head.length === 0) {
195
- return;
196
- }
197
-
198
- const existing = head.find('style[data-critical="docs"]').first();
199
- if (existing.length > 0) {
200
- return;
201
- }
202
-
203
- const docsStylesheet = document('link[rel="stylesheet"]').filter((_, element) => {
204
- const href = document(element).attr('href');
205
- return typeof href === 'string' && href.includes('/docs/');
206
- }).first();
207
-
208
- const styleTag = `<style data-critical="docs">\n${DOCS_SHELL_CRITICAL_CSS}\n</style>`;
209
- if (docsStylesheet.length > 0) {
210
- docsStylesheet.before(styleTag);
211
- } else {
212
- head.append(styleTag);
213
- }
198
+ const head = document('head').first();
199
+ if (head.length === 0) {
200
+ return;
201
+ }
202
+
203
+ const existing = head.find('style[data-critical="docs"]').first();
204
+ if (existing.length > 0) {
205
+ return;
206
+ }
207
+
208
+ const docsStylesheet = document('link[rel="stylesheet"]')
209
+ .filter((_, element) => {
210
+ const href = document(element).attr('href');
211
+ return typeof href === 'string' && href.includes('/docs/');
212
+ })
213
+ .first();
214
+
215
+ const styleTag = `<style data-critical="docs">\n${DOCS_SHELL_CRITICAL_CSS}\n</style>`;
216
+ if (docsStylesheet.length > 0) {
217
+ docsStylesheet.before(styleTag);
218
+ } else {
219
+ head.append(styleTag);
220
+ }
214
221
  }