@fastkit/plugboy 1.3.0 → 1.4.1

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 CHANGED
@@ -191,9 +191,67 @@ value is shallow-merged over the project one, so it only needs to restate the
191
191
  keys it changes.
192
192
 
193
193
  Plugins may seed defaults here during workspace setup — the vanilla-extract
194
- plugin, for instance, owns `splitting` and `fileName` because its CSS merge
195
- depends on them. A configured value always wins over a plugin default, so check
196
- the plugin's documentation before overriding a key it manages.
194
+ plugin, for instance, owns `splitting` and `fileName` so the emitted file matches
195
+ the CSS export plugboy declares. A configured value always wins over a plugin
196
+ default, so check the plugin's documentation before overriding a key it manages.
197
+
198
+ #### CSS Optimization
199
+
200
+ `optimizeCSS` applies plugboy's own postcss pass on top of whatever tsdown
201
+ produced: duplicate `@layer` / `@media` blocks are merged, and the selectors
202
+ listed in `combineRules` are combined into a single rule. Pass `false` to disable
203
+ it.
204
+
205
+ ```typescript
206
+ export default defineWorkspaceConfig({
207
+ optimizeCSS: {
208
+ combineRules: {
209
+ rules: [':root']
210
+ }
211
+ }
212
+ });
213
+ ```
214
+
215
+ It runs in `writeBundle`, on the stylesheets on disk, so it covers **every**
216
+ stylesheet the build writes — including the ones tsdown's own CSS pipeline emits,
217
+ which it does after every plugin has had its say.
218
+
219
+ #### Preserved at the top of a stylesheet
220
+
221
+ Two things tsdown's CSS pipeline would rewrite are restored afterwards, in this
222
+ order, above every rule:
223
+
224
+ 1. **The authored `@layer` order.** lightningcss prunes a name from an
225
+ `@layer a, b, c;` statement once a block for it appears in the same
226
+ stylesheet. For a library that is wrong whenever the statement also orders
227
+ layers owned by *other* packages — the pruned layer's position then depends on
228
+ where its block happens to land relative to those. plugboy reads the statements
229
+ before the transform and re-emits them verbatim.
230
+ 2. **External `@import`s.** A bare package specifier (e.g.
231
+ `@import url('material-symbols/rounded.css') layer(...)`) stays external
232
+ instead of being inlined, so the consumer's bundler resolves it and the
233
+ imported package's own relative asset URLs keep working.
234
+
235
+ Both are captured from every stylesheet in the module graph, whether its contents
236
+ were authored or generated by a plugin — vanilla-extract emits its `@layer`
237
+ statements into a virtual module, and those are covered too.
238
+
239
+ #### One stylesheet per CSS entry
240
+
241
+ Every entry with `css: true` gets a `./<entry>.css` export, and each of those files
242
+ is guaranteed to exist and to be usable on its own.
243
+
244
+ With several such entries the build emits one stylesheet per output *chunk*
245
+ (`css.splitting`), which does not line up: CSS reached from more than one entry is
246
+ moved into a shared chunk and emitted under a hashed name that no export points at,
247
+ and an entry whose CSS comes only from there gets no stylesheet at all. plugboy
248
+ rebuilds each entry's stylesheet from its own CSS plus the CSS of every chunk it
249
+ imports, dependencies first, and drops the leftover per-chunk files. Shared CSS is
250
+ duplicated into each entry that needs it, which is what makes a single
251
+ `./<entry>.css` import complete.
252
+
253
+ This is skipped when `css.inject` is on, since the JavaScript then imports the
254
+ per-chunk stylesheets by name.
197
255
 
198
256
  ### defineProjectConfig
199
257
 
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { i as getWorkspace, t as generateWorkspace } from "./workspace-CciZggrg.mjs";
1
+ import { i as getWorkspace, t as generateWorkspace } from "./workspace-CniX_JDc.mjs";
2
2
  import { cac } from "cac";
3
3
  //#region package.json
4
- var version = "1.3.0";
4
+ var version = "1.4.1";
5
5
  //#endregion
6
6
  //#region src/cli.ts
7
7
  async function main() {
package/dist/plugboy.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { $ as resolveOptimizeCSSOptions, A as resolveRawExposeEntriesSettings, B as resolveBundledConfigOutputFile, C as findProjectPlugin, D as loadProjectConfig, E as isProjectPackageJson, F as getFilename, G as mergeChunkAddons, H as stripDanglingDTSSourceMaps, I as isFileNotFoundException, J as isPromise, K as mergeExternals, L as pathExists, M as findConfig, N as findFile, O as resolveUserProjectConfig, P as getDirname, Q as WORKSPACE_REQUIRED_FIELDS, R as rmrf, S as extractProjectPlugins, T as defineProjectConfig, U as exitHook, V as DANGLING_DTS_SOURCE_MAP_RE, W as collectExternalStringPrefixes, X as PROJECT_REQUIRED_FIELDS, Y as resolveListable, Z as TSDOWN_SYNC_OPTIONS, _ as loadWorkspaceConfig, a as syncWorkspacePackageFields, b as resolveUserWorkspaceConfig, c as Builder, d as getWorkspacePackageJson, et as mergeDTSSettingsList, f as Path, g as isWorkspacePackageJson, h as defineWorkspaceConfig, i as getWorkspace, it as createHooksDefaults, j as copyDirSync, k as exposeEntries, l as findWorkspacePackages, m as resolveUserHooks, n as PlugboyWorkspace, nt as normalizeDTSPreserveTypeTarget, o as PlugboyProject, p as buildHooks, q as mergeNoExternals, r as WORKSPACE_PACKAGE_SYNC_FIELDS, rt as normalizeDTSSettings, s as getProject, t as generateWorkspace, tt as normalizeDTSPreserveTypeSettings, u as getProjectPackageJson, v as resolveRawWorkspaceEntries, w as resolveUserPluginOption, x as definePlugin, y as resolveRawWorkspaceEntry, z as writeFileAtomic } from "./workspace-CciZggrg.mjs";
1
+ import { $ as resolveOptimizeCSSOptions, A as resolveRawExposeEntriesSettings, B as resolveBundledConfigOutputFile, C as findProjectPlugin, D as loadProjectConfig, E as isProjectPackageJson, F as getFilename, G as mergeChunkAddons, H as stripDanglingDTSSourceMaps, I as isFileNotFoundException, J as isPromise, K as mergeExternals, L as pathExists, M as findConfig, N as findFile, O as resolveUserProjectConfig, P as getDirname, Q as WORKSPACE_REQUIRED_FIELDS, R as rmrf, S as extractProjectPlugins, T as defineProjectConfig, U as exitHook, V as DANGLING_DTS_SOURCE_MAP_RE, W as collectExternalStringPrefixes, X as PROJECT_REQUIRED_FIELDS, Y as resolveListable, Z as TSDOWN_SYNC_OPTIONS, _ as loadWorkspaceConfig, a as syncWorkspacePackageFields, b as resolveUserWorkspaceConfig, c as Builder, d as getWorkspacePackageJson, et as mergeDTSSettingsList, f as Path, g as isWorkspacePackageJson, h as defineWorkspaceConfig, i as getWorkspace, it as createHooksDefaults, j as copyDirSync, k as exposeEntries, l as findWorkspacePackages, m as resolveUserHooks, n as PlugboyWorkspace, nt as normalizeDTSPreserveTypeTarget, o as PlugboyProject, p as buildHooks, q as mergeNoExternals, r as WORKSPACE_PACKAGE_SYNC_FIELDS, rt as normalizeDTSSettings, s as getProject, t as generateWorkspace, tt as normalizeDTSPreserveTypeSettings, u as getProjectPackageJson, v as resolveRawWorkspaceEntries, w as resolveUserPluginOption, x as definePlugin, y as resolveRawWorkspaceEntry, z as writeFileAtomic } from "./workspace-CniX_JDc.mjs";
2
2
  export { Builder, DANGLING_DTS_SOURCE_MAP_RE, PROJECT_REQUIRED_FIELDS, Path, PlugboyProject, PlugboyWorkspace, TSDOWN_SYNC_OPTIONS, WORKSPACE_PACKAGE_SYNC_FIELDS, WORKSPACE_REQUIRED_FIELDS, buildHooks, collectExternalStringPrefixes, copyDirSync, createHooksDefaults, definePlugin, defineProjectConfig, defineWorkspaceConfig, exitHook, exposeEntries, extractProjectPlugins, findConfig, findFile, findProjectPlugin, findWorkspacePackages, generateWorkspace, getDirname, getFilename, getProject, getProjectPackageJson, getWorkspace, getWorkspacePackageJson, isFileNotFoundException, isProjectPackageJson, isPromise, isWorkspacePackageJson, loadProjectConfig, loadWorkspaceConfig, mergeChunkAddons, mergeDTSSettingsList, mergeExternals, mergeNoExternals, normalizeDTSPreserveTypeSettings, normalizeDTSPreserveTypeTarget, normalizeDTSSettings, pathExists, resolveBundledConfigOutputFile, resolveListable, resolveOptimizeCSSOptions, resolveRawExposeEntriesSettings, resolveRawWorkspaceEntries, resolveRawWorkspaceEntry, resolveUserHooks, resolveUserPluginOption, resolveUserProjectConfig, resolveUserWorkspaceConfig, rmrf, stripDanglingDTSSourceMaps, syncWorkspacePackageFields, writeFileAtomic };
@@ -1057,6 +1057,38 @@ async function getProject(searchDir, allowMissing, skipLoadConfig) {
1057
1057
  });
1058
1058
  }
1059
1059
  //#endregion
1060
+ //#region src/workspace/stylesheets.ts
1061
+ /** `foo.mjs` -> `foo.css`, mirroring how `@tsdown/css` names a chunk's CSS. */
1062
+ function toCssFileName(jsFileName) {
1063
+ return jsFileName.replace(/(?:\.module)?(\.[cm]?js)$/, ".css");
1064
+ }
1065
+ /** The stylesheets a package declares: `./<entry>.css` per `css: true` entry. */
1066
+ function declaredStylesheets(workspace) {
1067
+ return new Set(workspace.exports.filter((exp) => exp.id.endsWith(".css")).map((exp) => path.posix.basename(exp.id)));
1068
+ }
1069
+ /**
1070
+ * Every stylesheet the build has written to `dir`, as file names.
1071
+ *
1072
+ * The bundle is not the whole story: `plugboy-assemble-entry-css` writes an
1073
+ * entry's stylesheet directly to disk when the build produced none for it, so a
1074
+ * `writeBundle` stage that only walked the bundle assets would skip exactly the
1075
+ * file that needed the most work. The declared exports fill that in — they are the
1076
+ * stylesheets the package publishes, so anything present under one of those names
1077
+ * belongs in the set.
1078
+ */
1079
+ async function listEmittedStylesheets(workspace, dir, bundle) {
1080
+ const names = /* @__PURE__ */ new Set();
1081
+ for (const chunk of Object.values(bundle)) if (chunk.type === "asset" && chunk.fileName.endsWith(".css")) names.add(chunk.fileName);
1082
+ await Promise.all([...declaredStylesheets(workspace)].map(async (name) => {
1083
+ if (names.has(name)) return;
1084
+ try {
1085
+ await fs$1.access(path.join(dir, name));
1086
+ names.add(name);
1087
+ } catch {}
1088
+ }));
1089
+ return [...names];
1090
+ }
1091
+ //#endregion
1060
1092
  //#region src/postcss/plugin.ts
1061
1093
  async function getPostcss(options) {
1062
1094
  const { layer, media, combineRules, cssnano } = options;
@@ -1077,77 +1109,211 @@ async function getPostcss(options) {
1077
1109
  const SOURCE_MAPPING_URL_COMMENT_RE = /\/\*# sourceMappingURL=.+? \*\//g;
1078
1110
  const allLayerDefRe = /(^|\n)@layer\s+([a-zA-Z\d\-_$. ,]+);/g;
1079
1111
  const layerDefTrimRe = /((^|\n)@layer\s+|;)/g;
1080
- async function optimizeCSS(asset, options) {
1112
+ async function optimizeCSS(css, fileName, options) {
1081
1113
  const postcss = await getPostcss(options);
1082
- function prepare(css) {
1114
+ function prepare(source) {
1083
1115
  return (() => {
1084
- const matched = css.match(allLayerDefRe);
1116
+ const matched = source.match(allLayerDefRe);
1085
1117
  if (!matched) return "";
1086
1118
  const layerNames = [];
1087
1119
  matched.forEach((row) => {
1088
1120
  row.replace(layerDefTrimRe, "").split(",").forEach((chunk) => layerNames.push(chunk.trim()));
1089
1121
  });
1090
1122
  return `@layer ${Array.from(new Set(layerNames)).join(", ")};\n`;
1091
- })() + css.replace(allLayerDefRe, "");
1123
+ })() + source.replace(allLayerDefRe, "");
1092
1124
  }
1093
- const css = prepare(asset.source.toString());
1094
- asset.source = (await postcss.process(css, {
1095
- from: asset.fileName,
1096
- to: asset.fileName,
1125
+ return (await postcss.process(prepare(css), {
1126
+ from: fileName,
1127
+ to: fileName,
1097
1128
  map: { inline: false }
1098
1129
  })).css.replace(SOURCE_MAPPING_URL_COMMENT_RE, "");
1099
1130
  }
1131
+ /**
1132
+ * Applies {@link PlugboyWorkspace.optimizeCSSOptions} to every stylesheet the
1133
+ * build writes.
1134
+ *
1135
+ * This runs in `writeBundle`, on the files on disk, rather than on the bundle
1136
+ * assets in `generateBundle` — because not every stylesheet exists as a bundle
1137
+ * asset by then. tsdown's own CSS pipeline emits from a *post* plugin, which
1138
+ * runs after every user plugin's `generateBundle`, so CSS that tsdown produces
1139
+ * (a plain `.css` / `.scss` import, or — since the vanilla-extract plugin routes
1140
+ * its `.css.ts` through tsdown — extracted CSS) used to skip these
1141
+ * optimizations entirely, while CSS a plugin emitted itself received them. By
1142
+ * `writeBundle` every producer has finished and the whole set is on disk.
1143
+ *
1144
+ * `preserve-css-imports` re-injects external `@import`s in its own
1145
+ * `writeBundle`; it is registered after this plugin, so it always sees the
1146
+ * optimized file.
1147
+ */
1100
1148
  function OptimizeCSSPlugin(workspace) {
1149
+ const processed = /* @__PURE__ */ new Set();
1101
1150
  return {
1102
1151
  name: "plugboy-optimize-css",
1103
- async generateBundle(_options, bundle) {
1152
+ buildStart() {
1153
+ processed.clear();
1154
+ },
1155
+ async writeBundle(options, bundle) {
1104
1156
  const { optimizeCSSOptions } = workspace;
1105
1157
  if (!optimizeCSSOptions) return;
1158
+ const { dir } = options;
1159
+ if (!dir) return;
1160
+ const stylesheets = await listEmittedStylesheets(workspace, dir, bundle);
1161
+ await Promise.all(stylesheets.map(async (fileName) => {
1162
+ const filePath = path.join(dir, fileName);
1163
+ if (processed.has(filePath)) return;
1164
+ let css;
1165
+ try {
1166
+ css = await fs$1.readFile(filePath, "utf8");
1167
+ } catch {
1168
+ return;
1169
+ }
1170
+ processed.add(filePath);
1171
+ const optimized = await optimizeCSS(css, fileName, optimizeCSSOptions);
1172
+ if (optimized !== css) await fs$1.writeFile(filePath, optimized);
1173
+ }));
1174
+ }
1175
+ };
1176
+ }
1177
+ //#endregion
1178
+ //#region src/workspace/plugins/assemble-entry-css.ts
1179
+ function createAssembleEntryCssPlugin(workspace) {
1180
+ /** Chunk file name -> its identity and static imports, as of `generateBundle`. */
1181
+ const graph = /* @__PURE__ */ new Map();
1182
+ return definePlugin({
1183
+ name: "plugboy-assemble-entry-css",
1184
+ buildStart() {
1185
+ graph.clear();
1186
+ },
1187
+ generateBundle(_options, bundle) {
1106
1188
  for (const chunk of Object.values(bundle)) {
1107
- if (chunk.type !== "asset" || !chunk.fileName.endsWith(".css")) continue;
1108
- await optimizeCSS(chunk, optimizeCSSOptions);
1189
+ if (chunk.type !== "chunk") continue;
1190
+ graph.set(chunk.fileName, {
1191
+ name: chunk.name,
1192
+ isEntry: chunk.isEntry,
1193
+ imports: [...chunk.imports]
1194
+ });
1109
1195
  }
1196
+ },
1197
+ async writeBundle(options, bundle) {
1198
+ const { dir } = options;
1199
+ if (!dir) return;
1200
+ if (workspace.cssOptions?.inject) return;
1201
+ const targets = declaredStylesheets(workspace);
1202
+ if (!targets.size) return;
1203
+ const emitted = new Set(Object.values(bundle).filter((chunk) => chunk.type === "asset" && chunk.fileName.endsWith(".css")).map((chunk) => chunk.fileName));
1204
+ if (!emitted.size) return;
1205
+ /** The stylesheet a chunk owns, if the build emitted one. */
1206
+ const cssOf = (fileName) => {
1207
+ const css = toCssFileName(fileName);
1208
+ return css !== fileName && emitted.has(css) ? css : void 0;
1209
+ };
1210
+ /** Stylesheets an entry needs, dependencies first, each listed once. */
1211
+ const collect = (entryFileName) => {
1212
+ const visited = /* @__PURE__ */ new Set();
1213
+ const ordered = [];
1214
+ const walk = (fileName) => {
1215
+ if (visited.has(fileName)) return;
1216
+ visited.add(fileName);
1217
+ const node = graph.get(fileName);
1218
+ if (!node) return;
1219
+ for (const imported of node.imports) walk(imported);
1220
+ const css = cssOf(fileName);
1221
+ if (css && !ordered.includes(css)) ordered.push(css);
1222
+ };
1223
+ walk(entryFileName);
1224
+ return ordered;
1225
+ };
1226
+ const folded = /* @__PURE__ */ new Set();
1227
+ await Promise.all([...graph].map(async ([fileName, node]) => {
1228
+ if (!node.isEntry) return;
1229
+ const target = `${node.name}.css`;
1230
+ if (!targets.has(target)) return;
1231
+ const sources = collect(fileName);
1232
+ if (!sources.length) return;
1233
+ sources.forEach((source) => folded.add(source));
1234
+ if (sources.length === 1 && sources[0] === target) return;
1235
+ const css = (await Promise.all(sources.map((source) => fs$1.readFile(path.join(dir, source), "utf8").catch(() => "")))).filter(Boolean).join("\n");
1236
+ if (!css) return;
1237
+ await fs$1.writeFile(path.join(dir, target), css);
1238
+ folded.add(target);
1239
+ }));
1240
+ await Promise.all([...folded].filter((css) => !targets.has(css)).map((css) => fs$1.rm(path.join(dir, css), { force: true })));
1110
1241
  }
1111
- };
1242
+ });
1112
1243
  }
1113
1244
  //#endregion
1114
1245
  //#region src/workspace/plugins/raw-loader.ts
1115
- const rawLoaderPlugin = {
1116
- name: "raw-loader",
1117
- async resolveId(id, importer, options) {
1118
- if (!id.endsWith("?raw")) return null;
1119
- const rawPath = id.slice(0, -4);
1120
- const resolved = await this.resolve(rawPath, importer, {
1121
- ...options,
1122
- skipSelf: true
1123
- });
1124
- if (!resolved) return null;
1125
- return `\0raw:${resolved.id}`;
1126
- },
1127
- async load(id) {
1128
- if (!id.startsWith("\0raw:")) return null;
1129
- const realPath = id.slice(5);
1130
- const content = await fs$1.readFile(realPath, "utf-8");
1131
- return `export default ${JSON.stringify(content)};`;
1132
- }
1133
- };
1246
+ /**
1247
+ * Plugin that inlines the contents of a `?raw` import.
1248
+ *
1249
+ * The virtual module id is kept **relative to the workspace** and turned back
1250
+ * into a path only inside `load`. rolldown normalizes an ordinary module id
1251
+ * against the project when it prints the `//#region <id>` comment that precedes
1252
+ * each module in the output, but a virtual id — anything starting with `\0` — is
1253
+ * printed verbatim. Building the id from the resolved absolute path therefore
1254
+ * published the build machine's directory layout:
1255
+ *
1256
+ * ```js
1257
+ * //#region \0raw:/home/runner/work/acme-ui/acme-ui/packages/core/src/logo.svg
1258
+ * ```
1259
+ *
1260
+ * A workspace-relative id makes the output independent of where the build ran,
1261
+ * which also keeps two machines' `dist` diffable.
1262
+ */
1263
+ const PREFIX = "\0raw:";
1264
+ const SUFFIX = "?raw";
1265
+ function createRawLoaderPlugin(workspace) {
1266
+ const root = workspace.dir.value;
1267
+ return {
1268
+ name: "raw-loader",
1269
+ async resolveId(id, importer, options) {
1270
+ if (!id.endsWith(SUFFIX)) return null;
1271
+ const rawPath = id.slice(0, -4);
1272
+ const resolved = await this.resolve(rawPath, importer, {
1273
+ ...options,
1274
+ skipSelf: true
1275
+ });
1276
+ if (!resolved) return null;
1277
+ return `${PREFIX}${path.relative(root, resolved.id).split(path.sep).join(path.posix.sep)}`;
1278
+ },
1279
+ async load(id) {
1280
+ if (!id.startsWith(PREFIX)) return null;
1281
+ const content = await fs$1.readFile(path.resolve(root, id.slice(5)), "utf-8");
1282
+ return `export default ${JSON.stringify(content)};`;
1283
+ }
1284
+ };
1285
+ }
1134
1286
  //#endregion
1135
1287
  //#region src/workspace/plugins/preserve-css-imports.ts
1136
1288
  /**
1137
- * Plugin to preserve external CSS `@import` statements.
1289
+ * Plugin to preserve what tsdown's CSS pipeline would rewrite away at the top of
1290
+ * a stylesheet: external `@import` statements and the authored `@layer` order.
1138
1291
  *
1139
- * rolldown / tsdown's CSS pipeline (lightningcss) resolves and inlines every
1140
- * `@import` it can. For bare package specifiers (e.g.
1292
+ * **External `@import`s.** rolldown / tsdown's CSS pipeline (lightningcss)
1293
+ * resolves and inlines every `@import` it can. For bare package specifiers (e.g.
1141
1294
  * `@import url('material-symbols/rounded.css') layer(...)`) that is wrong for a
1142
1295
  * library build: it bloats the output and rebases the imported package's own
1143
1296
  * relative asset URLs (fonts) against our `dist`, breaking them. Such imports
1144
1297
  * should stay external so the consumer's bundler resolves them.
1145
1298
  *
1146
- * We can't intercept this in a `transform` hook by then tsdown has already
1147
- * inlined the imports. Instead we strip them in `load` (which runs before the
1148
- * CSS transform), remember them, and re-emit them in `writeBundle`, after every
1149
- * other CSS producer (including vanilla-extract's merge) has written the final
1150
- * file to disk.
1299
+ * **Layer order.** lightningcss drops a name from an `@layer a, b, c;` statement
1300
+ * when a block for it follows in the same stylesheet, since the block establishes
1301
+ * the same order. That holds for a standalone document, but not for a library
1302
+ * stylesheet whose statement also orders layers belonging to *other* packages:
1303
+ * once the name is gone, its position is decided by wherever its block happens to
1304
+ * land relative to those, and the authored order is lost. `@fastkit/vui` declares
1305
+ * `@layer vui-normalize, vui-color-scheme, …, vui;`, and losing `vui-normalize`
1306
+ * from it promoted the reset layer above the packages it is supposed to lose to.
1307
+ *
1308
+ * Both are captured from a `transform` hook declared `order: 'pre'`, which runs
1309
+ * ahead of tsdown's CSS handling even though that is registered as a *pre plugin*
1310
+ * — hook order wins over plugin order. It is the only point that sees the CSS of
1311
+ * **every** stylesheet in the graph, including a virtual one another plugin
1312
+ * supplies from `load`: vanilla-extract generates its `@layer` statements into
1313
+ * such a module, so a `load`-based capture would miss exactly the case where the
1314
+ * generated statement is the only record of the intended order. The captured
1315
+ * values are re-emitted in `writeBundle`, after every CSS producer has written
1316
+ * its final file to disk.
1151
1317
  */
1152
1318
  /**
1153
1319
  * Matches a single `@import` statement, capturing the quote (group 1) and the
@@ -1165,47 +1331,133 @@ function isInternalImportSpecifier(spec) {
1165
1331
  }
1166
1332
  /** Matches a top-level `@layer <names>;` statement (declaration, not a block). */
1167
1333
  const LAYER_STATEMENT_RE = /@layer\s+([^{};]+);[ \t]*\n?/g;
1168
- function createPreserveCssImportsPlugin() {
1334
+ /** Stylesheet ids, with any query (`?source=…`, `?inline`) still attached. */
1335
+ const STYLE_ID_RE = /\.(?:css|scss|sass|less|styl|stylus)(?:$|\?)/;
1336
+ /** The layer names a stylesheet declares, in the order it declares them. */
1337
+ function collectLayerNames(css) {
1338
+ const names = [];
1339
+ for (const [, group] of css.matchAll(LAYER_STATEMENT_RE)) for (const name of group.split(",")) {
1340
+ const trimmed = name.trim();
1341
+ if (trimmed && !names.includes(trimmed)) names.push(trimmed);
1342
+ }
1343
+ return names;
1344
+ }
1345
+ /**
1346
+ * Merge several declaration orders into one that contradicts none of them.
1347
+ *
1348
+ * Concatenating them and dropping repeats does not work, because a name's first
1349
+ * appearance is rarely where its order is decided: vanilla-extract re-declares a
1350
+ * layer at the top of *every* stylesheet that puts a rule in it, so a single
1351
+ * `@layer that-one;` from some component is seen before the module that declares
1352
+ * how all the layers relate — and once the component's name is in the list, the
1353
+ * declaring module's order is silently dropped for it.
1354
+ *
1355
+ * Each sequence is therefore read as a set of "must come before" constraints and
1356
+ * the result is a topological sort of them, preferring the earliest-seen name when
1357
+ * several are free. Earlier sequences win: a constraint that would contradict one
1358
+ * already recorded is skipped, so a stylesheet's own surviving statement — whose
1359
+ * order tsdown may have rewritten — can add names without reordering anything.
1360
+ */
1361
+ function mergeLayerOrder(sequences) {
1362
+ const nodes = [];
1363
+ const next = /* @__PURE__ */ new Map();
1364
+ const add = (name) => {
1365
+ if (next.has(name)) return;
1366
+ nodes.push(name);
1367
+ next.set(name, /* @__PURE__ */ new Set());
1368
+ };
1369
+ /** Whether `to` already has to come after `from`. */
1370
+ const precedes = (from, to) => {
1371
+ const seen = /* @__PURE__ */ new Set();
1372
+ const stack = [from];
1373
+ while (stack.length) {
1374
+ const current = stack.pop();
1375
+ if (current === to) return true;
1376
+ if (seen.has(current)) continue;
1377
+ seen.add(current);
1378
+ stack.push(...next.get(current) ?? []);
1379
+ }
1380
+ return false;
1381
+ };
1382
+ for (const sequence of sequences) {
1383
+ sequence.forEach(add);
1384
+ for (let i = 0; i + 1 < sequence.length; i++) {
1385
+ const from = sequence[i];
1386
+ const to = sequence[i + 1];
1387
+ if (from === to || precedes(to, from)) continue;
1388
+ next.get(from).add(to);
1389
+ }
1390
+ }
1391
+ const incoming = new Map(nodes.map((name) => [name, 0]));
1392
+ for (const [, targets] of next) for (const target of targets) incoming.set(target, (incoming.get(target) ?? 0) + 1);
1393
+ const remaining = new Set(nodes);
1394
+ const merged = [];
1395
+ while (remaining.size) {
1396
+ const name = nodes.find((it) => remaining.has(it) && incoming.get(it) === 0) ?? nodes.find((it) => remaining.has(it));
1397
+ remaining.delete(name);
1398
+ merged.push(name);
1399
+ for (const target of next.get(name) ?? []) incoming.set(target, (incoming.get(target) ?? 1) - 1);
1400
+ }
1401
+ return merged;
1402
+ }
1403
+ function createPreserveCssImportsPlugin(workspace) {
1169
1404
  const externalImports = [];
1405
+ const layersByModule = /* @__PURE__ */ new Map();
1406
+ const declaredLayers = [];
1170
1407
  const processed = /* @__PURE__ */ new Set();
1171
1408
  return definePlugin({
1172
1409
  name: "preserve-css-imports",
1173
1410
  buildStart() {
1174
1411
  externalImports.length = 0;
1412
+ layersByModule.clear();
1413
+ declaredLayers.length = 0;
1175
1414
  processed.clear();
1176
1415
  },
1177
- async load(id) {
1178
- const file = id.split("?")[0];
1179
- if (!file.endsWith(".css")) return null;
1180
- let code;
1181
- try {
1182
- code = await fs$1.readFile(file, "utf8");
1183
- } catch {
1184
- return null;
1416
+ transform: {
1417
+ order: "pre",
1418
+ filter: { id: STYLE_ID_RE },
1419
+ handler(code, id) {
1420
+ const file = id.split("?")[0];
1421
+ if (!STYLE_ID_RE.test(file)) return null;
1422
+ const names = collectLayerNames(code);
1423
+ if (names.length) layersByModule.set(id, names);
1424
+ if (!file.endsWith(".css") || !code.includes("@import")) return null;
1425
+ let changed = false;
1426
+ const stripped = code.replace(IMPORT_RE, (statement, _quote, spec) => {
1427
+ if (isInternalImportSpecifier(spec)) return statement;
1428
+ changed = true;
1429
+ const normalized = statement.trim();
1430
+ if (!externalImports.includes(normalized)) externalImports.push(normalized);
1431
+ return "";
1432
+ });
1433
+ if (!changed) return null;
1434
+ return {
1435
+ code: stripped,
1436
+ map: null
1437
+ };
1185
1438
  }
1186
- if (!code.includes("@import")) return null;
1187
- let changed = false;
1188
- const stripped = code.replace(IMPORT_RE, (statement, _quote, spec) => {
1189
- if (isInternalImportSpecifier(spec)) return statement;
1190
- changed = true;
1191
- const normalized = statement.trim();
1192
- if (!externalImports.includes(normalized)) externalImports.push(normalized);
1193
- return "";
1194
- });
1195
- if (!changed) return null;
1196
- return {
1197
- code: stripped,
1198
- map: null
1199
- };
1439
+ },
1440
+ generateBundle(_options, bundle) {
1441
+ if (!layersByModule.size) return;
1442
+ const sequences = [];
1443
+ for (const chunk of Object.values(bundle)) {
1444
+ if (chunk.type !== "chunk") continue;
1445
+ for (const id of chunk.moduleIds) {
1446
+ const names = layersByModule.get(id);
1447
+ if (names) sequences.push(names);
1448
+ }
1449
+ }
1450
+ declaredLayers.length = 0;
1451
+ declaredLayers.push(...mergeLayerOrder(sequences));
1200
1452
  },
1201
1453
  async writeBundle(options, bundle) {
1202
- if (!externalImports.length) return;
1454
+ if (!externalImports.length && !declaredLayers.length) return;
1203
1455
  const { dir } = options;
1204
1456
  if (!dir) return;
1205
- const importBlock = `${externalImports.join("\n")}\n`;
1206
- await Promise.all(Object.values(bundle).map(async (chunk) => {
1207
- if (chunk.type !== "asset" || !chunk.fileName.endsWith(".css")) return;
1208
- const filePath = path.join(dir, chunk.fileName);
1457
+ const importBlock = externalImports.length ? `${externalImports.join("\n")}\n` : "";
1458
+ const stylesheets = await listEmittedStylesheets(workspace, dir, bundle);
1459
+ await Promise.all(stylesheets.map(async (fileName) => {
1460
+ const filePath = path.join(dir, fileName);
1209
1461
  if (processed.has(filePath)) return;
1210
1462
  let css;
1211
1463
  try {
@@ -1214,14 +1466,10 @@ function createPreserveCssImportsPlugin() {
1214
1466
  return;
1215
1467
  }
1216
1468
  processed.add(filePath);
1217
- const layerNames = [];
1218
- for (const [, names] of css.matchAll(LAYER_STATEMENT_RE)) for (const name of names.split(",")) {
1219
- const trimmed = name.trim();
1220
- if (trimmed && !layerNames.includes(trimmed)) layerNames.push(trimmed);
1221
- }
1469
+ const layerNames = mergeLayerOrder([declaredLayers, collectLayerNames(css)]);
1222
1470
  const body = css.replace(LAYER_STATEMENT_RE, "");
1223
- const layerStatement = layerNames.length ? `@layer ${layerNames.join(", ")};\n` : "";
1224
- await fs$1.writeFile(filePath, `${layerStatement}${importBlock}${body}`);
1471
+ const next = `${layerNames.length ? `@layer ${layerNames.join(", ")};\n` : ""}${importBlock}${body}`;
1472
+ if (next !== css) await fs$1.writeFile(filePath, next);
1225
1473
  }));
1226
1474
  }
1227
1475
  });
@@ -1392,10 +1640,11 @@ var PlugboyWorkspace = class {
1392
1640
  createExternalImportsPlugin(this),
1393
1641
  createSuppressDtsSourcemapWarningPlugin(),
1394
1642
  ...plugins,
1643
+ createAssembleEntryCssPlugin(this),
1395
1644
  OptimizeCSSPlugin(this),
1396
1645
  WorkspaceEnvPlugin(this),
1397
- rawLoaderPlugin,
1398
- createPreserveCssImportsPlugin()
1646
+ createRawLoaderPlugin(this),
1647
+ createPreserveCssImportsPlugin(this)
1399
1648
  ];
1400
1649
  this.hooks = hooks;
1401
1650
  this.dts = dts;
@@ -1717,4 +1966,4 @@ export default defineWorkspaceConfig({
1717
1966
  //#endregion
1718
1967
  export { resolveOptimizeCSSOptions as $, resolveRawExposeEntriesSettings as A, resolveBundledConfigOutputFile as B, findProjectPlugin as C, loadProjectConfig as D, isProjectPackageJson as E, getFilename as F, mergeChunkAddons as G, stripDanglingDTSSourceMaps as H, isFileNotFoundException as I, isPromise as J, mergeExternals as K, pathExists as L, findConfig as M, findFile as N, resolveUserProjectConfig as O, getDirname as P, WORKSPACE_REQUIRED_FIELDS as Q, rmrf as R, extractProjectPlugins as S, defineProjectConfig as T, exitHook as U, DANGLING_DTS_SOURCE_MAP_RE as V, collectExternalStringPrefixes as W, PROJECT_REQUIRED_FIELDS as X, resolveListable as Y, TSDOWN_SYNC_OPTIONS as Z, loadWorkspaceConfig as _, syncWorkspacePackageFields as a, resolveUserWorkspaceConfig as b, Builder as c, getWorkspacePackageJson as d, mergeDTSSettingsList as et, Path as f, isWorkspacePackageJson as g, defineWorkspaceConfig as h, getWorkspace as i, createHooksDefaults as it, copyDirSync as j, exposeEntries as k, findWorkspacePackages as l, resolveUserHooks as m, PlugboyWorkspace as n, normalizeDTSPreserveTypeTarget as nt, PlugboyProject as o, buildHooks as p, mergeNoExternals as q, WORKSPACE_PACKAGE_SYNC_FIELDS as r, normalizeDTSSettings as rt, getProject as s, generateWorkspace as t, normalizeDTSPreserveTypeSettings as tt, getProjectPackageJson as u, resolveRawWorkspaceEntries as v, resolveUserPluginOption as w, definePlugin as x, resolveRawWorkspaceEntry as y, writeFileAtomic as z };
1719
1968
 
1720
- //# sourceMappingURL=workspace-CciZggrg.mjs.map
1969
+ //# sourceMappingURL=workspace-CniX_JDc.mjs.map