@kirana-style/angular 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.
@@ -0,0 +1,138 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "title": "Dev Server Target",
4
+ "description": "Dev Server target options for Build Facade.",
5
+ "type": "object",
6
+ "properties": {
7
+ "buildTarget": {
8
+ "type": "string",
9
+ "description": "A build builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
10
+ "pattern": "^[^:\\s]*:[^:\\s]*(:[^\\s]+)?$"
11
+ },
12
+ "port": {
13
+ "type": "number",
14
+ "description": "Port to listen on.",
15
+ "default": 4200
16
+ },
17
+ "host": {
18
+ "type": "string",
19
+ "description": "Host to listen on.",
20
+ "default": "localhost"
21
+ },
22
+ "proxyConfig": {
23
+ "type": "string",
24
+ "description": "Proxy configuration file. For more information, see https://angular.dev/tools/cli/serve#proxying-to-a-backend-server."
25
+ },
26
+ "ssl": {
27
+ "type": "boolean",
28
+ "description": "Serve using HTTPS.",
29
+ "default": false
30
+ },
31
+ "sslKey": {
32
+ "type": "string",
33
+ "description": "SSL key to use for serving HTTPS."
34
+ },
35
+ "sslCert": {
36
+ "type": "string",
37
+ "description": "SSL certificate to use for serving HTTPS."
38
+ },
39
+ "allowedHosts": {
40
+ "description": "The hosts that the development server will respond to. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts",
41
+ "default": [],
42
+ "oneOf": [
43
+ {
44
+ "type": "array",
45
+ "description": "A list of hosts that the development server will respond to.",
46
+ "items": {
47
+ "type": "string"
48
+ }
49
+ },
50
+ {
51
+ "type": "boolean",
52
+ "description": "Indicates that all hosts are allowed. This is not recommended and a security risk."
53
+ }
54
+ ]
55
+ },
56
+ "define": {
57
+ "description": "Defines global identifiers that will be replaced with a specified constant value when found in any JavaScript or TypeScript code including libraries. The value will be used directly. String values must be put in quotes. Identifiers within Angular metadata such as Component Decorators will not be replaced.",
58
+ "type": "object",
59
+ "additionalProperties": {
60
+ "type": "string"
61
+ }
62
+ },
63
+ "headers": {
64
+ "type": "object",
65
+ "description": "Custom HTTP headers to be added to all responses.",
66
+ "propertyNames": {
67
+ "pattern": "^[-_A-Za-z0-9]+$"
68
+ },
69
+ "additionalProperties": {
70
+ "type": "string"
71
+ }
72
+ },
73
+ "open": {
74
+ "type": "boolean",
75
+ "description": "Opens the url in default browser.",
76
+ "default": false,
77
+ "alias": "o"
78
+ },
79
+ "verbose": {
80
+ "type": "boolean",
81
+ "description": "Adds more details to output logging."
82
+ },
83
+ "liveReload": {
84
+ "type": "boolean",
85
+ "description": "Whether to reload the page on change, using live-reload.",
86
+ "default": true
87
+ },
88
+ "servePath": {
89
+ "type": "string",
90
+ "description": "The pathname where the application will be served."
91
+ },
92
+ "hmr": {
93
+ "type": "boolean",
94
+ "description": "Enable hot module replacement. Defaults to the value of 'liveReload'. Currently, only global and component stylesheets are supported."
95
+ },
96
+ "watch": {
97
+ "type": "boolean",
98
+ "description": "Rebuild on change.",
99
+ "default": true
100
+ },
101
+ "poll": {
102
+ "type": "number",
103
+ "description": "Enable and define the file watching poll time period in milliseconds."
104
+ },
105
+ "inspect": {
106
+ "default": false,
107
+ "description": "Activate debugging inspector. This option only has an effect when 'SSR' or 'SSG' are enabled.",
108
+ "oneOf": [
109
+ {
110
+ "type": "string",
111
+ "description": "Activate the inspector on host and port in the format of `[[host:]port]`. See the security warning in https://nodejs.org/docs/latest-v22.x/api/cli.html#warning-binding-inspector-to-a-public-ipport-combination-is-insecure regarding the host parameter usage."
112
+ },
113
+ { "type": "boolean" }
114
+ ]
115
+ },
116
+ "prebundle": {
117
+ "description": "Enable and control the Vite-based development server's prebundling capabilities. To enable prebundling, the Angular CLI cache must also be enabled.",
118
+ "default": true,
119
+ "oneOf": [
120
+ { "type": "boolean" },
121
+ {
122
+ "type": "object",
123
+ "properties": {
124
+ "exclude": {
125
+ "description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself. Note: specifying `@foo/bar` marks all paths within the `@foo/bar` package as excluded, including sub-paths like `@foo/bar/baz`.",
126
+ "type": "array",
127
+ "items": { "type": "string" }
128
+ }
129
+ },
130
+ "additionalProperties": false,
131
+ "required": ["exclude"]
132
+ }
133
+ ]
134
+ }
135
+ },
136
+ "additionalProperties": false,
137
+ "required": ["buildTarget"]
138
+ }
@@ -0,0 +1,3 @@
1
+ import { applicationBuilder } from "./builders.ts";
2
+ /** `@kirana-style/angular:application`, loaded by the Angular CLI from builders.json. */
3
+ export default applicationBuilder;
@@ -0,0 +1,3 @@
1
+ import { applicationBuilder } from "./builders.js";
2
+ /** `@kirana-style/angular:application`, loaded by the Angular CLI from builders.json. */
3
+ export default applicationBuilder;
@@ -0,0 +1,19 @@
1
+ import type { Builder, BuilderContext, BuilderOutput } from "@angular-devkit/architect";
2
+ import type { ApplicationBuilderOptions, DevServerBuilderOptions } from "@angular/build";
3
+ import type { Mirror } from "./mirror.ts";
4
+ /**
5
+ * Application options with the mirror's replacements and the stylesheet added. The entry files
6
+ * (`browser`, `server`) are not replaced: Angular type-checks only the files its tsconfig lists, and
7
+ * an entry it reads from a replacement it would bundle unchecked. An entry holds no styles in an
8
+ * Angular app; the components it imports are replaced.
9
+ */
10
+ export declare function withMirror<Options extends {
11
+ fileReplacements?: unknown[];
12
+ styles?: unknown[];
13
+ browser?: unknown;
14
+ server?: unknown;
15
+ }>(options: Options, mirror: Mirror): Options;
16
+ export declare function application(options: ApplicationBuilderOptions, context: BuilderContext): AsyncIterable<BuilderOutput>;
17
+ export declare function devServer(options: DevServerBuilderOptions, context: BuilderContext): AsyncIterable<BuilderOutput>;
18
+ export declare const applicationBuilder: Builder;
19
+ export declare const devServerBuilder: Builder;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The Angular CLI builders (`@kirana-style/angular:application`, `@kirana-style/angular:dev-server`): Angular's
3
+ * own, given Kirana's mirror (`mirror.ts`) as `fileReplacements` and its stylesheet in `styles`.
4
+ *
5
+ * "build": { "builder": "@kirana-style/angular:application", "options": { … as before … } }
6
+ * "serve": { "builder": "@kirana-style/angular:dev-server", … }
7
+ *
8
+ * The dev server is Angular's, run with a context whose build target's options carry the mirror:
9
+ * Angular reads those options itself and compiles them with its internal application build.
10
+ */
11
+ import path from "node:path";
12
+ import { createBuilder } from "@angular-devkit/architect";
13
+ import { buildApplication, executeDevServerBuilder } from "@angular/build";
14
+ import { STYLESHEET, createMirror, watchMirror } from "./mirror.js";
15
+ /** The project's source folder, from its configuration (`src` unless it says otherwise). */
16
+ async function sourceRootOf(context) {
17
+ const project = context.target?.project;
18
+ const metadata = project ? await context.getProjectMetadata(project) : {};
19
+ const root = typeof metadata.root === "string" ? metadata.root : "";
20
+ return typeof metadata.sourceRoot === "string"
21
+ ? metadata.sourceRoot
22
+ : path.join(root, "src");
23
+ }
24
+ /**
25
+ * Application options with the mirror's replacements and the stylesheet added. The entry files
26
+ * (`browser`, `server`) are not replaced: Angular type-checks only the files its tsconfig lists, and
27
+ * an entry it reads from a replacement it would bundle unchecked. An entry holds no styles in an
28
+ * Angular app; the components it imports are replaced.
29
+ */
30
+ export function withMirror(options, mirror) {
31
+ const entries = new Set([options.browser, options.server].map((entry) => path.normalize(String(entry))));
32
+ return {
33
+ ...options,
34
+ fileReplacements: [
35
+ ...(options.fileReplacements ?? []),
36
+ ...mirror.replacements.filter((replacement) => !entries.has(path.normalize(replacement.replace))),
37
+ ],
38
+ styles: [...(options.styles ?? []), STYLESHEET],
39
+ };
40
+ }
41
+ /** Prints the mirror's warnings and errors through the builder's logger (infos stay quiet). */
42
+ function report(context, mirror) {
43
+ for (const message of mirror.messages())
44
+ if (message.startsWith("error"))
45
+ context.logger.error(message);
46
+ else if (message.startsWith("warning"))
47
+ context.logger.warn(message);
48
+ }
49
+ export async function* application(options, context) {
50
+ const sourceRoot = await sourceRootOf(context);
51
+ const mirror = await createMirror(context.workspaceRoot, sourceRoot, {
52
+ minify: options.optimization !== false,
53
+ });
54
+ report(context, mirror);
55
+ const watcher = options.watch
56
+ ? watchMirror(context.workspaceRoot, sourceRoot, mirror, () => report(context, mirror))
57
+ : undefined;
58
+ try {
59
+ yield* buildApplication(withMirror(options, mirror), context);
60
+ }
61
+ finally {
62
+ watcher?.close();
63
+ }
64
+ }
65
+ export async function* devServer(options, context) {
66
+ const sourceRoot = await sourceRootOf(context);
67
+ const mirror = await createMirror(context.workspaceRoot, sourceRoot);
68
+ report(context, mirror);
69
+ const watcher = watchMirror(context.workspaceRoot, sourceRoot, mirror, () => report(context, mirror));
70
+ // Angular reads the build target's options through the context; these carry the mirror.
71
+ const getTargetOptions = context.getTargetOptions.bind(context);
72
+ const wrapped = Object.assign(Object.create(context), {
73
+ getTargetOptions: async (target) => withMirror(await getTargetOptions(target), mirror),
74
+ });
75
+ try {
76
+ yield* executeDevServerBuilder(options, wrapped);
77
+ }
78
+ finally {
79
+ watcher.close();
80
+ }
81
+ }
82
+ // Typed as Architect's `Builder`: the inferred type names `@angular-devkit/core`'s `JsonObject`,
83
+ // which this package does not depend on, so a declaration file could not spell it.
84
+ export const applicationBuilder = createBuilder(application);
85
+ export const devServerBuilder = createBuilder(devServer);
@@ -0,0 +1,3 @@
1
+ import { devServerBuilder } from "./builders.ts";
2
+ /** `@kirana-style/angular:dev-server`, loaded by the Angular CLI from builders.json. */
3
+ export default devServerBuilder;
@@ -0,0 +1,3 @@
1
+ import { devServerBuilder } from "./builders.js";
2
+ /** `@kirana-style/angular:dev-server`, loaded by the Angular CLI from builders.json. */
3
+ export default devServerBuilder;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `[sx]` for Angular templates (D-068). A template reaches only its component's members, so `sx`
3
+ * cannot be compiled away there as it is in JSX and in Vue, Svelte or Astro templates: this
4
+ * directive merges the handles at runtime with Kirana's runtime instead, and keeps the element's own
5
+ * classes and styles.
6
+ *
7
+ * The class carries Angular's partial declarations (`ɵɵngDeclareFactory`, `ɵɵngDeclareDirective`),
8
+ * the form `ngc` emits for a library and Angular's linker compiles in every app build. They are
9
+ * written here rather than generated because this workspace's TypeScript (7) has no JavaScript API
10
+ * for `ngc` to run on; the Analog and Angular CLI examples build them in CI. The static types are the
11
+ * ones `ngc` would declare, so templates type-check `[sx]` against `Sx`. The file sits under
12
+ * `fesm2022/`, the Angular Package Format's folder, because Analog links only files on such a path.
13
+ */
14
+ import * as i0 from "@angular/core";
15
+ import type { Sx } from "@kirana-style/core";
16
+ import type { Handle } from "@kirana-style/core/internal";
17
+ /** The class-name prefix (`names.prefix`); `k` unless the app provides another (`provideKirana`). */
18
+ export declare const KIRANA_PREFIX: i0.InjectionToken<string>;
19
+ /** Providers for an app whose `kirana.config` sets another class-name prefix. */
20
+ export declare function provideKirana(options: {
21
+ prefix: string;
22
+ }): i0.Provider[];
23
+ export declare class SxDirective implements i0.OnChanges {
24
+ #private;
25
+ /**
26
+ * The handles: one, or an array with falsy guards (`[styles.card, active && styles.on]`). Compiled
27
+ * handles are class strings (the Angular CLI builders type-check the compiled copy), so both are
28
+ * accepted.
29
+ */
30
+ sx: Sx | Handle;
31
+ ngOnChanges(): void;
32
+ static ɵfac: i0.ɵɵFactoryDeclaration<SxDirective, never>;
33
+ static ɵdir: i0.ɵɵDirectiveDeclaration<SxDirective, "[sx]", never, {
34
+ sx: {
35
+ alias: "sx";
36
+ required: false;
37
+ };
38
+ }, {}, never, never, true, never>;
39
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `[sx]` for Angular templates (D-068). A template reaches only its component's members, so `sx`
3
+ * cannot be compiled away there as it is in JSX and in Vue, Svelte or Astro templates: this
4
+ * directive merges the handles at runtime with Kirana's runtime instead, and keeps the element's own
5
+ * classes and styles.
6
+ *
7
+ * The class carries Angular's partial declarations (`ɵɵngDeclareFactory`, `ɵɵngDeclareDirective`),
8
+ * the form `ngc` emits for a library and Angular's linker compiles in every app build. They are
9
+ * written here rather than generated because this workspace's TypeScript (7) has no JavaScript API
10
+ * for `ngc` to run on; the Analog and Angular CLI examples build them in CI. The static types are the
11
+ * ones `ngc` would declare, so templates type-check `[sx]` against `Sx`. The file sits under
12
+ * `fesm2022/`, the Angular Package Format's folder, because Analog links only files on such a path.
13
+ */
14
+ import * as i0 from "@angular/core";
15
+ import { applySx } from "../sx.js";
16
+ /** The class-name prefix (`names.prefix`); `k` unless the app provides another (`provideKirana`). */
17
+ export const KIRANA_PREFIX = new i0.InjectionToken("kirana prefix", {
18
+ providedIn: "root",
19
+ factory: () => "k",
20
+ });
21
+ /** Providers for an app whose `kirana.config` sets another class-name prefix. */
22
+ export function provideKirana(options) {
23
+ return [{ provide: KIRANA_PREFIX, useValue: options.prefix }];
24
+ }
25
+ export class SxDirective {
26
+ /**
27
+ * The handles: one, or an array with falsy guards (`[styles.card, active && styles.on]`). Compiled
28
+ * handles are class strings (the Angular CLI builders type-check the compiled copy), so both are
29
+ * accepted.
30
+ */
31
+ sx;
32
+ #element = i0.inject(i0.ElementRef).nativeElement;
33
+ #renderer = i0.inject(i0.Renderer2);
34
+ #prefix = i0.inject(KIRANA_PREFIX);
35
+ #applied = { classes: new Set(), styles: new Set() };
36
+ ngOnChanges() {
37
+ this.#applied = applySx(this.#renderer, this.#element, this.#applied, this.sx, this.#prefix);
38
+ }
39
+ static ɵfac = i0.ɵɵngDeclareFactory({
40
+ minVersion: "12.0.0",
41
+ // A literal, as the linker reads it: the Angular version these were written against.
42
+ version: "22.2.1",
43
+ ngImport: i0,
44
+ type: SxDirective,
45
+ deps: [],
46
+ target: i0.ɵɵFactoryTarget.Directive,
47
+ });
48
+ static ɵdir = i0.ɵɵngDeclareDirective({
49
+ minVersion: "14.0.0",
50
+ // A literal, as the linker reads it: the Angular version these were written against.
51
+ version: "22.2.1",
52
+ type: SxDirective,
53
+ isStandalone: true,
54
+ selector: "[sx]",
55
+ inputs: { sx: "sx" },
56
+ usesOnChanges: true,
57
+ ngImport: i0,
58
+ });
59
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { KIRANA_PREFIX, SxDirective, provideKirana, } from "./fesm2022/directive.ts";
2
+ export { applySx, runtimeFor } from "./sx.ts";
3
+ export type { Applied, Renderer } from "./sx.ts";
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { KIRANA_PREFIX, SxDirective, provideKirana, } from "./fesm2022/directive.js";
2
+ export { applySx, runtimeFor } from "./sx.js";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Kirana for the Angular CLI (D-068, `design-notes/angular.md`). Angular's application builder
3
+ * compiles TypeScript from its own program and offers no hook to change a source first, but it
4
+ * honours `fileReplacements`, by redirecting module resolution to the replacement file. So the
5
+ * builders keep a mirror of the source folder in `.kirana/`: every file copied, the ones that hold
6
+ * styles compiled by Kirana. A replacement's relative imports and `templateUrl`s then resolve inside
7
+ * the mirror, where every file exists. Every TypeScript file is replaced, the stylesheet is written
8
+ * beside the mirror, and in watch mode each change is mirrored again — Angular, which watches the
9
+ * replacements, rebuilds from there.
10
+ */
11
+ import { watch as fsWatch } from "node:fs";
12
+ /** Where the mirror lives, under the workspace root. */
13
+ export declare const MIRROR = ".kirana";
14
+ /** The stylesheet the builders add to `styles`. */
15
+ export declare const STYLESHEET: string;
16
+ /** Every file under `directory`, relative to it, without installed packages or hidden folders. */
17
+ export declare function walk(directory: string, prefix?: string): string[];
18
+ export type Mirror = {
19
+ /** `fileReplacements` for every TypeScript file of the source folder. */
20
+ replacements: {
21
+ replace: string;
22
+ with: string;
23
+ }[];
24
+ /** Mirrors one changed or deleted file (and what read from it); returns whether anything changed. */
25
+ update: (file: string) => boolean;
26
+ /** Diagnostics from the last pass, formatted. */
27
+ messages: () => string[];
28
+ };
29
+ /** Builds the mirror of `sourceRoot` (relative to `root`) and writes the stylesheet. */
30
+ export declare function createMirror(root: string, sourceRoot: string, options?: {
31
+ minify?: boolean;
32
+ }): Promise<Mirror>;
33
+ /** Mirrors every change under the source folder until closed (watch builds and the dev server). */
34
+ export declare function watchMirror(root: string, sourceRoot: string, mirror: Mirror, onUpdate: () => void, watch?: typeof fsWatch): {
35
+ close: () => void;
36
+ };
package/src/mirror.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Kirana for the Angular CLI (D-068, `design-notes/angular.md`). Angular's application builder
3
+ * compiles TypeScript from its own program and offers no hook to change a source first, but it
4
+ * honours `fileReplacements`, by redirecting module resolution to the replacement file. So the
5
+ * builders keep a mirror of the source folder in `.kirana/`: every file copied, the ones that hold
6
+ * styles compiled by Kirana. A replacement's relative imports and `templateUrl`s then resolve inside
7
+ * the mirror, where every file exists. Every TypeScript file is replaced, the stylesheet is written
8
+ * beside the mirror, and in watch mode each change is mirrored again — Angular, which watches the
9
+ * replacements, rebuilds from there.
10
+ */
11
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, watch as fsWatch, writeFileSync, } from "node:fs";
12
+ import path from "node:path";
13
+ import { Driver, formatDiagnostic } from "@kirana-style/compiler";
14
+ import { SKIPPED, shouldCompile } from "@kirana-style/compiler/host";
15
+ /** Where the mirror lives, under the workspace root. */
16
+ export const MIRROR = ".kirana";
17
+ /** The stylesheet the builders add to `styles`. */
18
+ export const STYLESHEET = path.join(MIRROR, "kirana.css");
19
+ /** Every file under `directory`, relative to it, without installed packages or hidden folders. */
20
+ export function walk(directory, prefix = "") {
21
+ const out = [];
22
+ for (const entry of readdirSync(path.join(directory, prefix), {
23
+ withFileTypes: true,
24
+ })) {
25
+ const relative = path.join(prefix, entry.name);
26
+ if (entry.isDirectory()) {
27
+ if (!SKIPPED.has(entry.name) && !entry.name.startsWith("."))
28
+ out.push(...walk(directory, relative));
29
+ }
30
+ else
31
+ out.push(relative);
32
+ }
33
+ return out;
34
+ }
35
+ const SCRIPT = /\.[cm]?tsx?$/;
36
+ /** Builds the mirror of `sourceRoot` (relative to `root`) and writes the stylesheet. */
37
+ export async function createMirror(root, sourceRoot, options = {}) {
38
+ const driver = await Driver.create({ root });
39
+ const source = path.join(root, sourceRoot);
40
+ const target = path.join(root, MIRROR, sourceRoot);
41
+ const reported = [];
42
+ const texts = new Map();
43
+ const mirror = (relative) => {
44
+ const from = path.join(source, relative);
45
+ const to = path.join(target, relative);
46
+ if (!existsSync(from)) {
47
+ driver.remove(from);
48
+ rmSync(to, { force: true });
49
+ return;
50
+ }
51
+ mkdirSync(path.dirname(to), { recursive: true });
52
+ const text = SCRIPT.test(relative) ? readFileSync(from, "utf8") : undefined;
53
+ if (text === undefined || !shouldCompile(from, text, driver.libraries)) {
54
+ copyFileSync(from, to);
55
+ return;
56
+ }
57
+ const result = driver.transform(from, text);
58
+ texts.set(path.relative(root, from).replaceAll("\\", "/"), text);
59
+ reported.push(...result.diagnostics);
60
+ writeFileSync(to, result.code ?? text);
61
+ };
62
+ const stylesheet = () => {
63
+ reported.push(...driver.diagnostics());
64
+ const css = driver.stylesheet(options.minify ?? false);
65
+ const file = path.join(root, STYLESHEET);
66
+ const changed = !existsSync(file) || readFileSync(file, "utf8") !== css;
67
+ if (changed)
68
+ writeFileSync(file, css);
69
+ return changed;
70
+ };
71
+ rmSync(target, { recursive: true, force: true });
72
+ const files = walk(source);
73
+ for (const relative of files)
74
+ mirror(relative);
75
+ stylesheet();
76
+ return {
77
+ replacements: files
78
+ .filter((relative) => SCRIPT.test(relative) && !relative.endsWith(".d.ts"))
79
+ .map((relative) => ({
80
+ replace: path.join(sourceRoot, relative),
81
+ with: path.join(MIRROR, sourceRoot, relative),
82
+ })),
83
+ update(file) {
84
+ reported.length = 0;
85
+ const absolute = path.resolve(root, file);
86
+ // The changed file, and every module that read a value from it.
87
+ const affected = [absolute, ...driver.invalidate(absolute)];
88
+ for (const changed of affected)
89
+ mirror(path.relative(source, changed));
90
+ return stylesheet();
91
+ },
92
+ messages: () => reported.map((diagnostic) => formatDiagnostic(diagnostic, texts.get(diagnostic.file))),
93
+ };
94
+ }
95
+ /** Mirrors every change under the source folder until closed (watch builds and the dev server). */
96
+ export function watchMirror(root, sourceRoot, mirror, onUpdate, watch = fsWatch) {
97
+ return watch(path.join(root, sourceRoot), { recursive: true }, (_, name) => {
98
+ const relative = String(name);
99
+ // Editors' swap files and tools' folders are not sources.
100
+ if (relative
101
+ .split(/[\\/]/)
102
+ .some((part) => part.startsWith(".") || SKIPPED.has(part)))
103
+ return;
104
+ mirror.update(path.join(sourceRoot, relative));
105
+ onUpdate();
106
+ });
107
+ }
package/src/sx.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What `[sx]` does to an element, without Angular: merge the handles with Kirana's runtime (the same
3
+ * one compiled modules call), then bring the element's classes and custom properties from what the
4
+ * last value applied to what this one applies. Only what `[sx]` added is ever removed, so the
5
+ * element's own `class` and `style` stay.
6
+ */
7
+ import { runtime } from "@kirana-style/core/internal";
8
+ /** The part of Angular's `Renderer2` this needs. */
9
+ export type Renderer = {
10
+ addClass: (element: unknown, name: string) => void;
11
+ removeClass: (element: unknown, name: string) => void;
12
+ setStyle: (element: unknown, name: string, value: string, flags?: number) => void;
13
+ removeStyle: (element: unknown, name: string, flags?: number) => void;
14
+ };
15
+ /** What the last value applied: its classes and its custom properties. */
16
+ export type Applied = {
17
+ classes: Set<string>;
18
+ styles: Set<string>;
19
+ };
20
+ /** `RendererStyleFlags2.DashCase`: the name is a custom property, set as written. */
21
+ export declare const DASH_CASE = 2;
22
+ /** One runtime per class-name prefix, as each compiled module creates one. */
23
+ export declare function runtimeFor(prefix: string): ReturnType<typeof runtime>;
24
+ /** Applies `value` (a handle, or an array of handles and falsy guards); returns what is applied now. */
25
+ export declare function applySx(renderer: Renderer, element: unknown, previous: Applied, value: unknown, prefix: string): Applied;
package/src/sx.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What `[sx]` does to an element, without Angular: merge the handles with Kirana's runtime (the same
3
+ * one compiled modules call), then bring the element's classes and custom properties from what the
4
+ * last value applied to what this one applies. Only what `[sx]` added is ever removed, so the
5
+ * element's own `class` and `style` stay.
6
+ */
7
+ import { runtime } from "@kirana-style/core/internal";
8
+ /** `RendererStyleFlags2.DashCase`: the name is a custom property, set as written. */
9
+ export const DASH_CASE = 2;
10
+ const runtimes = new Map();
11
+ /** One runtime per class-name prefix, as each compiled module creates one. */
12
+ export function runtimeFor(prefix) {
13
+ let created = runtimes.get(prefix);
14
+ if (!created) {
15
+ created = runtime(prefix, "class");
16
+ runtimes.set(prefix, created);
17
+ }
18
+ return created;
19
+ }
20
+ /** Applies `value` (a handle, or an array of handles and falsy guards); returns what is applied now. */
21
+ export function applySx(renderer, element, previous, value, prefix) {
22
+ const handles = (Array.isArray(value) ? value : [value]);
23
+ const props = runtimeFor(prefix).sx(handles, [], []);
24
+ const classes = new Set((props.class ?? "").split(" ").filter(Boolean));
25
+ const styles = props.style ?? {};
26
+ for (const name of previous.classes)
27
+ if (!classes.has(name))
28
+ renderer.removeClass(element, name);
29
+ for (const name of classes)
30
+ if (!previous.classes.has(name))
31
+ renderer.addClass(element, name);
32
+ for (const name of previous.styles)
33
+ if (!(name in styles))
34
+ renderer.removeStyle(element, name, DASH_CASE);
35
+ for (const [name, setting] of Object.entries(styles))
36
+ renderer.setStyle(element, name, setting, DASH_CASE);
37
+ return { classes, styles: new Set(Object.keys(styles)) };
38
+ }
package/src/vite.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { KiranaViteOptions } from "@kirana-style/vite";
2
+ import type { Plugin } from "vite";
3
+ export type { KiranaViteOptions as KiranaAngularOptions };
4
+ export declare function kiranaAngular(options?: KiranaViteOptions): Plugin[];
5
+ export default kiranaAngular;
package/src/vite.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Kirana for Analog, Angular on Vite (D-068). Angular compiles each TypeScript file from its own
3
+ * program on disk, so a plugin that ran first would change nothing it reads. This is Kirana's Vite
4
+ * plugin run after Angular's instead: it compiles the JavaScript Angular emitted for each module,
5
+ * where `css()` calls and their imports are as written. The stylesheet and its hot updates work as
6
+ * they do in every Vite app.
7
+ *
8
+ * plugins: [analog(), kiranaAngular()]
9
+ */
10
+ import kirana from "@kirana-style/vite";
11
+ export function kiranaAngular(options = {}) {
12
+ const [main, late] = kirana(options);
13
+ return [{ ...main, enforce: "post" }, late];
14
+ }
15
+ export default kiranaAngular;