@fastkit/plugboy 1.3.0 → 1.4.0

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-B6BoRqWL.mjs";
2
2
  import { cac } from "cac";
3
3
  //#region package.json
4
- var version = "1.3.0";
4
+ var version = "1.4.0";
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-B6BoRqWL.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,38 +1109,137 @@ 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
@@ -1134,20 +1265,34 @@ const rawLoaderPlugin = {
1134
1265
  //#endregion
1135
1266
  //#region src/workspace/plugins/preserve-css-imports.ts
1136
1267
  /**
1137
- * Plugin to preserve external CSS `@import` statements.
1268
+ * Plugin to preserve what tsdown's CSS pipeline would rewrite away at the top of
1269
+ * a stylesheet: external `@import` statements and the authored `@layer` order.
1138
1270
  *
1139
- * rolldown / tsdown's CSS pipeline (lightningcss) resolves and inlines every
1140
- * `@import` it can. For bare package specifiers (e.g.
1271
+ * **External `@import`s.** rolldown / tsdown's CSS pipeline (lightningcss)
1272
+ * resolves and inlines every `@import` it can. For bare package specifiers (e.g.
1141
1273
  * `@import url('material-symbols/rounded.css') layer(...)`) that is wrong for a
1142
1274
  * library build: it bloats the output and rebases the imported package's own
1143
1275
  * relative asset URLs (fonts) against our `dist`, breaking them. Such imports
1144
1276
  * should stay external so the consumer's bundler resolves them.
1145
1277
  *
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.
1278
+ * **Layer order.** lightningcss drops a name from an `@layer a, b, c;` statement
1279
+ * when a block for it follows in the same stylesheet, since the block establishes
1280
+ * the same order. That holds for a standalone document, but not for a library
1281
+ * stylesheet whose statement also orders layers belonging to *other* packages:
1282
+ * once the name is gone, its position is decided by wherever its block happens to
1283
+ * land relative to those, and the authored order is lost. `@fastkit/vui` declares
1284
+ * `@layer vui-normalize, vui-color-scheme, …, vui;`, and losing `vui-normalize`
1285
+ * from it promoted the reset layer above the packages it is supposed to lose to.
1286
+ *
1287
+ * Both are captured from a `transform` hook declared `order: 'pre'`, which runs
1288
+ * ahead of tsdown's CSS handling even though that is registered as a *pre plugin*
1289
+ * — hook order wins over plugin order. It is the only point that sees the CSS of
1290
+ * **every** stylesheet in the graph, including a virtual one another plugin
1291
+ * supplies from `load`: vanilla-extract generates its `@layer` statements into
1292
+ * such a module, so a `load`-based capture would miss exactly the case where the
1293
+ * generated statement is the only record of the intended order. The captured
1294
+ * values are re-emitted in `writeBundle`, after every CSS producer has written
1295
+ * its final file to disk.
1151
1296
  */
1152
1297
  /**
1153
1298
  * Matches a single `@import` statement, capturing the quote (group 1) and the
@@ -1165,47 +1310,133 @@ function isInternalImportSpecifier(spec) {
1165
1310
  }
1166
1311
  /** Matches a top-level `@layer <names>;` statement (declaration, not a block). */
1167
1312
  const LAYER_STATEMENT_RE = /@layer\s+([^{};]+);[ \t]*\n?/g;
1168
- function createPreserveCssImportsPlugin() {
1313
+ /** Stylesheet ids, with any query (`?source=…`, `?inline`) still attached. */
1314
+ const STYLE_ID_RE = /\.(?:css|scss|sass|less|styl|stylus)(?:$|\?)/;
1315
+ /** The layer names a stylesheet declares, in the order it declares them. */
1316
+ function collectLayerNames(css) {
1317
+ const names = [];
1318
+ for (const [, group] of css.matchAll(LAYER_STATEMENT_RE)) for (const name of group.split(",")) {
1319
+ const trimmed = name.trim();
1320
+ if (trimmed && !names.includes(trimmed)) names.push(trimmed);
1321
+ }
1322
+ return names;
1323
+ }
1324
+ /**
1325
+ * Merge several declaration orders into one that contradicts none of them.
1326
+ *
1327
+ * Concatenating them and dropping repeats does not work, because a name's first
1328
+ * appearance is rarely where its order is decided: vanilla-extract re-declares a
1329
+ * layer at the top of *every* stylesheet that puts a rule in it, so a single
1330
+ * `@layer that-one;` from some component is seen before the module that declares
1331
+ * how all the layers relate — and once the component's name is in the list, the
1332
+ * declaring module's order is silently dropped for it.
1333
+ *
1334
+ * Each sequence is therefore read as a set of "must come before" constraints and
1335
+ * the result is a topological sort of them, preferring the earliest-seen name when
1336
+ * several are free. Earlier sequences win: a constraint that would contradict one
1337
+ * already recorded is skipped, so a stylesheet's own surviving statement — whose
1338
+ * order tsdown may have rewritten — can add names without reordering anything.
1339
+ */
1340
+ function mergeLayerOrder(sequences) {
1341
+ const nodes = [];
1342
+ const next = /* @__PURE__ */ new Map();
1343
+ const add = (name) => {
1344
+ if (next.has(name)) return;
1345
+ nodes.push(name);
1346
+ next.set(name, /* @__PURE__ */ new Set());
1347
+ };
1348
+ /** Whether `to` already has to come after `from`. */
1349
+ const precedes = (from, to) => {
1350
+ const seen = /* @__PURE__ */ new Set();
1351
+ const stack = [from];
1352
+ while (stack.length) {
1353
+ const current = stack.pop();
1354
+ if (current === to) return true;
1355
+ if (seen.has(current)) continue;
1356
+ seen.add(current);
1357
+ stack.push(...next.get(current) ?? []);
1358
+ }
1359
+ return false;
1360
+ };
1361
+ for (const sequence of sequences) {
1362
+ sequence.forEach(add);
1363
+ for (let i = 0; i + 1 < sequence.length; i++) {
1364
+ const from = sequence[i];
1365
+ const to = sequence[i + 1];
1366
+ if (from === to || precedes(to, from)) continue;
1367
+ next.get(from).add(to);
1368
+ }
1369
+ }
1370
+ const incoming = new Map(nodes.map((name) => [name, 0]));
1371
+ for (const [, targets] of next) for (const target of targets) incoming.set(target, (incoming.get(target) ?? 0) + 1);
1372
+ const remaining = new Set(nodes);
1373
+ const merged = [];
1374
+ while (remaining.size) {
1375
+ const name = nodes.find((it) => remaining.has(it) && incoming.get(it) === 0) ?? nodes.find((it) => remaining.has(it));
1376
+ remaining.delete(name);
1377
+ merged.push(name);
1378
+ for (const target of next.get(name) ?? []) incoming.set(target, (incoming.get(target) ?? 1) - 1);
1379
+ }
1380
+ return merged;
1381
+ }
1382
+ function createPreserveCssImportsPlugin(workspace) {
1169
1383
  const externalImports = [];
1384
+ const layersByModule = /* @__PURE__ */ new Map();
1385
+ const declaredLayers = [];
1170
1386
  const processed = /* @__PURE__ */ new Set();
1171
1387
  return definePlugin({
1172
1388
  name: "preserve-css-imports",
1173
1389
  buildStart() {
1174
1390
  externalImports.length = 0;
1391
+ layersByModule.clear();
1392
+ declaredLayers.length = 0;
1175
1393
  processed.clear();
1176
1394
  },
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;
1395
+ transform: {
1396
+ order: "pre",
1397
+ filter: { id: STYLE_ID_RE },
1398
+ handler(code, id) {
1399
+ const file = id.split("?")[0];
1400
+ if (!STYLE_ID_RE.test(file)) return null;
1401
+ const names = collectLayerNames(code);
1402
+ if (names.length) layersByModule.set(id, names);
1403
+ if (!file.endsWith(".css") || !code.includes("@import")) return null;
1404
+ let changed = false;
1405
+ const stripped = code.replace(IMPORT_RE, (statement, _quote, spec) => {
1406
+ if (isInternalImportSpecifier(spec)) return statement;
1407
+ changed = true;
1408
+ const normalized = statement.trim();
1409
+ if (!externalImports.includes(normalized)) externalImports.push(normalized);
1410
+ return "";
1411
+ });
1412
+ if (!changed) return null;
1413
+ return {
1414
+ code: stripped,
1415
+ map: null
1416
+ };
1417
+ }
1418
+ },
1419
+ generateBundle(_options, bundle) {
1420
+ if (!layersByModule.size) return;
1421
+ const sequences = [];
1422
+ for (const chunk of Object.values(bundle)) {
1423
+ if (chunk.type !== "chunk") continue;
1424
+ for (const id of chunk.moduleIds) {
1425
+ const names = layersByModule.get(id);
1426
+ if (names) sequences.push(names);
1427
+ }
1185
1428
  }
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
- };
1429
+ declaredLayers.length = 0;
1430
+ declaredLayers.push(...mergeLayerOrder(sequences));
1200
1431
  },
1201
1432
  async writeBundle(options, bundle) {
1202
- if (!externalImports.length) return;
1433
+ if (!externalImports.length && !declaredLayers.length) return;
1203
1434
  const { dir } = options;
1204
1435
  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);
1436
+ const importBlock = externalImports.length ? `${externalImports.join("\n")}\n` : "";
1437
+ const stylesheets = await listEmittedStylesheets(workspace, dir, bundle);
1438
+ await Promise.all(stylesheets.map(async (fileName) => {
1439
+ const filePath = path.join(dir, fileName);
1209
1440
  if (processed.has(filePath)) return;
1210
1441
  let css;
1211
1442
  try {
@@ -1214,14 +1445,10 @@ function createPreserveCssImportsPlugin() {
1214
1445
  return;
1215
1446
  }
1216
1447
  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
- }
1448
+ const layerNames = mergeLayerOrder([declaredLayers, collectLayerNames(css)]);
1222
1449
  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}`);
1450
+ const next = `${layerNames.length ? `@layer ${layerNames.join(", ")};\n` : ""}${importBlock}${body}`;
1451
+ if (next !== css) await fs$1.writeFile(filePath, next);
1225
1452
  }));
1226
1453
  }
1227
1454
  });
@@ -1392,10 +1619,11 @@ var PlugboyWorkspace = class {
1392
1619
  createExternalImportsPlugin(this),
1393
1620
  createSuppressDtsSourcemapWarningPlugin(),
1394
1621
  ...plugins,
1622
+ createAssembleEntryCssPlugin(this),
1395
1623
  OptimizeCSSPlugin(this),
1396
1624
  WorkspaceEnvPlugin(this),
1397
1625
  rawLoaderPlugin,
1398
- createPreserveCssImportsPlugin()
1626
+ createPreserveCssImportsPlugin(this)
1399
1627
  ];
1400
1628
  this.hooks = hooks;
1401
1629
  this.dts = dts;
@@ -1717,4 +1945,4 @@ export default defineWorkspaceConfig({
1717
1945
  //#endregion
1718
1946
  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
1947
 
1720
- //# sourceMappingURL=workspace-CciZggrg.mjs.map
1948
+ //# sourceMappingURL=workspace-B6BoRqWL.mjs.map