@teacss/vite 0.10.9 → 0.10.10

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.
@@ -0,0 +1,5 @@
1
+ import { type ResolvedBuildEnvironmentOptions, type ResolvedConfig } from "vite";
2
+ type BuildOptions = Pick<ResolvedBuildEnvironmentOptions, "cssMinify" | "cssTarget">;
3
+ export declare function finalizeBuildCss(css: string, config: ResolvedConfig, build: BuildOptions, viteVersion: string): Promise<string>;
4
+ export {};
5
+ //# sourceMappingURL=finalize.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { createRequire } from "node:module";
2
+ import { join } from "node:path";
3
+ import { preprocessCSS } from "vite";
4
+ import { RESOLVED_ENTRY } from "./virtual.js";
5
+ // esbuild and lightningcss are Vite's (a dependency or an optional peer, by version), not this package's: load them as Vite does.
6
+ const fromVite = createRequire(import.meta.resolve("vite"));
7
+ export async function finalizeBuildCss(css, config, build, viteVersion) {
8
+ const filename = join(config.root, RESOLVED_ENTRY.slice(1));
9
+ const minifier = minifierOf(build.cssMinify, viteVersion);
10
+ const lightningcss = config.css.transformer === "lightningcss";
11
+ // One pass: a second Lightning CSS pass repeats the prefixes the first one added.
12
+ if (lightningcss && minifier === "lightningcss")
13
+ return minifyWithLightningcss(css, filename, config, build.cssTarget);
14
+ const { code } = await preprocessCSS(css, filename, lightningcss ? withoutCssMinify(config) : config);
15
+ if (minifier === "esbuild")
16
+ return minifyWithEsbuild(code, filename, config, build.cssTarget);
17
+ if (minifier === "lightningcss")
18
+ return minifyWithLightningcss(code, filename, config, build.cssTarget);
19
+ return code;
20
+ }
21
+ /** `preprocessCSS` runs Lightning CSS with the client environment's `build`, minifying wherever its `cssMinify` is set. */
22
+ function withoutCssMinify(config) {
23
+ const environments = Object.fromEntries(Object.entries(config.environments).map(([name, options]) => [name, { ...options, build: { ...options.build, cssMinify: false } }]));
24
+ return { ...config, build: { ...config.build, cssMinify: false }, environments };
25
+ }
26
+ function minifierOf(cssMinify, viteVersion) {
27
+ if (cssMinify !== true)
28
+ return cssMinify;
29
+ // Vite 7 minifies with esbuild for `cssMinify: true`, Vite 8 with Lightning CSS.
30
+ return Number.parseInt(viteVersion, 10) >= 8 ? "lightningcss" : "esbuild";
31
+ }
32
+ async function minifyWithEsbuild(css, filename, config, cssTarget) {
33
+ const esbuild = fromVite("esbuild");
34
+ const result = await esbuild.transform(css, { ...resolveMinifyCssEsbuildOptions(config.esbuild || {}), loader: "css", sourcefile: filename, target: cssTarget || undefined });
35
+ for (const warning of result.warnings)
36
+ config.logger.warn(`[esbuild css minify] ${warning.text}`);
37
+ return result.code;
38
+ }
39
+ function resolveMinifyCssEsbuildOptions(options) {
40
+ const { charset, legalComments, logLevel, logLimit, logOverride, minifyIdentifiers, minifySyntax, minifyWhitespace } = options;
41
+ const base = { charset, legalComments, logLevel, logLimit, logOverride };
42
+ if (minifyIdentifiers == null && minifySyntax == null && minifyWhitespace == null)
43
+ return { ...base, minify: true };
44
+ return { ...base, minifyIdentifiers: minifyIdentifiers ?? true, minifySyntax: minifySyntax ?? true, minifyWhitespace: minifyWhitespace ?? true };
45
+ }
46
+ function minifyWithLightningcss(css, filename, config, cssTarget) {
47
+ const lightningcss = fromVite("lightningcss");
48
+ const result = lightningcss.transform({ ...config.css.lightningcss, filename, code: Buffer.from(css), minify: true, targets: convertTargets(cssTarget), cssModules: undefined });
49
+ for (const warning of result.warnings)
50
+ config.logger.warn(`[lightningcss minify] ${warning.message}`);
51
+ return new TextDecoder().decode(result.code);
52
+ }
53
+ const CSS_TARGET_BROWSERS = new Map([
54
+ ["chrome", "chrome"],
55
+ ["edge", "edge"],
56
+ ["firefox", "firefox"],
57
+ ["hermes", false],
58
+ ["ie", "ie"],
59
+ ["ios", "ios_saf"],
60
+ ["node", false],
61
+ ["opera", "opera"],
62
+ ["rhino", false],
63
+ ["safari", "safari"],
64
+ ]);
65
+ const ES_TARGETS = new Map([
66
+ [2015, ["chrome49", "edge13", "safari10", "ios10", "firefox44", "opera36"]],
67
+ [2016, ["chrome50", "edge13", "safari10", "ios10", "firefox43", "opera37"]],
68
+ [2017, ["chrome58", "edge15", "safari11", "ios11", "firefox52", "opera45"]],
69
+ [2018, ["chrome63", "edge79", "safari12", "ios12", "firefox58", "opera50"]],
70
+ [2019, ["chrome73", "edge79", "safari12.1", "ios12.1", "firefox64", "opera60"]],
71
+ [2020, ["chrome80", "edge80", "safari14.1", "ios14.5", "firefox80", "opera67"]],
72
+ [2021, ["chrome85", "edge85", "safari14.1", "ios14.5", "firefox80", "opera71"]],
73
+ [2022, ["chrome94", "edge94", "safari16.4", "ios16.4", "firefox93", "opera80"]],
74
+ [2023, ["chrome110", "edge110", "safari16.4", "ios16.4", "firefox146", "opera96"]],
75
+ [2024, ["chrome119", "edge119", "safari17.4", "ios17.4", "firefox145", "opera105"]],
76
+ [2025, ["chrome136", "edge136", "safari26.0", "ios26.0", "firefox138", "opera121"]],
77
+ ]);
78
+ function convertTargets(cssTarget) {
79
+ const targets = {};
80
+ for (const entry of [cssTarget || []].flat().flatMap(esBrowsers)) {
81
+ if (entry === "esnext")
82
+ continue;
83
+ const [, name = "", major, minor = "0"] = /^([a-z_]+)(\d+)(?:\.(\d+))?$/i.exec(entry) ?? [];
84
+ const browser = CSS_TARGET_BROWSERS.get(name.toLowerCase());
85
+ if (browser === false)
86
+ continue;
87
+ if (browser === undefined)
88
+ throw new Error(`Unsupported target "${entry}"`);
89
+ const version = (Number(major) << 16) | (Number(minor) << 8);
90
+ if (version < (targets[browser] ?? Number.POSITIVE_INFINITY))
91
+ targets[browser] = version;
92
+ }
93
+ return targets;
94
+ }
95
+ function esBrowsers(entry) {
96
+ const year = /^es(6|\d{4})$/.exec(entry)?.[1];
97
+ if (year === undefined)
98
+ return [entry];
99
+ const browsers = ES_TARGETS.get(year === "6" ? 2015 : Number(year));
100
+ if (browsers === undefined)
101
+ throw new Error(`Unsupported target "${entry}"`);
102
+ return browsers;
103
+ }
104
+ //# sourceMappingURL=finalize.js.map
@@ -0,0 +1,4 @@
1
+ export type { ShellOptions } from "@teacss/engine";
2
+ export { INSPECTOR_BASE, type TeacssViteOptions, type TeacssVitePluginApi, teacss } from "./plugin.js";
3
+ export { BUILD_PLACEHOLDER, BUILD_PLACEHOLDER_PATTERN, DEFAULT_PIPELINE_INCLUDE, ENTRY_IDS, RESOLVED_ENTRY } from "./virtual.js";
4
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { INSPECTOR_BASE, teacss } from "./plugin.js";
2
+ export { BUILD_PLACEHOLDER, BUILD_PLACEHOLDER_PATTERN, DEFAULT_PIPELINE_INCLUDE, ENTRY_IDS, RESOLVED_ENTRY } from "./virtual.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,29 @@
1
+ import { type Project, type ShellOptions } from "@teacss/engine";
2
+ import type { Plugin } from "vite";
3
+ export interface TeacssViteOptions extends ShellOptions {
4
+ /**
5
+ * The engine's own minify of the generated CSS, which drops the layer comments and line breaks. Default false:
6
+ * a build hands the generated CSS to Vite's minifier, `build.cssMinify`, whatever this option says.
7
+ */
8
+ minify?: boolean;
9
+ /** Serve `@teacss/inspector` at `INSPECTOR_BASE` on the dev server. Default true; a build never mounts it. */
10
+ inspector?: boolean;
11
+ /**
12
+ * In a build, set `css.transformer` and `build.cssMinify` to "lightningcss" where the user left them
13
+ * unset, top level and per environment (`cssMinify` only where `build.minify` is not false). Default:
14
+ * only when `lightningcss` resolves from the project root (it is Vite's optional peer).
15
+ */
16
+ lightningcss?: boolean;
17
+ }
18
+ export interface TeacssVitePluginApi {
19
+ getProject(): Project;
20
+ /**
21
+ * Sends the dev client an update of the generated CSS, as a changed module does: for the modules another plugin
22
+ * registers through `getProject().transform()`. A build has no client to update.
23
+ */
24
+ invalidate(): void;
25
+ }
26
+ /** The dev-server path the inspector's UI and `/api` routes live under; the UI's built asset URLs assume it. */
27
+ export declare const INSPECTOR_BASE = "/__teacss";
28
+ export declare function teacss(options?: TeacssViteOptions): Plugin[];
29
+ //# sourceMappingURL=plugin.d.ts.map
package/dist/plugin.js ADDED
@@ -0,0 +1,459 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { join, posix, resolve } from "node:path";
5
+ import { createProject, extractBundledFonts, isCssId, markers, replaceEntryWithMap } from "@teacss/engine";
6
+ import { createInspectorMiddleware } from "@teacss/inspector";
7
+ import { finalizeBuildCss } from "./finalize.js";
8
+ import { BUILD_PLACEHOLDER, BUILD_PLACEHOLDER_PATTERN, digestPlaceholder, hmrPath, isResolvedEntry, resolveEntryId, shouldTransform } from "./virtual.js";
9
+ const INCLUDE_COMMENT = markers().includeComment;
10
+ /** architecture §5: the files `loadProjectFromRoot` discovers the entry through. */
11
+ const ENTRY_CANDIDATES = ["package.json", "index.css", "src/index.css"];
12
+ /** Collapses the invalidations of one burst of transforms into one client update. */
13
+ const INVALIDATION_DELAY_MS = 10;
14
+ /** The dev-server path the inspector's UI and `/api` routes live under; the UI's built asset URLs assume it. */
15
+ export const INSPECTOR_BASE = "/__teacss";
16
+ /** Where, under Vite's `base`, the dev server serves the fonts of the generated CSS; the path teacss-ts uses. */
17
+ const DEV_FONTS = "__teacss_fonts";
18
+ const HTML_ENTRY = /\.html$/;
19
+ /** `vite:css-post`'s transform, and the stylesheets holding the placeholder it cached, by its hook object. */
20
+ const cssPostCaches = new WeakMap();
21
+ /**
22
+ * Vite names a CSS file by the hash of what `vite:css-post` cached for its stylesheets, when it renders a chunk,
23
+ * and the generated CSS replaces the placeholder only in `generateBundle`. So the stylesheets that hold the
24
+ * placeholder are recorded here, and `renderStart` caches them again with the generated CSS's digest in it.
25
+ */
26
+ function recordPlaceholderStylesheets(plugins) {
27
+ const hook = plugins.find((plugin) => plugin.name === "vite:css-post")?.transform;
28
+ if (typeof hook !== "object" || hook === null || cssPostCaches.has(hook))
29
+ return;
30
+ const transform = hook.handler;
31
+ const cached = new Map();
32
+ cssPostCaches.set(hook, { transform, cached });
33
+ const record = function (css, id, ...rest) {
34
+ if (typeof css === "string" && css.search(BUILD_PLACEHOLDER_PATTERN) !== -1)
35
+ cached.set(id, css);
36
+ return transform.call(this, css, id, ...rest);
37
+ };
38
+ hook.handler = record;
39
+ }
40
+ export function teacss(options = {}) {
41
+ const { inspector = true, lightningcss, ...shell } = options;
42
+ let project;
43
+ let config;
44
+ let server;
45
+ let viteVersion = "";
46
+ /** When `config` sets `build.cssMinify`: the environments whose own `build.cssMinify` the user set. */
47
+ let userCssMinify;
48
+ let invalidation;
49
+ /** What the last watcher event threw, shown in the error overlay until an event succeeds. */
50
+ let failure;
51
+ /** No live project yet, or `dispose()` released it. */
52
+ let disposed = true;
53
+ /** `@source` files registered as modules from disk, so the ones Vite never imports still count. */
54
+ const sources = new Set();
55
+ /** Stylesheet ids whose `@teacss;` took the generated CSS; they are invalidated with the virtual entry. */
56
+ const entries = new Set();
57
+ /** `@source` directories already reported as unreadable: Vite's builder resolves the config once per environment. */
58
+ const unreadable = new Set();
59
+ /** The bytes of each font the dev CSS links to, by its file name under `DEV_FONTS`, which carries its sha256. */
60
+ const devFonts = new Map();
61
+ function warnUnreadable() {
62
+ for (const path of project.info.unreadableDirectories) {
63
+ if (unreadable.has(path))
64
+ continue;
65
+ unreadable.add(path);
66
+ config.logger.warn(`[@teacss/vite] cannot read ${path} (E_IO)`);
67
+ }
68
+ }
69
+ function scanSource(file) {
70
+ let code;
71
+ try {
72
+ code = readFileSync(file, "utf8");
73
+ }
74
+ catch {
75
+ sources.delete(file);
76
+ return project.removeModule(file);
77
+ }
78
+ sources.add(file);
79
+ return project.transform(file, code).changed;
80
+ }
81
+ function syncSources() {
82
+ const next = new Set(project.info.sourceFiles);
83
+ let changed = false;
84
+ for (const file of sources) {
85
+ if (next.has(file))
86
+ continue;
87
+ sources.delete(file);
88
+ changed = project.removeModule(file) || changed;
89
+ }
90
+ for (const file of next) {
91
+ if (sources.has(file))
92
+ continue;
93
+ server?.watcher.add(file);
94
+ changed = scanSource(file) || changed;
95
+ }
96
+ return changed;
97
+ }
98
+ function isConfigFile(file) {
99
+ return project.engine.configFiles().includes(file) || ENTRY_CANDIDATES.some((candidate) => join(project.root, candidate) === file);
100
+ }
101
+ /** Forgets the modules `transform` registered for a deleted file, its `?query` variants included. */
102
+ function removeTransformed(file) {
103
+ let changed = false;
104
+ for (const id of project.modules()) {
105
+ if (id === file || id.startsWith(`${file}?`))
106
+ changed = project.removeModule(id) || changed;
107
+ }
108
+ return changed;
109
+ }
110
+ function onFileChange(file, event) {
111
+ if (event === "update" && !isConfigFile(file)) {
112
+ if (sources.has(file) && scanSource(file))
113
+ invalidateEntry();
114
+ // While the last reload failed, `configFiles()` is the last good program's: the file may be an import only the failed load read.
115
+ if (!failure)
116
+ return;
117
+ }
118
+ const removed = event === "delete" && removeTransformed(file);
119
+ const programChanged = project.reload();
120
+ watchConfig();
121
+ warnUnreadable();
122
+ const sourcesChanged = syncSources();
123
+ // After a failure the page also reloads to drop the error overlay, even when the fix restored the last good program.
124
+ if (programChanged || failure)
125
+ fullReload();
126
+ else if (sourcesChanged || removed)
127
+ invalidateEntry();
128
+ }
129
+ /**
130
+ * A dev server's watcher event. An error, such as a configuration error in the saved entry, goes to the log and
131
+ * the error overlay instead of ending the process; the engine keeps its last good program. Until a reload
132
+ * succeeds, every event reloads the project, and the one that succeeds reloads the page.
133
+ */
134
+ function onWatcherEvent(file, event) {
135
+ try {
136
+ onFileChange(file, event);
137
+ }
138
+ catch (error) {
139
+ const message = describeError(error);
140
+ failure = { message, stack: error instanceof Error ? (error.stack ?? "") : "", plugin: "teacss", id: failure?.id ?? file };
141
+ config.logger.error(`[@teacss/vite] ${message}`, { timestamp: true, error: error instanceof Error ? error : undefined });
142
+ server?.hot.send({ type: "error", err: failure });
143
+ watchFailedImports();
144
+ return;
145
+ }
146
+ failure = undefined;
147
+ }
148
+ /** The entry's imports can sit outside the root Vite watches, and chokidar reports a missing path once it appears. */
149
+ function watchConfig() {
150
+ server?.watcher.add([...project.engine.configFiles(), ...project.engine.missingImports()]);
151
+ }
152
+ /** The files the failed load read: a fix saved to an import outside the root then reaches the watcher too. */
153
+ function watchFailedImports() {
154
+ const { entryPath } = project.info;
155
+ if (!server || entryPath === null)
156
+ return;
157
+ try {
158
+ server.watcher.add(project.engine.inspectEntry({ root: project.root, entryPath }).configFiles);
159
+ }
160
+ catch {
161
+ // An unreadable entry is reported by the root watcher when it comes back.
162
+ }
163
+ }
164
+ function fullReload() {
165
+ if (!server)
166
+ return;
167
+ server.moduleGraph.invalidateAll();
168
+ server.hot.send({ type: "full-reload" });
169
+ }
170
+ function invalidateEntry() {
171
+ if (!server || invalidation)
172
+ return;
173
+ invalidation = setTimeout(() => {
174
+ invalidation = undefined;
175
+ sendEntryUpdate();
176
+ }, INVALIDATION_DELAY_MS);
177
+ }
178
+ function sendEntryUpdate() {
179
+ if (!server)
180
+ return;
181
+ const timestamp = Date.now();
182
+ const updates = [];
183
+ for (const [id, mod] of server.moduleGraph.idToModuleMap) {
184
+ if (!isResolvedEntry(id) && !entries.has(id))
185
+ continue;
186
+ server.moduleGraph.invalidateModule(mod);
187
+ // As in Vite's own HMR: a stylesheet requested directly (`<link>`, `?direct`) has no JS importer to accept a js-update.
188
+ const path = hmrPath(mod.url, viteVersion);
189
+ updates.push({ type: mod.type === "css" ? "css-update" : "js-update", path, acceptedPath: path, timestamp });
190
+ }
191
+ if (updates.length > 0)
192
+ server.hot.send({ type: "update", updates });
193
+ }
194
+ function open() {
195
+ const isBuild = config.command === "build";
196
+ project = createProject({
197
+ ...shell,
198
+ root: shell.root ?? config.root,
199
+ envMode: shell.envMode ?? (isBuild ? "build" : "dev"),
200
+ minify: shell.minify ?? false,
201
+ });
202
+ disposed = false;
203
+ sources.clear();
204
+ entries.clear();
205
+ warnUnreadable();
206
+ syncSources();
207
+ }
208
+ /**
209
+ * What stands for the generated CSS: the placeholder `generateBundle` replaces in a build, the CSS itself in dev,
210
+ * with its fonts linked from the dev server. While a watcher event has failed, the error goes out again: Vite's
211
+ * own update of a changed entry stylesheet clears the overlay before the client requests it.
212
+ */
213
+ function generated(warn) {
214
+ if (config.command === "build")
215
+ return BUILD_PLACEHOLDER;
216
+ if (failure)
217
+ server?.hot.send({ type: "error", err: failure });
218
+ const result = project.css();
219
+ project.reportUnmatched(result, { warn });
220
+ return linkDevFonts(result.css);
221
+ }
222
+ /** `css` with the fonts it inlines linked from the dev server, which serves the bytes recorded here. */
223
+ function linkDevFonts(css) {
224
+ // As Vite's own dev asset URLs: a page served by another server (`server.origin`) loads them from Vite.
225
+ const prefix = `${config.server.origin ?? ""}${config.base}${DEV_FONTS}/`;
226
+ const linked = extractBundledFonts(css, (digest, bytes) => {
227
+ const name = `teacss-${digest}.woff2`;
228
+ devFonts.set(name, bytes);
229
+ return `${prefix}${name}`;
230
+ }).css;
231
+ // Vite runs PostCSS on a stylesheet with a lowercase `url(`, and these URLs are final. CSS function names
232
+ // are case-insensitive, so writing them `URL(`, as teacss-ts does, lets the sheet skip PostCSS.
233
+ const quoted = JSON.stringify(prefix).slice(0, -1);
234
+ return linked.replaceAll(`url(${quoted}`, `URL(${quoted}`);
235
+ }
236
+ /** A font this instance has not linked yet, as in CSS another process generated, is looked up in the project's CSS. */
237
+ function devFont(name) {
238
+ if (!devFonts.has(name) && !disposed)
239
+ linkDevFonts(project.css().css);
240
+ return devFonts.get(name);
241
+ }
242
+ function dispose() {
243
+ if (invalidation)
244
+ clearTimeout(invalidation);
245
+ invalidation = undefined;
246
+ server = undefined;
247
+ if (disposed)
248
+ return;
249
+ disposed = true;
250
+ project.dispose();
251
+ }
252
+ /**
253
+ * Splices the generated CSS into every CSS file that holds the placeholder. Vite writes a chunk's CSS file
254
+ * when it renders the chunk, and with `build.cssCodeSplit: false` its single CSS file only in its own
255
+ * `generateBundle`, so `teacss:api` runs this again after it.
256
+ */
257
+ const spliceGenerated = async function (_options, bundle) {
258
+ let css;
259
+ const fonts = new Map();
260
+ for (const file of Object.values(bundle)) {
261
+ if (file.type !== "asset" || !file.fileName.endsWith(".css"))
262
+ continue;
263
+ const source = typeof file.source === "string" ? file.source : new TextDecoder().decode(file.source);
264
+ if (source.search(BUILD_PLACEHOLDER_PATTERN) === -1)
265
+ continue;
266
+ if (css === undefined) {
267
+ const result = project.css();
268
+ project.reportUnmatched(result, { warn: (message) => this.warn(message) });
269
+ css = await finalizeBuildCss(result.css, this.environment.getTopLevelConfig(), this.environment.config.build, viteVersion);
270
+ }
271
+ const emitted = new Set();
272
+ const spliced = extractBundledFonts(css, (digest, bytes) => {
273
+ let reference = fonts.get(digest);
274
+ if (reference === undefined) {
275
+ reference = this.emitFile({ type: "asset", name: `teacss-${digest}.woff2`, source: bytes });
276
+ fonts.set(digest, reference);
277
+ }
278
+ const fileName = this.getFileName(reference);
279
+ emitted.add(fileName);
280
+ return assetUrl(config.base, file.fileName, fileName);
281
+ }).css;
282
+ file.source = source.replace(BUILD_PLACEHOLDER_PATTERN, () => spliced);
283
+ // Vite's manifest lists a chunk's assets from `viteMetadata.importedAssets`; Astro moves the
284
+ // fonts of its server build to the client output from that list.
285
+ for (const chunk of Object.values(bundle)) {
286
+ if (chunk.type !== "chunk" || !chunk.viteMetadata?.importedCss.has(file.fileName))
287
+ continue;
288
+ for (const fileName of emitted)
289
+ chunk.viteMetadata.importedAssets.add(fileName);
290
+ }
291
+ }
292
+ };
293
+ const main = {
294
+ name: "teacss",
295
+ enforce: "pre",
296
+ config(user, env) {
297
+ userCssMinify = undefined;
298
+ if (lightningcss === false || env.command !== "build")
299
+ return null;
300
+ if (lightningcss === undefined && !resolves("lightningcss", user.root ?? process.cwd()))
301
+ return null;
302
+ const patch = {};
303
+ if (user.css?.transformer === undefined)
304
+ patch.css = { transformer: "lightningcss" };
305
+ if (user.build?.cssMinify !== undefined)
306
+ return patch;
307
+ userCssMinify = new Set(Object.entries(user.environments ?? {}).flatMap(([name, options]) => (options.build?.cssMinify === undefined ? [] : [name])));
308
+ // With `build.minify: false` Vite resolves an unset `cssMinify` to off; keep it that way.
309
+ if (user.build?.minify !== false)
310
+ patch.build = { cssMinify: "lightningcss" };
311
+ return patch;
312
+ },
313
+ // An environment's options carry the top-level `build` after every plugin's `config` hook, the
314
+ // `cssMinify` set above included, so a later plugin's choice is kept and an environment's own
315
+ // `build.minify: false` turns ours back off.
316
+ configEnvironment(name, environment) {
317
+ if (!userCssMinify || userCssMinify.has(name))
318
+ return null;
319
+ const { cssMinify, minify } = environment.build ?? {};
320
+ if (cssMinify === undefined)
321
+ return minify === false ? null : { build: { cssMinify: "lightningcss" } };
322
+ return cssMinify === "lightningcss" && minify === false ? { build: { cssMinify: false } } : null;
323
+ },
324
+ // Vite's builder (`vite build --app`, Astro) resolves the config once per environment on the same
325
+ // plugin objects, then builds the environments one after another, closing each bundle.
326
+ configResolved(resolved) {
327
+ dispose();
328
+ config = resolved;
329
+ viteVersion = this.meta.viteVersion;
330
+ if (resolved.command === "build")
331
+ recordPlaceholderStylesheets(resolved.plugins);
332
+ open();
333
+ },
334
+ async renderStart() {
335
+ if (config.command !== "build")
336
+ return;
337
+ const hook = this.environment.config.plugins.find((plugin) => plugin.name === "vite:css-post")?.transform;
338
+ const cache = typeof hook === "object" && hook !== null ? cssPostCaches.get(hook) : undefined;
339
+ if (!cache || cache.cached.size === 0)
340
+ return;
341
+ const digest = createHash("sha256").update(project.css().css).digest("hex").slice(0, 16);
342
+ for (const [id, css] of cache.cached)
343
+ await cache.transform.call(this, css.replace(BUILD_PLACEHOLDER_PATTERN, () => digestPlaceholder(digest)), id);
344
+ },
345
+ buildStart() {
346
+ if (disposed)
347
+ open();
348
+ for (const file of [...project.watchFiles(), ...project.engine.missingImports()])
349
+ this.addWatchFile(file);
350
+ },
351
+ configureServer(devServer) {
352
+ server = devServer;
353
+ watchConfig();
354
+ devServer.watcher
355
+ .on("add", (file) => onWatcherEvent(file, "create"))
356
+ .on("change", (file) => onWatcherEvent(file, "update"))
357
+ .on("unlink", (file) => onWatcherEvent(file, "delete"));
358
+ // Vite's client reloads the page on its first update while an overlay is open, which drops the overlay.
359
+ devServer.hot.on("vite:client:connect", (_data, client) => {
360
+ if (failure)
361
+ client.send({ type: "error", err: failure });
362
+ });
363
+ const serveFont = (request, response, next) => {
364
+ const bytes = devFont(request.url?.split("?", 1)[0]?.slice(1) ?? "");
365
+ if (!bytes)
366
+ return next();
367
+ response.setHeader("Content-Type", "font/woff2");
368
+ response.setHeader("Cache-Control", "public, max-age=31536000, immutable");
369
+ response.end(bytes);
370
+ };
371
+ // After Vite's CORS and host check, ahead of the middleware that strips `base`.
372
+ devServer.middlewares.use(`${config.base}${DEV_FONTS}`, serveFont);
373
+ // A parent app that mounts Vite's middlewares under `base` strips it before they run.
374
+ if (config.server.middlewareMode && config.base !== "/")
375
+ devServer.middlewares.use(`/${DEV_FONTS}`, serveFont);
376
+ if (inspector)
377
+ devServer.middlewares.use(INSPECTOR_BASE, createInspectorMiddleware(project));
378
+ },
379
+ watchChange(id, change) {
380
+ if (!server)
381
+ onFileChange(id, change.event);
382
+ },
383
+ resolveId(id) {
384
+ return resolveEntryId(id);
385
+ },
386
+ load(id) {
387
+ if (!isResolvedEntry(id))
388
+ return null;
389
+ return generated((message) => this.warn(message));
390
+ },
391
+ transform(code, id) {
392
+ // An HTML entry goes through `transformIndexHtml`; in a build Vite also hands it to `transform`
393
+ // first, and registering both the source and the rewritten page would drop its `:css:` groups.
394
+ if (HTML_ENTRY.test(id) || !shouldTransform(id, code, INCLUDE_COMMENT))
395
+ return null;
396
+ const result = project.transform(id, code);
397
+ const stylesheet = isCssId(id);
398
+ const entry = stylesheet ? replaceEntryWithMap(result.code, () => generated((message) => this.warn(message)), { id, original: code, map: result.map }) : null;
399
+ if (entry !== null) {
400
+ entries.add(id);
401
+ return { code: entry.code, map: { ...entry.map, file: entry.map.file ?? undefined } };
402
+ }
403
+ // A stylesheet's `changed` only means its code was rewritten: it registers no tokens, and
404
+ // invalidating on it would re-transform an entry stylesheet without end.
405
+ if (result.changed && !stylesheet)
406
+ invalidateEntry();
407
+ if (result.code === code)
408
+ return null;
409
+ return { code: result.code, map: result.map ? { ...result.map, file: result.map.file ?? undefined } : null };
410
+ },
411
+ transformIndexHtml: {
412
+ order: "pre",
413
+ handler(html, { filename }) {
414
+ const result = project.transform(filename, html);
415
+ if (result.changed)
416
+ invalidateEntry();
417
+ return result.code;
418
+ },
419
+ },
420
+ generateBundle: spliceGenerated,
421
+ closeBundle() {
422
+ if (!config.build.watch)
423
+ dispose();
424
+ },
425
+ closeWatcher() {
426
+ dispose();
427
+ },
428
+ };
429
+ const api = {
430
+ name: "teacss:api",
431
+ enforce: "post",
432
+ generateBundle: spliceGenerated,
433
+ api: { getProject: () => project, invalidate: invalidateEntry },
434
+ };
435
+ return [main, api];
436
+ }
437
+ /** The URL a stylesheet at `cssFileName` uses for an emitted asset under Vite's `base`. */
438
+ function assetUrl(base, cssFileName, assetFileName) {
439
+ if (base === "./" || base === "")
440
+ return `./${posix.relative(posix.dirname(cssFileName), assetFileName)}`;
441
+ return `${base}${assetFileName}`;
442
+ }
443
+ /** `E_CODE: message` for an engine error, the message otherwise. */
444
+ function describeError(error) {
445
+ if (!(error instanceof Error))
446
+ return String(error);
447
+ const { code } = error;
448
+ return typeof code === "string" && code.startsWith("E_") ? `${code}: ${error.message}` : error.message;
449
+ }
450
+ function resolves(specifier, root) {
451
+ try {
452
+ createRequire(join(resolve(root), "package.json")).resolve(specifier);
453
+ return true;
454
+ }
455
+ catch {
456
+ return false;
457
+ }
458
+ }
459
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1,32 @@
1
+ /** The ids a user imports to get the generated stylesheet (teacss-ts `ENTRY_IDS`). */
2
+ export declare const ENTRY_IDS: ReadonlySet<string>;
3
+ /** The resolved id of the virtual entry; Vite's CSS pipeline treats it as a `.css` module. */
4
+ export declare const RESOLVED_ENTRY = "\0virtual:teacss.css";
5
+ /**
6
+ * What `load` returns for the entry in a build; `generateBundle` replaces it with the generated CSS. A rule,
7
+ * because every minifier keeps one: lightningcss keeps a legal comment only at the top of a stylesheet and
8
+ * warns about an unknown at-rule, esbuild drops the comment, and other CSS can precede the entry in a chunk.
9
+ */
10
+ export declare const BUILD_PLACEHOLDER: string;
11
+ /** `BUILD_PLACEHOLDER` with the digest of the CSS that will replace it, which goes into the CSS file's name. */
12
+ export declare function digestPlaceholder(digest: string): string;
13
+ /** `BUILD_PLACEHOLDER` or a `digestPlaceholder` as any minifier prints it. */
14
+ export declare const BUILD_PLACEHOLDER_PATTERN: RegExp;
15
+ /** Resolves `virtual:teacss.css`, `teacss.css`, their `/`-prefixed forms and the resolved id itself, keeping a `?query`. */
16
+ export declare function resolveEntryId(id: string): string | null;
17
+ /**
18
+ * The path an HMR update names a module by: the id the client registered the module's hot context under.
19
+ * Vite 7 wraps a URL that is not a path (`\0virtual:teacss.css` → `/@id/__x00__virtual:teacss.css`, its
20
+ * `normalizeHmrUrl`); Vite 8 registers and sends the module graph's URL as is, and its client wraps it.
21
+ */
22
+ export declare function hmrPath(url: string, viteVersion: string): string;
23
+ /** Whether `id` is the resolved entry, with or without a `?query`. */
24
+ export declare function isResolvedEntry(id: string): boolean;
25
+ export declare const DEFAULT_PIPELINE_INCLUDE: RegExp;
26
+ /**
27
+ * Which modules `transform` hands to the project: never a virtual one; any module whose code carries
28
+ * `includeComment` (`@teacss-include`); otherwise, outside `node_modules`, CSS ids (`transformStylesheet`)
29
+ * and the ids `DEFAULT_PIPELINE_INCLUDE` matches. `@source` files are scanned from disk regardless.
30
+ */
31
+ export declare function shouldTransform(id: string, code: string, includeComment: string): boolean;
32
+ //# sourceMappingURL=virtual.d.ts.map
@@ -0,0 +1,59 @@
1
+ import { CSS_PLACEHOLDER, isCssId } from "@teacss/engine";
2
+ /** The ids a user imports to get the generated stylesheet (teacss-ts `ENTRY_IDS`). */
3
+ export const ENTRY_IDS = new Set(["virtual:teacss.css", "teacss.css"]);
4
+ /** The resolved id of the virtual entry; Vite's CSS pipeline treats it as a `.css` module. */
5
+ export const RESOLVED_ENTRY = "\0virtual:teacss.css";
6
+ const PLACEHOLDER_NAME = CSS_PLACEHOLDER.slice(1);
7
+ /**
8
+ * What `load` returns for the entry in a build; `generateBundle` replaces it with the generated CSS. A rule,
9
+ * because every minifier keeps one: lightningcss keeps a legal comment only at the top of a stylesheet and
10
+ * warns about an unknown at-rule, esbuild drops the comment, and other CSS can precede the entry in a chunk.
11
+ */
12
+ export const BUILD_PLACEHOLDER = `#${PLACEHOLDER_NAME}{--${PLACEHOLDER_NAME}:0}`;
13
+ /** `BUILD_PLACEHOLDER` with the digest of the CSS that will replace it, which goes into the CSS file's name. */
14
+ export function digestPlaceholder(digest) {
15
+ return `#${PLACEHOLDER_NAME}{--${PLACEHOLDER_NAME}:t${digest}}`;
16
+ }
17
+ /** `BUILD_PLACEHOLDER` or a `digestPlaceholder` as any minifier prints it. */
18
+ export const BUILD_PLACEHOLDER_PATTERN = new RegExp(`#${PLACEHOLDER_NAME}\\s*\\{\\s*--${PLACEHOLDER_NAME}\\s*:\\s*[\\w-]+\\s*;?\\s*\\}`, "g");
19
+ /** Resolves `virtual:teacss.css`, `teacss.css`, their `/`-prefixed forms and the resolved id itself, keeping a `?query`. */
20
+ export function resolveEntryId(id) {
21
+ const [bare = "", query] = splitQuery(id);
22
+ const isEntry = bare === RESOLVED_ENTRY || ENTRY_IDS.has(bare) || ENTRY_IDS.has(bare.replace(/^\//, ""));
23
+ return isEntry ? (query === undefined ? RESOLVED_ENTRY : `${RESOLVED_ENTRY}?${query}`) : null;
24
+ }
25
+ /**
26
+ * The path an HMR update names a module by: the id the client registered the module's hot context under.
27
+ * Vite 7 wraps a URL that is not a path (`\0virtual:teacss.css` → `/@id/__x00__virtual:teacss.css`, its
28
+ * `normalizeHmrUrl`); Vite 8 registers and sends the module graph's URL as is, and its client wraps it.
29
+ */
30
+ export function hmrPath(url, viteVersion) {
31
+ if (Number.parseInt(viteVersion, 10) >= 8 || url.startsWith("/") || url.startsWith("."))
32
+ return url;
33
+ return `/@id/${url.replace("\0", "__x00__")}`;
34
+ }
35
+ /** Whether `id` is the resolved entry, with or without a `?query`. */
36
+ export function isResolvedEntry(id) {
37
+ return splitQuery(id)[0] === RESOLVED_ENTRY;
38
+ }
39
+ function splitQuery(id) {
40
+ const index = id.indexOf("?");
41
+ return index === -1 ? [id, undefined] : [id.slice(0, index), id.slice(index + 1)];
42
+ }
43
+ // same as teacss-ts runtime defaultPipelineInclude
44
+ export const DEFAULT_PIPELINE_INCLUDE = /\.(vue|svelte|[jt]sx|vine\.ts|mdx?|astro|elm|php|phtml|marko|html)($|\?)/;
45
+ /**
46
+ * Which modules `transform` hands to the project: never a virtual one; any module whose code carries
47
+ * `includeComment` (`@teacss-include`); otherwise, outside `node_modules`, CSS ids (`transformStylesheet`)
48
+ * and the ids `DEFAULT_PIPELINE_INCLUDE` matches. `@source` files are scanned from disk regardless.
49
+ */
50
+ export function shouldTransform(id, code, includeComment) {
51
+ if (id.startsWith("\0"))
52
+ return false;
53
+ if (code.includes(includeComment))
54
+ return true;
55
+ if (id.includes("/node_modules/"))
56
+ return false;
57
+ return isCssId(id) || DEFAULT_PIPELINE_INCLUDE.test(id);
58
+ }
59
+ //# sourceMappingURL=virtual.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teacss/vite",
3
- "version": "0.10.9",
3
+ "version": "0.10.10",
4
4
  "description": "Vite plugin for TeaCSS on the Rust engine (@teacss/engine)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,8 +36,8 @@
36
36
  "vite": ">=7"
37
37
  },
38
38
  "dependencies": {
39
- "@teacss/engine": "0.10.9",
40
- "@teacss/inspector": "0.10.9"
39
+ "@teacss/engine": "0.10.10",
40
+ "@teacss/inspector": "0.10.10"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^22.20.5",