@aihu/css-engine 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/LICENSE +21 -0
- package/README.md +122 -0
- package/crates/aihu-css-core/Cargo.toml +22 -0
- package/crates/aihu-css-core/src/ast.rs +173 -0
- package/crates/aihu-css-core/src/bin/main.rs +73 -0
- package/crates/aihu-css-core/src/cache.rs +182 -0
- package/crates/aihu-css-core/src/emit.rs +236 -0
- package/crates/aihu-css-core/src/features/anchor.rs +41 -0
- package/crates/aihu-css-core/src/features/mod.rs +33 -0
- package/crates/aihu-css-core/src/features/popover.rs +40 -0
- package/crates/aihu-css-core/src/features/text_balance.rs +36 -0
- package/crates/aihu-css-core/src/features/view_transition.rs +38 -0
- package/crates/aihu-css-core/src/lib.rs +67 -0
- package/crates/aihu-css-core/src/progressive.rs +200 -0
- package/crates/aihu-css-core/src/scanner.rs +235 -0
- package/crates/aihu-css-core/src/theme.rs +179 -0
- package/crates/aihu-css-core/src/tokens.rs +470 -0
- package/crates/aihu-css-core/src/variants.rs +124 -0
- package/crates/aihu-css-core/tests/cache.rs +71 -0
- package/crates/aihu-css-core/tests/emit.rs +148 -0
- package/crates/aihu-css-core/tests/fixtures/button.ast.json +19 -0
- package/crates/aihu-css-core/tests/progressive_snapshot.rs +102 -0
- package/crates/aihu-css-core/tests/scanner.rs +99 -0
- package/crates/aihu-css-core/tests/scoped_snapshot.rs +73 -0
- package/crates/aihu-css-core/tests/snapshot.rs +24 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__anchor_snapshot.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__popover_snapshot.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__text_balance_snapshot.snap +23 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__view_transition_snapshot.snap +25 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__flat_output_for_class_list.snap +6 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_output_for_sfc.snap +25 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_authored_style_block.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_global_style_block.snap +24 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__standard_variants.snap +33 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__theme_default_vs_override.snap +45 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__wc_native_variants.snap +28 -0
- package/crates/aihu-css-core/tests/snapshots/snapshot__compiles_basic_class.snap +5 -0
- package/crates/aihu-css-core/tests/snapshots/snapshot__compiles_multiple_classes.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__arbitrary_values.snap +9 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_borders.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_colors.snap +10 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_effects.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_layout.snap +12 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_spacing.snap +11 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_typography.snap +11 -0
- package/crates/aihu-css-core/tests/tokens.rs +79 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/cn.d.ts +14 -0
- package/dist/runtime/cn.d.ts.map +1 -0
- package/dist/runtime/cn.js +107 -0
- package/dist/runtime/cn.js.map +1 -0
- package/dist/runtime/progressive.d.ts +54 -0
- package/dist/runtime/progressive.d.ts.map +1 -0
- package/dist/runtime/progressive.js +132 -0
- package/dist/runtime/progressive.js.map +1 -0
- package/package.json +54 -0
- package/styles/aihu-default.css +73 -0
- package/styles/aihu-graphite.css +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fellwork
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @aihu/css-engine
|
|
2
|
+
|
|
3
|
+
> **Aihu** — agentic discovery and interaction, for human purpose.
|
|
4
|
+
|
|
5
|
+
aihu CSS engine — Tailwind v4 hard fork with WC-native scoped output.
|
|
6
|
+
|
|
7
|
+
Part of the **compiler + toolchain** layer of Aihu. Build-time only — does not ship to the client. The compiler reads `.aihu` SFC source (per the [Block Structure spec](../../docs/superpowers/specs/2026-05-02-spec-block-structure.md)) and emits standards-compliant Web Components.
|
|
8
|
+
|
|
9
|
+
<!-- BEGIN_HANDWRITTEN: prose -->
|
|
10
|
+
> aihu CSS engine — a hard fork of Tailwind v4 with Web-Component-native scoped output, AST-aware scanning, and progressive feature emission.
|
|
11
|
+
|
|
12
|
+
**Status:** v0 — bootstrap. The fork's identity, perf optimizations, and scoped emitter are all under construction. See [`docs/superpowers/specs/2026-05-10-aihu-css-engine-and-primitives-design.md`](../../docs/superpowers/specs/2026-05-10-aihu-css-engine-and-primitives-design.md) for the full design.
|
|
13
|
+
|
|
14
|
+
### Status by capability (Plan 1 bootstrap)
|
|
15
|
+
|
|
16
|
+
| Capability | Plan that lands it |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Package builds; compile pipeline scaffolded | **Plan 1 (this one)** |
|
|
19
|
+
| AST scanner consuming `@aihu/compiler` | Plan 2 |
|
|
20
|
+
| Scoped-output mode (`:host` embedding) | Plan 2 |
|
|
21
|
+
| WC-native variants (`host:`, `slotted:`, `part-*:`) | Plan 2 |
|
|
22
|
+
| Progressive features (`view-transition:`, `anchor:`, etc.) | Plan 3 |
|
|
23
|
+
| Style packs (`aihu-default`, `aihu-graphite`) | Plan 3 |
|
|
24
|
+
| `cn()` runtime helper | Plan 3 |
|
|
25
|
+
|
|
26
|
+
### Local development
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Build Rust core (run from repo root or this dir)
|
|
30
|
+
cargo build --release -p aihu-css-core
|
|
31
|
+
|
|
32
|
+
# Build TS layer
|
|
33
|
+
bun run build
|
|
34
|
+
|
|
35
|
+
# Run tests
|
|
36
|
+
bun run test # vitest e2e
|
|
37
|
+
bun run test:rust # cargo + insta snapshots
|
|
38
|
+
```
|
|
39
|
+
<!-- END_HANDWRITTEN: prose -->
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
<!-- BEGIN_AUTOGEN: install -->
|
|
44
|
+
<!-- regenerate: bun scripts/sync-readme.ts (also runs in pre-commit + CI) -->
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install @aihu/css-engine
|
|
48
|
+
# or
|
|
49
|
+
bun add @aihu/css-engine
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.1.0`.</i></sub>
|
|
53
|
+
|
|
54
|
+
<!-- END_AUTOGEN: install -->
|
|
55
|
+
|
|
56
|
+
## Package facts
|
|
57
|
+
|
|
58
|
+
<!-- BEGIN_AUTOGEN: stats -->
|
|
59
|
+
<!-- regenerate: bun scripts/sync-readme.ts (also runs in pre-commit + CI) -->
|
|
60
|
+
|
|
61
|
+
| | |
|
|
62
|
+
|---|---|
|
|
63
|
+
| **Version** | `0.1.0` |
|
|
64
|
+
| **Tier** | D — Compiler — CSS engine (Tailwind v4 hard fork, WC-native scoped output) |
|
|
65
|
+
| **Published files** | 5 entries |
|
|
66
|
+
| **License** | MIT |
|
|
67
|
+
|
|
68
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.1.0`.</i></sub>
|
|
69
|
+
|
|
70
|
+
<!-- END_AUTOGEN: stats -->
|
|
71
|
+
|
|
72
|
+
## Exports
|
|
73
|
+
|
|
74
|
+
<!-- BEGIN_AUTOGEN: exports -->
|
|
75
|
+
<!-- regenerate: bun scripts/sync-readme.ts (also runs in pre-commit + CI) -->
|
|
76
|
+
|
|
77
|
+
| Subpath | ESM | CJS |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `.` | `./dist/index.js` | `—` |
|
|
80
|
+
| `./runtime/cn` | `./dist/runtime/cn.js` | `—` |
|
|
81
|
+
| `./runtime/progressive` | `./dist/runtime/progressive.js` | `—` |
|
|
82
|
+
|
|
83
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.1.0`.</i></sub>
|
|
84
|
+
|
|
85
|
+
<!-- END_AUTOGEN: exports -->
|
|
86
|
+
|
|
87
|
+
## Dependencies
|
|
88
|
+
|
|
89
|
+
<!-- BEGIN_AUTOGEN: deps -->
|
|
90
|
+
<!-- regenerate: bun scripts/sync-readme.ts (also runs in pre-commit + CI) -->
|
|
91
|
+
|
|
92
|
+
**Dependencies:**
|
|
93
|
+
|
|
94
|
+
- `@aihu/compiler` — `workspace:*`
|
|
95
|
+
|
|
96
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.1.0`.</i></sub>
|
|
97
|
+
|
|
98
|
+
<!-- END_AUTOGEN: deps -->
|
|
99
|
+
|
|
100
|
+
## See also
|
|
101
|
+
|
|
102
|
+
<!-- BEGIN_AUTOGEN: see-also -->
|
|
103
|
+
<!-- regenerate: bun scripts/sync-readme.ts (also runs in pre-commit + CI) -->
|
|
104
|
+
|
|
105
|
+
- [CSS Engine + Primitives design spec](../../docs/superpowers/specs/2026-05-10-aihu-css-engine-and-primitives-design.md)
|
|
106
|
+
- [@aihu/compiler](../compiler)
|
|
107
|
+
- [Aihu framework root](../../README.md)
|
|
108
|
+
|
|
109
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.1.0`.</i></sub>
|
|
110
|
+
|
|
111
|
+
<!-- END_AUTOGEN: see-also -->
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
<!-- BEGIN_AUTOGEN: license -->
|
|
116
|
+
<!-- regenerate: bun scripts/sync-readme.ts (also runs in pre-commit + CI) -->
|
|
117
|
+
|
|
118
|
+
MIT — see [LICENSE](../../LICENSE).
|
|
119
|
+
|
|
120
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.1.0`.</i></sub>
|
|
121
|
+
|
|
122
|
+
<!-- END_AUTOGEN: license -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "aihu-css-core"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
repository.workspace = true
|
|
7
|
+
description = "aihu CSS engine core — utility compilation and CSS emission."
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
crate-type = ["cdylib", "rlib"]
|
|
11
|
+
path = "src/lib.rs"
|
|
12
|
+
|
|
13
|
+
[[bin]]
|
|
14
|
+
name = "aihu-css-compile"
|
|
15
|
+
path = "src/bin/main.rs"
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
serde = { version = "1", features = ["derive"] }
|
|
19
|
+
serde_json = "1"
|
|
20
|
+
|
|
21
|
+
[dev-dependencies]
|
|
22
|
+
insta = { version = "1", features = ["yaml"] }
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
//! `ast.rs` — serde `Deserialize` mirror of the compiler's `--ast-json` output.
|
|
2
|
+
//!
|
|
3
|
+
//! This is the wire-format contract the scanner consumes. It mirrors the
|
|
4
|
+
//! `SfcAst` shape emitted by `@aihu/compiler`'s `compile_to_ast` /
|
|
5
|
+
//! `aihu-compile --ast-json` (see `docs/superpowers/specs/compiler-ast-export-hook.md`
|
|
6
|
+
//! §4.1 / §4.3, CSS-engine spec `22d3a66e` §3 edge #1).
|
|
7
|
+
//!
|
|
8
|
+
//! The three `SfcAttr` variants (Static / Binding / Macro) are frozen as part
|
|
9
|
+
//! of the v1.0 stability contract — the scanner's class-extraction correctness
|
|
10
|
+
//! depends entirely on them staying distinct. `ast_version` is asserted on
|
|
11
|
+
//! entry (§6 Q3 evolution policy): additive changes keep `1`; a breaking shape
|
|
12
|
+
//! change bumps it and is rejected here with a clear error.
|
|
13
|
+
|
|
14
|
+
use serde::Deserialize;
|
|
15
|
+
|
|
16
|
+
/// The AST schema version this scanner understands. A mismatch is rejected by
|
|
17
|
+
/// [`parse_ast`].
|
|
18
|
+
pub const SUPPORTED_AST_VERSION: u32 = 1;
|
|
19
|
+
|
|
20
|
+
/// Top-level AST export — one per `.aihu` SFC. Mirrors the compiler's
|
|
21
|
+
/// `SfcAstOwned` / the TS `SfcAst` interface (spec §4.1).
|
|
22
|
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
23
|
+
pub struct SfcAst {
|
|
24
|
+
/// Resolved custom-element tag name (`meta.name` → `route.name` → file stem).
|
|
25
|
+
pub tag: String,
|
|
26
|
+
/// The `@style` block, if the SFC declared one.
|
|
27
|
+
#[serde(default)]
|
|
28
|
+
pub style: Option<SfcStyleBlock>,
|
|
29
|
+
/// Parsed template tree. `None` when the SFC has no `@template` block.
|
|
30
|
+
#[serde(default)]
|
|
31
|
+
pub template: Option<Vec<SfcNode>>,
|
|
32
|
+
/// SFC-level metadata.
|
|
33
|
+
pub meta: SfcMeta,
|
|
34
|
+
/// AST schema version — bumped on any breaking shape change.
|
|
35
|
+
#[serde(rename = "astVersion")]
|
|
36
|
+
pub ast_version: u32,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
40
|
+
pub struct SfcStyleBlock {
|
|
41
|
+
/// Verbatim CSS body of the `@style` block (braces stripped, `$global`
|
|
42
|
+
/// token removed by the compiler).
|
|
43
|
+
pub content: String,
|
|
44
|
+
/// `Scoped` (default) or `Global` (`@style { $global ... }`).
|
|
45
|
+
pub scope: SfcStyleScope,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
|
49
|
+
#[serde(rename_all = "lowercase")]
|
|
50
|
+
pub enum SfcStyleScope {
|
|
51
|
+
Scoped,
|
|
52
|
+
Global,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
56
|
+
pub struct SfcMeta {
|
|
57
|
+
/// From `@meta { name }` / `@route { name }` / file stem.
|
|
58
|
+
pub name: String,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Discriminated union mirroring the compiler's `SfcNodeOwned` (spec §4.1).
|
|
62
|
+
/// Serialized with a `kind` tag (camelCase: `element`, `macroElement`, …).
|
|
63
|
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
64
|
+
#[serde(tag = "kind", rename_all = "camelCase")]
|
|
65
|
+
pub enum SfcNode {
|
|
66
|
+
Element {
|
|
67
|
+
tag: String,
|
|
68
|
+
#[serde(default)]
|
|
69
|
+
attrs: Vec<SfcAttr>,
|
|
70
|
+
#[serde(default)]
|
|
71
|
+
children: Vec<SfcNode>,
|
|
72
|
+
},
|
|
73
|
+
MacroElement {
|
|
74
|
+
name: String,
|
|
75
|
+
#[serde(default)]
|
|
76
|
+
attrs: Vec<SfcAttr>,
|
|
77
|
+
#[serde(default)]
|
|
78
|
+
children: Vec<SfcNode>,
|
|
79
|
+
},
|
|
80
|
+
Text {
|
|
81
|
+
value: String,
|
|
82
|
+
},
|
|
83
|
+
Interpolation {
|
|
84
|
+
expr: String,
|
|
85
|
+
},
|
|
86
|
+
IfBlock {
|
|
87
|
+
#[serde(default)]
|
|
88
|
+
branches: Vec<SfcIfBranch>,
|
|
89
|
+
},
|
|
90
|
+
#[serde(rename_all = "camelCase")]
|
|
91
|
+
EachBlock {
|
|
92
|
+
list: String,
|
|
93
|
+
item: String,
|
|
94
|
+
idx: Option<String>,
|
|
95
|
+
key: Option<String>,
|
|
96
|
+
#[serde(default)]
|
|
97
|
+
body: Vec<SfcNode>,
|
|
98
|
+
empty_body: Option<Vec<SfcNode>>,
|
|
99
|
+
},
|
|
100
|
+
HtmlBlock {
|
|
101
|
+
expr: String,
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
106
|
+
pub struct SfcIfBranch {
|
|
107
|
+
pub cond: String,
|
|
108
|
+
#[serde(default)]
|
|
109
|
+
pub body: Vec<SfcNode>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Discriminated union mirroring the compiler's `SfcAttrOwned` `Attr` — the
|
|
113
|
+
/// three class-forms key on `kind` (lowercase: `static` / `binding` / `macro`).
|
|
114
|
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
115
|
+
#[serde(tag = "kind", rename_all = "lowercase")]
|
|
116
|
+
pub enum SfcAttr {
|
|
117
|
+
/// Form A — `class="btn primary"`.
|
|
118
|
+
Static { name: String, value: String },
|
|
119
|
+
/// Form B — `$class={expr}` (and array `$class={[a, b]}`).
|
|
120
|
+
Binding { name: String, expr: String },
|
|
121
|
+
/// Form C — `$class:active={cond}` (and `on:`/`bind:`/`emit:`/`if`/…).
|
|
122
|
+
Macro {
|
|
123
|
+
name: String,
|
|
124
|
+
#[allow(dead_code)]
|
|
125
|
+
value: SfcMacroValue,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
130
|
+
#[serde(tag = "form", rename_all = "lowercase")]
|
|
131
|
+
pub enum SfcMacroValue {
|
|
132
|
+
Quoted { value: String },
|
|
133
|
+
Curly { expr: String },
|
|
134
|
+
Boolean,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Parse a `--ast-json` payload into the typed [`SfcAst`], validating the
|
|
138
|
+
/// schema version (spec §6 Q3). A version mismatch returns a descriptive
|
|
139
|
+
/// error rather than silently mis-scanning a future shape.
|
|
140
|
+
pub fn parse_ast(json: &str) -> Result<SfcAst, AstError> {
|
|
141
|
+
let ast: SfcAst = serde_json::from_str(json).map_err(AstError::Deserialize)?;
|
|
142
|
+
if ast.ast_version != SUPPORTED_AST_VERSION {
|
|
143
|
+
return Err(AstError::Version {
|
|
144
|
+
found: ast.ast_version,
|
|
145
|
+
supported: SUPPORTED_AST_VERSION,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
Ok(ast)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Errors from [`parse_ast`].
|
|
152
|
+
#[derive(Debug)]
|
|
153
|
+
pub enum AstError {
|
|
154
|
+
/// JSON did not match the `SfcAst` wire shape.
|
|
155
|
+
Deserialize(serde_json::Error),
|
|
156
|
+
/// `astVersion` is not the version this scanner supports.
|
|
157
|
+
Version { found: u32, supported: u32 },
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
impl std::fmt::Display for AstError {
|
|
161
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
162
|
+
match self {
|
|
163
|
+
AstError::Deserialize(e) => write!(f, "failed to deserialize SfcAst: {e}"),
|
|
164
|
+
AstError::Version { found, supported } => write!(
|
|
165
|
+
f,
|
|
166
|
+
"unsupported astVersion {found} (this engine supports {supported}); \
|
|
167
|
+
rebuild @aihu/compiler and @aihu/css-engine to matching versions"
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
impl std::error::Error for AstError {}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//! aihu-css-compile — CLI entry for the engine.
|
|
2
|
+
//!
|
|
3
|
+
//! Two input modes:
|
|
4
|
+
//!
|
|
5
|
+
//! - **Class-list mode** (Plan 1, default): reads utility class names (one per
|
|
6
|
+
//! line / whitespace-separated) from stdin or a file argument, emits flat
|
|
7
|
+
//! `.class { … }` CSS to stdout.
|
|
8
|
+
//! - **AST mode** (Plan 2, `--ast-json`): reads an `SfcAst` JSON (the output of
|
|
9
|
+
//! `aihu-compile --ast-json`) from stdin, emits scoped shadow-DOM CSS.
|
|
10
|
+
|
|
11
|
+
use std::io::{self, Read, Write};
|
|
12
|
+
|
|
13
|
+
fn main() {
|
|
14
|
+
let argv: Vec<String> = std::env::args().collect();
|
|
15
|
+
|
|
16
|
+
// Build-time mode: dump the conflict-group map (Plan 3 Task 9). The TS build
|
|
17
|
+
// step (`scripts/gen-cn-conflict-map.ts`) reads this to generate the `cn()`
|
|
18
|
+
// conflict map, so the runtime merge map never drifts from the utility table.
|
|
19
|
+
if argv.iter().any(|a| a == "--dump-conflict-groups") {
|
|
20
|
+
let groups = aihu_css_core::tokens::conflict_groups();
|
|
21
|
+
let json = serde_json::to_string(&groups).expect("serialize conflict groups");
|
|
22
|
+
println!("{json}");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let ast_mode = argv.iter().any(|a| a == "--ast-json");
|
|
27
|
+
|
|
28
|
+
// Positional file arg = first non-flag argument after argv[0].
|
|
29
|
+
let file_arg = argv
|
|
30
|
+
.iter()
|
|
31
|
+
.skip(1)
|
|
32
|
+
.find(|a| !a.starts_with("--"))
|
|
33
|
+
.cloned();
|
|
34
|
+
|
|
35
|
+
let mut buf = String::new();
|
|
36
|
+
match &file_arg {
|
|
37
|
+
Some(path) => {
|
|
38
|
+
buf = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
|
39
|
+
eprintln!("aihu-css-compile: failed to read {path}: {e}");
|
|
40
|
+
std::process::exit(2);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
None => {
|
|
44
|
+
io::stdin()
|
|
45
|
+
.read_to_string(&mut buf)
|
|
46
|
+
.expect("failed reading stdin");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let css = if ast_mode {
|
|
51
|
+
match aihu_css_core::parse_ast(&buf) {
|
|
52
|
+
Ok(ast) => aihu_css_core::compile_sfc_scoped(&ast),
|
|
53
|
+
Err(e) => {
|
|
54
|
+
eprintln!("aihu-css-compile: {e}");
|
|
55
|
+
std::process::exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
let classes: Vec<String> = buf
|
|
60
|
+
.lines()
|
|
61
|
+
.flat_map(|line| line.split_whitespace())
|
|
62
|
+
.filter(|s| !s.is_empty())
|
|
63
|
+
.map(String::from)
|
|
64
|
+
.collect();
|
|
65
|
+
aihu_css_core::compile_classes(&classes)
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let stdout = io::stdout();
|
|
69
|
+
let mut handle = stdout.lock();
|
|
70
|
+
handle
|
|
71
|
+
.write_all(css.as_bytes())
|
|
72
|
+
.expect("failed writing stdout");
|
|
73
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//! `cache.rs` — AST-hashed per-SFC incremental compilation cache (Plan 2 Task 8).
|
|
2
|
+
//!
|
|
3
|
+
//! Keyed by a stable hash over `(SfcAst, ThemeRegistry version)`: a change to
|
|
4
|
+
//! either input invalidates the entry. The hash is a fast non-cryptographic
|
|
5
|
+
//! `DefaultHasher` — we want change detection, not security. The cache lives
|
|
6
|
+
//! in-process (the Vite plugin / `aihu css build` holds it across the dev
|
|
7
|
+
//! session) so an unchanged SFC recompiles in near-zero time (the `css-6`
|
|
8
|
+
//! perf gate later asserts < 30 ms across a 50-SFC fixture).
|
|
9
|
+
|
|
10
|
+
use std::collections::HashMap;
|
|
11
|
+
use std::hash::{Hash, Hasher};
|
|
12
|
+
|
|
13
|
+
use crate::ast::{SfcAst, SfcAttr, SfcNode, SfcStyleScope};
|
|
14
|
+
use crate::emit::emit_sfc_scoped;
|
|
15
|
+
|
|
16
|
+
/// An in-process compilation cache. Construct one per dev session / build run.
|
|
17
|
+
#[derive(Debug, Default)]
|
|
18
|
+
pub struct CssCache {
|
|
19
|
+
entries: HashMap<u64, String>,
|
|
20
|
+
/// Number of full recompiles performed — used by tests/benches to prove a
|
|
21
|
+
/// cache hit skipped the compile path.
|
|
22
|
+
recompiles: u64,
|
|
23
|
+
/// Number of cache hits served.
|
|
24
|
+
hits: u64,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl CssCache {
|
|
28
|
+
pub fn new() -> Self {
|
|
29
|
+
Self::default()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Compile an SFC, returning a cached result on an unchanged-input hit.
|
|
33
|
+
/// `theme_version` participates in the key so a theme change invalidates
|
|
34
|
+
/// every entry.
|
|
35
|
+
pub fn compile(&mut self, ast: &SfcAst, theme_version: u64) -> String {
|
|
36
|
+
let key = hash_ast(ast, theme_version);
|
|
37
|
+
if let Some(cached) = self.entries.get(&key) {
|
|
38
|
+
self.hits += 1;
|
|
39
|
+
return cached.clone();
|
|
40
|
+
}
|
|
41
|
+
self.recompiles += 1;
|
|
42
|
+
let css = emit_sfc_scoped(ast);
|
|
43
|
+
self.entries.insert(key, css.clone());
|
|
44
|
+
css
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Total full recompiles since construction.
|
|
48
|
+
pub fn recompiles(&self) -> u64 {
|
|
49
|
+
self.recompiles
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Total cache hits since construction.
|
|
53
|
+
pub fn hits(&self) -> u64 {
|
|
54
|
+
self.hits
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Number of distinct entries currently cached.
|
|
58
|
+
pub fn len(&self) -> usize {
|
|
59
|
+
self.entries.len()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn is_empty(&self) -> bool {
|
|
63
|
+
self.entries.is_empty()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Drop all cached entries (keeps the hit/recompile counters).
|
|
67
|
+
pub fn clear(&mut self) {
|
|
68
|
+
self.entries.clear();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Compute a stable change-detection hash over the AST + theme version.
|
|
73
|
+
pub fn hash_ast(ast: &SfcAst, theme_version: u64) -> u64 {
|
|
74
|
+
let mut h = std::collections::hash_map::DefaultHasher::new();
|
|
75
|
+
theme_version.hash(&mut h);
|
|
76
|
+
hash_sfc(ast, &mut h);
|
|
77
|
+
h.finish()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn hash_sfc(ast: &SfcAst, h: &mut impl Hasher) {
|
|
81
|
+
ast.tag.hash(h);
|
|
82
|
+
ast.ast_version.hash(h);
|
|
83
|
+
match &ast.style {
|
|
84
|
+
Some(s) => {
|
|
85
|
+
1u8.hash(h);
|
|
86
|
+
s.content.hash(h);
|
|
87
|
+
matches!(s.scope, SfcStyleScope::Global).hash(h);
|
|
88
|
+
}
|
|
89
|
+
None => 0u8.hash(h),
|
|
90
|
+
}
|
|
91
|
+
match &ast.template {
|
|
92
|
+
Some(nodes) => {
|
|
93
|
+
1u8.hash(h);
|
|
94
|
+
for n in nodes {
|
|
95
|
+
hash_node(n, h);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
None => 0u8.hash(h),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn hash_node(node: &SfcNode, h: &mut impl Hasher) {
|
|
103
|
+
match node {
|
|
104
|
+
SfcNode::Element { tag, attrs, children } => {
|
|
105
|
+
0u8.hash(h);
|
|
106
|
+
tag.hash(h);
|
|
107
|
+
for a in attrs {
|
|
108
|
+
hash_attr(a, h);
|
|
109
|
+
}
|
|
110
|
+
for c in children {
|
|
111
|
+
hash_node(c, h);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
SfcNode::MacroElement { name, attrs, children } => {
|
|
115
|
+
1u8.hash(h);
|
|
116
|
+
name.hash(h);
|
|
117
|
+
for a in attrs {
|
|
118
|
+
hash_attr(a, h);
|
|
119
|
+
}
|
|
120
|
+
for c in children {
|
|
121
|
+
hash_node(c, h);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
SfcNode::Text { value } => {
|
|
125
|
+
2u8.hash(h);
|
|
126
|
+
value.hash(h);
|
|
127
|
+
}
|
|
128
|
+
SfcNode::Interpolation { expr } => {
|
|
129
|
+
3u8.hash(h);
|
|
130
|
+
expr.hash(h);
|
|
131
|
+
}
|
|
132
|
+
SfcNode::IfBlock { branches } => {
|
|
133
|
+
4u8.hash(h);
|
|
134
|
+
for b in branches {
|
|
135
|
+
b.cond.hash(h);
|
|
136
|
+
for c in &b.body {
|
|
137
|
+
hash_node(c, h);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
SfcNode::EachBlock { list, item, idx, key, body, empty_body } => {
|
|
142
|
+
5u8.hash(h);
|
|
143
|
+
list.hash(h);
|
|
144
|
+
item.hash(h);
|
|
145
|
+
idx.hash(h);
|
|
146
|
+
key.hash(h);
|
|
147
|
+
for c in body {
|
|
148
|
+
hash_node(c, h);
|
|
149
|
+
}
|
|
150
|
+
if let Some(eb) = empty_body {
|
|
151
|
+
for c in eb {
|
|
152
|
+
hash_node(c, h);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
SfcNode::HtmlBlock { expr } => {
|
|
157
|
+
6u8.hash(h);
|
|
158
|
+
expr.hash(h);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn hash_attr(attr: &SfcAttr, h: &mut impl Hasher) {
|
|
164
|
+
// Only the class-bearing attrs affect CSS output, but hashing all attrs is
|
|
165
|
+
// cheap and keeps the key robust against future emit changes.
|
|
166
|
+
match attr {
|
|
167
|
+
SfcAttr::Static { name, value } => {
|
|
168
|
+
0u8.hash(h);
|
|
169
|
+
name.hash(h);
|
|
170
|
+
value.hash(h);
|
|
171
|
+
}
|
|
172
|
+
SfcAttr::Binding { name, expr } => {
|
|
173
|
+
1u8.hash(h);
|
|
174
|
+
name.hash(h);
|
|
175
|
+
expr.hash(h);
|
|
176
|
+
}
|
|
177
|
+
SfcAttr::Macro { name, .. } => {
|
|
178
|
+
2u8.hash(h);
|
|
179
|
+
name.hash(h);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|