@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
@@ -3,550 +3,599 @@ import postcss from 'postcss';
3
3
  import autoprefixer from 'autoprefixer';
4
4
  import customMedia from 'postcss-custom-media';
5
5
  import * as cssoModule from 'csso';
6
- import { glob } from 'glob';
7
6
  import { FOLDERS, FILES, EXTENSIONS } from '../core/constants.js';
8
7
  import { ensureDir, pathExists, readFile, writeFile, remove, copy } from '../utils/fs.js';
8
+ import { scanGlob } from '../utils/glob.js';
9
9
  import type { Builder, BuilderContext } from './types.js';
10
10
  import { getPages } from '../core/pages.js';
11
11
  import { hashContent } from '../utils/hash.js';
12
- import { updatePageManifest, updateSharedAssets, readSharedAssets } from '../assets/assetManifest.js';
12
+ import {
13
+ updatePageManifest,
14
+ updateSharedAssets,
15
+ readSharedAssets,
16
+ } from '../assets/assetManifest.js';
13
17
  import { createCompressedVariants } from '../assets/precompression.js';
14
18
  import { shouldProcess } from '../utils/changedFile.js';
15
19
  import { findPageFromChangedFile } from '../utils/pathMatch.js';
16
20
 
17
21
  const MODULE_SUFFIX = '.module';
18
22
  const APP_CSS_BASENAME = 'app';
19
- const csso = ((cssoModule as unknown as { default?: typeof cssoModule }).default ?? cssoModule) as typeof cssoModule;
20
- const PAGE_IMPORT_PATTERN = /@import\s+(?:url\()?[\s]*['"]([^'"\)]+)['"][\s]*\)?\s*;?/g;
23
+ const csso = ((cssoModule as unknown as { default?: typeof cssoModule }).default ??
24
+ cssoModule) as typeof cssoModule;
25
+ const PAGE_IMPORT_PATTERN = /@import\s+(?:url\()?[\s]*['"]([^'"]+)['"][\s]*\)?\s*;?/g;
21
26
 
22
27
  interface SharedCssArtifacts {
23
- appCss?: string;
28
+ appCss?: string;
24
29
  }
25
30
 
26
31
  export function createCssBuilder(context: BuilderContext): Builder {
27
- return {
28
- name: 'css',
29
- async build(): Promise<void> {
30
- await processCss(context, false);
31
- },
32
- async publish(): Promise<void> {
33
- await processCss(context, true);
34
- }
35
- };
32
+ return {
33
+ name: 'css',
34
+ async build(): Promise<void> {
35
+ await processCss(context, false);
36
+ },
37
+ async publish(): Promise<void> {
38
+ await processCss(context, true);
39
+ },
40
+ };
36
41
  }
37
42
 
38
43
  async function processCss(context: BuilderContext, isProduction: boolean): Promise<void> {
39
- const { config } = context;
40
- if (!shouldProcess(context, [
41
- { directory: config.paths.src.pages, extensions: [EXTENSIONS.css] },
42
- { directory: config.paths.src.frontend, extensions: [EXTENSIONS.css] }
43
- ])) {
44
- return;
45
- }
44
+ const { config } = context;
45
+ if (
46
+ !shouldProcess(context, [
47
+ { directory: config.paths.src.pages, extensions: [EXTENSIONS.css] },
48
+ { directory: config.paths.src.frontend, extensions: [EXTENSIONS.css] },
49
+ ])
50
+ ) {
51
+ return;
52
+ }
53
+
54
+ const processor = createPostcssProcessor();
55
+ const customMediaPrelude = await loadCustomMediaPrelude(config);
56
+ const sharedArtifacts = await processAppCss(config, isProduction, processor, customMediaPrelude);
57
+ const targetPage = findPageFromChangedFile(context.changedFile, config.paths.src.pages);
58
+ const pages = await getPages(config.paths.src.pages);
59
+
60
+ for (const page of pages) {
61
+ if (targetPage && page.name !== targetPage) {
62
+ continue;
63
+ }
64
+ const entryPath = await resolveCssEntry(page.directory);
65
+ if (!entryPath) {
66
+ continue;
67
+ }
68
+
69
+ const css = await readFile(entryPath);
70
+ const inlinedCss = await inlinePageImports(css, page.directory);
71
+ const prepared = applyCustomMediaPrelude(inlinedCss, customMediaPrelude);
72
+ const processed = await processor.process(prepared, {
73
+ from: entryPath,
74
+ map: !isProduction ? { inline: true } : false,
75
+ });
76
+ const normalized = resolveAppImports(
77
+ processed.css,
78
+ isProduction ? sharedArtifacts.appCss : undefined,
79
+ );
46
80
 
47
- const processor = createPostcssProcessor();
48
- const customMediaPrelude = await loadCustomMediaPrelude(config);
49
- const sharedArtifacts = await processAppCss(config, isProduction, processor, customMediaPrelude);
50
- const targetPage = findPageFromChangedFile(context.changedFile, config.paths.src.pages);
51
- const pages = await getPages(config.paths.src.pages);
52
-
53
- for (const page of pages) {
54
- if (targetPage && page.name !== targetPage) {
55
- continue;
56
- }
57
- const entryPath = await resolveCssEntry(page.directory);
58
- if (!entryPath) {
59
- continue;
60
- }
61
-
62
- const css = await readFile(entryPath);
63
- const inlinedCss = await inlinePageImports(css, page.directory);
64
- const prepared = applyCustomMediaPrelude(inlinedCss, customMediaPrelude);
65
- const processed = await processor.process(prepared, { from: entryPath, map: !isProduction ? { inline: true } : false });
66
- const normalized = resolveAppImports(processed.css, isProduction ? sharedArtifacts.appCss : undefined);
67
-
68
- if (isProduction) {
69
- const inlined = await inlineAppImports(normalized, config.paths.dist.frontend);
70
- await emitProductionCss(config, page.name, inlined);
71
- } else {
72
- await emitDevelopmentCss(config, page.name, normalized);
73
- await syncPageCssAssetsForDevelopment(
74
- page.directory,
75
- path.join(config.paths.build.pages, page.name),
76
- entryPath
77
- );
78
- }
79
- }
81
+ if (isProduction) {
82
+ const inlined = await inlineAppImports(normalized, config.paths.dist.frontend);
83
+ await emitProductionCss(config, page.name, inlined);
84
+ } else {
85
+ await emitDevelopmentCss(config, page.name, normalized);
86
+ await syncPageCssAssetsForDevelopment(
87
+ page.directory,
88
+ path.join(config.paths.build.pages, page.name),
89
+ entryPath,
90
+ );
91
+ }
92
+ }
80
93
  }
81
94
 
82
- async function emitDevelopmentCss(config: BuilderContext['config'], pageName: string, css: string): Promise<void> {
83
- const outputDir = path.join(config.paths.build.pages, pageName);
84
- await ensureDir(outputDir);
85
- const outputPath = path.join(outputDir, `${FILES.index}${EXTENSIONS.css}`);
86
- await writeFile(outputPath, css);
95
+ async function emitDevelopmentCss(
96
+ config: BuilderContext['config'],
97
+ pageName: string,
98
+ css: string,
99
+ ): Promise<void> {
100
+ const outputDir = path.join(config.paths.build.pages, pageName);
101
+ await ensureDir(outputDir);
102
+ const outputPath = path.join(outputDir, `${FILES.index}${EXTENSIONS.css}`);
103
+ await writeFile(outputPath, css);
87
104
  }
88
105
 
89
- async function emitProductionCss(config: BuilderContext['config'], pageName: string, css: string): Promise<void> {
90
- const minified = csso.minify(css).css;
91
- const hash = hashContent(minified);
92
- const fileName = `${FILES.index}-${hash}${EXTENSIONS.css}`;
93
- const outputDir = path.join(config.paths.dist.pages, pageName);
94
- await ensureDir(outputDir);
95
- const outputPath = path.join(outputDir, fileName);
96
- await writeFile(outputPath, minified);
97
- if (config.features.precompression) {
98
- await createCompressedVariants(outputPath);
99
- } else {
100
- await Promise.all([
101
- remove(`${outputPath}${EXTENSIONS.br}`).catch(() => undefined),
102
- remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined)
103
- ]);
104
- }
105
- await updatePageManifest(outputDir, pageName, (manifest) => {
106
- manifest.css = fileName;
107
- });
106
+ async function emitProductionCss(
107
+ config: BuilderContext['config'],
108
+ pageName: string,
109
+ css: string,
110
+ ): Promise<void> {
111
+ const minified = csso.minify(css).css;
112
+ const hash = hashContent(minified);
113
+ const fileName = `${FILES.index}-${hash}${EXTENSIONS.css}`;
114
+ const outputDir = path.join(config.paths.dist.pages, pageName);
115
+ await ensureDir(outputDir);
116
+ const outputPath = path.join(outputDir, fileName);
117
+ await writeFile(outputPath, minified);
118
+ if (config.features.precompression) {
119
+ await createCompressedVariants(outputPath);
120
+ } else {
121
+ await Promise.all([
122
+ remove(`${outputPath}${EXTENSIONS.br}`).catch(() => undefined),
123
+ remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined),
124
+ ]);
125
+ }
126
+ await updatePageManifest(outputDir, pageName, (manifest) => {
127
+ manifest.css = fileName;
128
+ });
108
129
  }
109
130
 
110
131
  async function syncPageCssAssetsForDevelopment(
111
- pageDirectory: string,
112
- outputDir: string,
113
- entryPath: string
132
+ pageDirectory: string,
133
+ outputDir: string,
134
+ entryPath: string,
114
135
  ): Promise<void> {
115
- const sourceFiles = await glob('**/*.css', { cwd: pageDirectory, nodir: true });
116
- const entryRelative = normalizeForwardSlashes(path.relative(pageDirectory, entryPath));
117
-
118
- const copySet = new Set<string>();
119
- for (const relative of sourceFiles) {
120
- const normalized = normalizeForwardSlashes(relative);
121
- if (normalized === entryRelative) {
122
- continue;
123
- }
124
-
125
- copySet.add(normalized);
126
- const sourcePath = path.join(pageDirectory, relative);
127
- const destinationPath = path.join(outputDir, relative);
128
- await ensureDir(path.dirname(destinationPath));
129
- await copy(sourcePath, destinationPath);
136
+ const sourceFiles = await scanGlob('**/*.css', { cwd: pageDirectory });
137
+ const entryRelative = normalizeForwardSlashes(path.relative(pageDirectory, entryPath));
138
+
139
+ const copySet = new Set<string>();
140
+ for (const relative of sourceFiles) {
141
+ const normalized = normalizeForwardSlashes(relative);
142
+ if (normalized === entryRelative) {
143
+ continue;
130
144
  }
131
145
 
132
- const existingFiles = await glob('**/*.css', { cwd: outputDir, nodir: true });
133
- for (const relative of existingFiles) {
134
- const normalized = normalizeForwardSlashes(relative);
135
- if (normalized === `${FILES.index}${EXTENSIONS.css}`) {
136
- continue;
137
- }
146
+ copySet.add(normalized);
147
+ const sourcePath = path.join(pageDirectory, relative);
148
+ const destinationPath = path.join(outputDir, relative);
149
+ await ensureDir(path.dirname(destinationPath));
150
+ await copy(sourcePath, destinationPath);
151
+ }
138
152
 
139
- if (!copySet.has(normalized)) {
140
- await remove(path.join(outputDir, relative)).catch(() => undefined);
141
- }
153
+ const existingFiles = await scanGlob('**/*.css', { cwd: outputDir });
154
+ for (const relative of existingFiles) {
155
+ const normalized = normalizeForwardSlashes(relative);
156
+ if (normalized === `${FILES.index}${EXTENSIONS.css}`) {
157
+ continue;
142
158
  }
159
+
160
+ if (!copySet.has(normalized)) {
161
+ await remove(path.join(outputDir, relative)).catch(() => undefined);
162
+ }
163
+ }
143
164
  }
144
165
 
145
166
  async function processAppCss(
146
- config: BuilderContext['config'],
147
- isProduction: boolean,
148
- processor: postcss.Processor,
149
- customMediaPrelude: string
167
+ config: BuilderContext['config'],
168
+ isProduction: boolean,
169
+ processor: postcss.Processor,
170
+ customMediaPrelude: string,
150
171
  ): Promise<SharedCssArtifacts> {
151
- const appCssPath = path.join(config.paths.src.app, 'app.css');
152
- if (!(await pathExists(appCssPath))) {
153
- return {};
154
- }
155
-
156
- const source = applyCustomMediaPrelude(await readFile(appCssPath), customMediaPrelude);
157
-
158
- if (isProduction) {
159
- const stylesMap = await emitAppStylesProduction(config, processor, customMediaPrelude);
160
- const processed = await processor.process(source, { from: appCssPath, map: false });
161
- const rewritten = rewriteAppStyleImports(processed.css, stylesMap);
162
- const inlined = await inlineAppImports(rewritten, config.paths.dist.frontend);
163
- const fileName = await emitAppProductionCss(config, inlined);
164
- await updateSharedAssets(config.paths.dist.frontend, shared => {
165
- shared.css = fileName;
166
- });
167
- return { appCss: fileName };
168
- }
169
-
170
- const processed = await processor.process(source, { from: appCssPath, map: { inline: true } });
171
- const stylesVersion = await computeAppStylesVersion(config.paths.src.app);
172
- const rewritten = rewriteAppStyleImportsForDevelopment(processed.css, stylesVersion);
173
- await emitAppDevelopmentCss(config, rewritten);
174
- await syncAppStyles(config.paths.src.app, path.join(config.paths.build.frontend, FOLDERS.app), processor, customMediaPrelude);
172
+ const appCssPath = path.join(config.paths.src.app, 'app.css');
173
+ if (!(await pathExists(appCssPath))) {
175
174
  return {};
175
+ }
176
+
177
+ const source = applyCustomMediaPrelude(await readFile(appCssPath), customMediaPrelude);
178
+
179
+ if (isProduction) {
180
+ const stylesMap = await emitAppStylesProduction(config, processor, customMediaPrelude);
181
+ const processed = await processor.process(source, { from: appCssPath, map: false });
182
+ const rewritten = rewriteAppStyleImports(processed.css, stylesMap);
183
+ const inlined = await inlineAppImports(rewritten, config.paths.dist.frontend);
184
+ const fileName = await emitAppProductionCss(config, inlined);
185
+ await updateSharedAssets(config.paths.dist.frontend, (shared) => {
186
+ shared.css = fileName;
187
+ });
188
+ return { appCss: fileName };
189
+ }
190
+
191
+ const processed = await processor.process(source, { from: appCssPath, map: { inline: true } });
192
+ const stylesVersion = await computeAppStylesVersion(config.paths.src.app);
193
+ const rewritten = rewriteAppStyleImportsForDevelopment(processed.css, stylesVersion);
194
+ await emitAppDevelopmentCss(config, rewritten);
195
+ await syncAppStyles(
196
+ config.paths.src.app,
197
+ path.join(config.paths.build.frontend, FOLDERS.app),
198
+ processor,
199
+ customMediaPrelude,
200
+ );
201
+ return {};
176
202
  }
177
203
 
178
204
  function createPostcssProcessor(): postcss.Processor {
179
- return postcss([customMedia(), autoprefixer]);
205
+ return postcss([customMedia(), autoprefixer]);
180
206
  }
181
207
 
182
208
  async function loadCustomMediaPrelude(config: BuilderContext['config']): Promise<string> {
183
- const tokensPath = path.join(config.paths.src.app, 'styles', 'tokens.css');
184
- if (!(await pathExists(tokensPath))) {
185
- return '';
186
- }
187
-
188
- const contents = await readFile(tokensPath);
189
- const matches = contents.match(/^[\t ]*@custom-media[^\n]*;[\t ]*$/gm) ?? [];
190
- if (matches.length === 0) {
191
- return '';
192
- }
193
-
194
- return `${matches.join('\n')}\n`;
209
+ const tokensPath = path.join(config.paths.src.app, 'styles', 'tokens.css');
210
+ if (!(await pathExists(tokensPath))) {
211
+ return '';
212
+ }
213
+
214
+ const contents = await readFile(tokensPath);
215
+ const matches = contents.match(/^[\t ]*@custom-media[^\n]*;[\t ]*$/gm) ?? [];
216
+ if (matches.length === 0) {
217
+ return '';
218
+ }
219
+
220
+ return `${matches.join('\n')}\n`;
195
221
  }
196
222
 
197
223
  function applyCustomMediaPrelude(css: string, prelude: string): string {
198
- if (!prelude) {
199
- return css;
200
- }
224
+ if (!prelude) {
225
+ return css;
226
+ }
201
227
 
202
- if (!css.includes('@media (--')) {
203
- return css;
204
- }
228
+ if (!css.includes('@media (--')) {
229
+ return css;
230
+ }
205
231
 
206
- if (css.includes('@custom-media')) {
207
- return css;
208
- }
232
+ if (css.includes('@custom-media')) {
233
+ return css;
234
+ }
209
235
 
210
- return `${prelude}${css}`;
236
+ return `${prelude}${css}`;
211
237
  }
212
238
 
213
239
  async function emitAppDevelopmentCss(config: BuilderContext['config'], css: string): Promise<void> {
214
- const outputDir = path.join(config.paths.build.frontend, FOLDERS.app);
215
- await ensureDir(outputDir);
216
- await writeFile(path.join(outputDir, 'app.css'), css);
240
+ const outputDir = path.join(config.paths.build.frontend, FOLDERS.app);
241
+ await ensureDir(outputDir);
242
+ await writeFile(path.join(outputDir, 'app.css'), css);
217
243
  }
218
244
 
219
- async function emitAppProductionCss(config: BuilderContext['config'], css: string): Promise<string> {
220
- const { css: stripped, layerOrder } = stripAppLayerOrderStatement(css);
221
- const minified = restoreAppLayerOrderStatement(csso.minify(stripped).css, layerOrder);
222
- const hash = hashContent(minified);
223
- const fileName = `${APP_CSS_BASENAME}-${hash}${EXTENSIONS.css}`;
224
- const outputDir = path.join(config.paths.dist.frontend, FOLDERS.app);
225
- await ensureDir(outputDir);
226
- const outputPath = path.join(outputDir, fileName);
227
- await writeFile(outputPath, minified);
228
-
229
- if (config.features.precompression) {
230
- await createCompressedVariants(outputPath);
231
- } else {
232
- await Promise.all([
233
- remove(`${outputPath}${EXTENSIONS.br}`).catch(() => undefined),
234
- remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined)
235
- ]);
236
- }
237
-
238
- // Remove previously hashed variants to avoid stale files.
239
- const existing = await readSharedAssets(config.paths.dist.frontend);
240
- const previousFile = existing?.css;
241
- if (previousFile && previousFile !== fileName) {
242
- const previousPath = path.join(outputDir, previousFile);
243
- await remove(previousPath).catch(() => undefined);
244
- await remove(`${previousPath}${EXTENSIONS.br}`).catch(() => undefined);
245
- await remove(`${previousPath}${EXTENSIONS.gz}`).catch(() => undefined);
246
- }
247
-
248
- return fileName;
245
+ async function emitAppProductionCss(
246
+ config: BuilderContext['config'],
247
+ css: string,
248
+ ): Promise<string> {
249
+ const { css: stripped, layerOrder } = stripAppLayerOrderStatement(css);
250
+ const minified = restoreAppLayerOrderStatement(csso.minify(stripped).css, layerOrder);
251
+ const hash = hashContent(minified);
252
+ const fileName = `${APP_CSS_BASENAME}-${hash}${EXTENSIONS.css}`;
253
+ const outputDir = path.join(config.paths.dist.frontend, FOLDERS.app);
254
+ await ensureDir(outputDir);
255
+ const outputPath = path.join(outputDir, fileName);
256
+ await writeFile(outputPath, minified);
257
+
258
+ if (config.features.precompression) {
259
+ await createCompressedVariants(outputPath);
260
+ } else {
261
+ await Promise.all([
262
+ remove(`${outputPath}${EXTENSIONS.br}`).catch(() => undefined),
263
+ remove(`${outputPath}${EXTENSIONS.gz}`).catch(() => undefined),
264
+ ]);
265
+ }
266
+
267
+ // Remove previously hashed variants to avoid stale files.
268
+ const existing = await readSharedAssets(config.paths.dist.frontend);
269
+ const previousFile = existing?.css;
270
+ if (previousFile && previousFile !== fileName) {
271
+ const previousPath = path.join(outputDir, previousFile);
272
+ await remove(previousPath).catch(() => undefined);
273
+ await remove(`${previousPath}${EXTENSIONS.br}`).catch(() => undefined);
274
+ await remove(`${previousPath}${EXTENSIONS.gz}`).catch(() => undefined);
275
+ }
276
+
277
+ return fileName;
249
278
  }
250
279
 
251
280
  async function syncAppStyles(
252
- sourceAppDir: string,
253
- destinationAppDir: string,
254
- processor: postcss.Processor,
255
- customMediaPrelude: string
281
+ sourceAppDir: string,
282
+ destinationAppDir: string,
283
+ processor: postcss.Processor,
284
+ customMediaPrelude: string,
256
285
  ): Promise<void> {
257
- const stylesSource = path.join(sourceAppDir, 'styles');
258
- if (!(await pathExists(stylesSource))) {
259
- return;
260
- }
286
+ const stylesSource = path.join(sourceAppDir, 'styles');
287
+ if (!(await pathExists(stylesSource))) {
288
+ return;
289
+ }
261
290
 
262
- const stylesDestination = path.join(destinationAppDir, 'styles');
263
- await ensureDir(stylesDestination);
291
+ const stylesDestination = path.join(destinationAppDir, 'styles');
292
+ await ensureDir(stylesDestination);
264
293
 
265
- const files = await glob('**/*', { cwd: stylesSource, nodir: true });
266
- for (const relative of files) {
267
- const sourcePath = path.join(stylesSource, relative);
268
- const destinationPath = path.join(stylesDestination, relative);
269
- await ensureDir(path.dirname(destinationPath));
294
+ const files = await scanGlob('**/*', { cwd: stylesSource });
295
+ for (const relative of files) {
296
+ const sourcePath = path.join(stylesSource, relative);
297
+ const destinationPath = path.join(stylesDestination, relative);
298
+ await ensureDir(path.dirname(destinationPath));
270
299
 
271
- if (!relative.endsWith(EXTENSIONS.css)) {
272
- await copy(sourcePath, destinationPath);
273
- continue;
274
- }
275
-
276
- const source = applyCustomMediaPrelude(await readFile(sourcePath), customMediaPrelude);
277
- const processed = await processor.process(source, { from: sourcePath, map: { inline: true } });
278
- await writeFile(destinationPath, processed.css);
300
+ if (!relative.endsWith(EXTENSIONS.css)) {
301
+ await copy(sourcePath, destinationPath);
302
+ continue;
279
303
  }
304
+
305
+ const source = applyCustomMediaPrelude(await readFile(sourcePath), customMediaPrelude);
306
+ const processed = await processor.process(source, { from: sourcePath, map: { inline: true } });
307
+ await writeFile(destinationPath, processed.css);
308
+ }
280
309
  }
281
310
 
282
311
  async function computeAppStylesVersion(sourceAppDir: string): Promise<string> {
283
- const stylesDir = path.join(sourceAppDir, 'styles');
284
- if (!(await pathExists(stylesDir))) {
285
- return 'no-styles';
286
- }
287
-
288
- const files = (await glob('**/*.css', { cwd: stylesDir, nodir: true })).sort((a, b) => a.localeCompare(b));
289
- if (files.length === 0) {
290
- return 'no-styles';
291
- }
292
-
293
- let fingerprint = '';
294
- for (const relative of files) {
295
- const contents = await readFile(path.join(stylesDir, relative));
296
- fingerprint += `${normalizeForwardSlashes(relative)}\0${contents}\0`;
297
- }
298
-
299
- return hashContent(fingerprint, 10);
312
+ const stylesDir = path.join(sourceAppDir, 'styles');
313
+ if (!(await pathExists(stylesDir))) {
314
+ return 'no-styles';
315
+ }
316
+
317
+ const files = await scanGlob('**/*.css', { cwd: stylesDir });
318
+ if (files.length === 0) {
319
+ return 'no-styles';
320
+ }
321
+
322
+ let fingerprint = '';
323
+ for (const relative of files) {
324
+ const contents = await readFile(path.join(stylesDir, relative));
325
+ fingerprint += `${normalizeForwardSlashes(relative)}\0${contents}\0`;
326
+ }
327
+
328
+ return hashContent(fingerprint, 10);
300
329
  }
301
330
 
302
331
  function rewriteAppStyleImportsForDevelopment(css: string, stylesVersion: string): string {
303
- const importPattern = /(@import\s+['"])(?:\.\/)?(styles\/[^'"]+?\.css)(\?v=[^'"]+)?(['"];?)/g;
304
- return css.replace(importPattern, `$1./$2?v=${stylesVersion}$4`);
332
+ const importPattern = /(@import\s+['"])(?:\.\/)?(styles\/[^'"]+?\.css)(\?v=[^'"]+)?(['"];?)/g;
333
+ return css.replace(importPattern, `$1./$2?v=${stylesVersion}$4`);
305
334
  }
306
335
 
307
336
  function resolveAppImports(css: string, appCssFile?: string): string {
308
- let result = css;
337
+ let result = css;
309
338
 
310
- if (appCssFile) {
311
- result = result.replace(/@import\s+['"]@app\/app\.css['"];?/g, `@import "/app/${appCssFile}";`);
312
- }
339
+ if (appCssFile) {
340
+ result = result.replace(/@import\s+['"]@app\/app\.css['"];?/g, `@import "/app/${appCssFile}";`);
341
+ }
313
342
 
314
- return result.replace(/@app\//g, '/app/');
343
+ return result.replace(/@app\//g, '/app/');
315
344
  }
316
345
 
317
- async function inlinePageImports(css: string, pageDirectory: string, seen: Set<string> = new Set()): Promise<string> {
318
- const segments: string[] = [];
319
- let lastIndex = 0;
320
-
321
- for (const match of css.matchAll(PAGE_IMPORT_PATTERN)) {
322
- const index = match.index ?? 0;
323
- segments.push(css.slice(lastIndex, index));
324
-
325
- const importPath = String(match[1] ?? '').trim();
326
- if (!shouldInlinePageImport(importPath)) {
327
- segments.push(match[0]);
328
- lastIndex = index + match[0].length;
329
- continue;
330
- }
331
-
332
- const resolved = path.resolve(pageDirectory, importPath);
333
- if (!isWithin(resolved, pageDirectory)) {
334
- segments.push(match[0]);
335
- lastIndex = index + match[0].length;
336
- continue;
337
- }
338
-
339
- const key = resolved;
340
- if (seen.has(key)) {
341
- lastIndex = index + match[0].length;
342
- continue;
343
- }
344
-
345
- if (!(await pathExists(resolved))) {
346
- segments.push(match[0]);
347
- lastIndex = index + match[0].length;
348
- continue;
349
- }
350
-
351
- seen.add(key);
352
- const imported = await readFile(resolved);
353
- const inlined = await inlinePageImports(imported, pageDirectory, seen);
354
- seen.delete(key);
355
- segments.push(inlined);
356
-
357
- lastIndex = index + match[0].length;
358
- }
346
+ async function inlinePageImports(
347
+ css: string,
348
+ pageDirectory: string,
349
+ seen: Set<string> = new Set(),
350
+ ): Promise<string> {
351
+ const segments: string[] = [];
352
+ let lastIndex = 0;
359
353
 
360
- segments.push(css.slice(lastIndex));
361
- return segments.join('');
362
- }
354
+ for (const match of css.matchAll(PAGE_IMPORT_PATTERN)) {
355
+ const index = match.index ?? 0;
356
+ segments.push(css.slice(lastIndex, index));
363
357
 
364
- function shouldInlinePageImport(importPath: string): boolean {
365
- if (importPath.length === 0) {
366
- return false;
358
+ const importPath = String(match[1] ?? '').trim();
359
+ if (!shouldInlinePageImport(importPath)) {
360
+ segments.push(match[0]);
361
+ lastIndex = index + match[0].length;
362
+ continue;
367
363
  }
368
364
 
369
- if (!importPath.endsWith(EXTENSIONS.css)) {
370
- return false;
365
+ const resolved = path.resolve(pageDirectory, importPath);
366
+ if (!isWithin(resolved, pageDirectory)) {
367
+ segments.push(match[0]);
368
+ lastIndex = index + match[0].length;
369
+ continue;
371
370
  }
372
371
 
373
- if (importPath.startsWith('/') || importPath.startsWith('http:') || importPath.startsWith('https:')) {
374
- return false;
372
+ const key = resolved;
373
+ if (seen.has(key)) {
374
+ lastIndex = index + match[0].length;
375
+ continue;
375
376
  }
376
377
 
377
- if (importPath.startsWith('@') || importPath.includes('?') || importPath.includes('#')) {
378
- return false;
378
+ if (!(await pathExists(resolved))) {
379
+ segments.push(match[0]);
380
+ lastIndex = index + match[0].length;
381
+ continue;
379
382
  }
380
383
 
381
- if (importPath.includes('..')) {
382
- return false;
383
- }
384
+ seen.add(key);
385
+ const imported = await readFile(resolved);
386
+ const inlined = await inlinePageImports(imported, pageDirectory, seen);
387
+ seen.delete(key);
388
+ segments.push(inlined);
384
389
 
385
- return true;
386
- }
390
+ lastIndex = index + match[0].length;
391
+ }
387
392
 
388
- function isWithin(candidate: string, root: string): boolean {
389
- const relative = path.relative(root, candidate);
390
- return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative);
393
+ segments.push(css.slice(lastIndex));
394
+ return segments.join('');
391
395
  }
392
396
 
393
- async function inlineAppImports(css: string, distRoot: string, seen: Set<string> = new Set()): Promise<string> {
394
- const importPattern = /@import\s+(?:url\()?[\s]*['"]\/app\/([^'"\)]+)['"][\s]*\)?;?/g;
395
- const segments: string[] = [];
396
- let lastIndex = 0;
397
-
398
- for (const match of css.matchAll(importPattern)) {
399
- const index = match.index ?? 0;
400
- segments.push(css.slice(lastIndex, index));
397
+ function shouldInlinePageImport(importPath: string): boolean {
398
+ if (importPath.length === 0) {
399
+ return false;
400
+ }
401
+
402
+ if (!importPath.endsWith(EXTENSIONS.css)) {
403
+ return false;
404
+ }
405
+
406
+ if (
407
+ importPath.startsWith('/') ||
408
+ importPath.startsWith('http:') ||
409
+ importPath.startsWith('https:')
410
+ ) {
411
+ return false;
412
+ }
413
+
414
+ if (importPath.startsWith('@') || importPath.includes('?') || importPath.includes('#')) {
415
+ return false;
416
+ }
417
+
418
+ if (importPath.includes('..')) {
419
+ return false;
420
+ }
421
+
422
+ return true;
423
+ }
401
424
 
402
- const relative = normalizeForwardSlashes(match[1] ?? '');
403
- const inlined = await inlineAppImport(relative, distRoot, seen);
404
- if (inlined !== null) {
405
- segments.push(inlined);
406
- } else {
407
- segments.push(match[0]);
408
- }
425
+ function isWithin(candidate: string, root: string): boolean {
426
+ const relative = path.relative(root, candidate);
427
+ return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative);
428
+ }
409
429
 
410
- lastIndex = index + match[0].length;
430
+ async function inlineAppImports(
431
+ css: string,
432
+ distRoot: string,
433
+ seen: Set<string> = new Set(),
434
+ ): Promise<string> {
435
+ const importPattern = /@import\s+(?:url\()?[\s]*['"]\/app\/([^'"]+)['"][\s]*\)?;?/g;
436
+ const segments: string[] = [];
437
+ let lastIndex = 0;
438
+
439
+ for (const match of css.matchAll(importPattern)) {
440
+ const index = match.index ?? 0;
441
+ segments.push(css.slice(lastIndex, index));
442
+
443
+ const relative = normalizeForwardSlashes(match[1] ?? '');
444
+ const inlined = await inlineAppImport(relative, distRoot, seen);
445
+ if (inlined !== null) {
446
+ segments.push(inlined);
447
+ } else {
448
+ segments.push(match[0]);
411
449
  }
412
450
 
413
- segments.push(css.slice(lastIndex));
414
- return segments.join('');
451
+ lastIndex = index + match[0].length;
452
+ }
453
+
454
+ segments.push(css.slice(lastIndex));
455
+ return segments.join('');
415
456
  }
416
457
 
417
- async function inlineAppImport(relativePath: string, distRoot: string, seen: Set<string>): Promise<string | null> {
418
- if (relativePath.length === 0 || relativePath.includes('..')) {
419
- return null;
420
- }
458
+ async function inlineAppImport(
459
+ relativePath: string,
460
+ distRoot: string,
461
+ seen: Set<string>,
462
+ ): Promise<string | null> {
463
+ if (relativePath.length === 0 || relativePath.includes('..')) {
464
+ return null;
465
+ }
421
466
 
422
- const resolved = path.join(distRoot, FOLDERS.app, relativePath);
423
- if (!(await pathExists(resolved))) {
424
- return null;
425
- }
467
+ const resolved = path.join(distRoot, FOLDERS.app, relativePath);
468
+ if (!(await pathExists(resolved))) {
469
+ return null;
470
+ }
426
471
 
427
- const key = resolved;
428
- if (seen.has(key)) {
429
- return '';
430
- }
472
+ const key = resolved;
473
+ if (seen.has(key)) {
474
+ return '';
475
+ }
431
476
 
432
- seen.add(key);
433
- const content = await readFile(resolved);
434
- const inlined = await inlineAppImports(content, distRoot, seen);
435
- seen.delete(key);
477
+ seen.add(key);
478
+ const content = await readFile(resolved);
479
+ const inlined = await inlineAppImports(content, distRoot, seen);
480
+ seen.delete(key);
436
481
 
437
- return inlined;
482
+ return inlined;
438
483
  }
439
484
 
440
485
  async function emitAppStylesProduction(
441
- config: BuilderContext['config'],
442
- processor: postcss.Processor,
443
- customMediaPrelude: string
486
+ config: BuilderContext['config'],
487
+ processor: postcss.Processor,
488
+ customMediaPrelude: string,
444
489
  ): Promise<Map<string, string>> {
445
- const sourceDir = path.join(config.paths.src.app, 'styles');
446
- const mapping = new Map<string, string>();
447
-
448
- if (!(await pathExists(sourceDir))) {
449
- const destinationDir = path.join(config.paths.dist.frontend, FOLDERS.app, 'styles');
450
- await remove(destinationDir).catch(() => undefined);
451
- return mapping;
452
- }
490
+ const sourceDir = path.join(config.paths.src.app, 'styles');
491
+ const mapping = new Map<string, string>();
453
492
 
493
+ if (!(await pathExists(sourceDir))) {
454
494
  const destinationDir = path.join(config.paths.dist.frontend, FOLDERS.app, 'styles');
455
495
  await remove(destinationDir).catch(() => undefined);
496
+ return mapping;
497
+ }
456
498
 
457
- const files = await glob('**/*.css', { cwd: sourceDir, nodir: true });
458
- for (const relative of files) {
459
- const sourcePath = path.join(sourceDir, relative);
460
- const source = applyCustomMediaPrelude(await readFile(sourcePath), customMediaPrelude);
461
- const processed = await processor.process(source, { from: sourcePath, map: false });
462
- const minified = csso.minify(processed.css).css;
463
- const hash = hashContent(minified);
464
- const parsed = path.parse(relative);
465
- const hashedName = `${parsed.name}-${hash}${EXTENSIONS.css}`;
466
- const relativeHashedPath = parsed.dir ? path.join(parsed.dir, hashedName) : hashedName;
467
- const destinationPath = path.join(destinationDir, relativeHashedPath);
468
- await ensureDir(path.dirname(destinationPath));
469
- await writeFile(destinationPath, minified);
470
-
471
- if (config.features.precompression) {
472
- await createCompressedVariants(destinationPath);
473
- } else {
474
- await Promise.all([
475
- remove(`${destinationPath}${EXTENSIONS.br}`).catch(() => undefined),
476
- remove(`${destinationPath}${EXTENSIONS.gz}`).catch(() => undefined)
477
- ]);
478
- }
479
-
480
- mapping.set(normalizeForwardSlashes(relative), normalizeForwardSlashes(path.join('styles', relativeHashedPath)));
481
- }
499
+ const destinationDir = path.join(config.paths.dist.frontend, FOLDERS.app, 'styles');
500
+ await remove(destinationDir).catch(() => undefined);
482
501
 
483
- return mapping;
484
- }
502
+ const files = await scanGlob('**/*.css', { cwd: sourceDir });
503
+ for (const relative of files) {
504
+ const sourcePath = path.join(sourceDir, relative);
505
+ const source = applyCustomMediaPrelude(await readFile(sourcePath), customMediaPrelude);
506
+ const processed = await processor.process(source, { from: sourcePath, map: false });
507
+ const minified = csso.minify(processed.css).css;
508
+ const hash = hashContent(minified);
509
+ const parsed = path.parse(relative);
510
+ const hashedName = `${parsed.name}-${hash}${EXTENSIONS.css}`;
511
+ const relativeHashedPath = parsed.dir ? path.join(parsed.dir, hashedName) : hashedName;
512
+ const destinationPath = path.join(destinationDir, relativeHashedPath);
513
+ await ensureDir(path.dirname(destinationPath));
514
+ await writeFile(destinationPath, minified);
485
515
 
486
- function rewriteAppStyleImports(css: string, stylesMap: Map<string, string>): string {
487
- if (stylesMap.size === 0) {
488
- return css;
516
+ if (config.features.precompression) {
517
+ await createCompressedVariants(destinationPath);
518
+ } else {
519
+ await Promise.all([
520
+ remove(`${destinationPath}${EXTENSIONS.br}`).catch(() => undefined),
521
+ remove(`${destinationPath}${EXTENSIONS.gz}`).catch(() => undefined),
522
+ ]);
489
523
  }
490
524
 
491
- let result = css;
492
- for (const [original, hashed] of stylesMap.entries()) {
493
- const normalizedOriginal = original.startsWith('styles/') ? original : `styles/${original}`;
494
- const escaped = escapeRegExp(normalizedOriginal);
495
- const pattern = new RegExp(`(@import\\s+['"])(?:\.\/)?${escaped}(['"];?)`, 'g');
496
- result = result.replace(pattern, `$1/app/${hashed}$2`);
497
- }
525
+ mapping.set(
526
+ normalizeForwardSlashes(relative),
527
+ normalizeForwardSlashes(path.join('styles', relativeHashedPath)),
528
+ );
529
+ }
498
530
 
499
- return result;
531
+ return mapping;
532
+ }
533
+
534
+ function rewriteAppStyleImports(css: string, stylesMap: Map<string, string>): string {
535
+ if (stylesMap.size === 0) {
536
+ return css;
537
+ }
538
+
539
+ let result = css;
540
+ for (const [original, hashed] of stylesMap.entries()) {
541
+ const normalizedOriginal = original.startsWith('styles/') ? original : `styles/${original}`;
542
+ const escaped = escapeRegExp(normalizedOriginal);
543
+ const pattern = new RegExp(`(@import\\s+['"])(?:\\./)?${escaped}(['"];?)`, 'g');
544
+ result = result.replace(pattern, `$1/app/${hashed}$2`);
545
+ }
546
+
547
+ return result;
500
548
  }
501
549
 
502
550
  function normalizeForwardSlashes(value: string): string {
503
- return value.replace(/\\/g, '/');
551
+ return value.replace(/\\/g, '/');
504
552
  }
505
553
 
506
554
  function escapeRegExp(value: string): string {
507
- return value.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&');
555
+ return value.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&');
508
556
  }
509
557
 
510
558
  function stripAppLayerOrderStatement(css: string): { css: string; layerOrder?: string } {
511
- const layerMatch = css.match(/@layer[^;]*;/);
512
- if (!layerMatch || layerMatch.index === undefined) {
513
- return { css };
514
- }
515
-
516
- const layerText = layerMatch[0];
517
- if (layerText.includes('{')) {
518
- return { css };
519
- }
520
-
521
- const withoutLayer = css.slice(0, layerMatch.index) + css.slice(layerMatch.index + layerText.length);
522
- return { css: withoutLayer, layerOrder: layerText.trim() };
559
+ const layerMatch = css.match(/@layer[^;]*;/);
560
+ if (!layerMatch || layerMatch.index === undefined) {
561
+ return { css };
562
+ }
563
+
564
+ const layerText = layerMatch[0];
565
+ if (layerText.includes('{')) {
566
+ return { css };
567
+ }
568
+
569
+ const withoutLayer =
570
+ css.slice(0, layerMatch.index) + css.slice(layerMatch.index + layerText.length);
571
+ return { css: withoutLayer, layerOrder: layerText.trim() };
523
572
  }
524
573
 
525
574
  function restoreAppLayerOrderStatement(css: string, layerOrder?: string): string {
526
- if (!layerOrder) {
527
- return css;
528
- }
529
-
530
- const charsetMatch = css.match(/^@charset[^;]*;/);
531
- if (charsetMatch && charsetMatch.index === 0) {
532
- const charsetText = charsetMatch[0];
533
- const rest = css.slice(charsetText.length);
534
- return `${charsetText}${layerOrder}${rest}`;
535
- }
536
-
537
- return `${layerOrder}${css}`;
575
+ if (!layerOrder) {
576
+ return css;
577
+ }
578
+
579
+ const charsetMatch = css.match(/^@charset[^;]*;/);
580
+ if (charsetMatch && charsetMatch.index === 0) {
581
+ const charsetText = charsetMatch[0];
582
+ const rest = css.slice(charsetText.length);
583
+ return `${charsetText}${layerOrder}${rest}`;
584
+ }
585
+
586
+ return `${layerOrder}${css}`;
538
587
  }
539
588
 
540
589
  async function resolveCssEntry(pageDirectory: string): Promise<string | null> {
541
- const modulePath = path.join(pageDirectory, `${FILES.index}${MODULE_SUFFIX}${EXTENSIONS.css}`);
542
- if (await pathExists(modulePath)) {
543
- return modulePath;
544
- }
590
+ const modulePath = path.join(pageDirectory, `${FILES.index}${MODULE_SUFFIX}${EXTENSIONS.css}`);
591
+ if (await pathExists(modulePath)) {
592
+ return modulePath;
593
+ }
545
594
 
546
- const plainPath = path.join(pageDirectory, `${FILES.index}${EXTENSIONS.css}`);
547
- if (await pathExists(plainPath)) {
548
- return plainPath;
549
- }
595
+ const plainPath = path.join(pageDirectory, `${FILES.index}${EXTENSIONS.css}`);
596
+ if (await pathExists(plainPath)) {
597
+ return plainPath;
598
+ }
550
599
 
551
- return null;
600
+ return null;
552
601
  }