@kirana-style/rolldown 0.0.0-stage → 0.1.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
@@ -1,3 +1,25 @@
1
- # Temporary Holding Version
1
+ # @kirana-style/rolldown
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Rolldown adapter for Kirana ([11-integrations](../../../../spec/11-integrations.md)).
4
+
5
+ ```js
6
+ // rolldown.config.mjs
7
+ import kirana from "@kirana-style/rolldown"
8
+ import { defineConfig } from "rolldown"
9
+
10
+ export default defineConfig({
11
+ input: "src/main.tsx",
12
+ output: { dir: "dist" },
13
+ plugins: [kirana()],
14
+ })
15
+ ```
16
+
17
+ - **Transform.** Modules that mention `@kirana-style/` or an `sx` attribute are compiled; errors fail the
18
+ build with the source line, warnings are printed. A watch rebuild re-runs the importers of changed
19
+ tokens and relations.
20
+ - **Stylesheet.** Rolldown does not bundle CSS, so the plugin emits the finished stylesheet as
21
+ `kirana.css` (`kirana({ fileName })` to change it) and importing `@kirana-style/core/stylesheet.css`
22
+ yields that name. Link it from your HTML.
23
+
24
+ Options: `root` (default: the working directory), `config` (overrides `kirana.config.*` key by key),
25
+ `reportInfo`, `fileName`. `@kirana-style/vite` is this plugin plus Vite's dev server.
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@kirana-style/rolldown",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Kirana for Rolldown: compiles modules and emits the stylesheet.",
5
+ "license": "MIT",
6
+ "author": "The Kirana authors",
7
+ "homepage": "https://kirana.style",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kirana-style/kirana.git",
11
+ "directory": "packages/kirana/npm/rolldown"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/kirana-style/kirana/issues"
15
+ },
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./src/index.d.ts",
20
+ "default": "./src/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "dependencies": {
25
+ "@kirana-style/compiler": "0.1.0",
26
+ "@kirana-style/core": "0.1.0"
27
+ },
28
+ "peerDependencies": {
29
+ "rolldown": "^1.2.12"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "rolldown": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "files": [
37
+ "src",
38
+ "bin",
39
+ "*.d.ts",
40
+ "*.css",
41
+ "README.md",
42
+ "builders.json",
43
+ "schemas",
44
+ "gatsby-node.mjs",
45
+ "rules",
46
+ "recommended.json",
47
+ "strict.json"
48
+ ]
49
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The Rolldown host adapter (KRN-INT-001, KRN-INT-002). It selects modules, forwards transform
3
+ * results, emits the stylesheet entry and forwards file changes to the Driver; it never interprets
4
+ * styles (D-028).
5
+ *
6
+ * The Vite adapter is this plugin plus Vite's dev-server hooks, because Vite's plugin API extends
7
+ * Rolldown's: one implementation of the build path serves both hosts. The one difference is CSS:
8
+ * Vite bundles it, so the stylesheet entry is a placeholder Vite processes and the plugin fills in;
9
+ * Rolldown alone does not, so the plugin emits the stylesheet as an asset of its own.
10
+ */
11
+ import { Driver } from "@kirana-style/compiler";
12
+ import type { DriverOptions } from "@kirana-style/compiler";
13
+ import type { KiranaConfig } from "@kirana-style/core/config";
14
+ import type { OutputBundle, Plugin } from "rolldown";
15
+ export type KiranaRolldownOptions = {
16
+ /** The project root: where `kirana.config.*` and `@kirana-style/core` are found. Default: the working directory. */
17
+ root?: string;
18
+ /** Options that override `kirana.config.*`. */
19
+ config?: KiranaConfig;
20
+ /** Report `info` diagnostics (such as `dynamic-bridged`) as warnings. */
21
+ reportInfo?: boolean;
22
+ /**
23
+ * Where the stylesheet is written in the output, when Rolldown builds on its own. Rolldown does
24
+ * not bundle CSS, so importing `@kirana-style/core/stylesheet.css` yields this name and the finished
25
+ * stylesheet is emitted under it; link it from your HTML. Default `kirana.css`. Under Vite, which
26
+ * bundles CSS, the stylesheet follows Vite's asset naming instead.
27
+ */
28
+ fileName?: string;
29
+ /** Injected Driver options (tests). */
30
+ driver?: Pick<DriverOptions, "native" | "importModule">;
31
+ };
32
+ /** What a host that extends this plugin supplies. */
33
+ export type Host = {
34
+ /** True while a dev server serves the live stylesheet instead of the placeholder. */
35
+ development?: () => boolean;
36
+ /** A transform changed the stylesheet. */
37
+ cssChanged?: () => void;
38
+ /** The host bundles CSS (Vite): the entry is a placeholder stylesheet it processes like any other. */
39
+ bundlesCss?: boolean;
40
+ /**
41
+ * False when something later renames the finished stylesheet itself (`@kirana-style/astro`, because
42
+ * Astro links each page's CSS by the name it recorded before `generateBundle`): the asset is filled
43
+ * where it is, and its name is added to `filled`.
44
+ */
45
+ renames?: boolean;
46
+ };
47
+ /**
48
+ * The plugin, with the session and the settings a host that extends it may change (the Vite
49
+ * adapter sets the root from Vite's resolved configuration).
50
+ */
51
+ export declare function createKirana(options?: KiranaRolldownOptions, host?: Host): {
52
+ plugin: {
53
+ name: string;
54
+ buildStart(): Promise<void>;
55
+ load: {
56
+ filter: {
57
+ id: RegExp[];
58
+ };
59
+ handler(this: import("rolldown").PluginContext, id: string): Promise<string | {
60
+ code: string;
61
+ map: string | undefined;
62
+ moduleType?: undefined;
63
+ } | {
64
+ map?: undefined;
65
+ code: string;
66
+ moduleType: "js";
67
+ } | null>;
68
+ };
69
+ transform: {
70
+ filter: {
71
+ id: {
72
+ include: RegExp;
73
+ exclude: RegExp[];
74
+ };
75
+ code: RegExp;
76
+ };
77
+ handler(this: import("rolldown").TransformPluginContext, code: string, id: string): Promise<{
78
+ code: string;
79
+ map: string | undefined;
80
+ } | null>;
81
+ };
82
+ /** A watch rebuild: modules that read the changed file's tokens or relations compile again. */
83
+ watchChange(id: string): Promise<void>;
84
+ buildEnd(this: import("rolldown").PluginContext): Promise<void>;
85
+ generateBundle: {
86
+ order: "pre";
87
+ handler(this: import("rolldown").PluginContext, _output: import("rolldown").NormalizedOutputOptions, bundle: OutputBundle): Promise<void>;
88
+ };
89
+ };
90
+ session: () => Promise<Driver>;
91
+ state: {
92
+ root: string;
93
+ stylesheetFile: string;
94
+ };
95
+ filled: Set<string>;
96
+ fill: (bundle: OutputBundle, emitFile: (file: {
97
+ type: "asset";
98
+ fileName: string;
99
+ source: string;
100
+ }) => unknown) => Promise<void>;
101
+ };
102
+ export default function kirana(options?: KiranaRolldownOptions): Plugin;
package/src/index.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The Rolldown host adapter (KRN-INT-001, KRN-INT-002). It selects modules, forwards transform
3
+ * results, emits the stylesheet entry and forwards file changes to the Driver; it never interprets
4
+ * styles (D-028).
5
+ *
6
+ * The Vite adapter is this plugin plus Vite's dev-server hooks, because Vite's plugin API extends
7
+ * Rolldown's: one implementation of the build path serves both hosts. The one difference is CSS:
8
+ * Vite bundles it, so the stylesheet entry is a placeholder Vite processes and the plugin fills in;
9
+ * Rolldown alone does not, so the plugin emits the stylesheet as an asset of its own.
10
+ */
11
+ import { COMPONENT, Driver } from "@kirana-style/compiler";
12
+ import { ASSET_QUERY, PLACEHOLDER, SCRIPT, STYLE_BEARING, cleanId, createReporter, fillStylesheet, renameAsset, stylesheetPath, } from "@kirana-style/compiler/host";
13
+ import { readFile } from "node:fs/promises";
14
+ import path from "node:path";
15
+ /** A request for part of a Vue, Svelte or Astro component, which carries a query. */
16
+ const COMPONENT_PART = /\.(?:vue|svelte|astro)\?/;
17
+ /**
18
+ * The plugin, with the session and the settings a host that extends it may change (the Vite
19
+ * adapter sets the root from Vite's resolved configuration).
20
+ */
21
+ export function createKirana(options = {}, host = {}) {
22
+ const state = { root: options.root ?? process.cwd(), stylesheetFile: "" };
23
+ let driver;
24
+ const reporter = createReporter(options);
25
+ const renamed = new Map();
26
+ /** Stylesheet assets filled in place (`Host.renames` false), by output file name. */
27
+ const filled = new Set();
28
+ const development = () => host.development?.() ?? false;
29
+ const fileName = options.fileName ?? "kirana.css";
30
+ const session = () => {
31
+ driver ??= Driver.create({
32
+ root: state.root,
33
+ config: options.config,
34
+ ...options.driver,
35
+ });
36
+ return driver;
37
+ };
38
+ const stylesheetFile = () => (state.stylesheetFile ||= stylesheetPath(state.root));
39
+ const plugin = {
40
+ name: "kirana",
41
+ async buildStart() {
42
+ await session();
43
+ },
44
+ load: {
45
+ // No `node_modules` exclusion here: in an installed app the stylesheet entry lives there.
46
+ filter: { id: [/stylesheet\.css/, COMPONENT] },
47
+ async handler(id) {
48
+ // A Vue, Svelte or Astro component is compiled as it is read, before any framework plugin's
49
+ // `transform` — Astro's compiles `.astro` files ahead of every user plugin, so a transform
50
+ // would only ever see its output (design-notes/sfc-masking.md).
51
+ if (COMPONENT.test(id)) {
52
+ // An installed component is compiled only when its package is in `libraries`.
53
+ const compiler = await session();
54
+ if (!compiler.compiles(id))
55
+ return null;
56
+ const source = await readFile(id, "utf8");
57
+ const result = await compiler.transformComponent(id, source);
58
+ for (const dependency of result.dependencies)
59
+ this.addWatchFile(dependency);
60
+ reporter.module(this, id, source, result.diagnostics);
61
+ if (result.cssChanged)
62
+ host.cssChanged?.();
63
+ return result.code === undefined
64
+ ? null
65
+ : { code: result.code, map: result.map };
66
+ }
67
+ if (cleanId(id) !== stylesheetFile())
68
+ return null;
69
+ // Rolldown alone: the import is a module naming the file emitted at the end of the build.
70
+ if (!host.bundlesCss) {
71
+ return {
72
+ code: `export default ${JSON.stringify(fileName)}`,
73
+ moduleType: "js",
74
+ };
75
+ }
76
+ // `?inline` asks for the stylesheet as a string: in development that is the live stylesheet
77
+ // (SvelteKit's server render inlines CSS this way). `?url` and `?raw`, and `?inline` in a
78
+ // build, are Vite's to answer from the file.
79
+ if (ASSET_QUERY.test(id)) {
80
+ return development() && /[?&]inline\b/.test(id)
81
+ ? (await session()).stylesheet(false)
82
+ : null;
83
+ }
84
+ return development() ? (await session()).stylesheet(false) : PLACEHOLDER;
85
+ },
86
+ },
87
+ transform: {
88
+ filter: {
89
+ id: {
90
+ include: SCRIPT,
91
+ // A framework's own requests for a component's parts (`Card.vue?vue&type=script&lang.ts`)
92
+ // hold what it derived from the component, which `load` compiled already.
93
+ // Installed packages are excluded in the handler, which knows `libraries`.
94
+ exclude: [ASSET_QUERY, COMPONENT_PART],
95
+ },
96
+ code: STYLE_BEARING,
97
+ },
98
+ async handler(code, id) {
99
+ const file = cleanId(id);
100
+ const compiler = await session();
101
+ if (!compiler.compiles(file))
102
+ return null;
103
+ const result = compiler.transform(file, code);
104
+ for (const dependency of result.dependencies)
105
+ this.addWatchFile(dependency);
106
+ reporter.module(this, file, code, result.diagnostics);
107
+ if (result.cssChanged)
108
+ host.cssChanged?.();
109
+ return result.code === undefined
110
+ ? null
111
+ : { code: result.code, map: result.map };
112
+ },
113
+ },
114
+ /** A watch rebuild: modules that read the changed file's tokens or relations compile again. */
115
+ async watchChange(id) {
116
+ ;
117
+ (await session()).invalidate(id);
118
+ },
119
+ async buildEnd() {
120
+ if (development())
121
+ return;
122
+ reporter.project(this, state.root, (await session()).diagnostics());
123
+ },
124
+ // First, so every plugin that reads CSS assets reads the finished stylesheet: Astro inlines
125
+ // small stylesheets into its pages in its own `generateBundle`. Vite writes HTML later from each
126
+ // chunk's `viteMetadata.importedCss`, which is renamed here along with the asset.
127
+ generateBundle: {
128
+ order: "pre",
129
+ async handler(_output, bundle) {
130
+ if (!host.bundlesCss) {
131
+ const css = (await session()).stylesheet(true);
132
+ this.emitFile({ type: "asset", fileName, source: css });
133
+ return;
134
+ }
135
+ await fill(bundle, this.emitFile.bind(this));
136
+ },
137
+ },
138
+ };
139
+ /**
140
+ * Fills every stylesheet asset still holding the placeholder, renaming it by content unless the
141
+ * host keeps names, and points the chunks and assets that named it at the new name. A second pass
142
+ * finds nothing to do, so a host may run it again later in `generateBundle` for assets another
143
+ * plugin added after the first pass (Qwik's).
144
+ */
145
+ const fill = async (bundle, emitFile) => {
146
+ const css = (await session()).stylesheet(true);
147
+ for (const [key, asset] of Object.entries(bundle)) {
148
+ if (asset.type !== "asset")
149
+ continue;
150
+ const source = fillStylesheet(String(asset.source), css);
151
+ if (source === undefined)
152
+ continue;
153
+ if (host.renames === false) {
154
+ asset.source = source;
155
+ filled.add(asset.fileName);
156
+ continue;
157
+ }
158
+ const hashed = renamed.get(asset.fileName) ?? renameAsset(asset.fileName, source);
159
+ renamed.set(asset.fileName, hashed);
160
+ delete bundle[key];
161
+ emitFile({ type: "asset", fileName: hashed, source });
162
+ }
163
+ // Every rename so far, not only this pass's: a server bundle names the client's asset.
164
+ for (const [from, to] of renamed) {
165
+ const oldBase = path.posix.basename(from);
166
+ const newBase = path.posix.basename(to);
167
+ for (const file of Object.values(bundle)) {
168
+ if (file.type === "chunk") {
169
+ file.code = file.code.replaceAll(oldBase, newBase);
170
+ const imported = file.viteMetadata?.importedCss;
171
+ if (imported?.delete(from))
172
+ imported.add(to);
173
+ }
174
+ else if (typeof file.source === "string") {
175
+ file.source = file.source.replaceAll(oldBase, newBase);
176
+ }
177
+ }
178
+ }
179
+ };
180
+ return { plugin, session, state, filled, fill };
181
+ }
182
+ export default function kirana(options = {}) {
183
+ return createKirana(options).plugin;
184
+ }