diff --git a/Cargo.lock b/Cargo.lock index 0622f81..7998a70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -552,6 +561,16 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.90", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -769,6 +788,16 @@ dependencies = [ "toml 0.5.11", ] +[[package]] +name = "float-pigment-css-napi" +version = "0.9.0" +dependencies = [ + "float-pigment-css", + "napi", + "napi-build", + "napi-derive", +] + [[package]] name = "float-pigment-forest" version = "0.9.0" @@ -1316,6 +1345,64 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags 2.6.0", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", + "tokio", +] + +[[package]] +name = "napi-build" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn 2.0.90", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading 0.8.6", +] + [[package]] name = "ndk" version = "0.7.0" @@ -1541,6 +1628,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "piston" version = "0.53.2" @@ -1962,6 +2055,12 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.216" @@ -2204,6 +2303,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "pin-project-lite", +] + [[package]] name = "toml" version = "0.5.11" @@ -2282,6 +2390,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 3b749c5..56d3393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "float-pigment-mlp", "float-pigment-css-macro", "float-pigment-css", + "float-pigment-css-napi", "float-pigment-layout", "float-pigment-forest-macro", "float-pigment-forest", diff --git a/float-pigment-css-napi/.gitignore b/float-pigment-css-napi/.gitignore new file mode 100644 index 0000000..8b0e6d3 --- /dev/null +++ b/float-pigment-css-napi/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +target/ +# Ignore .node files in root (napi-rs build intermediates) +# but NOT in prebuilds/ (those are tracked as release artifacts) +/*.node diff --git a/float-pigment-css-napi/.npmignore b/float-pigment-css-napi/.npmignore new file mode 100644 index 0000000..3fefdad --- /dev/null +++ b/float-pigment-css-napi/.npmignore @@ -0,0 +1,6 @@ +target/ +src/ +build.rs +Cargo.toml +Cargo.lock +.cargo/ diff --git a/float-pigment-css-napi/Cargo.toml b/float-pigment-css-napi/Cargo.toml new file mode 100644 index 0000000..1b9c64f --- /dev/null +++ b/float-pigment-css-napi/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "float-pigment-css-napi" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Node.js N-API binding for float-pigment-css" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = { version = "2", features = ["async", "napi6"] } +napi-derive = "2" +float-pigment-css = { workspace = true, features = ["std", "serialize", "serialize_json"] } + +[build-dependencies] +napi-build = "2" + +[lints] +workspace = true diff --git a/float-pigment-css-napi/README.md b/float-pigment-css-napi/README.md new file mode 100644 index 0000000..7455840 --- /dev/null +++ b/float-pigment-css-napi/README.md @@ -0,0 +1,217 @@ +# float-pigment-css-napi + +Node.js N-API binding for [float-pigment-css](https://github.com/wechat-miniprogram/float-pigment), providing high-performance CSS compilation with binary/JSON serialization support. + +## Features + +- Pure Rust implementation via [napi-rs](https://napi.rs/) — no C++ intermediate layer +- ABI-stable N-API — works across Node.js and Electron versions without recompilation +- Async compilation via libuv thread pool (non-blocking) +- Sync compilation for simple use cases +- Output formats: `bincode` (binary, production), `json` (readable, debug), `none` (validation only) +- Cross-platform prebuilds: macOS (arm64/x64), Windows (x64/x86) + +## Installation + +Build from source (not yet published to npm): + +```bash +cd float-pigment-css-napi +npm install +npm run build +``` + +This builds all 4 supported platforms in one command: +- `darwin-arm64` (macOS Apple Silicon) +- `darwin-x64` (macOS Intel) +- `win32-x64` (Windows 64-bit) +- `win32-x32` (Windows 32-bit) + +After build, the directory structure is: + +``` +prebuilds/ +├── darwin-arm64/node.napi.node +├── darwin-x64/node.napi.node +├── win32-x32/node.napi.node +└── win32-x64/node.napi.node +index.js ← platform loader +type.d.ts ← TypeScript declarations +``` + +### Integrating into your project + +```bash +# Option 1: Copy artifacts directly +cp -r prebuilds index.js type.d.ts /path/to/your-project/deps/float-pigment-css-napi/ + +# Option 2: Use file: reference (suitable for monorepo) +# In your project's package.json: +# "dependencies": { +# "float-pigment-css-napi": "file:../float-pigment-css-napi" +# } +``` + +Then in your code: + +```javascript +// Option 1: copied into project +const { compile, compileSync } = require('./deps/float-pigment-css-napi') + +// Option 2: file: reference +const { compile, compileSync } = require('float-pigment-css-napi') +``` + +## Usage + +```javascript +const { compile, compileSync, compileSingle, compileSingleSync } = require('float-pigment-css-napi') + +// Batch compile multiple files (async) +const result = await compile({ + src: [ + { path: 'pages/index/index.wxss', content: Buffer.from('.container { display: flex; }') }, + { path: 'pages/home/home.wxss', content: Buffer.from('.title { color: red; }') }, + ], + outputType: 'bincode', + tagNamePrefix: 'wx-', // optional, default "wx-" +}) + +// result.files: [{ path, file: { content: Buffer, warnings: [] } }, ...] +// result.importIndex: Buffer + +// Batch compile (sync) +const syncResult = compileSync({ + src: [{ path: 'app.wxss', content: Buffer.from('page { margin: 0; }') }], + outputType: 'json', +}) + +// Single file compile (async) +const file = await compileSingle({ + fileName: 'components/button/button.wxss', + fileContent: Buffer.from('.btn { padding: 10px; }'), + outputType: 'bincode', +}) + +// file.content: Buffer +// file.warnings: [{ start: { line, column }, end: { line, column }, message }] + +// Single file compile (sync) +const fileSync = compileSingleSync({ + fileName: 'app.wxss', + fileContent: Buffer.from('body { font-size: 14px; }'), + outputType: 'none', // validation only, content will be null +}) +``` + +## API + +### `compile(args: CompileArgument): Promise` + +Compile multiple CSS files asynchronously (runs on libuv thread pool). + +### `compileSync(args: CompileArgument): CompileResult` + +Synchronous version of `compile`. + +### `compileSingle(args: CompileSingleArgument): Promise` + +Compile a single CSS file asynchronously. + +### `compileSingleSync(args: CompileSingleArgument): FileResult` + +Synchronous version of `compileSingle`. + +### Types + +```typescript +type OutputType = 'bincode' | 'json' | 'none' + +interface CompileArgument { + src: SourceEntry[] + outputType: OutputType + tagNamePrefix?: string // default: "wx-" +} + +interface SourceEntry { + path: string + content: Buffer +} + +interface CompileResult { + files: FileEntry[] + importIndex: Buffer | null +} + +interface FileEntry { + path: string + file: FileResult +} + +interface CompileSingleArgument { + fileName: string + fileContent: Buffer + outputType: OutputType + tagNamePrefix?: string // default: "wx-" +} + +interface FileResult { + content: Buffer | null + warnings: CompileWarning[] +} + +interface CompileWarning { + start: { line: number; column: number } + end: { line: number; column: number } + message: string +} +``` + +## Build + +### Prerequisites + +- Rust toolchain (1.92.0+) +- Node.js 16+ +- npm or pnpm +- Cross-compile dependencies (for Windows targets from macOS): + ```bash + rustup target add x86_64-pc-windows-msvc i686-pc-windows-msvc + cargo install cargo-xwin + ``` + +### Build scripts + +| Command | Description | +|---------|-------------| +| `npm run build` | Build all 4 platforms (darwin-arm64, darwin-x64, win32-x64, win32-x32) | +| `npm run build:current` | Build for current platform only | +| `npm run build:debug` | Build current platform in debug mode | +| `npm run build:target ` | Build for a specific Rust target triple | + +### Verify build + +```bash +# Check prebuilds structure +ls prebuilds/*/node.napi.node + +# Test loading +node -e "const m = require('./'); console.log(Object.keys(m))" +# Expected: [ 'compile', 'compileSync', 'compileSingle', 'compileSingleSync', ... ] +``` + +## Runtime Compatibility + +| Runtime | Supported | +|---------|-----------| +| Node.js 16+ | ✓ | +| Node.js 22 | ✓ | +| Electron (main process) | ✓ | +| Electron (utility process) | ✓ | +| NW.js | ✓ | + +N-API is ABI-stable — the same `.node` binary works across all compatible Node.js and Electron versions without recompilation. + +## License + +MIT diff --git a/float-pigment-css-napi/README_CN.md b/float-pigment-css-napi/README_CN.md new file mode 100644 index 0000000..83bea92 --- /dev/null +++ b/float-pigment-css-napi/README_CN.md @@ -0,0 +1,217 @@ +# float-pigment-css-napi + +[float-pigment-css](https://github.com/wechat-miniprogram/float-pigment) 的 Node.js N-API 绑定,提供高性能 CSS 编译与二进制/JSON 序列化支持。 + +## 特性 + +- 纯 Rust 实现,通过 [napi-rs](https://napi.rs/) 绑定 — 无 C++ 中间层 +- ABI 稳定的 N-API — 跨 Node.js 和 Electron 版本无需重新编译 +- 异步编译通过 libuv 线程池执行(不阻塞主线程) +- 同步编译用于简单场景 +- 输出格式:`bincode`(二进制,生产环境)、`json`(可读,调试用)、`none`(仅校验) +- 跨平台预编译:macOS (arm64/x64)、Windows (x64/x86) + +## 安装 + +从源码构建(尚未发布到 npm): + +```bash +cd float-pigment-css-napi +npm install +npm run build +``` + +一条命令编译全部 4 个平台: +- `darwin-arm64`(macOS Apple Silicon) +- `darwin-x64`(macOS Intel) +- `win32-x64`(Windows 64 位) +- `win32-x32`(Windows 32 位) + +构建后目录结构: + +``` +prebuilds/ +├── darwin-arm64/node.napi.node +├── darwin-x64/node.napi.node +├── win32-x32/node.napi.node +└── win32-x64/node.napi.node +index.js ← 平台加载器 +type.d.ts ← TypeScript 类型声明 +``` + +### 集成到你的项目 + +```bash +# 方式一:直接拷贝产物 +cp -r prebuilds index.js type.d.ts /path/to/your-project/deps/float-pigment-css-napi/ + +# 方式二:通过 file: 相对路径引用(适合 monorepo) +# 在项目 package.json 中: +# "dependencies": { +# "float-pigment-css-napi": "file:../float-pigment-css-napi" +# } +``` + +然后在代码中: + +```javascript +// 方式一:拷贝到项目内 +const { compile, compileSync } = require('./deps/float-pigment-css-napi') + +// 方式二:file: 引用 +const { compile, compileSync } = require('float-pigment-css-napi') +``` + +## 使用方法 + +```javascript +const { compile, compileSync, compileSingle, compileSingleSync } = require('float-pigment-css-napi') + +// 批量编译多个文件(异步) +const result = await compile({ + src: [ + { path: 'pages/index/index.wxss', content: Buffer.from('.container { display: flex; }') }, + { path: 'pages/home/home.wxss', content: Buffer.from('.title { color: red; }') }, + ], + outputType: 'bincode', + tagNamePrefix: 'wx-', // 可选,默认 "wx-" +}) + +// result.files: [{ path, file: { content: Buffer, warnings: [] } }, ...] +// result.importIndex: Buffer + +// 批量编译(同步) +const syncResult = compileSync({ + src: [{ path: 'app.wxss', content: Buffer.from('page { margin: 0; }') }], + outputType: 'json', +}) + +// 单文件编译(异步) +const file = await compileSingle({ + fileName: 'components/button/button.wxss', + fileContent: Buffer.from('.btn { padding: 10px; }'), + outputType: 'bincode', +}) + +// file.content: Buffer +// file.warnings: [{ start: { line, column }, end: { line, column }, message }] + +// 单文件编译(同步) +const fileSync = compileSingleSync({ + fileName: 'app.wxss', + fileContent: Buffer.from('body { font-size: 14px; }'), + outputType: 'none', // 仅校验,content 为 null +}) +``` + +## API + +### `compile(args: CompileArgument): Promise` + +异步批量编译多个 CSS 文件(在 libuv 线程池中执行)。 + +### `compileSync(args: CompileArgument): CompileResult` + +`compile` 的同步版本。 + +### `compileSingle(args: CompileSingleArgument): Promise` + +异步编译单个 CSS 文件。 + +### `compileSingleSync(args: CompileSingleArgument): FileResult` + +`compileSingle` 的同步版本。 + +### 类型定义 + +```typescript +type OutputType = 'bincode' | 'json' | 'none' + +interface CompileArgument { + src: SourceEntry[] + outputType: OutputType + tagNamePrefix?: string // 默认: "wx-" +} + +interface SourceEntry { + path: string + content: Buffer +} + +interface CompileResult { + files: FileEntry[] + importIndex: Buffer | null +} + +interface FileEntry { + path: string + file: FileResult +} + +interface CompileSingleArgument { + fileName: string + fileContent: Buffer + outputType: OutputType + tagNamePrefix?: string // 默认: "wx-" +} + +interface FileResult { + content: Buffer | null + warnings: CompileWarning[] +} + +interface CompileWarning { + start: { line: number; column: number } + end: { line: number; column: number } + message: string +} +``` + +## 构建 + +### 前置条件 + +- Rust 工具链 (1.92.0+) +- Node.js 16+ +- npm 或 pnpm +- 交叉编译依赖(从 macOS 编译 Windows 目标): + ```bash + rustup target add x86_64-pc-windows-msvc i686-pc-windows-msvc + cargo install cargo-xwin + ``` + +### 构建脚本 + +| 命令 | 说明 | +|------|------| +| `npm run build` | 编译全部 4 个平台 (darwin-arm64, darwin-x64, win32-x64, win32-x32) | +| `npm run build:current` | 仅编译当前平台 | +| `npm run build:debug` | 当前平台 debug 模式 | +| `npm run build:target ` | 编译指定 Rust target triple | + +### 验证构建 + +```bash +# 检查 prebuilds 结构 +ls prebuilds/*/node.napi.node + +# 测试加载 +node -e "const m = require('./'); console.log(Object.keys(m))" +# 期望输出: [ 'compile', 'compileSync', 'compileSingle', 'compileSingleSync', ... ] +``` + +## 运行时兼容性 + +| 运行时 | 支持 | +|--------|------| +| Node.js 16+ | ✓ | +| Node.js 22 | ✓ | +| Electron (主进程) | ✓ | +| Electron (utility process) | ✓ | +| NW.js | ✓ | + +N-API 是 ABI 稳定的 — 同一个 `.node` 二进制在所有兼容的 Node.js 和 Electron 版本上通用,无需重新编译。 + +## 许可证 + +MIT diff --git a/float-pigment-css-napi/build.rs b/float-pigment-css-napi/build.rs new file mode 100644 index 0000000..9fc2367 --- /dev/null +++ b/float-pigment-css-napi/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/float-pigment-css-napi/index.js b/float-pigment-css-napi/index.js new file mode 100644 index 0000000..7c21921 --- /dev/null +++ b/float-pigment-css-napi/index.js @@ -0,0 +1,21 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +const platform = process.platform; +const arch = process.arch; + +const archMap = { ia32: 'x32', x64: 'x64', arm64: 'arm64' }; +const dirName = platform + '-' + (archMap[arch] || arch); + +const bindingPath = path.join(__dirname, 'prebuilds', dirName, 'node.napi.node'); + +if (!fs.existsSync(bindingPath)) { + throw new Error( + 'No native binding found for ' + platform + '-' + arch + '. ' + + 'Looked in: ' + bindingPath + ); +} + +module.exports = require(bindingPath); diff --git a/float-pigment-css-napi/package.json b/float-pigment-css-napi/package.json new file mode 100644 index 0000000..1e99116 --- /dev/null +++ b/float-pigment-css-napi/package.json @@ -0,0 +1,36 @@ +{ + "name": "float-pigment-css-napi", + "version": "0.9.0", + "description": "Node.js N-API binding for float-pigment-css", + "main": "index.js", + "types": "type.d.ts", + "napi": { + "name": "float-pigment-css", + "triples": { + "defaults": true, + "additional": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", + "i686-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu" + ] + } + }, + "scripts": { + "build": "node scripts/build-all.js", + "build:current": "node scripts/build-current.js", + "build:debug": "node scripts/build-current.js --debug", + "build:target": "node scripts/build-target.js" + }, + "files": [ + "index.js", + "type.d.ts", + "prebuilds/**/node.napi.node" + ], + "license": "MIT", + "devDependencies": { + "@napi-rs/cli": "^2" + } +} diff --git a/float-pigment-css-napi/pnpm-lock.yaml b/float-pigment-css-napi/pnpm-lock.yaml new file mode 100644 index 0000000..ddbbc93 --- /dev/null +++ b/float-pigment-css-napi/pnpm-lock.yaml @@ -0,0 +1,18 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + '@napi-rs/cli': + specifier: ^2 + version: 2.18.4 + +packages: + + /@napi-rs/cli@2.18.4: + resolution: {integrity: sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==} + engines: {node: '>= 10'} + hasBin: true + dev: true diff --git a/float-pigment-css-napi/prebuilds/darwin-arm64/node.napi.node b/float-pigment-css-napi/prebuilds/darwin-arm64/node.napi.node new file mode 100755 index 0000000..8f1374c Binary files /dev/null and b/float-pigment-css-napi/prebuilds/darwin-arm64/node.napi.node differ diff --git a/float-pigment-css-napi/prebuilds/darwin-x64/node.napi.node b/float-pigment-css-napi/prebuilds/darwin-x64/node.napi.node new file mode 100755 index 0000000..57aac3f Binary files /dev/null and b/float-pigment-css-napi/prebuilds/darwin-x64/node.napi.node differ diff --git a/float-pigment-css-napi/prebuilds/win32-x32/node.napi.node b/float-pigment-css-napi/prebuilds/win32-x32/node.napi.node new file mode 100755 index 0000000..3e11f74 Binary files /dev/null and b/float-pigment-css-napi/prebuilds/win32-x32/node.napi.node differ diff --git a/float-pigment-css-napi/prebuilds/win32-x64/node.napi.node b/float-pigment-css-napi/prebuilds/win32-x64/node.napi.node new file mode 100755 index 0000000..3efcd75 Binary files /dev/null and b/float-pigment-css-napi/prebuilds/win32-x64/node.napi.node differ diff --git a/float-pigment-css-napi/scripts/build-all.js b/float-pigment-css-napi/scripts/build-all.js new file mode 100644 index 0000000..8f33e4e --- /dev/null +++ b/float-pigment-css-napi/scripts/build-all.js @@ -0,0 +1,34 @@ +/** + * Build all target platforms sequentially. + * Targets: darwin-arm64, darwin-x64, win32-x64, win32-x32 + */ +const { execSync } = require('child_process'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +const napi = path.join(root, 'node_modules', '.bin', 'napi'); + +const TARGETS = [ + 'aarch64-apple-darwin', + 'x86_64-apple-darwin', + 'x86_64-pc-windows-msvc', + 'i686-pc-windows-msvc', +]; + +for (const target of TARGETS) { + console.log(`\n=== Building ${target} ===\n`); + try { + execSync( + `${napi} build --platform --release --target ${target} --js false --dts type.d.ts`, + { cwd: root, stdio: 'inherit' } + ); + } catch (e) { + console.error(`Failed to build ${target}: ${e.message}`); + console.error('Make sure the target is installed: rustup target add ' + target); + console.error('For Windows cross-compile, install cargo-xwin: cargo install cargo-xwin'); + process.exit(1); + } +} + +// Move all .node files into prebuilds//node.napi.node +require('./postbuild.js'); diff --git a/float-pigment-css-napi/scripts/build-current.js b/float-pigment-css-napi/scripts/build-current.js new file mode 100644 index 0000000..a5d945b --- /dev/null +++ b/float-pigment-css-napi/scripts/build-current.js @@ -0,0 +1,37 @@ +/** + * Build for current platform only (convenience for local dev). + */ +const { execSync } = require('child_process'); +const path = require('path'); +const os = require('os'); + +const root = path.resolve(__dirname, '..'); +const napi = path.join(root, 'node_modules', '.bin', 'napi'); + +// Detect current platform target triple +const PLATFORM_MAP = { + 'darwin-arm64': 'aarch64-apple-darwin', + 'darwin-x64': 'x86_64-apple-darwin', + 'linux-x64': 'x86_64-unknown-linux-gnu', + 'linux-arm64': 'aarch64-unknown-linux-gnu', + 'win32-x64': 'x86_64-pc-windows-msvc', + 'win32-ia32': 'i686-pc-windows-msvc', +}; + +const key = `${process.platform}-${process.arch}`; +const target = PLATFORM_MAP[key]; + +if (!target) { + console.error(`Unsupported platform: ${key}`); + process.exit(1); +} + +const profile = process.argv.includes('--debug') ? '' : '--release'; + +console.log(`Building for ${target}...\n`); +execSync( + `${napi} build --platform ${profile} --target ${target} --js false --dts type.d.ts`, + { cwd: root, stdio: 'inherit' } +); + +require('./postbuild.js'); diff --git a/float-pigment-css-napi/scripts/build-target.js b/float-pigment-css-napi/scripts/build-target.js new file mode 100644 index 0000000..01f5589 --- /dev/null +++ b/float-pigment-css-napi/scripts/build-target.js @@ -0,0 +1,25 @@ +/** + * Build for a specific target platform. + * Usage: node scripts/build-target.js + * Example: node scripts/build-target.js x86_64-pc-windows-msvc + */ +const { execSync } = require('child_process'); +const path = require('path'); + +const target = process.argv[2]; +if (!target) { + console.error('Usage: node scripts/build-target.js '); + console.error('Example: node scripts/build-target.js x86_64-pc-windows-msvc'); + process.exit(1); +} + +const root = path.resolve(__dirname, '..'); +const napi = path.join(root, 'node_modules', '.bin', 'napi'); + +execSync( + `${napi} build --platform --release --target ${target} --js false --dts type.d.ts`, + { cwd: root, stdio: 'inherit' } +); + +// Move .node into prebuilds/ +require('./postbuild.js'); diff --git a/float-pigment-css-napi/scripts/postbuild.js b/float-pigment-css-napi/scripts/postbuild.js new file mode 100644 index 0000000..29f57d2 --- /dev/null +++ b/float-pigment-css-napi/scripts/postbuild.js @@ -0,0 +1,46 @@ +/** + * Post-build script: moves napi-rs output (.node) into + * prebuilds/-/node.napi.node + * to match the node-gyp-build directory convention used by the original C++ addon. + */ +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); + +// napi-rs generates files like: float-pigment-css.darwin-arm64.node +const nodeFiles = fs.readdirSync(root).filter(f => f.endsWith('.node') && f.startsWith('float-pigment-css.')); + +// Mapping from napi-rs triple suffix to node-gyp-build directory name +const TRIPLE_MAP = { + 'darwin-arm64': 'darwin-arm64', + 'darwin-x64': 'darwin-x64', + 'darwin-universal': 'darwin-universal', + 'win32-x64-msvc': 'win32-x64', + 'win32-ia32-msvc': 'win32-x32', + 'linux-x64-gnu': 'linux-x64', + 'linux-arm64-gnu': 'linux-arm64', + 'linux-x64-musl': 'linux-x64-musl', + 'linux-arm64-musl': 'linux-arm64-musl', +}; + +for (const file of nodeFiles) { + const match = file.match(/^float-pigment-css\.(.+)\.node$/); + if (!match) continue; + + const triple = match[1]; + const dirName = TRIPLE_MAP[triple] || triple; + const destDir = path.join(root, 'prebuilds', dirName); + + fs.mkdirSync(destDir, { recursive: true }); + + const src = path.join(root, file); + const dest = path.join(destDir, 'node.napi.node'); + + fs.renameSync(src, dest); + console.log(` ${file} -> prebuilds/${dirName}/node.napi.node`); +} + +if (nodeFiles.length === 0) { + console.warn('Warning: no .node files found to move'); +} diff --git a/float-pigment-css-napi/src/lib.rs b/float-pigment-css-napi/src/lib.rs new file mode 100644 index 0000000..e20534f --- /dev/null +++ b/float-pigment-css-napi/src/lib.rs @@ -0,0 +1,251 @@ +use napi::bindgen_prelude::*; +use napi_derive::napi; + +use float_pigment_css::parser::Warning; +use float_pigment_css::{StyleSheetImportIndex, StyleSheetResource}; + +// ---------- types ---------- + +#[napi(string_enum)] +pub enum OutputType { + #[napi(value = "bincode")] + Bincode, + #[napi(value = "json")] + Json, + #[napi(value = "none")] + None, +} + +#[napi(object)] +pub struct SourceCodePosition { + pub line: u32, + pub column: u32, +} + +#[napi(object)] +pub struct CompileWarning { + pub start: SourceCodePosition, + pub end: SourceCodePosition, + pub message: String, +} + +#[napi(object)] +pub struct FileResult { + pub content: Option, + pub warnings: Vec, +} + +#[napi(object)] +pub struct CompileResult { + pub files: Vec, + pub import_index: Option, +} + +#[napi(object)] +pub struct FileEntry { + pub path: String, + pub file: FileResult, +} + +#[napi(object)] +pub struct CompileArgument { + /// Source files to compile + pub src: Vec, + /// Output encoding format + pub output_type: OutputType, + /// Prefix for tag name selectors, default "wx-" + pub tag_name_prefix: Option, +} + +#[napi(object)] +pub struct SourceEntry { + pub path: String, + pub content: Buffer, +} + +#[napi(object)] +pub struct CompileSingleArgument { + /// CSS file name + pub file_name: String, + /// CSS file content + pub file_content: Buffer, + /// Output encoding format + pub output_type: OutputType, + /// Prefix for tag name selectors, default "wx-" + pub tag_name_prefix: Option, +} + +// ---------- helpers ---------- + +fn convert_warnings(warnings: &[Warning]) -> Vec { + warnings + .iter() + .map(|w| CompileWarning { + start: SourceCodePosition { + line: w.start_line, + column: w.start_col, + }, + end: SourceCodePosition { + line: w.end_line, + column: w.end_col, + }, + message: w.message.to_string(), + }) + .collect() +} + +fn serialize_sheet( + resource: &StyleSheetResource, + path: &str, + output_type: &OutputType, +) -> Option { + match output_type { + OutputType::Bincode => resource.serialize_bincode(path).map(Buffer::from), + OutputType::Json => resource + .serialize_json(path) + .map(|s| Buffer::from(s.into_bytes())), + OutputType::None => None, + } +} + +fn serialize_import_index(index: &StyleSheetImportIndex, output_type: &OutputType) -> Option { + match output_type { + OutputType::Bincode => Some(Buffer::from(index.serialize_bincode())), + OutputType::Json => Some(Buffer::from(index.serialize_json().into_bytes())), + OutputType::None => None, + } +} + +// ---------- core compile logic ---------- + +fn do_compile(args: CompileArgument) -> Result { + let tag_prefix = args.tag_name_prefix.as_deref().unwrap_or("wx-").to_owned(); + let output_type = args.output_type; + let mut resource = StyleSheetResource::new(); + let mut file_entries = Vec::with_capacity(args.src.len()); + + for entry in args.src { + let source = std::str::from_utf8(entry.content.as_ref()).map_err(|e| { + Error::new( + Status::InvalidArg, + format!("Invalid UTF-8 in source for '{}': {e}", entry.path), + ) + })?; + + let warnings = resource.add_source(&entry.path, source); + resource.add_tag_name_prefix(&entry.path, &tag_prefix); + + let content = serialize_sheet(&resource, &entry.path, &output_type); + + file_entries.push(FileEntry { + path: entry.path, + file: FileResult { + content, + warnings: convert_warnings(&warnings), + }, + }); + } + + let import_index = if !matches!(output_type, OutputType::None) { + let idx = resource.generate_import_indexes(); + serialize_import_index(&idx, &output_type) + } else { + None + }; + + Ok(CompileResult { + files: file_entries, + import_index, + }) +} + +fn do_compile_single(args: CompileSingleArgument) -> Result { + let tag_prefix = args.tag_name_prefix.as_deref().unwrap_or("wx-"); + let mut resource = StyleSheetResource::new(); + + let source = std::str::from_utf8(args.file_content.as_ref()).map_err(|e| { + Error::new( + Status::InvalidArg, + format!("Invalid UTF-8 in fileContent: {e}"), + ) + })?; + + let warnings = resource.add_source(&args.file_name, source); + resource.add_tag_name_prefix(&args.file_name, tag_prefix); + + let content = serialize_sheet(&resource, &args.file_name, &args.output_type); + + Ok(FileResult { + content, + warnings: convert_warnings(&warnings), + }) +} + +// ---------- sync exports ---------- + +#[napi] +pub fn compile_sync(args: CompileArgument) -> Result { + do_compile(args) +} + +#[napi] +pub fn compile_single_sync(args: CompileSingleArgument) -> Result { + do_compile_single(args) +} + +// ---------- async exports ---------- + +pub struct CompileTask { + args: Option, +} + +#[napi] +impl Task for CompileTask { + type Output = CompileResult; + type JsValue = CompileResult; + + fn compute(&mut self) -> Result { + let args = self.args.take().ok_or_else(|| { + Error::new(Status::GenericFailure, "compile task args already consumed") + })?; + do_compile(args) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi] +pub fn compile(args: CompileArgument) -> AsyncTask { + AsyncTask::new(CompileTask { args: Some(args) }) +} + +pub struct CompileSingleTask { + args: Option, +} + +#[napi] +impl Task for CompileSingleTask { + type Output = FileResult; + type JsValue = FileResult; + + fn compute(&mut self) -> Result { + let args = self.args.take().ok_or_else(|| { + Error::new( + Status::GenericFailure, + "compile_single task args already consumed", + ) + })?; + do_compile_single(args) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi] +pub fn compile_single(args: CompileSingleArgument) -> AsyncTask { + AsyncTask::new(CompileSingleTask { args: Some(args) }) +} diff --git a/float-pigment-css-napi/tsconfig.json b/float-pigment-css-napi/tsconfig.json new file mode 100644 index 0000000..38895f6 --- /dev/null +++ b/float-pigment-css-napi/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["type.d.ts"], + "compilerOptions": { + "baseUrl": "./", + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "module": "CommonJS", + "moduleResolution": "node", + "skipLibCheck": true, + "target": "esnext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": ["esnext"] + } +} diff --git a/float-pigment-css-napi/type.d.ts b/float-pigment-css-napi/type.d.ts new file mode 100644 index 0000000..c826a02 --- /dev/null +++ b/float-pigment-css-napi/type.d.ts @@ -0,0 +1,57 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +export const enum OutputType { + Bincode = 'bincode', + Json = 'json', + None = 'none' +} +export interface SourceCodePosition { + line: number + column: number +} +export interface CompileWarning { + start: SourceCodePosition + end: SourceCodePosition + message: string +} +export interface FileResult { + content?: Buffer + warnings: Array +} +export interface CompileResult { + files: Array + importIndex?: Buffer +} +export interface FileEntry { + path: string + file: FileResult +} +export interface CompileArgument { + /** Source files to compile */ + src: Array + /** Output encoding format */ + outputType: OutputType + /** Prefix for tag name selectors, default "wx-" */ + tagNamePrefix?: string +} +export interface SourceEntry { + path: string + content: Buffer +} +export interface CompileSingleArgument { + /** CSS file name */ + fileName: string + /** CSS file content */ + fileContent: Buffer + /** Output encoding format */ + outputType: OutputType + /** Prefix for tag name selectors, default "wx-" */ + tagNamePrefix?: string +} +export declare function compileSync(args: CompileArgument): CompileResult +export declare function compileSingleSync(args: CompileSingleArgument): FileResult +export declare function compile(args: CompileArgument): Promise +export declare function compileSingle(args: CompileSingleArgument): Promise