@fastkit/plugboy-vanilla-extract-plugin 4.1.2 → 4.2.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
|
@@ -6,8 +6,8 @@ A plugin that integrates [Vanilla Extract](https://vanilla-extract.style/) into
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- **Single CSS output**: Combines the styles extracted from a package's `.css.ts` files into one `dist/<package>.css`.
|
|
10
|
-
- **
|
|
9
|
+
- **Single CSS output**: Combines the styles extracted from a package's `.css.ts` files into one `dist/<package>.css`, together with any plain `.css` / `.scss` the package imports.
|
|
10
|
+
- **Handled by tsdown's CSS pipeline**: The extracted CSS is handed to tsdown as an ordinary CSS module, so every [`css` option](https://github.com/dadajam4/fastkit/blob/main/packages/plugboy/README.md#css-options) applies to it — `target` (vendor prefixes and syntax lowering), `transformer` (lightningcss or postcss), `minify`, preprocessor options — as do plugboy's own `optimizeCSS` optimizations.
|
|
11
11
|
- **Vite integration**: Ships a Vite plugin for use in dev servers, Storybook, and similar environments.
|
|
12
12
|
- **Layer helpers**: `@fastkit/plugboy-vanilla-extract-plugin/css` exposes utilities for defining cascade layers in a type-safe way.
|
|
13
13
|
|
|
@@ -89,21 +89,42 @@ The main options accepted by `createVanillaExtractPlugin(options)` / `ViteVanill
|
|
|
89
89
|
### Reserved `css` options
|
|
90
90
|
|
|
91
91
|
This plugin sets plugboy's `css.splitting` and `css.fileName` for the workspaces
|
|
92
|
-
it is registered in,
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
it is registered in, so the emitted stylesheets match the CSS exports plugboy
|
|
93
|
+
declares — `./<entry>.css` for every entry with `css: true`:
|
|
94
|
+
|
|
95
|
+
- One such entry (the common case): `splitting: false` and
|
|
96
|
+
`fileName: '<package>.css'` — a single stylesheet for the package.
|
|
97
|
+
- Several: `splitting: true`, so tsdown emits one stylesheet per output chunk,
|
|
98
|
+
named after it. `splitting: false` would collect the package into one file and
|
|
99
|
+
keep only one chunk's CSS, silently dropping the rest.
|
|
100
|
+
|
|
101
|
+
Configuration wins over plugin defaults, so declaring either key is applied as
|
|
102
|
+
written, and the emitted file names may then no longer match those exports. Every
|
|
103
|
+
other `css` option (`target`, `transformer`, `minify`, `preprocessorOptions`,
|
|
104
|
+
`lightningcss`, `postcss`, `modules`, …) is free to use and applies to the
|
|
105
|
+
extracted CSS as well.
|
|
106
|
+
|
|
107
|
+
> [!NOTE]
|
|
108
|
+
> With several CSS entries, a `.css.ts` imported by more than one of them is placed
|
|
109
|
+
> in a shared chunk. plugboy folds that chunk's stylesheet back into each entry that
|
|
110
|
+
> needs it, so every `./<entry>.css` stays complete — see
|
|
111
|
+
> [One stylesheet per CSS entry](https://github.com/dadajam4/fastkit/blob/main/packages/plugboy/README.md#one-stylesheet-per-css-entry).
|
|
112
|
+
|
|
113
|
+
### How the CSS reaches the output
|
|
114
|
+
|
|
115
|
+
`@vanilla-extract/rollup-plugin` compiles a `.css.ts` to JavaScript plus an
|
|
116
|
+
`import` of a virtual stylesheet, which it then resolves as an **external**
|
|
117
|
+
module — and, in `extract` mode, emits itself as a bundler asset. Either way the
|
|
118
|
+
CSS never enters tsdown's CSS pipeline, so none of the `css` options can reach it.
|
|
119
|
+
|
|
120
|
+
This plugin resolves that virtual stylesheet as a real module instead — the
|
|
121
|
+
approach [`@vanilla-extract/vite-plugin`](https://vanilla-extract.style/documentation/integrations/vite/)
|
|
122
|
+
takes — so the extracted CSS is an ordinary `.css` module in the graph and tsdown
|
|
123
|
+
owns it from there.
|
|
124
|
+
|
|
125
|
+
If you write a plugin that emits CSS for a plugboy build, prefer the same shape.
|
|
126
|
+
Emitting a stylesheet as a bundler asset puts it outside the configured pipeline,
|
|
127
|
+
where nothing about `css.target` or `css.transformer` applies to it.
|
|
107
128
|
|
|
108
129
|
## License
|
|
109
130
|
|
|
@@ -1,139 +1,43 @@
|
|
|
1
1
|
import { t as PLUGIN_NAME } from "./types-Djg9yxv0.mjs";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { definePlugin, findFile } from "@fastkit/plugboy";
|
|
5
4
|
import { vanillaExtractPlugin } from "@vanilla-extract/rollup-plugin";
|
|
5
|
+
import { getSourceFromVirtualCssFile, virtualCssFileFilter } from "@vanilla-extract/integration";
|
|
6
6
|
//#region src/plugin.ts
|
|
7
|
-
/**
|
|
8
|
-
* Temporary file name for the CSS that tsdown's own CSS pipeline emits.
|
|
9
|
-
*
|
|
10
|
-
* A package can have two independent sources of CSS:
|
|
11
|
-
* - tsdown's built-in CSS handling, for plain `.css` / `.scss` imports.
|
|
12
|
-
* - `@vanilla-extract/rollup-plugin`, for `.css.ts` files (extracted into a
|
|
13
|
-
* single bundle named after the package).
|
|
14
|
-
*
|
|
15
|
-
* If both are pointed at the same final file name they collide
|
|
16
|
-
* (`FILE_NAME_CONFLICT` — one silently overwrites the other, dropping all of
|
|
17
|
-
* the vanilla-extract component CSS). To avoid that we route tsdown's CSS to
|
|
18
|
-
* this temporary name and merge it into the vanilla-extract bundle in
|
|
19
|
-
* `writeBundle`.
|
|
20
|
-
*/
|
|
21
|
-
const TSDOWN_CSS_FILE_NAME = "__ve-tsdown__.css";
|
|
22
7
|
async function createVanillaExtractPlugin(options = {}) {
|
|
23
8
|
return definePlugin({
|
|
24
9
|
name: PLUGIN_NAME,
|
|
25
10
|
_options: options,
|
|
26
|
-
hooks: { async setupWorkspace(ctx
|
|
11
|
+
hooks: { async setupWorkspace(ctx) {
|
|
27
12
|
const entryIds = Object.keys(ctx.config.entries);
|
|
28
13
|
const cssFileName = `${entryIds.includes(".") ? ctx.dir.basename : entryIds[0] ?? ctx.dir.basename}.css`;
|
|
29
|
-
const
|
|
30
|
-
let plainCssFallback = cssFileName;
|
|
14
|
+
const cssEntryCount = Object.values(ctx.config.entries).filter((entry) => entry.css).length;
|
|
31
15
|
ctx.mergeExternals(/@vanilla-extract/);
|
|
32
16
|
ctx.meta.hasVanillaExtract = !!await findFile(ctx.dirs.src.value, /\.css\.ts$/);
|
|
33
17
|
ctx.css = {
|
|
34
|
-
splitting:
|
|
35
|
-
fileName:
|
|
18
|
+
splitting: cssEntryCount > 1,
|
|
19
|
+
fileName: cssFileName,
|
|
36
20
|
...ctx.css
|
|
37
21
|
};
|
|
38
|
-
if (ctx.meta.hasVanillaExtract)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
},
|
|
58
|
-
generateBundle(_options, bundle) {
|
|
59
|
-
const collectEntryCss = (root) => {
|
|
60
|
-
const visited = /* @__PURE__ */ new Set();
|
|
61
|
-
const seenCss = /* @__PURE__ */ new Set();
|
|
62
|
-
const out = [];
|
|
63
|
-
let hasPlain = false;
|
|
64
|
-
const walk = (id) => {
|
|
65
|
-
if (visited.has(id)) return;
|
|
66
|
-
visited.add(id);
|
|
67
|
-
const info = this.getModuleInfo(id);
|
|
68
|
-
if (!info) return;
|
|
69
|
-
for (const dep of info.importedIds ?? []) {
|
|
70
|
-
const css = this.getModuleInfo(dep)?.meta?.css;
|
|
71
|
-
if (typeof css === "string") {
|
|
72
|
-
if (!seenCss.has(dep)) {
|
|
73
|
-
seenCss.add(dep);
|
|
74
|
-
out.push(css);
|
|
75
|
-
}
|
|
76
|
-
} else if (/\.css(\?|$)/i.test(dep)) hasPlain = true;
|
|
77
|
-
walk(dep);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
walk(root);
|
|
81
|
-
return {
|
|
82
|
-
css: out,
|
|
83
|
-
hasPlain
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
const entryCss = [];
|
|
87
|
-
for (const chunk of Object.values(bundle)) {
|
|
88
|
-
if (chunk.type !== "chunk" || !chunk.isEntry || !chunk.fileName.endsWith(".mjs") || !chunk.facadeModuleId) continue;
|
|
89
|
-
const { css, hasPlain } = collectEntryCss(chunk.facadeModuleId);
|
|
90
|
-
if (css.length) entryCss.push({
|
|
91
|
-
name: chunk.name,
|
|
92
|
-
source: css.join("\n"),
|
|
93
|
-
hasPlain
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
if (entryCss.length <= 1) return;
|
|
97
|
-
const reused = /* @__PURE__ */ new Set();
|
|
98
|
-
for (const { name, source } of entryCss) {
|
|
99
|
-
const fileName = `${name}.css`;
|
|
100
|
-
const existing = bundle[fileName];
|
|
101
|
-
if (existing && existing.type === "asset") {
|
|
102
|
-
existing.source = source;
|
|
103
|
-
reused.add(fileName);
|
|
104
|
-
} else this.emitFile({
|
|
105
|
-
type: "asset",
|
|
106
|
-
fileName,
|
|
107
|
-
source
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
const combined = bundle[cssFileName];
|
|
111
|
-
if (combined && combined.type === "asset" && !reused.has(cssFileName)) delete bundle[cssFileName];
|
|
112
|
-
plainCssTargets.clear();
|
|
113
|
-
plainCssFallback = `${entryCss[0].name}.css`;
|
|
114
|
-
for (const { name, hasPlain } of entryCss) if (hasPlain) plainCssTargets.add(`${name}.css`);
|
|
115
|
-
},
|
|
116
|
-
async writeBundle(outputOptions, bundle) {
|
|
117
|
-
const tmp = bundle[TSDOWN_CSS_FILE_NAME];
|
|
118
|
-
if (!tmp) return;
|
|
119
|
-
const tmpCss = tmp.type === "asset" ? tmp.source.toString() : "";
|
|
120
|
-
const dir = outputOptions.dir ?? ".";
|
|
121
|
-
const tmpPath = path.join(dir, TSDOWN_CSS_FILE_NAME);
|
|
122
|
-
if (tmpCss) {
|
|
123
|
-
const targets = plainCssTargets.size ? [...plainCssTargets] : [plainCssFallback];
|
|
124
|
-
await Promise.all(targets.map(async (target) => {
|
|
125
|
-
const targetPath = path.join(dir, target);
|
|
126
|
-
let targetCss = "";
|
|
127
|
-
try {
|
|
128
|
-
targetCss = await fs.readFile(targetPath, "utf8");
|
|
129
|
-
} catch {}
|
|
130
|
-
await fs.writeFile(targetPath, targetCss ? `${tmpCss}\n${targetCss}` : tmpCss);
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
await fs.rm(tmpPath, { force: true });
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
22
|
+
if (!ctx.meta.hasVanillaExtract) return;
|
|
23
|
+
const virtualCss = /* @__PURE__ */ new Map();
|
|
24
|
+
ctx.plugins.push({
|
|
25
|
+
name: `${PLUGIN_NAME}:virtual-css`,
|
|
26
|
+
buildStart() {
|
|
27
|
+
virtualCss.clear();
|
|
28
|
+
},
|
|
29
|
+
async resolveId(source) {
|
|
30
|
+
if (!virtualCssFileFilter.test(source)) return null;
|
|
31
|
+
const { fileName, source: css } = await getSourceFromVirtualCssFile(source);
|
|
32
|
+
const id = path.resolve(ctx.dir.value, fileName);
|
|
33
|
+
virtualCss.set(id, css);
|
|
34
|
+
return id;
|
|
35
|
+
},
|
|
36
|
+
load(id) {
|
|
37
|
+
return virtualCss.get(id) ?? null;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
ctx.plugins.push(vanillaExtractPlugin(options));
|
|
137
41
|
} }
|
|
138
42
|
});
|
|
139
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugboy-vanilla-extract-plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { definePlugin, findFile, type Plugin } from '@fastkit/plugboy';\nimport { vanillaExtractPlugin } from '@vanilla-extract/rollup-plugin';\nimport { VanillaExtractPlugin, PluginOptions, PLUGIN_NAME } from './types';\n\ndeclare module '@fastkit/plugboy' {\n export interface WorkspaceMeta {\n hasVanillaExtract: boolean;\n }\n}\n\n/**\n * Temporary file name for the CSS that tsdown's own CSS pipeline emits.\n *\n * A package can have two independent sources of CSS:\n * - tsdown's built-in CSS handling, for plain `.css` / `.scss` imports.\n * - `@vanilla-extract/rollup-plugin`, for `.css.ts` files (extracted into a\n * single bundle named after the package).\n *\n * If both are pointed at the same final file name they collide\n * (`FILE_NAME_CONFLICT` — one silently overwrites the other, dropping all of\n * the vanilla-extract component CSS). To avoid that we route tsdown's CSS to\n * this temporary name and merge it into the vanilla-extract bundle in\n * `writeBundle`.\n */\nconst TSDOWN_CSS_FILE_NAME = '__ve-tsdown__.css';\n\nexport async function createVanillaExtractPlugin(options: PluginOptions = {}) {\n return definePlugin<VanillaExtractPlugin>({\n name: PLUGIN_NAME,\n _options: options,\n hooks: {\n async setupWorkspace(ctx, getWorkspace) {\n // Derive the final CSS file name from the workspace entry, mirroring the\n // way plugboy names the JS output: the main entry (`.`) maps to the\n // package directory name (e.g. `vue-app-layout`), other entries keep\n // their id. Since `splitting: false` produces a single combined CSS for\n // the package, we base it on the main entry (falling back to the first).\n // Result: `dist/vue-app-layout.css` instead of `dist/__ve-tmp__.css`.\n const entryIds = Object.keys(ctx.config.entries);\n const cssBaseName = entryIds.includes('.')\n ? ctx.dir.basename\n : (entryIds[0] ?? ctx.dir.basename);\n const cssFileName = `${cssBaseName}.css`;\n\n // Routing for tsdown's plain-CSS blob (see `writeBundle`). plugboy's CSS\n // pipeline emits ALL plain (non-`.css.ts`) CSS — e.g. an `@font-face`\n // sheet `import`ed by an entry — into a single combined file\n // (`TSDOWN_CSS_FILE_NAME`); it cannot be split per entry (tsdown's own CSS\n // `splitting` drops the styles entirely when vanilla-extract is present).\n // The per-entry split in `generateBundle` fills these so the blob is\n // merged into the right entry CSS files instead of an orphan\n // `cssFileName`. Defaults (no split) keep the original single-file\n // behavior. `plainCssTargets`: entry CSS files (`a.css`) that import plain\n // CSS. `plainCssFallback`: where the blob goes when the split fired but no\n // entry with a CSS file owns the plain CSS (avoids re-creating an orphan).\n const plainCssTargets = new Set<string>();\n let plainCssFallback = cssFileName;\n\n ctx.mergeExternals(/@vanilla-extract/);\n\n ctx.meta.hasVanillaExtract = !!(await findFile(\n ctx.dirs.src.value,\n /\\.css\\.ts$/,\n ));\n\n // When vanilla-extract is in play, route tsdown's own CSS to a temporary\n // name so it doesn't collide with the vanilla-extract bundle that also\n // targets `cssFileName`; the two are merged into a single `cssFileName`\n // by the `writeBundle` hook below. Without vanilla-extract there is no\n // second producer, so tsdown emits `cssFileName` directly.\n //\n // These are defaults, not overrides: `...ctx.css` comes last so anything\n // the workspace/project configuration declares wins. `splitting` and\n // `fileName` are load-bearing for the merge below, though — the README\n // tells consumers to leave them to this plugin.\n ctx.css = {\n splitting: false,\n fileName: ctx.meta.hasVanillaExtract\n ? TSDOWN_CSS_FILE_NAME\n : cssFileName,\n ...ctx.css,\n };\n\n if (ctx.meta.hasVanillaExtract) {\n const originalPlugin = vanillaExtractPlugin({\n ...options,\n extract: {\n name: cssFileName,\n sourcemap: false,\n },\n });\n\n // `@vanilla-extract/rollup-plugin` returns a rollup `Plugin`, but\n // plugboy's `ctx.plugins` expects a tsdown (rolldown) `Plugin`. The two\n // are structurally almost identical, but hooks like `outputOptions`\n // type `this` as rollup's `PluginContext` vs rolldown's\n // `MinimalPluginContext`, which makes them unassignable (the `this`\n // type is contravariant). rolldown accepts rollup plugins at runtime,\n // so this is harmless — cast to work around the type mismatch.\n ctx.plugins.push(originalPlugin as unknown as Plugin);\n\n // `@vanilla-extract/rollup-plugin` emits the extracted CSS via\n // `emitFile({ type: 'asset', name: cssFileName })`. Because it uses\n // `name` (a hint) rather than `fileName`, rolldown runs it through the\n // default `assetFileNames` pattern (`assets/[name]-[hash][extname]`),\n // producing e.g. `dist/assets/vue-app-layout-dry0z-1l.css`.\n //\n // We can't *rename* an asset in `generateBundle` because rolldown\n // ignores mutations to a bundle entry's `fileName`. Instead, override\n // `assetFileNames` via the `outputOptions` hook so this single CSS\n // asset keeps its derived name verbatim (no hash, no `assets/` dir)\n // while every other asset keeps its original naming.\n ctx.plugins.push({\n name: `${PLUGIN_NAME}:rename-css`,\n outputOptions(opts) {\n const original = opts.assetFileNames;\n opts.assetFileNames = (assetInfo) => {\n if (assetInfo.names.includes(cssFileName)) {\n return cssFileName;\n }\n if (typeof original === 'function') return original(assetInfo);\n return original ?? 'assets/[name]-[hash][extname]';\n };\n return opts;\n },\n // Split vanilla-extract's single bundle into one CSS file per entry.\n //\n // `extract: { name }` mode collects the CSS of EVERY `.css.ts` in the\n // graph into one asset (`cssFileName`), which loses plugboy's per-entry\n // CSS contract: every entry with `css: true` declares a `./<entry>.css`\n // export, but only `cssFileName` is ever produced. We rebuild the\n // per-entry files from data vanilla-extract already exposes:\n // `moduleInfo.meta.css` (its public hand-off for extracted CSS, the\n // same field its own bundler reads).\n //\n // CSS is attributed to an entry by walking the INPUT module graph from\n // that entry's `facadeModuleId` via `getModuleInfo().importedIds`, NOT\n // by reading the output chunk's `imports`. `chunk.imports` lists output\n // chunk file names plus external ids, and whether vanilla-extract's\n // external virtual CSS ids appear there is rolldown-version-dependent\n // (some versions only list real output chunks, so the lookup finds\n // nothing and the split silently no-ops). The input graph is stable: it\n // always yields the `.css.ts` modules an entry reaches, regardless of\n // how rolldown chunks the output. A `.css.ts` shared by several entries\n // is included in each entry's file (self-contained per-entry CSS).\n //\n // We never parse vanilla-extract's asset names or re-add the import\n // statements it strips, so the only coupling is `meta.css`.\n //\n // Runs before `plugboy-optimize-css` (appended later in\n // `workspace.plugins`), so each emitted per-entry file still goes\n // through the postcss optimizations.\n //\n // Only splits when more than one entry actually has CSS. With a single\n // CSS entry (the common case) vanilla-extract's bundle is already the\n // correct, fully-ordered output, so it is left untouched and existing\n // single-entry packages are byte-for-byte unaffected.\n generateBundle(_options, bundle) {\n // Walk the input module graph from `root`, collecting (deduped, in\n // import order) the CSS of every `.css.ts` reached (`meta.css`), and\n // flagging whether any plain (non-`.css.ts`) CSS file is imported.\n const collectEntryCss = (\n root: string,\n ): { css: string[]; hasPlain: boolean } => {\n const visited = new Set<string>();\n const seenCss = new Set<string>();\n const out: string[] = [];\n let hasPlain = false;\n const walk = (id: string) => {\n if (visited.has(id)) return;\n visited.add(id);\n const info = this.getModuleInfo(id);\n if (!info) return;\n for (const dep of info.importedIds ?? []) {\n const css = this.getModuleInfo(dep)?.meta?.css;\n if (typeof css === 'string') {\n // vanilla-extract virtual CSS (`*.vanilla.css`).\n if (!seenCss.has(dep)) {\n seenCss.add(dep);\n out.push(css);\n }\n } else if (/\\.css(\\?|$)/i.test(dep)) {\n // Plain CSS import (no `meta.css`) — its content lives in\n // tsdown's combined blob, merged per entry in `writeBundle`.\n hasPlain = true;\n }\n walk(dep);\n }\n };\n walk(root);\n return { css: out, hasPlain };\n };\n\n const entryCss: {\n name: string;\n source: string;\n hasPlain: boolean;\n }[] = [];\n\n for (const chunk of Object.values(bundle)) {\n if (\n chunk.type !== 'chunk' ||\n !chunk.isEntry ||\n !chunk.fileName.endsWith('.mjs') ||\n !chunk.facadeModuleId\n ) {\n continue;\n }\n const { css, hasPlain } = collectEntryCss(chunk.facadeModuleId);\n if (css.length) {\n entryCss.push({\n name: chunk.name,\n source: css.join('\\n'),\n hasPlain,\n });\n }\n }\n\n // 0 or 1 CSS entry → vanilla-extract's bundle is already correct,\n // and the single-file `writeBundle` fallback applies unchanged.\n if (entryCss.length <= 1) return;\n\n // Replace vanilla-extract's combined bundle with per-entry files.\n // The entry whose file name matches `cssFileName` overwrites the\n // existing asset in place — re-`emitFile`ing the same name would\n // trip rolldown's FILE_NAME_CONFLICT, since deleting the bundle\n // entry does not release the reserved file name. Other entries are\n // emitted as new assets.\n const reused = new Set<string>();\n for (const { name, source } of entryCss) {\n const fileName = `${name}.css`;\n const existing = bundle[fileName];\n if (existing && existing.type === 'asset') {\n existing.source = source;\n reused.add(fileName);\n } else {\n this.emitFile({ type: 'asset', fileName, source });\n }\n }\n\n // Drop vanilla-extract's combined bundle if no entry reused it.\n const combined = bundle[cssFileName];\n if (\n combined &&\n combined.type === 'asset' &&\n !reused.has(cssFileName)\n ) {\n delete bundle[cssFileName];\n }\n\n // Tell `writeBundle` where to merge tsdown's plain-CSS blob: into\n // each entry CSS file whose entry imports plain CSS. Falling back to\n // the first entry's file keeps the blob attached to a real, exported\n // CSS file instead of resurrecting the now-deleted `cssFileName`.\n plainCssTargets.clear();\n plainCssFallback = `${entryCss[0].name}.css`;\n for (const { name, hasPlain } of entryCss) {\n if (hasPlain) plainCssTargets.add(`${name}.css`);\n }\n },\n // Merge tsdown's own plain-CSS blob (emitted to\n // `TSDOWN_CSS_FILE_NAME`) into the vanilla-extract CSS file(s).\n //\n // This has to happen in `writeBundle`, not `generateBundle`: tsdown\n // emits its CSS in a separate output pass, so `TSDOWN_CSS_FILE_NAME`\n // is absent from the bundle our `generateBundle` sees but present by\n // `writeBundle`. By then the vanilla-extract files are already on disk,\n // so we merge on disk rather than mutating bundle sources. tsdown's\n // CSS goes first so its `@layer` declarations / resets / `@font-face`\n // are established before the extracted component styles.\n //\n // Targets:\n // - Per-entry split fired → the entry CSS files whose entries import\n // plain CSS (`plainCssTargets`), falling back to the first entry's\n // file. Never `cssFileName`, which the split deleted (no orphan).\n // - No split (single CSS entry) → `cssFileName`, the one combined file.\n // The combined blob cannot be partitioned per entry, so when several\n // entries import distinct plain CSS each target receives the whole\n // blob; in practice plain CSS (fonts/resets) is imported by one entry.\n async writeBundle(outputOptions, bundle) {\n // Only the output pass that emitted tsdown's CSS performs the merge\n // (it is the one whose `bundle` contains `TSDOWN_CSS_FILE_NAME`).\n // This also prevents a second output from re-injecting the imports.\n const tmp = bundle[TSDOWN_CSS_FILE_NAME];\n if (!tmp) return;\n\n const tmpCss = tmp.type === 'asset' ? tmp.source.toString() : '';\n const dir = outputOptions.dir ?? '.';\n const tmpPath = path.join(dir, TSDOWN_CSS_FILE_NAME);\n\n if (tmpCss) {\n const targets = plainCssTargets.size\n ? [...plainCssTargets]\n : [plainCssFallback];\n\n await Promise.all(\n targets.map(async (target) => {\n const targetPath = path.join(dir, target);\n let targetCss = '';\n try {\n targetCss = await fs.readFile(targetPath, 'utf8');\n } catch {\n // No vanilla-extract file for this target (e.g. `.css.ts`\n // produced no rules) — tsdown's CSS becomes the whole file.\n }\n await fs.writeFile(\n targetPath,\n targetCss ? `${tmpCss}\\n${targetCss}` : tmpCss,\n );\n }),\n );\n }\n\n await fs.rm(tmpPath, { force: true });\n },\n });\n }\n },\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugboy-vanilla-extract-plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import path from 'node:path';\nimport { definePlugin, findFile, type Plugin } from '@fastkit/plugboy';\nimport { vanillaExtractPlugin } from '@vanilla-extract/rollup-plugin';\nimport {\n virtualCssFileFilter,\n getSourceFromVirtualCssFile,\n} from '@vanilla-extract/integration';\nimport { VanillaExtractPlugin, PluginOptions, PLUGIN_NAME } from './types';\n\ndeclare module '@fastkit/plugboy' {\n export interface WorkspaceMeta {\n hasVanillaExtract: boolean;\n }\n}\n\nexport async function createVanillaExtractPlugin(options: PluginOptions = {}) {\n return definePlugin<VanillaExtractPlugin>({\n name: PLUGIN_NAME,\n _options: options,\n hooks: {\n async setupWorkspace(ctx) {\n // Derive the CSS file name from the workspace entry, mirroring the way\n // plugboy names the JS output: the main entry (`.`) maps to the package\n // directory name (e.g. `vue-app-layout`), other entries keep their id.\n // Since `splitting: false` produces a single combined CSS for the\n // package, we base it on the main entry (falling back to the first).\n // Result: `dist/vue-app-layout.css`, matching the `./vue-app-layout.css`\n // export plugboy declares for a `css: true` entry.\n const entryIds = Object.keys(ctx.config.entries);\n const cssBaseName = entryIds.includes('.')\n ? ctx.dir.basename\n : (entryIds[0] ?? ctx.dir.basename);\n const cssFileName = `${cssBaseName}.css`;\n\n // plugboy declares a `./<entry>.css` export for every entry with\n // `css: true`, so more than one of them needs one stylesheet per entry —\n // which is what tsdown's `css.splitting` produces (one file per output\n // chunk, named after it). `splitting: false` collects the whole package\n // into `fileName`, and with several entries in play it keeps only one\n // chunk's CSS, silently dropping the rest.\n const cssEntryCount = Object.values(ctx.config.entries).filter(\n (entry) => entry.css,\n ).length;\n\n ctx.mergeExternals(/@vanilla-extract/);\n\n ctx.meta.hasVanillaExtract = !!(await findFile(\n ctx.dirs.src.value,\n /\\.css\\.ts$/,\n ));\n\n // Defaults, not overrides: `...ctx.css` comes last so anything the\n // workspace/project configuration declares wins. See the README for why\n // these two keys should be left to the plugin.\n ctx.css = {\n splitting: cssEntryCount > 1,\n fileName: cssFileName,\n ...ctx.css,\n };\n\n if (!ctx.meta.hasVanillaExtract) return;\n\n // Hand vanilla-extract's extracted CSS to tsdown's own CSS pipeline.\n //\n // `@vanilla-extract/rollup-plugin` resolves its virtual stylesheets\n // (`<file>.vanilla.css?source=<serialized>`) to `{ external: true }` and,\n // in `extract` mode, emits the collected CSS itself as a rolldown asset.\n // Either way tsdown never sees the CSS, so none of the `css` options\n // apply to it — `css.target` in particular, which is why extracted CSS\n // used to ship a bare `user-select` while `.scss` in the same repo got\n // its `-webkit-` prefix.\n //\n // Resolving the virtual id to a real module instead — the approach the\n // official `@vanilla-extract/vite-plugin` takes — puts the CSS in the\n // module graph as an ordinary `.css` module. tsdown then owns it: the\n // configured `transformer` (lightningcss *or* postcss), `target`,\n // `minify` and preprocessor options all apply, and its CSS pipeline\n // collects and emits it under `css.fileName`. plugboy's `optimizeCSS`\n // runs afterwards, on the written file.\n //\n // Two details are load-bearing:\n // - The `?source=` query MUST be stripped. `@tsdown/css` skips any CSS id\n // carrying a non-`?inline` query (`shouldSkipTransform`), in both its\n // compile and its collect plugin — an id with the query intact is\n // dropped from the CSS output entirely.\n // - This plugin must be registered *before* the vanilla-extract plugin so\n // its `resolveId` wins; otherwise the module is marked external again.\n const virtualCss = new Map<string, string>();\n\n ctx.plugins.push({\n name: `${PLUGIN_NAME}:virtual-css`,\n buildStart() {\n virtualCss.clear();\n },\n async resolveId(source) {\n if (!virtualCssFileFilter.test(source)) return null;\n const { fileName, source: css } =\n await getSourceFromVirtualCssFile(source);\n // `fileName` is `<package-relative path>.vanilla.css`; resolving it\n // against the package gives a stable, unique module id that still\n // ends in `.css`. No file exists there — `load` below supplies the\n // content — so the path only has to be unique.\n const id = path.resolve(ctx.dir.value, fileName);\n virtualCss.set(id, css);\n return id;\n },\n load(id) {\n return virtualCss.get(id) ?? null;\n },\n });\n\n // `@vanilla-extract/rollup-plugin` returns a rollup `Plugin`, but\n // plugboy's `ctx.plugins` expects a tsdown (rolldown) `Plugin`. The two\n // are structurally almost identical, but hooks like `outputOptions` type\n // `this` as rollup's `PluginContext` vs rolldown's\n // `MinimalPluginContext`, which makes them unassignable (the `this` type\n // is contravariant). rolldown accepts rollup plugins at runtime, so this\n // is harmless — cast to work around the type mismatch.\n //\n // Only its `transform` hook is used here (compiling `.css.ts` to JS).\n // `extract` stays off: its asset emission is what this plugin replaces.\n ctx.plugins.push(vanillaExtractPlugin(options) as unknown as Plugin);\n },\n },\n });\n}\n"],"mappings":";;;;;;AAeA,eAAsB,2BAA2B,UAAyB,CAAC,GAAG;CAC5E,OAAO,aAAmC;EACxC,MAAM;EACN,UAAU;EACV,OAAO,EACL,MAAM,eAAe,KAAK;GAQxB,MAAM,WAAW,OAAO,KAAK,IAAI,OAAO,OAAO;GAI/C,MAAM,cAAc,GAHA,SAAS,SAAS,GAAG,IACrC,IAAI,IAAI,WACP,SAAS,MAAM,IAAI,IAAI,SACO;GAQnC,MAAM,gBAAgB,OAAO,OAAO,IAAI,OAAO,OAAO,CAAC,CAAC,QACrD,UAAU,MAAM,GACnB,CAAC,CAAC;GAEF,IAAI,eAAe,kBAAkB;GAErC,IAAI,KAAK,oBAAoB,CAAC,CAAE,MAAM,SACpC,IAAI,KAAK,IAAI,OACb,YACF;GAKA,IAAI,MAAM;IACR,WAAW,gBAAgB;IAC3B,UAAU;IACV,GAAG,IAAI;GACT;GAEA,IAAI,CAAC,IAAI,KAAK,mBAAmB;GA2BjC,MAAM,6BAAa,IAAI,IAAoB;GAE3C,IAAI,QAAQ,KAAK;IACf,MAAM,GAAG,YAAY;IACrB,aAAa;KACX,WAAW,MAAM;IACnB;IACA,MAAM,UAAU,QAAQ;KACtB,IAAI,CAAC,qBAAqB,KAAK,MAAM,GAAG,OAAO;KAC/C,MAAM,EAAE,UAAU,QAAQ,QACxB,MAAM,4BAA4B,MAAM;KAK1C,MAAM,KAAK,KAAK,QAAQ,IAAI,IAAI,OAAO,QAAQ;KAC/C,WAAW,IAAI,IAAI,GAAG;KACtB,OAAO;IACT;IACA,KAAK,IAAI;KACP,OAAO,WAAW,IAAI,EAAE,KAAK;IAC/B;GACF,CAAC;GAYD,IAAI,QAAQ,KAAK,qBAAqB,OAAO,CAAsB;EACrE,EACF;CACF,CAAC;AACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastkit/plugboy-vanilla-extract-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": {
|
|
@@ -42,17 +42,18 @@
|
|
|
42
42
|
"dist"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"@vanilla-extract/integration": "^8.0.10",
|
|
45
46
|
"@vanilla-extract/rollup-plugin": "^1.5.4"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@vanilla-extract/vite-plugin": "^5.2.5",
|
|
49
50
|
"vite": "^8.1.5",
|
|
50
|
-
"@fastkit/plugboy": "^1.
|
|
51
|
+
"@fastkit/plugboy": "^1.4.0"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
53
54
|
"@vanilla-extract/vite-plugin": "^5.0.0",
|
|
54
55
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
|
55
|
-
"@fastkit/plugboy": "^1.
|
|
56
|
+
"@fastkit/plugboy": "^1.4.0"
|
|
56
57
|
},
|
|
57
58
|
"peerDependenciesMeta": {
|
|
58
59
|
"@vanilla-extract/vite-plugin": {
|