@kirana-style/webpack 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,32 @@
1
- # Temporary Holding Version
1
+ # @kirana-style/webpack
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 webpack 5 adapter for Kirana, which Rspack runs unchanged
4
+ ([11-integrations](../../../../spec/11-integrations.md)).
5
+
6
+ ```js
7
+ // webpack.config.js
8
+ import { KiranaPlugin } from "@kirana-style/webpack"
9
+
10
+ export default {
11
+ experiments: { css: true }, // or css-loader with mini-css-extract-plugin
12
+ plugins: [new KiranaPlugin()],
13
+ }
14
+ ```
15
+
16
+ ```js
17
+ // next.config.js — Next.js with webpack (`next build --webpack`)
18
+ import { withKirana } from "@kirana-style/webpack/next"
19
+
20
+ export default withKirana({})
21
+ ```
22
+
23
+ - **Loader.** The plugin registers a `pre` loader for scripts, so `sx` is read before `swc-loader`,
24
+ `babel-loader` or Next.js compile JSX away.
25
+ - **Stylesheet.** `@kirana-style/core/stylesheet.css` is ordinary CSS to webpack. The plugin writes the
26
+ finished stylesheet into the emitted asset at `processAssets`' optimize stage — after every module,
27
+ before the CSS minimizer and before `realContentHash` renames the file by its content.
28
+ - **Several compilers.** Every compiler of a project (Next.js's server, edge and client) shares one
29
+ compiler session, so atoms from server components reach the client's stylesheet.
30
+
31
+ Options: `root` (default: the compiler's context), `config`, `reportInfo`. Watch builds rewrite the
32
+ stylesheet; CSS hot updates on the dev server are not implemented yet.
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@kirana-style/webpack",
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 webpack 5 and Rspack: a loader that compiles modules and a plugin that finishes 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/webpack"
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
+ "./loader": {
23
+ "types": "./src/loader.d.ts",
24
+ "default": "./src/loader.js"
25
+ },
26
+ "./next": {
27
+ "types": "./src/next.d.ts",
28
+ "default": "./src/next.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "dependencies": {
33
+ "@kirana-style/compiler": "0.1.0",
34
+ "@kirana-style/core": "0.1.0"
35
+ },
36
+ "peerDependencies": {
37
+ "webpack": "^5.90.0",
38
+ "@rspack/core": "^2.2.8"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "webpack": {
42
+ "optional": true
43
+ },
44
+ "@rspack/core": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "files": [
49
+ "src",
50
+ "bin",
51
+ "*.d.ts",
52
+ "*.css",
53
+ "README.md",
54
+ "builders.json",
55
+ "schemas",
56
+ "gatsby-node.mjs",
57
+ "rules",
58
+ "recommended.json",
59
+ "strict.json"
60
+ ]
61
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { KiranaWebpackOptions } from "./session.ts";
2
+ export type { KiranaWebpackOptions } from "./session.ts";
3
+ type Asset = {
4
+ name: string;
5
+ source: {
6
+ source: () => string | Buffer;
7
+ };
8
+ };
9
+ type Compilation = {
10
+ warnings: Error[];
11
+ errors: Error[];
12
+ getAssets: () => readonly Asset[];
13
+ updateAsset: (name: string, source: unknown) => void;
14
+ hooks: {
15
+ finishModules: {
16
+ tapPromise: (name: string, hook: (modules: Iterable<unknown>) => Promise<void>) => void;
17
+ };
18
+ processAssets: {
19
+ tapPromise: (options: {
20
+ name: string;
21
+ stage: number;
22
+ }, hook: () => Promise<void>) => void;
23
+ };
24
+ };
25
+ };
26
+ /** The part of webpack's and Rspack's compiler the plugin uses. */
27
+ export type Compiler = {
28
+ context: string;
29
+ options: {
30
+ mode?: string;
31
+ module: {
32
+ rules: unknown[];
33
+ };
34
+ };
35
+ webpack: {
36
+ WebpackError: new (message: string) => Error;
37
+ Compilation: {
38
+ PROCESS_ASSETS_STAGE_OPTIMIZE: number;
39
+ };
40
+ sources: {
41
+ RawSource: new (source: string) => unknown;
42
+ };
43
+ };
44
+ hooks: {
45
+ thisCompilation: {
46
+ tap: (name: string, hook: (compilation: Compilation) => void) => void;
47
+ };
48
+ invalid: {
49
+ tap: (name: string, hook: (file: string | null) => void) => void;
50
+ };
51
+ watchRun: {
52
+ tapPromise: (name: string, hook: () => Promise<void>) => void;
53
+ };
54
+ done: {
55
+ tapPromise: (name: string, hook: () => Promise<void>) => void;
56
+ };
57
+ };
58
+ };
59
+ export declare class KiranaPlugin {
60
+ readonly options: KiranaWebpackOptions;
61
+ constructor(options?: KiranaWebpackOptions);
62
+ apply(compiler: Compiler): void;
63
+ }
64
+ export default KiranaPlugin;
package/src/index.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The webpack 5 and Rspack host adapter (KRN-INT-001, KRN-INT-003): a `pre` loader compiles modules,
3
+ * and the plugin finishes the stylesheet entry. Rspack implements webpack's plugin and loader API,
4
+ * so one adapter serves both. It never interprets styles (D-028).
5
+ *
6
+ * The stylesheet entry is ordinary CSS to the host — native CSS, `css-loader` with
7
+ * `mini-css-extract-plugin`, Rsbuild's or Next.js's pipeline — so the host names and links it as it
8
+ * does any stylesheet. The loader answers it with the placeholder rule, and the plugin writes the
9
+ * finished stylesheet into the emitted asset at `processAssets`' optimize stage: after every module
10
+ * is built, before the CSS minimizer, and before `realContentHash` renames the file by its real
11
+ * content (on by default in production in both bundlers).
12
+ *
13
+ * Not `rebuildModule` at `finishModules`, which would let the host hash the right content from the
14
+ * start: Rspack 2 panics ("should have event sender") rebuilding a module under Rsbuild's CSS
15
+ * extraction.
16
+ */
17
+ import { fillStylesheet, shouldCompile, stylesheetPath, } from "@kirana-style/compiler/host";
18
+ import { createHash } from "node:crypto";
19
+ import { readFile, writeFile } from "node:fs/promises";
20
+ import { tmpdir } from "node:os";
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { createSession, sessions } from "./session.js";
24
+ /** The loader, next to this file with the same extension (TypeScript source in the workspace, JavaScript when packed). */
25
+ const LOADER = fileURLToPath(new URL(`./loader${import.meta.url.slice(import.meta.url.lastIndexOf("."))}`, import.meta.url));
26
+ const NAME = "kirana";
27
+ const SCRIPT_FILE = /\.[cm]?[jt]sx?$/;
28
+ export class KiranaPlugin {
29
+ options;
30
+ constructor(options = {}) {
31
+ this.options = options;
32
+ }
33
+ apply(compiler) {
34
+ const root = this.options.root ?? compiler.context;
35
+ const stylesheetFile = stylesheetPath(root);
36
+ const id = createSession(this.options, root, stylesheetFile, compiler.options.mode !== "development");
37
+ const session = sessions.get(id);
38
+ const loader = {
39
+ loader: this.options.loader ?? LOADER,
40
+ options: { session: id },
41
+ };
42
+ compiler.options.module.rules.unshift({
43
+ enforce: "pre",
44
+ test: SCRIPT_FILE,
45
+ // Installed packages pass through the loader untouched unless they are in `libraries`.
46
+ use: [loader],
47
+ }, {
48
+ enforce: "pre",
49
+ test: (file) => file === stylesheetFile,
50
+ use: [loader],
51
+ });
52
+ // A watch rebuild: modules that read the changed file's tokens or relations compile again.
53
+ compiler.hooks.invalid.tap(NAME, (file) => {
54
+ if (file)
55
+ void session.driver().then((driver) => driver.invalidate(file));
56
+ });
57
+ // Watching: the stylesheet entry follows a file the plugin rewrites whenever the stylesheet
58
+ // changes, outside `node_modules`, which bundlers treat as immutable and do not watch.
59
+ compiler.hooks.watchRun.tapPromise(NAME, async () => {
60
+ if (session.live)
61
+ return;
62
+ session.live = path.join(tmpdir(), `kirana-${createHash("sha256").update(root).digest("hex").slice(0, 12)}.css`);
63
+ await writeFile(session.live, "");
64
+ });
65
+ compiler.hooks.done.tapPromise(NAME, async () => {
66
+ if (!session.live)
67
+ return;
68
+ const css = (await session.driver()).stylesheet(false);
69
+ if ((await readFile(session.live, "utf8")) !== css)
70
+ await writeFile(session.live, css);
71
+ });
72
+ compiler.hooks.thisCompilation.tap(NAME, (compilation) => {
73
+ const { Compilation, sources, WebpackError } = compiler.webpack;
74
+ compilation.hooks.finishModules.tapPromise(NAME, async (modules) => {
75
+ const driver = await session.driver();
76
+ // Modules a persistent cache restored never reached the loader: compile them from disk, so
77
+ // their atoms are in the stylesheet. Their cached code already names the same classes.
78
+ for (const module of modules) {
79
+ // A module without a file (webpack's runtime) has no `resource`, and is no script.
80
+ const file = String(module.resource).split("?")[0];
81
+ if (!SCRIPT_FILE.test(file) || session.compiled.has(file))
82
+ continue;
83
+ const source = await readFile(file, "utf8");
84
+ if (!shouldCompile(file, source, driver.libraries))
85
+ continue;
86
+ driver.transform(file, source);
87
+ session.compiled.add(file);
88
+ }
89
+ try {
90
+ session.reporter.project({
91
+ warn: (message) => compilation.warnings.push(new WebpackError(message)),
92
+ error: (message) => {
93
+ throw new WebpackError(message);
94
+ },
95
+ }, root, driver.diagnostics());
96
+ }
97
+ catch (error) {
98
+ compilation.errors.push(error);
99
+ }
100
+ });
101
+ compilation.hooks.processAssets.tapPromise({ name: NAME, stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE }, async () => {
102
+ const css = (await session.driver()).stylesheet(session.minify);
103
+ for (const asset of compilation.getAssets()) {
104
+ if (!asset.name.endsWith(".css"))
105
+ continue;
106
+ const source = fillStylesheet(String(asset.source.source()), css);
107
+ if (source !== undefined)
108
+ compilation.updateAsset(asset.name, new sources.RawSource(source));
109
+ }
110
+ });
111
+ });
112
+ }
113
+ }
114
+ export default KiranaPlugin;
@@ -0,0 +1,12 @@
1
+ /** The part of webpack's and Rspack's loader context the loader uses. */
2
+ export type LoaderContext = {
3
+ resourcePath: string;
4
+ resourceQuery: string;
5
+ getOptions: () => {
6
+ session: string;
7
+ };
8
+ async: () => (error: Error | null, code?: string, map?: unknown) => void;
9
+ addDependency: (file: string) => void;
10
+ emitWarning: (warning: Error) => void;
11
+ };
12
+ export default function kiranaLoader(this: LoaderContext, source: string, map?: unknown): void;
package/src/loader.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The loader half of the webpack and Rspack adapter (KRN-INT-001): it compiles one module, or
3
+ * answers the stylesheet entry with the placeholder rule the plugin later replaces. The plugin
4
+ * registers it as a `pre` loader, so it sees the source before any other loader strips types or JSX.
5
+ */
6
+ import { PLACEHOLDER, shouldCompile } from "@kirana-style/compiler/host";
7
+ import { sessions } from "./session.js";
8
+ export default function kiranaLoader(source, map) {
9
+ const callback = this.async();
10
+ const session = sessions.get(this.getOptions().session);
11
+ if (!session) {
12
+ callback(new Error("@kirana-style/webpack/loader runs only through the Kirana plugin: add `new KiranaPlugin()` to `plugins`"));
13
+ return;
14
+ }
15
+ const file = this.resourcePath;
16
+ session
17
+ .driver()
18
+ .then((driver) => {
19
+ // In a build the plugin writes the finished stylesheet into the emitted asset; while watching,
20
+ // the entry is the live stylesheet (see index.ts).
21
+ if (file === session.stylesheetFile) {
22
+ if (session.live) {
23
+ this.addDependency(session.live);
24
+ callback(null, driver.stylesheet(false));
25
+ }
26
+ else
27
+ callback(null, PLACEHOLDER);
28
+ return;
29
+ }
30
+ if (!shouldCompile(file + this.resourceQuery, source, driver.libraries)) {
31
+ callback(null, source, map);
32
+ return;
33
+ }
34
+ const result = driver.transform(file, source);
35
+ session.compiled.add(file);
36
+ for (const dependency of result.dependencies)
37
+ this.addDependency(dependency);
38
+ session.reporter.module({
39
+ warn: (message) => this.emitWarning(new Error(message)),
40
+ error: (message) => {
41
+ throw new Error(message);
42
+ },
43
+ }, file, source, result.diagnostics);
44
+ if (result.code === undefined)
45
+ callback(null, source, map);
46
+ // An object, as loaders pass maps to each other: Next.js's SWC loader encodes a string again.
47
+ else
48
+ callback(null, result.code, result.map && JSON.parse(result.map));
49
+ })
50
+ .catch((error) => callback(error));
51
+ }
package/src/next.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { KiranaWebpackOptions } from "./session.ts";
2
+ type WebpackConfig = {
3
+ plugins?: unknown[];
4
+ };
5
+ type NextContext = {
6
+ dir: string;
7
+ };
8
+ type NextConfig = {
9
+ webpack?: (config: WebpackConfig, context: NextContext) => WebpackConfig;
10
+ [key: string]: unknown;
11
+ };
12
+ export declare function withKirana<Config extends NextConfig>(nextConfig?: Config, options?: KiranaWebpackOptions): Omit<Config, "webpack"> & Required<Pick<NextConfig, "webpack">>;
13
+ export {};
package/src/next.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Next.js with webpack (`next build --webpack`, KRN-INT-003): `withKirana` adds the plugin to every
3
+ * compiler Next creates — server, client and edge — at the project directory, and keeps any
4
+ * `webpack` function the configuration already has. For Turbopack, `@kirana-style/next` configures both.
5
+ */
6
+ import { KiranaPlugin } from "./index.js";
7
+ export function withKirana(nextConfig = {}, options = {}) {
8
+ return {
9
+ ...nextConfig,
10
+ webpack(config, context) {
11
+ ;
12
+ (config.plugins ??= []).push(new KiranaPlugin({ root: context.dir, ...options }));
13
+ return nextConfig.webpack ? nextConfig.webpack(config, context) : config;
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What the plugin and the loader share: one Driver per compiler, found by an id the plugin puts in
3
+ * the loader's options. An id rather than the object, because Rspack hands loader options across its
4
+ * JavaScript–Rust boundary as data.
5
+ */
6
+ import { Driver } from "@kirana-style/compiler";
7
+ import type { DriverOptions } from "@kirana-style/compiler";
8
+ import { createReporter } from "@kirana-style/compiler/host";
9
+ import type { KiranaConfig } from "@kirana-style/core/config";
10
+ export type KiranaWebpackOptions = {
11
+ /** The project root: where `kirana.config.*` and `@kirana-style/core` are found. Default: the compiler's context. */
12
+ root?: string;
13
+ /** Options that override `kirana.config.*`. */
14
+ config?: KiranaConfig;
15
+ /** Report `info` diagnostics (such as `dynamic-bridged`) as warnings. */
16
+ reportInfo?: boolean;
17
+ /** Injected Driver options (tests). */
18
+ driver?: Pick<DriverOptions, "native" | "importModule">;
19
+ /** The loader's path, in place of this package's (tests: one instance of the loader to measure). */
20
+ loader?: string;
21
+ };
22
+ export type Session = {
23
+ root: string;
24
+ /** `@kirana-style/core/stylesheet.css` as the compiler resolves it. */
25
+ stylesheetFile: string;
26
+ minify: boolean;
27
+ driver: () => Promise<Driver>;
28
+ reporter: ReturnType<typeof createReporter>;
29
+ /**
30
+ * The scripts this process's loader compiled. A persistent cache (`cache: { type: "filesystem" }`,
31
+ * which Meteor turns on) hands back a module without running loaders, so the plugin compiles the
32
+ * rest from disk before it writes the stylesheet.
33
+ */
34
+ compiled: Set<string>;
35
+ /**
36
+ * Set while the compiler watches (a dev server, `--watch`): the stylesheet entry then holds the
37
+ * live stylesheet and depends on this file, which the plugin rewrites when the stylesheet changes
38
+ * so the bundler rebuilds the entry and sends its own CSS hot update (KRN-INT-009).
39
+ */
40
+ live?: string;
41
+ };
42
+ /**
43
+ * Registries live on `globalThis` under registered symbols, because the loader is loaded by the
44
+ * bundler's own module loader and may be a second instance of this module (a test runner's, or a
45
+ * duplicated install); every instance must find the same entries.
46
+ */
47
+ export declare const sessions: Map<string, Session>;
48
+ export declare function createSession(options: KiranaWebpackOptions, root: string, stylesheetFile: string, minify: boolean): string;
package/src/session.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * What the plugin and the loader share: one Driver per compiler, found by an id the plugin puts in
3
+ * the loader's options. An id rather than the object, because Rspack hands loader options across its
4
+ * JavaScript–Rust boundary as data.
5
+ */
6
+ import { Driver } from "@kirana-style/compiler";
7
+ import { createReporter } from "@kirana-style/compiler/host";
8
+ /**
9
+ * Registries live on `globalThis` under registered symbols, because the loader is loaded by the
10
+ * bundler's own module loader and may be a second instance of this module (a test runner's, or a
11
+ * duplicated install); every instance must find the same entries.
12
+ */
13
+ export const sessions = registry("sessions");
14
+ /**
15
+ * One Driver and one reporter per project — root and configuration — shared by every compiler that
16
+ * builds it. Next.js runs a server, an edge and a client compiler over one app: a server component is
17
+ * compiled only by the server compiler while the stylesheet is emitted by the client one, so a Driver
18
+ * per compiler would leave the client's stylesheet without the server components' atoms. Next.js builds
19
+ * the server first, so the client's stylesheet, filled at the end of its own build, holds them all.
20
+ * Sharing also reports a module's warnings once rather than once per compiler.
21
+ */
22
+ const projects = registry("projects");
23
+ function registry(name) {
24
+ return (globalThis[Symbol.for(`kirana.webpack.${name}`)] ??= new Map());
25
+ }
26
+ export function createSession(options, root, stylesheetFile, minify) {
27
+ const id = `kirana-${sessions.size}`;
28
+ const key = JSON.stringify([
29
+ root,
30
+ options.config ?? null,
31
+ options.reportInfo ?? false,
32
+ ]);
33
+ // Injected engines (tests) are never shared, in either direction.
34
+ let project = options.driver ? undefined : projects.get(key);
35
+ if (!project) {
36
+ let driver;
37
+ project = {
38
+ driver: () => (driver ??= Driver.create({
39
+ root,
40
+ config: options.config,
41
+ ...options.driver,
42
+ })),
43
+ reporter: createReporter(options),
44
+ compiled: new Set(),
45
+ };
46
+ if (!options.driver)
47
+ projects.set(key, project);
48
+ }
49
+ sessions.set(id, { root, stylesheetFile, minify, ...project });
50
+ return id;
51
+ }