@teacss/bun 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.
- package/dist/index.d.ts +32 -0
- package/dist/index.js +188 -0
- package/package.json +2 -2
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ShellOptions } from "@teacss/engine";
|
|
2
|
+
import type { BuildConfig, BuildOutput, BunPlugin } from "bun";
|
|
3
|
+
export type { ShellOptions } from "@teacss/engine";
|
|
4
|
+
/**
|
|
5
|
+
* `import "virtual:teacss.css"` (or `"teacss.css"`) becomes the generated stylesheet; every other
|
|
6
|
+
* module goes through `project.transform`, and in a CSS file whose top level has `@teacss;` the generated
|
|
7
|
+
* stylesheet then takes that rule's place. `minify` defaults to false, as teacss-ts's Bun plugin emits the
|
|
8
|
+
* stylesheet unminified.
|
|
9
|
+
*/
|
|
10
|
+
export declare function teacss(options?: ShellOptions): BunPlugin;
|
|
11
|
+
export interface WatchOptions {
|
|
12
|
+
/** The `Bun.build` configuration; a plugin of this package already in `plugins` is replaced. */
|
|
13
|
+
build: BuildConfig;
|
|
14
|
+
shell?: ShellOptions;
|
|
15
|
+
/** Milliseconds between the last file event and the rebuild. Default 200. */
|
|
16
|
+
debounce?: number;
|
|
17
|
+
/** Called with a failed build's error or a watcher error. Default `console.error`. */
|
|
18
|
+
onError?: (error: unknown) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface WatchSession {
|
|
21
|
+
/** Successful builds so far, the first one included. */
|
|
22
|
+
readonly builds: number;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds once, then rebuilds whenever a file in a directory of `project.watchFiles()` (or the root) changes,
|
|
27
|
+
* or a file anywhere under a `@source` glob's directory (`project.sourceDirectories()`), outside `outdir`.
|
|
28
|
+
* Bun's plugin API has no dependency registration, so the watch owns the file list; it resolves after the
|
|
29
|
+
* first build has been reported.
|
|
30
|
+
*/
|
|
31
|
+
export declare function watch(options: WatchOptions, onBuild: (result: BuildOutput) => void | Promise<void>): Promise<WatchSession>;
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, watch as watchDirectory } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { createProject, extractBundledFonts, replaceEntry } from "@teacss/engine";
|
|
5
|
+
const PLUGIN = "teacss";
|
|
6
|
+
const VIRTUAL_ENTRY = /^(?:virtual:)?teacss\.css$/;
|
|
7
|
+
const MODULE = /\.(?:[cm]?[jt]sx?|html)$/;
|
|
8
|
+
const CSS = /\.css$/;
|
|
9
|
+
const NODE_MODULES = `${sep}node_modules${sep}`;
|
|
10
|
+
// teacss-ts `rewriteBundledAssets` writes each bundled font as `teacss-fonts/teacss-<sha256>.woff2` beside the CSS.
|
|
11
|
+
const FONT_URL_PREFIX = "./teacss-fonts/teacss-";
|
|
12
|
+
const FONT_URL = /^\.\/teacss-fonts\/teacss-[a-f0-9]{64}\.woff2$/;
|
|
13
|
+
/**
|
|
14
|
+
* The tokens of every `@source` file read once and of `inline.content.inline`. `warned` holds the
|
|
15
|
+
* unmatched-token warnings already printed by earlier builds of the same plugin.
|
|
16
|
+
*/
|
|
17
|
+
function generateCss(project, minify, warned) {
|
|
18
|
+
const union = project.engine.extractFilesUnion(project.info.sourceFiles);
|
|
19
|
+
for (const path of project.info.unreadableDirectories)
|
|
20
|
+
console.warn(`[@teacss/bun] cannot read ${path} (E_IO)`);
|
|
21
|
+
for (const { path, error } of union.failed)
|
|
22
|
+
console.warn(`[@teacss/bun] cannot read ${path} (${error})`);
|
|
23
|
+
const tokens = [...new Set([...union.tokens, ...project.inlineTokens()])];
|
|
24
|
+
const result = project.engine.generate({ kind: "tokens", tokens }, { minify, css: true });
|
|
25
|
+
project.reportUnmatched(result, {
|
|
26
|
+
tokens,
|
|
27
|
+
warn: (message) => {
|
|
28
|
+
if (warned.has(message))
|
|
29
|
+
return;
|
|
30
|
+
warned.add(message);
|
|
31
|
+
console.warn(`[@teacss/bun] ${message}`);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return result.css;
|
|
35
|
+
}
|
|
36
|
+
function externalizeFonts(css, fonts) {
|
|
37
|
+
const extracted = extractBundledFonts(css, (digest) => `${FONT_URL_PREFIX}${digest}.woff2`);
|
|
38
|
+
for (const font of extracted.fonts)
|
|
39
|
+
fonts.set(font.digest, font.bytes);
|
|
40
|
+
return extracted.css;
|
|
41
|
+
}
|
|
42
|
+
async function writeFonts(result, fonts) {
|
|
43
|
+
if (!result.success || fonts.size === 0)
|
|
44
|
+
return;
|
|
45
|
+
for (const output of result.outputs) {
|
|
46
|
+
if (!output.path.endsWith(".css"))
|
|
47
|
+
continue;
|
|
48
|
+
const css = await output.text();
|
|
49
|
+
const directory = join(dirname(output.path), "teacss-fonts");
|
|
50
|
+
for (const [digest, bytes] of fonts) {
|
|
51
|
+
const name = `teacss-${digest}.woff2`;
|
|
52
|
+
if (!css.includes(`teacss-fonts/${name}`))
|
|
53
|
+
continue;
|
|
54
|
+
await mkdir(directory, { recursive: true });
|
|
55
|
+
await writeFile(join(directory, name), bytes);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function projectRoot(options, config) {
|
|
60
|
+
return resolve(options.root ?? config.root ?? process.cwd());
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* `import "virtual:teacss.css"` (or `"teacss.css"`) becomes the generated stylesheet; every other
|
|
64
|
+
* module goes through `project.transform`, and in a CSS file whose top level has `@teacss;` the generated
|
|
65
|
+
* stylesheet then takes that rule's place. `minify` defaults to false, as teacss-ts's Bun plugin emits the
|
|
66
|
+
* stylesheet unminified.
|
|
67
|
+
*/
|
|
68
|
+
export function teacss(options = {}) {
|
|
69
|
+
// `watch()` runs every build through this one plugin; each build gets a fresh project, so that no module or
|
|
70
|
+
// compile-class definition of a file deleted or renamed since lingers, and the warnings outlive it here.
|
|
71
|
+
const warned = new Set();
|
|
72
|
+
return {
|
|
73
|
+
name: PLUGIN,
|
|
74
|
+
setup(build) {
|
|
75
|
+
const project = createProject({ ...options, root: projectRoot(options, build.config) });
|
|
76
|
+
const fonts = new Map();
|
|
77
|
+
const externalize = build.config.outdir ? (css) => externalizeFonts(css, fonts) : (css) => css;
|
|
78
|
+
const generated = () => externalize(generateCss(project, options.minify ?? false, warned));
|
|
79
|
+
build.onResolve({ filter: VIRTUAL_ENTRY }, () => ({ path: "teacss.css", namespace: PLUGIN }));
|
|
80
|
+
// Bun bundles every `url()` of a stylesheet; the fonts exist only after `onEnd` writes them.
|
|
81
|
+
build.onResolve({ filter: FONT_URL }, (args) => (fonts.has(args.path.slice(FONT_URL_PREFIX.length, -".woff2".length)) ? { path: args.path, external: true } : undefined));
|
|
82
|
+
build.onLoad({ filter: /.*/, namespace: PLUGIN }, () => ({ contents: generated(), loader: "css" }));
|
|
83
|
+
build.onLoad({ filter: CSS }, async (args) => {
|
|
84
|
+
const { code } = project.transform(args.path, await readFile(args.path, "utf8"));
|
|
85
|
+
return { contents: replaceEntry(code, generated, args.path) ?? externalize(code), loader: "css" };
|
|
86
|
+
});
|
|
87
|
+
build.onLoad({ filter: MODULE }, async (args) => {
|
|
88
|
+
const code = await readFile(args.path, "utf8");
|
|
89
|
+
if (args.path.includes(NODE_MODULES) && !code.includes("@teacss-include"))
|
|
90
|
+
return;
|
|
91
|
+
const result = project.transform(args.path, code);
|
|
92
|
+
return result.changed ? { contents: result.code, loader: args.loader } : undefined;
|
|
93
|
+
});
|
|
94
|
+
build.onEnd(async (result) => {
|
|
95
|
+
project.dispose();
|
|
96
|
+
await writeFonts(result, fonts);
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Builds once, then rebuilds whenever a file in a directory of `project.watchFiles()` (or the root) changes,
|
|
103
|
+
* or a file anywhere under a `@source` glob's directory (`project.sourceDirectories()`), outside `outdir`.
|
|
104
|
+
* Bun's plugin API has no dependency registration, so the watch owns the file list; it resolves after the
|
|
105
|
+
* first build has been reported.
|
|
106
|
+
*/
|
|
107
|
+
export async function watch(options, onBuild) {
|
|
108
|
+
const { build, shell = {}, debounce = 200, onError = console.error } = options;
|
|
109
|
+
const root = projectRoot(shell, build);
|
|
110
|
+
const plugins = [...(build.plugins ?? []).filter((plugin) => plugin.name !== PLUGIN), teacss({ ...shell, root })];
|
|
111
|
+
const project = createProject({ ...shell, root });
|
|
112
|
+
const outdir = build.outdir === undefined ? undefined : resolve(build.outdir);
|
|
113
|
+
const watchers = new Map();
|
|
114
|
+
let timer;
|
|
115
|
+
let running = false;
|
|
116
|
+
let queued = false;
|
|
117
|
+
let closed = false;
|
|
118
|
+
let builds = 0;
|
|
119
|
+
function syncWatchers() {
|
|
120
|
+
project.reload();
|
|
121
|
+
const wanted = new Map([root, ...project.watchFiles().map((file) => dirname(file))].map((directory) => [directory, false]));
|
|
122
|
+
for (const { dir } of project.sourceDirectories())
|
|
123
|
+
if (existsSync(dir))
|
|
124
|
+
wanted.set(dir, true);
|
|
125
|
+
for (const [directory, { watcher, recursive }] of watchers) {
|
|
126
|
+
if (wanted.get(directory) === recursive)
|
|
127
|
+
continue;
|
|
128
|
+
watcher.close();
|
|
129
|
+
watchers.delete(directory);
|
|
130
|
+
}
|
|
131
|
+
for (const [directory, recursive] of wanted) {
|
|
132
|
+
if (watchers.has(directory))
|
|
133
|
+
continue;
|
|
134
|
+
const watcher = watchDirectory(directory, { recursive }, (_event, file) => {
|
|
135
|
+
if (file === null || !isOutput(resolve(directory, file)))
|
|
136
|
+
schedule();
|
|
137
|
+
});
|
|
138
|
+
watchers.set(directory, { watcher: watcher.on("error", onError), recursive });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function isOutput(file) {
|
|
142
|
+
return outdir !== undefined && (file === outdir || file.startsWith(`${outdir}${sep}`));
|
|
143
|
+
}
|
|
144
|
+
async function run() {
|
|
145
|
+
running = true;
|
|
146
|
+
do {
|
|
147
|
+
queued = false;
|
|
148
|
+
try {
|
|
149
|
+
syncWatchers();
|
|
150
|
+
const result = await Bun.build({ ...build, plugins });
|
|
151
|
+
if (!result.success)
|
|
152
|
+
throw new AggregateError(result.logs, "[@teacss/bun] build failed");
|
|
153
|
+
builds++;
|
|
154
|
+
await onBuild(result);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
onError(error);
|
|
158
|
+
}
|
|
159
|
+
} while (queued && !closed);
|
|
160
|
+
running = false;
|
|
161
|
+
}
|
|
162
|
+
function schedule() {
|
|
163
|
+
if (closed)
|
|
164
|
+
return;
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
timer = setTimeout(() => {
|
|
167
|
+
if (running)
|
|
168
|
+
queued = true;
|
|
169
|
+
else
|
|
170
|
+
void run();
|
|
171
|
+
}, debounce);
|
|
172
|
+
}
|
|
173
|
+
await run();
|
|
174
|
+
return {
|
|
175
|
+
get builds() {
|
|
176
|
+
return builds;
|
|
177
|
+
},
|
|
178
|
+
close() {
|
|
179
|
+
closed = true;
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
for (const { watcher } of watchers.values())
|
|
182
|
+
watcher.close();
|
|
183
|
+
watchers.clear();
|
|
184
|
+
project.dispose();
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teacss/bun",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.10",
|
|
4
4
|
"description": "Bun.build plugin for TeaCSS: virtual:teacss.css and the entry's @teacss become the CSS the Rust engine generates",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=26"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@teacss/engine": "0.10.
|
|
34
|
+
"@teacss/engine": "0.10.10"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "1.4.3",
|