Repository: parcel-bundler/lightningcss Branch: master Commit: df63db2c51c4 Files: 219 Total size: 3.6 MB Directory structure: gitextract_yxy6uf4y/ ├── .cargo/ │ └── config.toml ├── .github/ │ └── workflows/ │ ├── release-crates.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── bench.js ├── c/ │ ├── Cargo.toml │ ├── build.rs │ ├── cbindgen.toml │ ├── lightningcss.h │ ├── src/ │ │ └── lib.rs │ └── test.c ├── cli/ │ ├── .gitignore │ ├── lightningcss │ └── postinstall.js ├── derive/ │ ├── Cargo.toml │ └── src/ │ ├── lib.rs │ ├── parse.rs │ ├── to_css.rs │ └── visit.rs ├── examples/ │ ├── custom_at_rule.rs │ ├── schema.rs │ └── serialize.rs ├── napi/ │ ├── Cargo.toml │ └── src/ │ ├── at_rule_parser.rs │ ├── lib.rs │ ├── threadsafe_function.rs │ ├── transformer.rs │ └── utils.rs ├── node/ │ ├── Cargo.toml │ ├── ast.d.ts │ ├── browserslistToTargets.js │ ├── build.rs │ ├── composeVisitors.js │ ├── flags.js │ ├── index.d.ts │ ├── index.js │ ├── index.mjs │ ├── src/ │ │ └── lib.rs │ ├── targets.d.ts │ ├── test/ │ │ ├── bundle.test.mjs │ │ ├── composeVisitors.test.mjs │ │ ├── customAtRules.mjs │ │ ├── transform.test.mjs │ │ └── visitor.test.mjs │ └── tsconfig.json ├── package.json ├── patches/ │ ├── @babel+types+7.26.3.patch │ └── json-schema-to-typescript+11.0.5.patch ├── rust-toolchain.toml ├── rustfmt.toml ├── scripts/ │ ├── build-ast.js │ ├── build-flow.js │ ├── build-npm.js │ ├── build-prefixes.js │ ├── build-wasm.js │ └── build.js ├── selectors/ │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── attr.rs │ ├── bloom.rs │ ├── build.rs │ ├── builder.rs │ ├── context.rs │ ├── lib.rs │ ├── matching.rs │ ├── nth_index_cache.rs │ ├── parser.rs │ ├── serialization.rs │ ├── sink.rs │ ├── tree.rs │ └── visitor.rs ├── src/ │ ├── bundler.rs │ ├── compat.rs │ ├── context.rs │ ├── css_modules.rs │ ├── declaration.rs │ ├── dependencies.rs │ ├── error.rs │ ├── lib.rs │ ├── logical.rs │ ├── macros.rs │ ├── main.rs │ ├── media_query.rs │ ├── parser.rs │ ├── prefixes.rs │ ├── printer.rs │ ├── properties/ │ │ ├── align.rs │ │ ├── animation.rs │ │ ├── background.rs │ │ ├── border.rs │ │ ├── border_image.rs │ │ ├── border_radius.rs │ │ ├── box_shadow.rs │ │ ├── contain.rs │ │ ├── css_modules.rs │ │ ├── custom.rs │ │ ├── display.rs │ │ ├── effects.rs │ │ ├── flex.rs │ │ ├── font.rs │ │ ├── grid.rs │ │ ├── list.rs │ │ ├── margin_padding.rs │ │ ├── masking.rs │ │ ├── mod.rs │ │ ├── outline.rs │ │ ├── overflow.rs │ │ ├── position.rs │ │ ├── prefix_handler.rs │ │ ├── size.rs │ │ ├── svg.rs │ │ ├── text.rs │ │ ├── transform.rs │ │ ├── transition.rs │ │ └── ui.rs │ ├── rules/ │ │ ├── container.rs │ │ ├── counter_style.rs │ │ ├── custom_media.rs │ │ ├── document.rs │ │ ├── font_face.rs │ │ ├── font_feature_values.rs │ │ ├── font_palette_values.rs │ │ ├── import.rs │ │ ├── keyframes.rs │ │ ├── layer.rs │ │ ├── media.rs │ │ ├── mod.rs │ │ ├── namespace.rs │ │ ├── nesting.rs │ │ ├── page.rs │ │ ├── property.rs │ │ ├── scope.rs │ │ ├── starting_style.rs │ │ ├── style.rs │ │ ├── supports.rs │ │ ├── unknown.rs │ │ ├── view_transition.rs │ │ └── viewport.rs │ ├── selector.rs │ ├── serialization.rs │ ├── stylesheet.rs │ ├── targets.rs │ ├── test_helpers.rs │ ├── traits.rs │ ├── values/ │ │ ├── alpha.rs │ │ ├── angle.rs │ │ ├── calc.rs │ │ ├── color.rs │ │ ├── easing.rs │ │ ├── gradient.rs │ │ ├── ident.rs │ │ ├── image.rs │ │ ├── length.rs │ │ ├── mod.rs │ │ ├── number.rs │ │ ├── percentage.rs │ │ ├── position.rs │ │ ├── ratio.rs │ │ ├── rect.rs │ │ ├── resolution.rs │ │ ├── shape.rs │ │ ├── size.rs │ │ ├── string.rs │ │ ├── syntax.rs │ │ ├── time.rs │ │ └── url.rs │ ├── vendor_prefix.rs │ └── visitor.rs ├── static-self/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── static-self-derive/ │ ├── Cargo.toml │ └── src/ │ ├── into_owned.rs │ └── lib.rs ├── test-integration.mjs ├── test.js ├── tests/ │ ├── cli_integration_tests.rs │ ├── test_cssom.rs │ ├── test_custom_parser.rs │ ├── test_serde.rs │ └── testdata/ │ ├── a.css │ ├── apply.css │ ├── b.css │ ├── baz.css │ ├── foo.css │ ├── has_external.css │ ├── hello/ │ │ └── world.css │ └── mixin.css ├── wasm/ │ ├── .gitignore │ ├── async.mjs │ ├── import.meta.url-polyfill.js │ ├── index.mjs │ └── wasm-node.mjs └── website/ ├── .posthtmlrc ├── bundling.html ├── css-modules.html ├── docs.css ├── docs.html ├── docs.js ├── include/ │ └── layout.html ├── index.html ├── minification.html ├── pages/ │ ├── bundling.md │ ├── css-modules.md │ ├── docs.md │ ├── minification.md │ ├── transforms.md │ └── transpilation.md ├── playground/ │ ├── index.html │ └── playground.js ├── synthwave.css ├── transforms.html └── transpilation.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [target.'cfg(target_env = "gnu")'] rustflags = ["-C", "link-args=-Wl,-z,nodelete"] [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" [target.aarch64-unknown-linux-musl] linker = "aarch64-linux-musl-gcc" rustflags = ["-C", "target-feature=-crt-static"] [target.wasm32-unknown-unknown] rustflags = [ "-C", "link-arg=--export-table", '--cfg', 'getrandom_backend="custom"', ] # Statically link Visual Studio redistributables on Windows builds [target.x86_64-pc-windows-msvc] rustflags = ["-C", "target-feature=+crt-static"] [target.aarch64-pc-windows-msvc] rustflags = ["-C", "target-feature=+crt-static"] ================================================ FILE: .github/workflows/release-crates.yml ================================================ name: release-crates on: workflow_dispatch: jobs: release-crates: runs-on: ubuntu-latest name: Release Rust crate steps: - uses: actions/checkout@v3 - uses: bahmutov/npm-install@v1.8.32 - name: Install Rust uses: dtolnay/rust-toolchain@stable - run: cargo login ${CRATES_IO_TOKEN} env: CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - run: | cargo install cargo-workspaces cargo workspaces publish --no-remove-dev-deps --from-git -y ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: workflow_dispatch: jobs: build: strategy: fail-fast: false matrix: include: # Windows - os: windows-latest target: x86_64-pc-windows-msvc binary: lightningcss.exe - os: windows-latest target: aarch64-pc-windows-msvc binary: lightningcss.exe # Mac OS - os: macos-latest target: x86_64-apple-darwin strip: strip -x # Must use -x on macOS. This produces larger results on linux. binary: lightningcss name: build-${{ matrix.target }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Install Node.JS uses: actions/setup-node@v3 with: node-version: 18 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Setup rust target run: rustup target add ${{ matrix.target }} - uses: bahmutov/npm-install@v1.8.32 - name: Build release run: yarn build-release env: RUST_TARGET: ${{ matrix.target }} - name: Build CLI run: | cargo build --release --features cli --target ${{ matrix.target }} node -e "require('fs').renameSync('target/${{ matrix.target }}/release/${{ matrix.binary }}', '${{ matrix.binary }}')" - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 if: ${{ matrix.strip }} run: ${{ matrix.strip }} *.node ${{ matrix.binary }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: | *.node ${{ matrix.binary }} build-apple-silicon: name: build-apple-silicon runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Install Node.JS uses: actions/setup-node@v3 with: node-version: 18 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Setup rust target run: rustup target add aarch64-apple-darwin - uses: bahmutov/npm-install@v1.8.32 - name: Build release run: yarn build-release env: RUST_TARGET: aarch64-apple-darwin JEMALLOC_SYS_WITH_LG_PAGE: 14 - name: Build CLI run: | export CC=$(xcrun -f clang); export CXX=$(xcrun -f clang++); SYSROOT=$(xcrun --sdk macosx --show-sdk-path); export CFLAGS="-isysroot $SYSROOT -isystem $SYSROOT"; export MACOSX_DEPLOYMENT_TARGET="10.9"; cargo build --release --features cli --target aarch64-apple-darwin mv target/aarch64-apple-darwin/release/lightningcss lightningcss env: JEMALLOC_SYS_WITH_LG_PAGE: 14 - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 run: strip -x *.node lightningcss - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-aarch64-apple-darwin path: | *.node lightningcss build-linux: strategy: fail-fast: false matrix: include: - target: x86_64-unknown-linux-gnu strip: strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian setup: npm install --global yarn@1 - target: aarch64-unknown-linux-gnu strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - target: aarch64-linux-android strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - target: armv7-unknown-linux-gnueabihf strip: llvm-strip image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:c22284b2d79092d3e885f64ede00f6afdeb2ccef7e2b6e78be52e7909091cd57 - target: aarch64-unknown-linux-musl image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:78c9ab1f117f8c535b93c4b91a2f19063dda6e4dba48a6187df49810625992c1 strip: aarch64-linux-musl-strip - target: x86_64-unknown-linux-musl image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:78c9ab1f117f8c535b93c4b91a2f19063dda6e4dba48a6187df49810625992c1 strip: strip name: build-${{ matrix.target }} runs-on: ubuntu-latest container: image: ${{ matrix.image }} steps: - uses: actions/checkout@v3 - name: Install Node.JS uses: actions/setup-node@v3 with: node-version: 18 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Setup Android NDK if: ${{ matrix.target == 'aarch64-linux-android' }} run: | sudo apt update && sudo apt install unzip -y cd /tmp wget -q https://dl.google.com/android/repository/android-ndk-r28-linux.zip -O /tmp/ndk.zip unzip ndk.zip - name: Setup cross compile toolchain if: ${{ matrix.setup }} run: ${{ matrix.setup }} - name: Setup rust target run: rustup target add ${{ matrix.target }} - uses: bahmutov/npm-install@v1.8.32 - name: Build release run: yarn build-release env: ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28 RUST_TARGET: ${{ matrix.target }} - name: Build CLI env: ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28 run: | yarn napi build --bin lightningcss --release --features cli --target ${{ matrix.target }} mv target/${{ matrix.target }}/release/lightningcss lightningcss - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 if: ${{ matrix.strip }} run: ${{ matrix.strip }} *.node lightningcss - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.target }} path: | *.node lightningcss build-freebsd: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build FreeBSD uses: cross-platform-actions/action@v0.25.0 env: DEBUG: napi:* RUSTUP_HOME: /usr/local/rustup CARGO_HOME: /usr/local/cargo RUSTUP_IO_THREADS: 1 with: operating_system: freebsd version: '14.0' memory: 13G cpu_count: 3 environment_variables: 'DEBUG RUSTUP_IO_THREADS' shell: bash run: | sudo pkg install -y -f curl node libnghttp2 npm yarn curl https://sh.rustup.rs -sSf --output rustup.sh sh rustup.sh -y --profile minimal --default-toolchain beta source "$HOME/.cargo/env" echo "~~~~ rustc --version ~~~~" rustc --version echo "~~~~ node -v ~~~~" node -v echo "~~~~ yarn --version ~~~~" yarn --version yarn install || true yarn build-release strip -x *.node cargo build --release --features cli mv target/release/lightningcss lightningcss node -e "require('.')" ./lightningcss --help rm -rf node_modules rm -rf target rm -rf .yarn/cache - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: bindings-x86_64-unknown-freebsd path: | *.node lightningcss build-wasm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Node.JS uses: actions/setup-node@v3 with: node-version: 18 - uses: bahmutov/npm-install@v1.8.32 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown - name: Setup rust target run: rustup target add wasm32-unknown-unknown - name: Install wasm-opt run: | curl -L -O https://github.com/WebAssembly/binaryen/releases/download/version_111/binaryen-version_111-x86_64-linux.tar.gz tar -xf binaryen-version_111-x86_64-linux.tar.gz - name: Build wasm run: | export PATH="$PATH:./binaryen-version_111/bin" yarn wasm:build-release - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: wasm path: wasm/lightningcss_node.wasm release: runs-on: ubuntu-latest name: Build and release needs: - build - build-linux - build-apple-silicon - build-freebsd - build-wasm steps: - uses: actions/checkout@v3 - uses: bahmutov/npm-install@v1.8.32 - name: Download artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: Show artifacts run: ls -R artifacts - name: Build npm packages run: | node scripts/build-npm.js cp artifacts/wasm/* wasm/. node scripts/build-wasm.js - run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > ~/.npmrc env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish to npm run: | for pkg in npm/*; do echo "Publishing $pkg..." cd $pkg; npm publish; cd ../..; done cd wasm echo "Publishing lightningcss-wasm..."; npm publish cd .. cd cli echo "Publishing lightningcss-cli..."; npm publish cd .. echo "Publishing lightningcss..."; npm publish release-crates: runs-on: ubuntu-latest name: Release Rust crate steps: - uses: actions/checkout@v3 - uses: bahmutov/npm-install@v1.8.32 - name: Install Rust uses: dtolnay/rust-toolchain@stable - run: cargo login ${CRATES_IO_TOKEN} env: CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - run: | cargo install cargo-workspaces cargo workspaces publish --no-remove-dev-deps --from-git -y ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: [master] pull_request: branches: [master] jobs: test: runs-on: ubuntu-latest env: CARGO_TERM_COLOR: always RUST_BACKTRACE: full RUSTFLAGS: -D warnings steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo fmt - run: cargo test --all-features test-js: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - uses: bahmutov/npm-install@v1.8.32 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: yarn build - run: yarn test - run: yarn tsc test-wasm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - uses: bahmutov/npm-install@v1.8.32 - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown - name: Setup rust target run: rustup target add wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - name: Install wasm-opt run: | curl -L -O https://github.com/WebAssembly/binaryen/releases/download/version_111/binaryen-version_111-x86_64-linux.tar.gz tar -xf binaryen-version_111-x86_64-linux.tar.gz - name: Build wasm run: | export PATH="$PATH:./binaryen-version_111/bin" yarn wasm:build-release - run: TEST_WASM=node yarn test - run: TEST_WASM=browser yarn test ================================================ FILE: .gitignore ================================================ .DS_Store *.node node_modules/ target/ pkg/ dist/ .parcel-cache node/*.flow artifacts npm node/ast.json ================================================ FILE: .prettierrc ================================================ { "bracketSpacing": false, "endOfLine": "lf", "singleQuote": true, "trailingComma": "all" } ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Welcome, we really appreciate if you're considering to contribute, the joint effort of our contributors make projects like this possible! The goal of this document is to provide guidance on how you can get involved. ## Getting started with bug fixing In order to make it easier to get familiar with the codebase we labeled simpler issues using [Good First Issue](https://github.com/parcel-bundler/lightningcss/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) and [Help Wanted](https://github.com/parcel-bundler/lightningcss/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22). Before starting make sure you have the following requirements installed: [git](https://git-scm.com), [Node](https://nodejs.org), [Yarn](https://yarnpkg.com) and [Rust](https://www.rust-lang.org/tools/install). The process starts by [forking](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the project and setup a new branch to work in. It's important that the changes are made in separated branches in order to ensure a pull request only includes the commits related to a bug or feature. Clone the forked repository locally and install the dependencies: ```sh git clone https://github.com/USERNAME/lightningcss.git cd lightningcss yarn install ``` ## Testing In order to test, you first need to build the core package: ```sh yarn build ``` Then you can run the tests: ```sh yarn test # js tests cargo test # rust tests ``` ## Building There are different build targets available, with "release" being a production build: ```sh yarn build yarn build-release yarn wasm:build yarn wasm:build-release ``` Note: If you plan to build the WASM target, ensure that you have the required toolchain and binaries installed. ```sh rustup target add wasm32-unknown-unknown cargo install wasm-opt ``` ## Website The website is built using [Parcel](https://parceljs.org). You can start the development server by running: ```sh yarn website:start ``` ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "node", "napi", "selectors", "c", "derive", "static-self", "static-self-derive", ] [package] authors = ["Devon Govett "] name = "lightningcss" version = "1.0.0-alpha.71" description = "A CSS parser, transformer, and minifier" license = "MPL-2.0" edition = "2021" keywords = ["CSS", "minifier", "Parcel"] repository = "https://github.com/parcel-bundler/lightningcss" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [[bin]] name = "lightningcss" path = "src/main.rs" required-features = ["cli"] [lib] name = "lightningcss" path = "src/lib.rs" crate-type = ["rlib"] [features] default = ["bundler", "nodejs", "sourcemap"] browserslist = ["browserslist-rs"] bundler = ["dashmap", "sourcemap", "rayon"] cli = ["atty", "clap", "serde_json", "browserslist", "jemallocator"] jsonschema = ["schemars", "serde", "parcel_selectors/jsonschema"] nodejs = ["dep:serde", "dep:serde-content"] serde = [ "dep:serde", "dep:serde-content", "bitflags/serde", "smallvec/serde", "cssparser/serde", "parcel_selectors/serde", "into_owned", ] sourcemap = ["parcel_sourcemap"] visitor = [] into_owned = [ "static-self", "static-self/smallvec", "static-self/indexmap", "parcel_selectors/into_owned", ] substitute_variables = ["visitor", "into_owned"] [dependencies] serde = { version = "1.0.228", features = ["derive"], optional = true } serde-content = { version = "0.1.2", features = ["serde"], optional = true } cssparser = "0.33.0" cssparser-color = "0.1.0" parcel_selectors = { version = "0.28.2", path = "./selectors" } itertools = "0.10.1" smallvec = { version = "1.7.0", features = ["union"] } bitflags = "2.2.1" parcel_sourcemap = { version = "2.1.1", features = ["json"], optional = true } data-encoding = "2.3.2" lazy_static = "1.4.0" const-str = "0.3.1" pathdiff = "0.2.1" ahash = "0.8.7" pastey = "0.1.0" indexmap = { version = "2.2.6", features = ["serde"] } # CLI deps atty = { version = "0.2", optional = true } clap = { version = "3.0.6", features = ["derive"], optional = true } browserslist-rs = { version = "0.19.0", optional = true } rayon = { version = "1.5.1", optional = true } dashmap = { version = "5.0.0", optional = true } serde_json = { version = "1.0.78", optional = true } lightningcss-derive = { version = "=1.0.0-alpha.43", path = "./derive" } schemars = { version = "0.8.19", features = ["smallvec", "indexmap2"], optional = true } static-self = { version = "0.1.2", path = "static-self", optional = true } [target.'cfg(target_os = "macos")'.dependencies] jemallocator = { version = "0.3.2", features = [ "disable_initial_exec_tls", ], optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.3", default-features = false } [dev-dependencies] indoc = "1.0.3" assert_cmd = "2.0" assert_fs = "1.0" predicates = "2.1" serde_json = "1" pretty_assertions = "1.4.0" [[test]] name = "cli_integration_tests" path = "tests/cli_integration_tests.rs" required-features = ["cli"] [[example]] name = "custom_at_rule" required-features = ["visitor"] [[example]] name = "serialize" required-features = ["serde"] [profile.release] lto = true codegen-units = 1 panic = 'abort' ================================================ FILE: LICENSE ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ # ⚡️ Lightning CSS An extremely fast CSS parser, transformer, and minifier written in Rust. Use it with [Parcel](https://parceljs.org), as a standalone library or CLI, or via a plugin with any other tool. performance and build size charts performance and build size charts ## Features - **Extremely fast** – Parsing and minifying large files is completed in milliseconds, often with significantly smaller output than other tools. See [benchmarks](#benchmarks) below. - **Typed property values** – many other CSS parsers treat property values as an untyped series of tokens. This means that each transformer that wants to do something with these values must interpret them itself, leading to duplicate work and inconsistencies. Lightning CSS parses all values using the grammar from the CSS specification, and exposes a specific value type for each property. - **Browser-grade parser** – Lightning CSS is built on the [cssparser](https://github.com/servo/rust-cssparser) and [selectors](https://github.com/servo/stylo/tree/main/selectors) crates created by Mozilla and used by Firefox and Servo. These provide a solid general purpose CSS-parsing foundation on top of which Lightning CSS implements support for all specific CSS rules and properties. - **Minification** – One of the main purposes of Lightning CSS is to minify CSS to make it smaller. This includes many optimizations including: - Combining longhand properties into shorthands where possible. - Merging adjacent rules with the same selectors or declarations when it is safe to do so. - Combining CSS transforms into a single matrix or vice versa when smaller. - Removing vendor prefixes that are not needed, based on the provided browser targets. - Reducing `calc()` expressions where possible. - Converting colors to shorter hex notation where possible. - Minifying gradients. - Minifying CSS grid templates. - Normalizing property value order. - Removing default property sub-values which will be inferred by browsers. - Many micro-optimizations, e.g. converting to shorter units, removing unnecessary quotation marks, etc. - **Vendor prefixing** – Lightning CSS accepts a list of browser targets, and automatically adds (and removes) vendor prefixes. - **Browserslist configuration** – Lightning CSS supports opt-in browserslist configuration discovery to resolve browser targets and integrate with your existing tools and config setup. - **Syntax lowering** – Lightning CSS parses modern CSS syntax, and generates more compatible output where needed, based on browser targets. - CSS Nesting - Custom media queries (draft spec) - Logical properties * [Color Level 5](https://drafts.csswg.org/css-color-5/) - `color-mix()` function - Relative color syntax, e.g. `lab(from purple calc(l * .8) a b)` - [Color Level 4](https://drafts.csswg.org/css-color-4/) - `lab()`, `lch()`, `oklab()`, and `oklch()` colors - `color()` function supporting predefined color spaces such as `display-p3` and `xyz` - Space separated components in `rgb` and `hsl` functions - Hex with alpha syntax - `hwb()` color syntax - Percent syntax for opacity - `#rgba` and `#rrggbbaa` hex colors - Selectors - `:not` with multiple arguments - `:lang` with multiple arguments - `:dir` - `:is` - Double position gradient stops (e.g. `red 40% 80%`) - `clamp()`, `round()`, `rem()`, and `mod()` math functions - Alignment shorthands (e.g. `place-items`) - Two-value `overflow` shorthand - Media query range syntax (e.g. `@media (width <= 100px)` or `@media (100px < width < 500px)`) - Multi-value `display` property (e.g. `inline flex`) - `system-ui` font family fallbacks - **CSS modules** – Lightning CSS supports compiling a subset of [CSS modules](https://github.com/css-modules/css-modules) features. - Locally scoped class and id selectors - Locally scoped custom identifiers, e.g. `@keyframes` names, grid lines/areas, `@counter-style` names, etc. - Opt-in support for locally scoped CSS variables and other dashed identifiers. - `:local()` and `:global()` selectors - The `composes` property - **Custom transforms** – The Lightning CSS visitor API can be used to implement custom transform plugins. ## Documentation Lightning CSS can be used from [Parcel](https://parceljs.org), as a standalone library from JavaScript or Rust, using a standalone CLI, or wrapped as a plugin within any other tool. See the [Lightning CSS website](https://lightningcss.dev/docs.html) for documentation. ## Benchmarks performance and build size charts performance and build size charts ``` $ node bench.js bootstrap-4.css cssnano: 544.809ms 159636 bytes esbuild: 17.199ms 160332 bytes lightningcss: 4.16ms 143091 bytes $ node bench.js animate.css cssnano: 283.105ms 71723 bytes esbuild: 11.858ms 72183 bytes lightningcss: 1.973ms 23666 bytes $ node bench.js tailwind.css cssnano: 2.198s 1925626 bytes esbuild: 107.668ms 1961642 bytes lightningcss: 43.368ms 1824130 bytes ``` For more benchmarks comparing more tools and input, see [here](http://goalsmashers.github.io/css-minification-benchmark/). Note that some of the tools shown perform unsafe optimizations that may change the behavior of the original CSS in favor of smaller file size. Lightning CSS does not do this – the output CSS should always behave identically to the input. Keep this in mind when comparing file sizes between tools. ================================================ FILE: bench.js ================================================ const css = require('./'); const cssnano = require('cssnano'); const postcss = require('postcss'); const esbuild = require('esbuild'); let opts = { filename: process.argv[process.argv.length - 1], code: require('fs').readFileSync(process.argv[process.argv.length - 1]), minify: true, // source_map: true, targets: { chrome: 95 << 16 } }; async function run() { await doCssNano(); console.time('esbuild'); let r = await esbuild.transform(opts.code.toString(), { sourcefile: opts.filename, loader: 'css', minify: true }); console.timeEnd('esbuild'); console.log(r.code.length + ' bytes'); console.log(''); console.time('lightningcss'); let res = css.transform(opts); console.timeEnd('lightningcss'); console.log(res.code.length + ' bytes'); } async function doCssNano() { console.time('cssnano'); const result = await postcss([ cssnano, ]).process(opts.code, {from: opts.filename}); console.timeEnd('cssnano'); console.log(result.css.length + ' bytes'); console.log(''); } run(); ================================================ FILE: c/Cargo.toml ================================================ [package] authors = ["Devon Govett "] name = "lightningcss_c_bindings" version = "0.1.0" edition = "2021" publish = false [lib] crate-type = ["cdylib"] [dependencies] lightningcss = { path = "../", features = ["browserslist"] } parcel_sourcemap = { version = "2.1.1", features = ["json"] } browserslist-rs = { version = "0.19.0" } [build-dependencies] cbindgen = "0.24.3" ================================================ FILE: c/build.rs ================================================ use std::env; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); cbindgen::generate(crate_dir) .expect("Unable to generate bindings") .write_to_file("lightningcss.h"); } ================================================ FILE: c/cbindgen.toml ================================================ language = "C" [parse] parse_deps = false include = ["lightningcss"] [export.rename] StyleSheetWrapper = "StyleSheet" [enum] prefix_with_name = true ================================================ FILE: c/lightningcss.h ================================================ #include #include #include #include typedef struct CssError CssError; typedef struct StyleSheet StyleSheet; typedef struct Targets { uint32_t android; uint32_t chrome; uint32_t edge; uint32_t firefox; uint32_t ie; uint32_t ios_saf; uint32_t opera; uint32_t safari; uint32_t samsung; } Targets; typedef struct ParseOptions { const char *filename; bool nesting; bool custom_media; bool css_modules; const char *css_modules_pattern; bool css_modules_dashed_idents; bool error_recovery; } ParseOptions; typedef struct TransformOptions { struct Targets targets; char **unused_symbols; uintptr_t unused_symbols_len; } TransformOptions; typedef struct RawString { char *text; uintptr_t len; } RawString; typedef enum CssModuleReference_Tag { /** * A local reference. */ CssModuleReference_Local, /** * A global reference. */ CssModuleReference_Global, /** * A reference to an export in a different file. */ CssModuleReference_Dependency, } CssModuleReference_Tag; typedef struct CssModuleReference_Local_Body { /** * The local (compiled) name for the reference. */ struct RawString name; } CssModuleReference_Local_Body; typedef struct CssModuleReference_Global_Body { /** * The referenced global name. */ struct RawString name; } CssModuleReference_Global_Body; typedef struct CssModuleReference_Dependency_Body { /** * The name to reference within the dependency. */ struct RawString name; /** * The dependency specifier for the referenced file. */ struct RawString specifier; } CssModuleReference_Dependency_Body; typedef struct CssModuleReference { CssModuleReference_Tag tag; union { CssModuleReference_Local_Body local; CssModuleReference_Global_Body global; CssModuleReference_Dependency_Body dependency; }; } CssModuleReference; typedef struct CssModuleExport { struct RawString exported; struct RawString local; bool is_referenced; struct CssModuleReference *composes; uintptr_t composes_len; } CssModuleExport; typedef struct CssModulePlaceholder { struct RawString placeholder; struct CssModuleReference reference; } CssModulePlaceholder; typedef struct ToCssResult { struct RawString code; struct RawString map; struct CssModuleExport *exports; uintptr_t exports_len; struct CssModulePlaceholder *references; uintptr_t references_len; } ToCssResult; typedef struct PseudoClasses { const char *hover; const char *active; const char *focus; const char *focus_visible; const char *focus_within; } PseudoClasses; typedef struct ToCssOptions { bool minify; bool source_map; const char *input_source_map; uintptr_t input_source_map_len; const char *project_root; struct Targets targets; bool analyze_dependencies; struct PseudoClasses pseudo_classes; } ToCssOptions; bool lightningcss_browserslist_to_targets(const char *query, struct Targets *targets, struct CssError **error); struct StyleSheet *lightningcss_stylesheet_parse(const char *source, uintptr_t len, struct ParseOptions options, struct CssError **error); bool lightningcss_stylesheet_transform(struct StyleSheet *stylesheet, struct TransformOptions options, struct CssError **error); struct ToCssResult lightningcss_stylesheet_to_css(struct StyleSheet *stylesheet, struct ToCssOptions options, struct CssError **error); void lightningcss_stylesheet_free(struct StyleSheet *stylesheet); void lightningcss_to_css_result_free(struct ToCssResult result); const char *lightningcss_error_message(struct CssError *error); void lightningcss_error_free(struct CssError *error); uintptr_t lightningcss_stylesheet_get_warning_count(struct StyleSheet *stylesheet); const char *lightningcss_stylesheet_get_warning(struct StyleSheet *stylesheet, uintptr_t index); ================================================ FILE: c/src/lib.rs ================================================ #![allow(clippy::not_unsafe_ptr_arg_deref)] use std::collections::HashSet; use std::ffi::{CStr, CString}; use std::mem::ManuallyDrop; use std::os::raw::c_char; use std::sync::{Arc, RwLock}; use lightningcss::css_modules::PatternParseError; use lightningcss::error::{Error, MinifyErrorKind, ParserError, PrinterError}; use lightningcss::stylesheet::{MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, StyleSheet}; use lightningcss::targets::Browsers; use parcel_sourcemap::SourceMap; pub struct StyleSheetWrapper<'i, 'o> { stylesheet: StyleSheet<'i, 'o>, source: &'i str, warnings: Vec>, } pub struct CssError<'i> { kind: ErrorKind<'i>, message: Option, } impl<'i> CssError<'i> { fn message(&mut self) -> *const c_char { if let Some(message) = &self.message { return message.as_ptr(); } let string: String = match &self.kind { ErrorKind::ParserError(err) => err.to_string().into(), ErrorKind::MinifyError(err) => err.to_string().into(), ErrorKind::PrinterError(err) => err.to_string().into(), ErrorKind::PatternParseError(err) => err.to_string().into(), ErrorKind::BrowserslistError(err) => err.to_string().into(), ErrorKind::SourceMapError(err) => err.to_string().into(), }; self.message = Some(CString::new(string).unwrap()); self.message.as_ref().unwrap().as_ptr() } } pub enum ErrorKind<'i> { ParserError(Error>), MinifyError(Error), PrinterError(PrinterError), PatternParseError(PatternParseError), BrowserslistError(browserslist::Error), SourceMapError(parcel_sourcemap::SourceMapError), } macro_rules! impl_from { ($name: ident, $t: ty) => { impl<'i> From<$t> for CssError<'i> { fn from(err: $t) -> Self { CssError { kind: ErrorKind::$name(err), message: None, } } } }; } impl_from!(ParserError, Error>); impl_from!(MinifyError, Error); impl_from!(PrinterError, PrinterError); impl_from!(PatternParseError, PatternParseError); impl_from!(BrowserslistError, browserslist::Error); impl_from!(SourceMapError, parcel_sourcemap::SourceMapError); #[repr(C)] pub struct ParseOptions { filename: *const c_char, nesting: bool, custom_media: bool, css_modules: bool, css_modules_pattern: *const c_char, css_modules_dashed_idents: bool, error_recovery: bool, } #[repr(C)] #[derive(Default, PartialEq)] pub struct Targets { android: u32, chrome: u32, edge: u32, firefox: u32, ie: u32, ios_saf: u32, opera: u32, safari: u32, samsung: u32, } impl Into for Targets { fn into(self) -> Browsers { macro_rules! browser { ($val: expr) => { if $val > 0 { Some($val) } else { None } }; } Browsers { android: browser!(self.android), chrome: browser!(self.chrome), edge: browser!(self.edge), firefox: browser!(self.firefox), ie: browser!(self.ie), ios_saf: browser!(self.ios_saf), opera: browser!(self.opera), safari: browser!(self.safari), samsung: browser!(self.samsung), } } } macro_rules! unwrap { ($result: expr, $error: ident, $ret: expr) => { match $result { Ok(v) => v, Err(err) => unsafe { *$error = Box::into_raw(Box::new(err.into())); return $ret; }, } }; } #[no_mangle] pub extern "C" fn lightningcss_browserslist_to_targets( query: *const c_char, targets: *mut Targets, error: *mut *mut CssError, ) -> bool { let string = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(query).to_bytes()) }; match Browsers::from_browserslist([string]) { Ok(Some(browsers)) => { let targets = unsafe { &mut *targets }; targets.android = browsers.android.unwrap_or_default(); targets.chrome = browsers.chrome.unwrap_or_default(); targets.edge = browsers.edge.unwrap_or_default(); targets.firefox = browsers.firefox.unwrap_or_default(); targets.ie = browsers.ie.unwrap_or_default(); targets.ios_saf = browsers.ios_saf.unwrap_or_default(); targets.opera = browsers.opera.unwrap_or_default(); targets.safari = browsers.safari.unwrap_or_default(); targets.samsung = browsers.samsung.unwrap_or_default(); true } Ok(None) => true, Err(err) => unsafe { *error = Box::into_raw(Box::new(err.into())); false }, } } #[repr(C)] pub struct TransformOptions { targets: Targets, unused_symbols: *mut *mut c_char, unused_symbols_len: usize, } impl Into for TransformOptions { fn into(self) -> MinifyOptions { let mut unused_symbols = HashSet::new(); let slice = unsafe { std::slice::from_raw_parts(self.unused_symbols, self.unused_symbols_len) }; for symbol in slice { let string = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(*symbol).to_bytes()).to_owned() }; unused_symbols.insert(string); } MinifyOptions { targets: if self.targets != Targets::default() { Some(self.targets.into()).into() } else { Default::default() }, unused_symbols, } } } #[repr(C)] pub struct ToCssOptions { minify: bool, source_map: bool, input_source_map: *const c_char, input_source_map_len: usize, project_root: *const c_char, targets: Targets, analyze_dependencies: bool, pseudo_classes: PseudoClasses, } #[derive(PartialEq)] #[repr(C)] pub struct PseudoClasses { hover: *const c_char, active: *const c_char, focus: *const c_char, focus_visible: *const c_char, focus_within: *const c_char, } impl Default for PseudoClasses { fn default() -> Self { PseudoClasses { hover: std::ptr::null(), active: std::ptr::null(), focus: std::ptr::null(), focus_visible: std::ptr::null(), focus_within: std::ptr::null(), } } } impl<'a> Into> for PseudoClasses { fn into(self) -> lightningcss::printer::PseudoClasses<'a> { macro_rules! pc { ($ptr: expr) => { if $ptr.is_null() { None } else { Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr($ptr).to_bytes()) }) } }; } lightningcss::printer::PseudoClasses { hover: pc!(self.hover), active: pc!(self.active), focus: pc!(self.focus), focus_visible: pc!(self.focus_visible), focus_within: pc!(self.focus_within), } } } #[no_mangle] pub extern "C" fn lightningcss_stylesheet_parse( source: *const c_char, len: usize, options: ParseOptions, error: *mut *mut CssError, ) -> *mut StyleSheetWrapper { let slice = unsafe { std::slice::from_raw_parts(source as *const u8, len) }; let code = unsafe { std::str::from_utf8_unchecked(slice) }; let warnings = Arc::new(RwLock::new(Vec::new())); let mut flags = ParserFlags::empty(); flags.set(ParserFlags::CUSTOM_MEDIA, options.custom_media); let opts = ParserOptions { filename: if options.filename.is_null() { String::new() } else { unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.filename).to_bytes()).to_owned() } }, flags, css_modules: if options.css_modules { let pattern = if !options.css_modules_pattern.is_null() { let pattern = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.css_modules_pattern).to_bytes()) }; unwrap!( lightningcss::css_modules::Pattern::parse(pattern), error, std::ptr::null_mut() ) } else { lightningcss::css_modules::Pattern::default() }; Some(lightningcss::css_modules::Config { pattern, dashed_idents: options.css_modules_dashed_idents, ..Default::default() }) } else { None }, error_recovery: options.error_recovery, source_index: 0, warnings: Some(warnings.clone()), }; let stylesheet = unwrap!(StyleSheet::parse(code, opts), error, std::ptr::null_mut()); Box::into_raw(Box::new(StyleSheetWrapper { stylesheet, source: code, warnings: warnings.clone().read().unwrap().iter().map(|w| w.clone().into()).collect(), })) } #[no_mangle] pub extern "C" fn lightningcss_stylesheet_transform( stylesheet: *mut StyleSheetWrapper, options: TransformOptions, error: *mut *mut CssError, ) -> bool { let wrapper = unsafe { stylesheet.as_mut() }.unwrap(); unwrap!(wrapper.stylesheet.minify(options.into()), error, false); true } #[no_mangle] pub extern "C" fn lightningcss_stylesheet_to_css( stylesheet: *mut StyleSheetWrapper, options: ToCssOptions, error: *mut *mut CssError, ) -> ToCssResult { let wrapper = unsafe { stylesheet.as_mut() }.unwrap(); let mut source_map = if options.source_map { let mut sm = SourceMap::new("/"); sm.add_source(&wrapper.stylesheet.sources[0]); unwrap!(sm.set_source_content(0, wrapper.source), error, ToCssResult::default()); Some(sm) } else { None }; let opts = PrinterOptions { minify: options.minify, project_root: if options.project_root.is_null() { None } else { Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.project_root).to_bytes()) }) }, source_map: source_map.as_mut(), targets: if options.targets != Targets::default() { Some(options.targets.into()).into() } else { Default::default() }, analyze_dependencies: if options.analyze_dependencies { Some(Default::default()) } else { None }, pseudo_classes: if options.pseudo_classes != PseudoClasses::default() { Some(options.pseudo_classes.into()) } else { None }, }; let res = unwrap!(wrapper.stylesheet.to_css(opts), error, ToCssResult::default()); let map = if let Some(mut source_map) = source_map { if !options.input_source_map.is_null() { let slice = unsafe { std::slice::from_raw_parts(options.input_source_map as *const u8, options.input_source_map_len) }; let input_source_map = unsafe { std::str::from_utf8_unchecked(slice) }; let mut sm = unwrap!( SourceMap::from_json("/", input_source_map), error, ToCssResult::default() ); unwrap!(source_map.extends(&mut sm), error, ToCssResult::default()); } unwrap!(source_map.to_json(None), error, ToCssResult::default()).into() } else { RawString::default() }; let (exports, exports_len) = if let Some(exports) = res.exports { let exports: Vec = exports .into_iter() .map(|(k, v)| { let composes_len = v.composes.len(); let composes = if !v.composes.is_empty() { let composes: Vec = v.composes.into_iter().map(|composes| composes.into()).collect(); ManuallyDrop::new(composes).as_mut_ptr() } else { std::ptr::null_mut() }; CssModuleExport { exported: k.into(), local: v.name.into(), is_referenced: v.is_referenced, composes, composes_len, } }) .collect(); let mut exports = ManuallyDrop::new(exports); (exports.as_mut_ptr(), exports.len()) } else { (std::ptr::null_mut(), 0) }; let (references, references_len) = if let Some(references) = res.references { let references: Vec = references .into_iter() .map(|(k, v)| CssModulePlaceholder { placeholder: k.into(), reference: v.into(), }) .collect(); let mut references = ManuallyDrop::new(references); (references.as_mut_ptr(), references.len()) } else { (std::ptr::null_mut(), 0) }; ToCssResult { code: res.code.into(), map, exports, exports_len, references, references_len, } } #[no_mangle] pub extern "C" fn lightningcss_stylesheet_free(stylesheet: *mut StyleSheetWrapper) { if !stylesheet.is_null() { drop(unsafe { Box::from_raw(stylesheet) }) } } #[repr(C)] pub struct ToCssResult { code: RawString, map: RawString, exports: *mut CssModuleExport, exports_len: usize, references: *mut CssModulePlaceholder, references_len: usize, } impl Default for ToCssResult { fn default() -> Self { ToCssResult { code: RawString::default(), map: RawString::default(), exports: std::ptr::null_mut(), exports_len: 0, references: std::ptr::null_mut(), references_len: 0, } } } impl Drop for ToCssResult { fn drop(&mut self) { if !self.exports.is_null() { let exports = unsafe { Vec::from_raw_parts(self.exports, self.exports_len, self.exports_len) }; drop(exports); self.exports = std::ptr::null_mut(); } if !self.references.is_null() { let references = unsafe { Vec::from_raw_parts(self.references, self.references_len, self.references_len) }; drop(references); self.references = std::ptr::null_mut(); } } } #[no_mangle] pub extern "C" fn lightningcss_to_css_result_free(result: ToCssResult) { drop(result) } #[repr(C)] pub struct CssModuleExport { exported: RawString, local: RawString, is_referenced: bool, composes: *mut CssModuleReference, composes_len: usize, } impl Drop for CssModuleExport { fn drop(&mut self) { if !self.composes.is_null() { let composes = unsafe { Vec::from_raw_parts(self.composes, self.composes_len, self.composes_len) }; drop(composes); self.composes = std::ptr::null_mut(); } } } #[repr(C)] pub enum CssModuleReference { /// A local reference. Local { /// The local (compiled) name for the reference. name: RawString, }, /// A global reference. Global { /// The referenced global name. name: RawString, }, /// A reference to an export in a different file. Dependency { /// The name to reference within the dependency. name: RawString, /// The dependency specifier for the referenced file. specifier: RawString, }, } impl From for CssModuleReference { fn from(reference: lightningcss::css_modules::CssModuleReference) -> Self { use lightningcss::css_modules::CssModuleReference::*; match reference { Local { name } => CssModuleReference::Local { name: name.into() }, Global { name } => CssModuleReference::Global { name: name.into() }, Dependency { name, specifier } => CssModuleReference::Dependency { name: name.into(), specifier: specifier.into(), }, } } } #[repr(C)] pub struct CssModulePlaceholder { placeholder: RawString, reference: CssModuleReference, } #[repr(C)] pub struct RawString { text: *mut c_char, len: usize, } impl Default for RawString { fn default() -> Self { RawString { text: std::ptr::null_mut(), len: 0, } } } impl From for RawString { fn from(string: String) -> RawString { RawString { len: string.len(), text: Box::into_raw(string.into_boxed_str()) as *mut c_char, } } } impl Drop for RawString { fn drop(&mut self) { if self.text.is_null() { return; } drop(unsafe { Box::from_raw(self.text) }); self.text = std::ptr::null_mut(); } } #[no_mangle] pub extern "C" fn lightningcss_error_message(error: *mut CssError) -> *const c_char { match unsafe { error.as_mut() } { Some(err) => err.message(), None => std::ptr::null(), } } #[no_mangle] pub extern "C" fn lightningcss_error_free(error: *mut CssError) { if !error.is_null() { drop(unsafe { Box::from_raw(error) }) } } #[no_mangle] pub extern "C" fn lightningcss_stylesheet_get_warning_count<'i>( stylesheet: *mut StyleSheetWrapper<'i, '_>, ) -> usize { match unsafe { stylesheet.as_mut() } { Some(s) => s.warnings.len(), None => 0, } } #[no_mangle] pub extern "C" fn lightningcss_stylesheet_get_warning<'i>( stylesheet: *mut StyleSheetWrapper<'i, '_>, index: usize, ) -> *const c_char { let stylesheet = match unsafe { stylesheet.as_mut() } { Some(s) => s, None => return std::ptr::null(), }; match stylesheet.warnings.get_mut(index) { Some(w) => w.message(), None => std::ptr::null(), } } ================================================ FILE: c/test.c ================================================ #include #include #include "lightningcss.h" int print_error(CssError *error); int main() { char *source = ".foo {" " color: lch(50.998% 135.363 338);" "}" ".bar {" " color: yellow;" " composes: foo from './bar.css';" "}" ".baz:hover {" " color: var(--foo from './baz.css');" "}"; ParseOptions parse_opts = { .filename = "test.css", .css_modules = true, .css_modules_pattern = "yo_[name]_[local]", .css_modules_dashed_idents = true}; CssError *error = NULL; StyleSheet *stylesheet = lightningcss_stylesheet_parse(source, strlen(source), parse_opts, &error); if (!stylesheet) goto cleanup; char *unused_symbols[1] = {"bar"}; TransformOptions transform_opts = { .unused_symbols = unused_symbols, .unused_symbols_len = 0}; if (!lightningcss_browserslist_to_targets("last 2 versions, not IE <= 11", &transform_opts.targets, &error)) goto cleanup; if (!lightningcss_stylesheet_transform(stylesheet, transform_opts, &error)) goto cleanup; ToCssOptions to_css_opts = { .minify = true, .source_map = true, .pseudo_classes = { .hover = "is-hovered"}}; ToCssResult result = lightningcss_stylesheet_to_css(stylesheet, to_css_opts, &error); if (error) goto cleanup; size_t warning_count = lightningcss_stylesheet_get_warning_count(stylesheet); for (size_t i = 0; i < warning_count; i++) { printf("warning: %s\n", lightningcss_stylesheet_get_warning(stylesheet, i)); } fwrite(result.code.text, sizeof(char), result.code.len, stdout); printf("\n"); fwrite(result.map.text, sizeof(char), result.map.len, stdout); printf("\n"); for (int i = 0; i < result.exports_len; i++) { printf("%.*s -> %.*s\n", (int)result.exports[i].exported.len, result.exports[i].exported.text, (int)result.exports[i].local.len, result.exports[i].local.text); for (int j = 0; j < result.exports[i].composes_len; j++) { const CssModuleReference *ref = &result.exports[i].composes[j]; switch (ref->tag) { case CssModuleReference_Local: printf(" composes local: %.*s\n", (int)ref->local.name.len, ref->local.name.text); break; case CssModuleReference_Global: printf(" composes global: %.*s\n", (int)ref->global.name.len, ref->global.name.text); break; case CssModuleReference_Dependency: printf(" composes dependency: %.*s from %.*s\n", (int)ref->dependency.name.len, ref->dependency.name.text, (int)ref->dependency.specifier.len, ref->dependency.specifier.text); break; } } } for (int i = 0; i < result.references_len; i++) { printf("placeholder: %.*s\n", (int)result.references[i].placeholder.len, result.references[i].placeholder.text); } cleanup: lightningcss_stylesheet_free(stylesheet); lightningcss_to_css_result_free(result); if (error) { printf("error: %s\n", lightningcss_error_message(error)); lightningcss_error_free(error); return 1; } } ================================================ FILE: cli/.gitignore ================================================ package.json README.md .DS_Store lightningcss.exe ================================================ FILE: cli/lightningcss ================================================ This file is required so that npm creates the lightningcss binary on Windows. ================================================ FILE: cli/postinstall.js ================================================ let fs = require('fs'); let path = require('path'); let parts = [process.platform, process.arch]; if (process.platform === 'linux') { const {MUSL, familySync} = require('detect-libc'); const family = familySync(); if (family === MUSL) { parts.push('musl'); } else if (process.arch === 'arm') { parts.push('gnueabihf'); } else { parts.push('gnu'); } } else if (process.platform === 'win32') { parts.push('msvc'); } let binary = process.platform === 'win32' ? 'lightningcss.exe' : 'lightningcss'; let pkgPath; try { pkgPath = path.dirname(require.resolve(`lightningcss-cli-${parts.join('-')}/package.json`)); } catch (err) { pkgPath = path.join(__dirname, '..', 'target', 'release'); if (!fs.existsSync(path.join(pkgPath, binary))) { pkgPath = path.join(__dirname, '..', 'target', 'debug'); } } try { fs.linkSync(path.join(pkgPath, binary), path.join(__dirname, binary)); } catch (err) { try { fs.copyFileSync(path.join(pkgPath, binary), path.join(__dirname, binary)); } catch (err) { console.error('Failed to move lightningcss-cli binary into place.'); process.exit(1); } } if (process.platform === 'win32') { try { fs.unlinkSync(path.join(__dirname, 'lightningcss')); } catch (err) { } } ================================================ FILE: derive/Cargo.toml ================================================ [package] authors = ["Devon Govett "] name = "lightningcss-derive" description = "Derive macros for lightningcss" version = "1.0.0-alpha.43" license = "MPL-2.0" edition = "2021" repository = "https://github.com/parcel-bundler/lightningcss" [lib] proc-macro = true [dependencies] syn = { version = "1.0", features = ["extra-traits"] } quote = "1.0" proc-macro2 = "1.0" convert_case = "0.6.0" ================================================ FILE: derive/src/lib.rs ================================================ use proc_macro::TokenStream; mod parse; mod to_css; mod visit; #[proc_macro_derive(Visit, attributes(visit, skip_visit, skip_type, visit_types))] pub fn derive_visit_children(input: TokenStream) -> TokenStream { visit::derive_visit_children(input) } #[proc_macro_derive(Parse, attributes(css))] pub fn derive_parse(input: TokenStream) -> TokenStream { parse::derive_parse(input) } #[proc_macro_derive(ToCss, attributes(css))] pub fn derive_to_css(input: TokenStream) -> TokenStream { to_css::derive_to_css(input) } ================================================ FILE: derive/src/parse.rs ================================================ use convert_case::Casing; use proc_macro::{self, TokenStream}; use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; use quote::quote; use syn::{ parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Fields, Ident, Token, }; pub fn derive_parse(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, mut generics, attrs, .. } = parse_macro_input!(input); let opts = CssOptions::parse_attributes(&attrs).unwrap(); let cloned_generics = generics.clone(); let (_, ty_generics, _) = cloned_generics.split_for_impl(); if generics.lifetimes().next().is_none() { generics.params.insert(0, parse_quote! { 'i }) } let lifetime = generics.lifetimes().next().unwrap().clone(); let (impl_generics, _, where_clause) = generics.split_for_impl(); let imp = match &data { Data::Enum(data) => derive_enum(&data, &ident, &opts), _ => todo!(), }; let output = quote! { impl #impl_generics Parse<#lifetime> for #ident #ty_generics #where_clause { fn parse<'t>(input: &mut Parser<#lifetime, 't>) -> Result>> { #imp } } }; output.into() } fn derive_enum(data: &DataEnum, ident: &Ident, opts: &CssOptions) -> TokenStream2 { let mut idents = Vec::new(); let mut non_idents = Vec::new(); for (index, variant) in data.variants.iter().enumerate() { let name = &variant.ident; let fields = variant .fields .iter() .enumerate() .map(|(index, field)| { field.ident.as_ref().map_or_else( || Ident::new(&format!("_{}", index), Span::call_site()), |ident| ident.clone(), ) }) .collect::>(); let mut expr = match &variant.fields { Fields::Unit => { idents.push(( Literal::string(&variant.ident.to_string().to_case(opts.case)), name.clone(), )); continue; } Fields::Named(_) => { quote! { return Ok(#ident::#name { #(#fields),* }) } } Fields::Unnamed(_) => { quote! { return Ok(#ident::#name(#(#fields),*)) } } }; // Group multiple ident branches together. if !idents.is_empty() { if idents.len() == 1 { let (s, name) = idents.remove(0); non_idents.push(quote! { if input.try_parse(|input| input.expect_ident_matching(#s)).is_ok() { return Ok(#ident::#name) } }); } else { let matches = idents .iter() .map(|(s, name)| { quote! { #s => return Ok(#ident::#name), } }) .collect::>(); non_idents.push(quote! { { let state = input.state(); if let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) { cssparser::match_ignore_ascii_case! { &*ident, #(#matches)* _ => {} } input.reset(&state); } } }); idents.clear(); } } let is_last = index == data.variants.len() - 1; for (index, field) in variant.fields.iter().enumerate().rev() { let ty = &field.ty; let field_name = field.ident.as_ref().map_or_else( || Ident::new(&format!("_{}", index), Span::call_site()), |ident| ident.clone(), ); if is_last { expr = quote! { let #field_name = <#ty>::parse(input)?; #expr }; } else { expr = quote! { if let Ok(#field_name) = input.try_parse(<#ty>::parse) { #expr } }; } } non_idents.push(expr); } let idents = if idents.is_empty() { quote! {} } else if idents.len() == 1 { let (s, name) = idents.remove(0); quote! { input.expect_ident_matching(#s)?; Ok(#ident::#name) } } else { let idents = idents .into_iter() .map(|(s, name)| { quote! { #s => Ok(#ident::#name), } }) .collect::>(); quote! { let location = input.current_source_location(); let ident = input.expect_ident()?; cssparser::match_ignore_ascii_case! { &*ident, #(#idents)* _ => Err(location.new_unexpected_token_error( cssparser::Token::Ident(ident.clone()) )) } } }; let output = quote! { #(#non_idents)* #idents }; output.into() } pub struct CssOptions { pub case: convert_case::Case, } impl CssOptions { pub fn parse_attributes(attrs: &Vec) -> syn::Result { for attr in attrs { if attr.path.is_ident("css") { let opts: CssOptions = attr.parse_args()?; return Ok(opts); } } Ok(CssOptions { case: convert_case::Case::Kebab, }) } } impl Parse for CssOptions { fn parse(input: syn::parse::ParseStream) -> syn::Result { let mut case = convert_case::Case::Kebab; while !input.is_empty() { let k: Ident = input.parse()?; let _: Token![=] = input.parse()?; let v: Ident = input.parse()?; if k == "case" { if v == "lower" { case = convert_case::Case::Flat; } } } Ok(Self { case }) } } ================================================ FILE: derive/src/to_css.rs ================================================ use convert_case::Casing; use proc_macro::{self, TokenStream}; use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; use quote::quote; use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident, Type}; use crate::parse::CssOptions; pub fn derive_to_css(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, generics, attrs, .. } = parse_macro_input!(input); let opts = CssOptions::parse_attributes(&attrs).unwrap(); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let imp = match &data { Data::Enum(data) => derive_enum(&data, &opts), _ => todo!(), }; let output = quote! { impl #impl_generics ToCss for #ident #ty_generics #where_clause { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { #imp } } }; output.into() } fn derive_enum(data: &DataEnum, opts: &CssOptions) -> TokenStream2 { let variants = data .variants .iter() .map(|variant| { let name = &variant.ident; let fields = variant .fields .iter() .enumerate() .map(|(index, field)| { field.ident.as_ref().map_or_else( || Ident::new(&format!("_{}", index), Span::call_site()), |ident| ident.clone(), ) }) .collect::>(); #[derive(PartialEq)] enum NeedsSpace { Yes, No, Maybe, } let mut needs_space = NeedsSpace::No; let mut fields_iter = variant.fields.iter().zip(fields.iter()).peekable(); let mut writes = Vec::new(); let mut has_needs_space = false; while let Some((field, name)) = fields_iter.next() { writes.push(if fields.len() > 1 { let space = match needs_space { NeedsSpace::Yes => quote! { dest.write_char(' ')?; }, NeedsSpace::No => quote! {}, NeedsSpace::Maybe => { has_needs_space = true; quote! { if needs_space { dest.write_char(' ')?; } } } }; if is_option(&field.ty) { needs_space = NeedsSpace::Maybe; let after_space = if matches!(fields_iter.peek(), Some((field, _)) if !is_option(&field.ty)) { // If the next field is non-optional, just insert the space here. needs_space = NeedsSpace::No; quote! { dest.write_char(' ')?; } } else { quote! {} }; quote! { if let Some(v) = #name { #space v.to_css(dest)?; #after_space } } } else { needs_space = NeedsSpace::Yes; quote! { #space #name.to_css(dest)?; } } } else { quote! { #name.to_css(dest) } }); } if writes.len() > 1 { writes.push(quote! { Ok(()) }); } if has_needs_space { writes.insert(0, quote! { let mut needs_space = false }); } match variant.fields { Fields::Unit => { let s = Literal::string(&variant.ident.to_string().to_case(opts.case)); quote! { Self::#name => dest.write_str(#s) } } Fields::Named(_) => { quote! { Self::#name { #(#fields),* } => { #(#writes)* } } } Fields::Unnamed(_) => { quote! { Self::#name(#(#fields),*) => { #(#writes)* } } } } }) .collect::>(); let output = quote! { match self { #(#variants),* } }; output.into() } fn is_option(ty: &Type) -> bool { matches!(&ty, Type::Path(p) if p.qself.is_none() && p.path.segments.iter().next().unwrap().ident == "Option") } ================================================ FILE: derive/src/visit.rs ================================================ use std::collections::HashSet; use proc_macro::{self, TokenStream}; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; use syn::{ parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields, GenericParam, Generics, Ident, Member, Token, Type, Visibility, }; pub fn derive_visit_children(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, generics, attrs, .. } = parse_macro_input!(input); let options: Vec = attrs .iter() .filter_map(|attr| { if attr.path.is_ident("visit") { let opts: VisitOptions = attr.parse_args().unwrap(); Some(opts) } else { None } }) .collect(); let visit_types = if let Some(attr) = attrs.iter().find(|attr| attr.path.is_ident("visit_types")) { let types: VisitTypes = attr.parse_args().unwrap(); let types = types.types; Some(quote! { crate::visit_types!(#(#types)|*) }) } else { None }; if options.is_empty() { derive(&ident, &data, &generics, None, visit_types) } else { options .into_iter() .map(|options| derive(&ident, &data, &generics, Some(options), visit_types.clone())) .collect() } } fn derive( ident: &Ident, data: &Data, generics: &Generics, options: Option, visit_types: Option, ) -> TokenStream { let mut impl_generics = generics.clone(); let mut type_defs = quote! {}; let generics = if let Some(VisitOptions { generic: Some(generic), .. }) = &options { let mappings = generics .type_params() .zip(generic.type_params()) .map(|(a, b)| quote! { type #a = #b; }); type_defs = quote! { #(#mappings)* }; impl_generics.params.clear(); generic } else { &generics }; if impl_generics.lifetimes().next().is_none() { impl_generics.params.insert(0, parse_quote! { 'i }) } let lifetime = impl_generics.lifetimes().next().unwrap().clone(); let t = impl_generics.type_params().find(|g| &g.ident.to_string() == "R"); let v = quote! { __V }; let t = if let Some(t) = t { GenericParam::Type(t.ident.clone().into()) } else { let t: GenericParam = parse_quote! { __T }; impl_generics .params .push(parse_quote! { #t: crate::visitor::Visit<#lifetime, __T, #v> }); t }; impl_generics .params .push(parse_quote! { #v: ?Sized + crate::visitor::Visitor<#lifetime, #t> }); for ty in generics.type_params() { let name = &ty.ident; impl_generics.make_where_clause().predicates.push(parse_quote! { #name: Visit<#lifetime, #t, #v> }) } let mut seen_types = HashSet::new(); let mut child_types = Vec::new(); let mut visit = Vec::new(); match data { Data::Struct(s) => { for ( index, Field { vis, ty, ident, attrs, .. }, ) in s.fields.iter().enumerate() { if attrs.iter().any(|attr| attr.path.is_ident("skip_visit")) { continue; } if matches!(ty, Type::Reference(_)) || !matches!(vis, Visibility::Public(..)) { continue; } if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) { seen_types.insert(ty.clone()); child_types.push(quote! { <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() }); } let name = ident .as_ref() .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone())); visit.push(quote! { self.#name.visit(visitor)?; }) } } Data::Enum(DataEnum { variants, .. }) => { let variants = variants .iter() .map(|variant| { let name = &variant.ident; let mut field_names = Vec::new(); let mut visit_fields = Vec::new(); for (index, Field { ty, ident, attrs, .. }) in variant.fields.iter().enumerate() { let name = ident.as_ref().map_or_else( || Ident::new(&format!("_{}", index), Span::call_site()), |ident| ident.clone(), ); field_names.push(name.clone()); if matches!(ty, Type::Reference(_)) { continue; } if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) && !skip_type(&variant.attrs) { seen_types.insert(ty.clone()); child_types.push(quote! { <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits() }); } visit_fields.push(quote! { #name.visit(visitor)?; }) } match variant.fields { Fields::Unnamed(_) => { quote! { Self::#name(#(#field_names),*) => { #(#visit_fields)* } } } Fields::Named(_) => { quote! { Self::#name { #(#field_names),* } => { #(#visit_fields)* } } } Fields::Unit => quote! {}, } }) .collect::(); visit.push(quote! { match self { #variants _ => {} } }) } _ => {} } if visit_types.is_none() && child_types.is_empty() { child_types.push(quote! { crate::visitor::VisitTypes::empty().bits() }); } let (_, ty_generics, _) = generics.split_for_impl(); let (impl_generics, _, where_clause) = impl_generics.split_for_impl(); let self_visit = if let Some(VisitOptions { visit: Some(visit), kind: Some(kind), .. }) = &options { child_types.push(quote! { crate::visitor::VisitTypes::#kind.bits() }); quote! { fn visit(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { if visitor.visit_types().contains(crate::visitor::VisitTypes::#kind) { visitor.#visit(self) } else { self.visit_children(visitor) } } } } else { quote! {} }; let child_types = visit_types.unwrap_or_else(|| { quote! { { #type_defs crate::visitor::VisitTypes::from_bits_retain(#(#child_types)|*) } } }); let output = quote! { impl #impl_generics Visit<#lifetime, #t, #v> for #ident #ty_generics #where_clause { const CHILD_TYPES: crate::visitor::VisitTypes = #child_types; #self_visit fn visit_children(&mut self, visitor: &mut #v) -> Result<(), #v::Error> { if !>::CHILD_TYPES.intersects(visitor.visit_types()) { return Ok(()) } #(#visit)* Ok(()) } } }; output.into() } fn skip_type(attrs: &Vec) -> bool { attrs.iter().any(|attr| attr.path.is_ident("skip_type")) } struct VisitOptions { visit: Option, kind: Option, generic: Option, } impl Parse for VisitOptions { fn parse(input: syn::parse::ParseStream) -> syn::Result { let (visit, kind, comma) = if input.peek(Ident) { let visit: Ident = input.parse()?; let _: Token![,] = input.parse()?; let kind: Ident = input.parse()?; let comma: Result = input.parse(); (Some(visit), Some(kind), comma.is_ok()) } else { (None, None, true) }; let generic: Option = if comma { Some(input.parse()?) } else { None }; Ok(Self { visit, kind, generic }) } } struct VisitTypes { types: Vec, } impl Parse for VisitTypes { fn parse(input: syn::parse::ParseStream) -> syn::Result { let first: Ident = input.parse()?; let mut types = vec![first]; while input.parse::().is_ok() { let id: Ident = input.parse()?; types.push(id); } Ok(Self { types }) } } ================================================ FILE: examples/custom_at_rule.rs ================================================ use std::{collections::HashMap, convert::Infallible}; use cssparser::*; use lightningcss::{ declaration::DeclarationBlock, error::PrinterError, printer::Printer, properties::custom::{Token, TokenOrValue}, rules::{style::StyleRule, CssRule, CssRuleList, Location}, selector::{Component, Selector}, stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, targets::Browsers, traits::{AtRuleParser, ToCss}, values::{ color::{CssColor, RGBA}, length::LengthValue, }, vendor_prefix::VendorPrefix, visit_types, visitor::{Visit, VisitTypes, Visitor}, }; fn main() { let args: Vec = std::env::args().collect(); let source = std::fs::read_to_string(&args[1]).unwrap(); let opts = ParserOptions { filename: args[1].clone(), ..Default::default() }; let mut stylesheet = StyleSheet::parse_with(&source, opts, &mut TailwindAtRuleParser).unwrap(); println!("{:?}", stylesheet); let mut style_rules = HashMap::new(); stylesheet .visit(&mut StyleRuleCollector { rules: &mut style_rules, }) .unwrap(); println!("{:?}", style_rules); stylesheet.visit(&mut ApplyVisitor { rules: &style_rules }).unwrap(); let result = stylesheet .to_css(PrinterOptions { targets: Browsers { chrome: Some(100 << 16), ..Browsers::default() } .into(), ..PrinterOptions::default() }) .unwrap(); println!("{}", result.code); } /// An @tailwind directive. #[derive(Debug, Clone)] enum TailwindDirective { Base, Components, Utilities, Variants, } /// A custom at rule prelude. enum Prelude { Tailwind(TailwindDirective), Apply(Vec), } /// A @tailwind rule. #[derive(Debug, Clone)] struct TailwindRule { directive: TailwindDirective, loc: SourceLocation, } /// An @apply rule. #[derive(Debug, Clone)] struct ApplyRule { names: Vec, loc: SourceLocation, } /// A custom at rule. #[derive(Debug, Clone)] enum AtRule { Tailwind(TailwindRule), Apply(ApplyRule), } #[derive(Debug)] struct TailwindAtRuleParser; impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { type Prelude = Prelude; type Error = Infallible; type AtRule = AtRule; fn parse_prelude<'t>( &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, _options: &ParserOptions<'_, 'i>, ) -> Result> { match_ignore_ascii_case! {&*name, "tailwind" => { let location = input.current_source_location(); let ident = input.expect_ident()?; let directive = match_ignore_ascii_case! { &*ident, "base" => TailwindDirective::Base, "components" => TailwindDirective::Components, "utilities" => TailwindDirective::Utilities, "variants" => TailwindDirective::Variants, _ => return Err(location.new_unexpected_token_error( cssparser::Token::Ident(ident.clone()) )) }; Ok(Prelude::Tailwind(directive)) }, "apply" => { let mut names = Vec::new(); loop { if let Ok(name) = input.try_parse(|input| input.expect_ident_cloned()) { names.push(name.as_ref().into()); } else { break } } Ok(Prelude::Apply(names)) }, _ => Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) } } fn rule_without_block( &mut self, prelude: Self::Prelude, start: &ParserState, _options: &ParserOptions<'_, 'i>, _is_nested: bool, ) -> Result { let loc = start.source_location(); match prelude { Prelude::Tailwind(directive) => Ok(AtRule::Tailwind(TailwindRule { directive, loc })), Prelude::Apply(names) => Ok(AtRule::Apply(ApplyRule { names, loc })), } } } struct StyleRuleCollector<'i, 'a> { rules: &'a mut HashMap>, } impl<'i, 'a> Visitor<'i, AtRule> for StyleRuleCollector<'i, 'a> { type Error = Infallible; fn visit_types(&self) -> VisitTypes { VisitTypes::RULES } fn visit_rule(&mut self, rule: &mut lightningcss::rules::CssRule<'i, AtRule>) -> Result<(), Self::Error> { match rule { CssRule::Style(rule) => { for selector in rule.selectors.0.iter() { if selector.len() != 1 { continue; // TODO } for component in selector.iter_raw_match_order() { match component { Component::Class(name) => { self.rules.insert(name.0.to_string(), rule.declarations.clone()); } _ => {} } } } } _ => {} } rule.visit_children(self) } } struct ApplyVisitor<'a, 'i> { rules: &'a HashMap>, } impl<'a, 'i> Visitor<'i, AtRule> for ApplyVisitor<'a, 'i> { type Error = Infallible; fn visit_types(&self) -> VisitTypes { visit_types!(RULES | COLORS | LENGTHS | DASHED_IDENTS | SELECTORS | TOKENS) } fn visit_rule(&mut self, rule: &mut CssRule<'i, AtRule>) -> Result<(), Self::Error> { // Replace @apply rule with nested style rule. if let CssRule::Custom(AtRule::Apply(apply)) = rule { let mut declarations = DeclarationBlock::new(); for name in &apply.names { let Some(applied) = self.rules.get(name) else { continue; }; declarations .important_declarations .extend(applied.important_declarations.iter().cloned()); declarations.declarations.extend(applied.declarations.iter().cloned()); } *rule = CssRule::Style(StyleRule { selectors: Component::Nesting.into(), vendor_prefix: VendorPrefix::None, declarations, rules: CssRuleList(vec![]), loc: Location { source_index: 0, line: apply.loc.line, column: apply.loc.column, }, }) } rule.visit_children(self) } fn visit_url(&mut self, url: &mut lightningcss::values::url::Url<'i>) -> Result<(), Self::Error> { url.url = format!("https://mywebsite.com/{}", url.url).into(); Ok(()) } fn visit_color(&mut self, color: &mut lightningcss::values::color::CssColor) -> Result<(), Self::Error> { *color = color.to_lab().unwrap(); Ok(()) } fn visit_length(&mut self, length: &mut lightningcss::values::length::LengthValue) -> Result<(), Self::Error> { match length { LengthValue::Px(px) => *length = LengthValue::Rem(*px / 16.0), _ => {} } Ok(()) } fn visit_dashed_ident( &mut self, ident: &mut lightningcss::values::ident::DashedIdent, ) -> Result<(), Self::Error> { ident.0 = format!("--tw-{}", &ident.0[2..]).into(); Ok(()) } fn visit_selector(&mut self, selector: &mut Selector<'i>) -> Result<(), Self::Error> { for c in selector.iter_mut_raw_match_order() { match c { Component::Class(c) => { *c = format!("tw-{}", c).into(); } _ => {} } } Ok(()) } fn visit_token(&mut self, token: &mut TokenOrValue<'i>) -> Result<(), Self::Error> { match token { TokenOrValue::Function(f) if f.name == "theme" => match f.arguments.0.first() { Some(TokenOrValue::Token(Token::String(s))) => match s.as_ref() { "blue-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(0, 0, 255, 1.0))), "red-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(255, 0, 0, 1.0))), _ => {} }, _ => {} }, _ => {} } token.visit_children(self) } } #[cfg(feature = "visitor")] impl<'i, V: Visitor<'i, AtRule>> Visit<'i, AtRule, V> for AtRule { const CHILD_TYPES: VisitTypes = VisitTypes::empty(); fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> { Ok(()) } } impl ToCss for AtRule { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> { match self { AtRule::Tailwind(rule) => { let _ = rule.loc; // TODO: source maps let directive = match rule.directive { TailwindDirective::Base => "TAILWIND BASE HERE", TailwindDirective::Components => "TAILWIND COMPONENTS HERE", TailwindDirective::Utilities => "TAILWIND UTILITIES HERE", TailwindDirective::Variants => "TAILWIND VARIANTS HERE", }; dest.write_str(directive) } AtRule::Apply(_) => Ok(()), } } } ================================================ FILE: examples/schema.rs ================================================ fn main() { #[cfg(feature = "jsonschema")] { let schema = schemars::schema_for!(lightningcss::stylesheet::StyleSheet); let output = serde_json::to_string_pretty(&schema).unwrap(); let _ = std::fs::write("node/ast.json", output); } } ================================================ FILE: examples/serialize.rs ================================================ fn main() { parse(); } #[cfg(feature = "serde")] fn parse() { use lightningcss::stylesheet::{ParserOptions, StyleSheet}; use std::{env, fs}; let args: Vec = env::args().collect(); let contents = fs::read_to_string(&args[1]).unwrap(); let stylesheet = StyleSheet::parse( &contents, ParserOptions { filename: args[1].clone(), ..ParserOptions::default() }, ) .unwrap(); let json = serde_json::to_string(&stylesheet).unwrap(); println!("{}", json); } #[cfg(not(feature = "serde"))] fn parse() { panic!("serde feature is not enabled") } ================================================ FILE: napi/Cargo.toml ================================================ [package] authors = ["Devon Govett "] name = "lightningcss-napi" version = "0.4.8" description = "Node-API bindings for Lightning CSS" license = "MPL-2.0" repository = "https://github.com/parcel-bundler/lightningcss" edition = "2021" [features] default = [] visitor = ["lightningcss/visitor"] bundler = ["dep:crossbeam-channel", "dep:rayon"] [dependencies] serde = { version = "1.0.201", features = ["derive"] } serde-content = { version = "0.1.2", features = ["serde"] } serde_bytes = "0.11.5" cssparser = "0.33.0" lightningcss = { version = "1.0.0-alpha.71", path = "../", features = [ "nodejs", "serde", ] } parcel_sourcemap = { version = "2.1.1", features = ["json"] } serde-detach = "0.0.1" smallvec = { version = "1.7.0", features = ["union"] } napi = { version = "2", default-features = false, features = [ "napi4", "napi5", "serde-json", ] } crossbeam-channel = { version = "0.5.6", optional = true } rayon = { version = "1.5.1", optional = true } ================================================ FILE: napi/src/at_rule_parser.rs ================================================ use std::collections::HashMap; use cssparser::*; use lightningcss::{ declaration::DeclarationBlock, error::ParserError, rules::{CssRuleList, Location}, stylesheet::ParserOptions, traits::{AtRuleParser, ToCss}, values::{ string::CowArcStr, syntax::{ParsedComponent, SyntaxString}, }, }; use serde::{Deserialize, Deserializer, Serialize}; #[derive(Deserialize, Debug, Clone)] pub struct CustomAtRuleConfig { #[serde(default, deserialize_with = "deserialize_prelude")] prelude: Option, body: Option, } fn deserialize_prelude<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let s = Option::>::deserialize(deserializer)?; if let Some(s) = s { Ok(Some( SyntaxString::parse_string(&s).map_err(|_| serde::de::Error::custom("invalid syntax string"))?, )) } else { Ok(None) } } #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] enum CustomAtRuleBodyType { DeclarationList, RuleList, StyleBlock, } pub struct Prelude<'i> { name: CowArcStr<'i>, prelude: Option>, } #[derive(Serialize, Deserialize, Clone)] pub struct AtRule<'i> { #[serde(borrow)] pub name: CowArcStr<'i>, pub prelude: Option>, pub body: Option>, pub loc: Location, } #[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type", content = "value", rename_all = "kebab-case")] pub enum AtRuleBody<'i> { #[serde(borrow)] DeclarationList(DeclarationBlock<'i>), RuleList(CssRuleList<'i, AtRule<'i>>), } #[derive(Clone)] pub struct CustomAtRuleParser { pub configs: HashMap, } impl<'i> AtRuleParser<'i> for CustomAtRuleParser { type Prelude = Prelude<'i>; type Error = ParserError<'i>; type AtRule = AtRule<'i>; fn parse_prelude<'t>( &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, _options: &ParserOptions<'_, 'i>, ) -> Result> { if let Some(config) = self.configs.get(name.as_ref()) { let prelude = if let Some(prelude) = &config.prelude { Some(prelude.parse_value(input)?) } else { None }; Ok(Prelude { name: name.into(), prelude, }) } else { Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) } } fn parse_block<'t>( &mut self, prelude: Self::Prelude, start: &ParserState, input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>, is_nested: bool, ) -> Result> { let config = self.configs.get(prelude.name.as_ref()).unwrap(); let body = if let Some(body) = &config.body { match body { CustomAtRuleBodyType::DeclarationList => { Some(AtRuleBody::DeclarationList(DeclarationBlock::parse(input, options)?)) } CustomAtRuleBodyType::RuleList => { Some(AtRuleBody::RuleList(CssRuleList::parse_with(input, options, self)?)) } CustomAtRuleBodyType::StyleBlock => Some(AtRuleBody::RuleList(CssRuleList::parse_style_block_with( input, options, self, is_nested, )?)), } } else { return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)); }; let loc = start.source_location(); Ok(AtRule { name: prelude.name, prelude: prelude.prelude, body, loc: Location { source_index: options.source_index, line: loc.line, column: loc.column, }, }) } fn rule_without_block( &mut self, prelude: Self::Prelude, start: &ParserState, options: &ParserOptions<'_, 'i>, _is_nested: bool, ) -> Result { let config = self.configs.get(prelude.name.as_ref()).unwrap(); if config.body.is_some() { return Err(()); } let loc = start.source_location(); Ok(AtRule { name: prelude.name, prelude: prelude.prelude, body: None, loc: Location { source_index: options.source_index, line: loc.line, column: loc.column, }, }) } } impl<'i> ToCss for AtRule<'i> { fn to_css( &self, dest: &mut lightningcss::printer::Printer, ) -> Result<(), lightningcss::error::PrinterError> where W: std::fmt::Write, { dest.write_char('@')?; serialize_identifier(&self.name, dest)?; if let Some(prelude) = &self.prelude { dest.write_char(' ')?; prelude.to_css(dest)?; } if let Some(body) = &self.body { match body { AtRuleBody::DeclarationList(decls) => { decls.to_css_block(dest)?; } AtRuleBody::RuleList(rules) => { dest.whitespace()?; dest.write_char('{')?; dest.indent(); dest.newline()?; rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}')?; } } } Ok(()) } } #[cfg(feature = "visitor")] use lightningcss::visitor::{Visit, VisitTypes, Visitor}; #[cfg(feature = "visitor")] impl<'i, V: Visitor<'i, AtRule<'i>>> Visit<'i, AtRule<'i>, V> for AtRule<'i> { const CHILD_TYPES: VisitTypes = VisitTypes::empty(); fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> { self.prelude.visit(visitor)?; match &mut self.body { Some(AtRuleBody::DeclarationList(decls)) => decls.visit(visitor), Some(AtRuleBody::RuleList(rules)) => rules.visit(visitor), None => Ok(()), } } } ================================================ FILE: napi/src/lib.rs ================================================ #[cfg(feature = "bundler")] use at_rule_parser::AtRule; use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser}; use lightningcss::bundler::BundleErrorKind; #[cfg(feature = "bundler")] use lightningcss::bundler::{Bundler, SourceProvider}; use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; use lightningcss::dependencies::{Dependency, DependencyOptions}; use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind}; use lightningcss::stylesheet::{ MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, }; use lightningcss::targets::{Browsers, Features, Targets}; use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; use napi::{CallContext, Env, JsObject, JsUnknown}; use parcel_sourcemap::SourceMap; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; mod at_rule_parser; #[cfg(feature = "bundler")] #[cfg(not(target_arch = "wasm32"))] mod threadsafe_function; #[cfg(feature = "visitor")] mod transformer; mod utils; #[cfg(feature = "visitor")] use transformer::JsVisitor; #[cfg(not(feature = "visitor"))] struct JsVisitor; #[cfg(feature = "visitor")] use lightningcss::visitor::Visit; use utils::get_named_property; #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct TransformResult<'i> { #[serde(with = "serde_bytes")] code: Vec, #[serde(with = "serde_bytes")] map: Option>, exports: Option, references: Option, dependencies: Option>, warnings: Vec>, } impl<'i> TransformResult<'i> { fn into_js(self, env: Env) -> napi::Result { // Manually construct buffers so we avoid a copy and work around // https://github.com/napi-rs/napi-rs/issues/1124. let mut obj = env.create_object()?; let buf = env.create_buffer_with_data(self.code)?; obj.set_named_property("code", buf.into_raw())?; obj.set_named_property( "map", if let Some(map) = self.map { let buf = env.create_buffer_with_data(map)?; buf.into_raw().into_unknown() } else { env.get_null()?.into_unknown() }, )?; obj.set_named_property("exports", env.to_js_value(&self.exports)?)?; obj.set_named_property("references", env.to_js_value(&self.references)?)?; obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?; obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?; Ok(obj.into_unknown()) } } #[cfg(feature = "visitor")] fn get_visitor(env: Env, opts: &JsObject) -> Option { if let Ok(visitor) = get_named_property::(opts, "visitor") { Some(JsVisitor::new(env, visitor)) } else { None } } #[cfg(not(feature = "visitor"))] fn get_visitor(_env: Env, _opts: &JsObject) -> Option { None } pub fn transform(ctx: CallContext) -> napi::Result { let opts = ctx.get::(0)?; let mut visitor = get_visitor(*ctx.env, &opts); let config: Config = ctx.env.from_js_value(opts)?; let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; let res = compile(code, &config, &mut visitor); match res { Ok(res) => res.into_js(*ctx.env), Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), } } pub fn transform_style_attribute(ctx: CallContext) -> napi::Result { let opts = ctx.get::(0)?; let mut visitor = get_visitor(*ctx.env, &opts); let config: AttrConfig = ctx.env.from_js_value(opts)?; let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; let res = compile_attr(code, &config, &mut visitor); match res { Ok(res) => res.into_js(ctx), Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), } } #[cfg(feature = "bundler")] #[cfg(not(target_arch = "wasm32"))] mod bundle { use super::*; use crossbeam_channel::{self, Receiver, Sender}; use lightningcss::bundler::{FileProvider, ResolveResult}; use napi::{Env, JsBoolean, JsFunction, JsString, NapiRaw}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Mutex; use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}; pub fn bundle(ctx: CallContext) -> napi::Result { let opts = ctx.get::(0)?; let mut visitor = get_visitor(*ctx.env, &opts); let config: BundleConfig = ctx.env.from_js_value(opts)?; let fs = FileProvider::new(); // This is pretty silly, but works around a rust limitation that you cannot // explicitly annotate lifetime bounds on closures. fn annotate<'i, 'o, F>(f: F) -> F where F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, { f } let res = compile_bundle( &fs, &config, visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), ); match res { Ok(res) => res.into_js(*ctx.env), Err(err) => Err(err.into_js_error(*ctx.env, None)?), } } // A SourceProvider which calls JavaScript functions to resolve and read files. struct JsSourceProvider { resolve: Option>, read: Option>, inputs: Mutex>, } unsafe impl Sync for JsSourceProvider {} unsafe impl Send for JsSourceProvider {} // Allocate a single channel per thread to communicate with the JS thread. thread_local! { static CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); static RESOLVER_CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); } impl SourceProvider for JsSourceProvider { type Error = napi::Error; fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { let source = if let Some(read) = &self.read { CHANNEL.with(|channel| { let message = ReadMessage { file: file.to_str().unwrap().to_owned(), tx: channel.0.clone(), }; read.call(message, ThreadsafeFunctionCallMode::Blocking); channel.1.recv().unwrap() }) } else { Ok(std::fs::read_to_string(file)?) }; match source { Ok(source) => { // cache the result let ptr = Box::into_raw(Box::new(source)); self.inputs.lock().unwrap().push(ptr); // SAFETY: this is safe because the pointer is not dropped // until the JsSourceProvider is, and we never remove from the // list of pointers stored in the vector. Ok(unsafe { &*ptr }) } Err(e) => Err(e), } } fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { if let Some(resolve) = &self.resolve { return RESOLVER_CHANNEL.with(|channel| { let message = ResolveMessage { specifier: specifier.to_owned(), originating_file: originating_file.to_str().unwrap().to_owned(), tx: channel.0.clone(), }; resolve.call(message, ThreadsafeFunctionCallMode::Blocking); channel.1.recv().unwrap() }); } Ok(originating_file.with_file_name(specifier).into()) } } struct ResolveMessage { specifier: String, originating_file: String, tx: Sender>, } struct ReadMessage { file: String, tx: Sender>, } struct VisitMessage { stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>, tx: Sender>, } fn await_promise(env: Env, result: JsUnknown, tx: Sender>, parse: Cb) -> napi::Result<()> where T: 'static, Cb: 'static + Fn(JsUnknown) -> Result, { // If the result is a promise, wait for it to resolve, and send the result to the channel. // Otherwise, send the result immediately. if result.is_promise()? { let result: JsObject = result.try_into()?; let then: JsFunction = get_named_property(&result, "then")?; let tx2 = tx.clone(); let cb = env.create_function_from_closure("callback", move |ctx| { let res = parse(ctx.get::(0)?)?; tx.send(Ok(res)).unwrap(); ctx.env.get_undefined() })?; let eb = env.create_function_from_closure("error_callback", move |ctx| { let res = ctx.get::(0)?; tx2.send(Err(napi::Error::from(res))).unwrap(); ctx.env.get_undefined() })?; then.call(Some(&result), &[cb, eb])?; } else { let result = parse(result)?; tx.send(Ok(result)).unwrap(); } Ok(()) } fn resolve_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { let specifier = ctx.env.create_string(&ctx.value.specifier)?; let originating_file = ctx.env.create_string(&ctx.value.originating_file)?; let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?; await_promise(ctx.env, result, ctx.value.tx, move |unknown| { ctx.env.from_js_value(unknown) }) } fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { match res { Ok(_) => Ok(()), Err(e) => { tx.send(Err(e)).expect("send error"); Ok(()) } } } fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { let tx = ctx.value.tx.clone(); handle_error(tx, resolve_on_js_thread(ctx)) } fn read_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { let file = ctx.env.create_string(&ctx.value.file)?; let result = ctx.callback.unwrap().call(None, &[file])?; await_promise(ctx.env, result, ctx.value.tx, |unknown| { JsString::try_from(unknown)?.into_utf8()?.into_owned() }) } fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { let tx = ctx.value.tx.clone(); handle_error(tx, read_on_js_thread(ctx)) } pub fn bundle_async(ctx: CallContext) -> napi::Result { let opts = ctx.get::(0)?; let visitor = get_visitor(*ctx.env, &opts); let config: BundleConfig = ctx.env.from_js_value(&opts)?; if let Ok(resolver) = get_named_property::(&opts, "resolver") { let read = if resolver.has_named_property("read")? { let read = get_named_property::(&resolver, "read")?; Some(ThreadsafeFunction::create( ctx.env.raw(), unsafe { read.raw() }, 0, read_on_js_thread_wrapper, )?) } else { None }; let resolve = if resolver.has_named_property("resolve")? { let resolve = get_named_property::(&resolver, "resolve")?; Some(ThreadsafeFunction::create( ctx.env.raw(), unsafe { resolve.raw() }, 0, resolve_on_js_thread_wrapper, )?) } else { None }; let provider = JsSourceProvider { resolve, read, inputs: Mutex::new(Vec::new()), }; run_bundle_task(provider, config, visitor, *ctx.env) } else { let provider = FileProvider::new(); run_bundle_task(provider, config, visitor, *ctx.env) } } // Runs bundling on a background thread managed by rayon. This is similar to AsyncTask from napi-rs, however, // because we call back into the JS thread, which might call other tasks in the node threadpool (e.g. fs.readFile), // we may end up deadlocking if the number of rayon threads exceeds node's threadpool size. Therefore, we must // run bundling from a thread not managed by Node. fn run_bundle_task( provider: P, config: BundleConfig, visitor: Option, env: Env, ) -> napi::Result where P::Error: IntoJsError, { let (deferred, promise) = env.create_deferred()?; let tsfn = if let Some(mut visitor) = visitor { Some(ThreadsafeFunction::create( env.raw(), std::ptr::null_mut(), 0, move |ctx: ThreadSafeCallContext| { if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) { ctx.value.tx.send(Err(err)).expect("send error"); return Ok(()); } ctx.value.tx.send(Ok(Default::default())).expect("send error"); Ok(()) }, )?) } else { None }; // Run bundling task in rayon threadpool. rayon::spawn(move || { let res = compile_bundle( unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, &config, tsfn.map(move |tsfn| { move |stylesheet: &mut StyleSheet| { CHANNEL.with(|channel| { let message = VisitMessage { // SAFETY: we immediately lock the thread until we get a response, // so stylesheet cannot be dropped in that time. stylesheet: unsafe { std::mem::transmute::< &'_ mut StyleSheet<'_, '_, AtRule>, &'static mut StyleSheet<'static, 'static, AtRule>, >(stylesheet) }, tx: channel.0.clone(), }; tsfn.call(message, ThreadsafeFunctionCallMode::Blocking); channel.1.recv().expect("recv error").map(|_| ()) }) } }), ); deferred.resolve(move |env| match res { Ok(v) => v.into_js(env), Err(err) => Err(err.into_js_error(env, None)?), }); }); Ok(promise) } } #[cfg(feature = "bundler")] #[cfg(target_arch = "wasm32")] mod bundle { use super::*; use lightningcss::bundler::ResolveResult; use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref}; use std::cell::UnsafeCell; use std::path::Path; pub fn bundle(ctx: CallContext) -> napi::Result { let opts = ctx.get::(0)?; let mut visitor = get_visitor(*ctx.env, &opts); let resolver = get_named_property::(&opts, "resolver")?; let read = get_named_property::(&resolver, "read")?; let resolve = if resolver.has_named_property("resolve")? { let resolve = get_named_property::(&resolver, "resolve")?; Some(ctx.env.create_reference(resolve)?) } else { None }; let config: BundleConfig = ctx.env.from_js_value(opts)?; let provider = JsSourceProvider { env: ctx.env.clone(), resolve, read: ctx.env.create_reference(read)?, inputs: UnsafeCell::new(Vec::new()), }; // This is pretty silly, but works around a rust limitation that you cannot // explicitly annotate lifetime bounds on closures. fn annotate<'i, 'o, F>(f: F) -> F where F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, { f } let res = compile_bundle( &provider, &config, visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), ); match res { Ok(res) => res.into_js(*ctx.env), Err(err) => Err(err.into_js_error(*ctx.env, None)?), } } struct JsSourceProvider { env: Env, resolve: Option>, read: Ref<()>, inputs: UnsafeCell>, } impl Drop for JsSourceProvider { fn drop(&mut self) { if let Some(resolve) = &mut self.resolve { drop(resolve.unref(self.env)); } drop(self.read.unref(self.env)); } } unsafe impl Sync for JsSourceProvider {} unsafe impl Send for JsSourceProvider {} // This relies on Binaryen's Asyncify transform to allow Rust to call async JS functions from sync code. // See the comments in async.mjs for more details about how this works. extern "C" { fn await_promise_sync( promise: napi::sys::napi_value, result: *mut napi::sys::napi_value, error: *mut napi::sys::napi_value, ); } fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { if value.is_promise()? { let mut result = std::ptr::null_mut(); let mut error = std::ptr::null_mut(); unsafe { await_promise_sync(value.raw(), &mut result, &mut error) }; if !error.is_null() { let error = unsafe { JsUnknown::from_raw(env.raw(), error)? }; return Err(napi::Error::from(error)); } if result.is_null() { return Err(napi::Error::new(napi::Status::GenericFailure, "No result".to_string())); } value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; } Ok(value) } impl SourceProvider for JsSourceProvider { type Error = napi::Error; fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?; let file = self.env.create_string(file.to_str().unwrap())?; let source: JsUnknown = read.call(None, &[file])?; let source = get_result(self.env, source)?; let source: JsString = source.try_into()?; let source = source.into_utf8()?.into_owned()?; // cache the result let ptr = Box::into_raw(Box::new(source)); let inputs = unsafe { &mut *self.inputs.get() }; inputs.push(ptr); // SAFETY: this is safe because the pointer is not dropped // until the JsSourceProvider is, and we never remove from the // list of pointers stored in the vector. Ok(unsafe { &*ptr }) } fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { if let Some(resolve) = &self.resolve { let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?; let specifier = self.env.create_string(specifier)?; let originating_file = self.env.create_string(originating_file.to_str().unwrap())?; let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?; let result = get_result(self.env, result)?; let result = self.env.from_js_value(result)?; Ok(result) } else { Ok(ResolveResult::File(originating_file.with_file_name(specifier))) } } } } #[cfg(feature = "bundler")] pub use bundle::*; // --------------------------------------------- #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct Config { pub filename: Option, pub project_root: Option, #[serde(with = "serde_bytes")] pub code: Vec, pub targets: Option, #[serde(default)] pub include: u32, #[serde(default)] pub exclude: u32, pub minify: Option, pub source_map: Option, pub input_source_map: Option, pub drafts: Option, pub non_standard: Option, pub css_modules: Option, pub analyze_dependencies: Option, pub pseudo_classes: Option, pub unused_symbols: Option>, pub error_recovery: Option, pub custom_at_rules: Option>, } #[derive(Debug, Deserialize)] #[serde(untagged)] enum AnalyzeDependenciesOption { Bool(bool), Config(AnalyzeDependenciesConfig), } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct AnalyzeDependenciesConfig { preserve_imports: bool, } #[derive(Debug, Deserialize)] #[serde(untagged)] enum CssModulesOption { Bool(bool), Config(CssModulesConfig), } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct CssModulesConfig { pattern: Option, dashed_idents: Option, animation: Option, container: Option, grid: Option, custom_idents: Option, pure: Option, } #[cfg(feature = "bundler")] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BundleConfig { pub filename: String, pub project_root: Option, pub targets: Option, #[serde(default)] pub include: u32, #[serde(default)] pub exclude: u32, pub minify: Option, pub source_map: Option, pub drafts: Option, pub non_standard: Option, pub css_modules: Option, pub analyze_dependencies: Option, pub pseudo_classes: Option, pub unused_symbols: Option>, pub error_recovery: Option, pub custom_at_rules: Option>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct OwnedPseudoClasses { pub hover: Option, pub active: Option, pub focus: Option, pub focus_visible: Option, pub focus_within: Option, } impl<'a> Into> for &'a OwnedPseudoClasses { fn into(self) -> PseudoClasses<'a> { PseudoClasses { hover: self.hover.as_deref(), active: self.active.as_deref(), focus: self.focus.as_deref(), focus_visible: self.focus_visible.as_deref(), focus_within: self.focus_within.as_deref(), } } } #[derive(Serialize, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct Drafts { #[serde(default)] custom_media: bool, } #[derive(Serialize, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct NonStandard { #[serde(default)] deep_selector_combinator: bool, } fn compile<'i>( code: &'i str, config: &Config, #[allow(unused_variables)] visitor: &mut Option, ) -> Result, CompileError<'i, napi::Error>> { let drafts = config.drafts.as_ref(); let non_standard = config.non_standard.as_ref(); let warnings = Some(Arc::new(RwLock::new(Vec::new()))); let filename = config.filename.clone().unwrap_or_default(); let project_root = config.project_root.as_ref().map(|p| p.as_ref()); let mut source_map = if config.source_map.unwrap_or_default() { let mut sm = SourceMap::new(project_root.unwrap_or("/")); sm.add_source(&filename); sm.set_source_content(0, code)?; Some(sm) } else { None }; let res = { let mut flags = ParserFlags::empty(); flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); flags.set( ParserFlags::DEEP_SELECTOR_COMBINATOR, matches!(non_standard, Some(v) if v.deep_selector_combinator), ); let mut stylesheet = StyleSheet::parse_with( &code, ParserOptions { filename: filename.clone(), flags, css_modules: if let Some(css_modules) = &config.css_modules { match css_modules { CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), CssModulesOption::Bool(false) => None, CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { pattern: if let Some(pattern) = c.pattern.as_ref() { match lightningcss::css_modules::Pattern::parse(pattern) { Ok(p) => p, Err(e) => return Err(CompileError::PatternError(e)), } } else { Default::default() }, dashed_idents: c.dashed_idents.unwrap_or_default(), animation: c.animation.unwrap_or(true), container: c.container.unwrap_or(true), grid: c.grid.unwrap_or(true), custom_idents: c.custom_idents.unwrap_or(true), pure: c.pure.unwrap_or_default(), }), } } else { None }, source_index: 0, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), }, &mut CustomAtRuleParser { configs: config.custom_at_rules.clone().unwrap_or_default(), }, )?; #[cfg(feature = "visitor")] if let Some(visitor) = visitor.as_mut() { stylesheet.visit(visitor).map_err(CompileError::JsError)?; } let targets = Targets { browsers: config.targets, include: Features::from_bits_truncate(config.include), exclude: Features::from_bits_truncate(config.exclude), }; stylesheet.minify(MinifyOptions { targets, unused_symbols: config.unused_symbols.clone().unwrap_or_default(), })?; stylesheet.to_css(PrinterOptions { minify: config.minify.unwrap_or_default(), source_map: source_map.as_mut(), project_root, targets, analyze_dependencies: if let Some(d) = &config.analyze_dependencies { match d { AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { remove_imports: !c.preserve_imports, }), _ => None, } } else { None }, pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), })? }; let map = if let Some(mut source_map) = source_map { if let Some(input_source_map) = &config.input_source_map { if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) { let _ = source_map.extends(&mut sm); } } source_map.to_json(None).ok() } else { None }; Ok(TransformResult { code: res.code.into_bytes(), map: map.map(|m| m.into_bytes()), exports: res.exports, references: res.references, dependencies: res.dependencies, warnings: warnings.map_or(Vec::new(), |w| { Arc::try_unwrap(w) .unwrap() .into_inner() .unwrap() .into_iter() .map(|w| w.into()) .collect() }), }) } #[cfg(feature = "bundler")] fn compile_bundle< 'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, >( fs: &'i P, config: &'o BundleConfig, visit: Option, ) -> Result, CompileError<'i, P::Error>> { use std::path::Path; let project_root = config.project_root.as_ref().map(|p| p.as_ref()); let mut source_map = if config.source_map.unwrap_or_default() { Some(SourceMap::new(project_root.unwrap_or("/"))) } else { None }; let warnings = Some(Arc::new(RwLock::new(Vec::new()))); let res = { let drafts = config.drafts.as_ref(); let non_standard = config.non_standard.as_ref(); let mut flags = ParserFlags::empty(); flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); flags.set( ParserFlags::DEEP_SELECTOR_COMBINATOR, matches!(non_standard, Some(v) if v.deep_selector_combinator), ); let parser_options = ParserOptions { flags, css_modules: if let Some(css_modules) = &config.css_modules { match css_modules { CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), CssModulesOption::Bool(false) => None, CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { pattern: if let Some(pattern) = c.pattern.as_ref() { match lightningcss::css_modules::Pattern::parse(pattern) { Ok(p) => p, Err(e) => return Err(CompileError::PatternError(e)), } } else { Default::default() }, dashed_idents: c.dashed_idents.unwrap_or_default(), animation: c.animation.unwrap_or(true), container: c.container.unwrap_or(true), grid: c.grid.unwrap_or(true), custom_idents: c.custom_idents.unwrap_or(true), pure: c.pure.unwrap_or_default(), }), } } else { None }, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), filename: String::new(), source_index: 0, }; let mut at_rule_parser = CustomAtRuleParser { configs: config.custom_at_rules.clone().unwrap_or_default(), }; let mut bundler = Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser); let mut stylesheet = bundler.bundle(Path::new(&config.filename))?; if let Some(visit) = visit { visit(&mut stylesheet).map_err(CompileError::JsError)?; } let targets = Targets { browsers: config.targets, include: Features::from_bits_truncate(config.include), exclude: Features::from_bits_truncate(config.exclude), }; stylesheet.minify(MinifyOptions { targets, unused_symbols: config.unused_symbols.clone().unwrap_or_default(), })?; stylesheet.to_css(PrinterOptions { minify: config.minify.unwrap_or_default(), source_map: source_map.as_mut(), project_root, targets, analyze_dependencies: if let Some(d) = &config.analyze_dependencies { match d { AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { remove_imports: !c.preserve_imports, }), _ => None, } } else { None }, pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), })? }; let map = if let Some(source_map) = &mut source_map { source_map.to_json(None).ok() } else { None }; Ok(TransformResult { code: res.code.into_bytes(), map: map.map(|m| m.into_bytes()), exports: res.exports, references: res.references, dependencies: res.dependencies, warnings: warnings.map_or(Vec::new(), |w| { Arc::try_unwrap(w) .unwrap() .into_inner() .unwrap() .into_iter() .map(|w| w.into()) .collect() }), }) } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct AttrConfig { pub filename: Option, #[serde(with = "serde_bytes")] pub code: Vec, pub targets: Option, #[serde(default)] pub include: u32, #[serde(default)] pub exclude: u32, #[serde(default)] pub minify: bool, #[serde(default)] pub analyze_dependencies: bool, #[serde(default)] pub error_recovery: bool, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct AttrResult<'i> { #[serde(with = "serde_bytes")] code: Vec, dependencies: Option>, warnings: Vec>, } impl<'i> AttrResult<'i> { fn into_js(self, ctx: CallContext) -> napi::Result { // Manually construct buffers so we avoid a copy and work around // https://github.com/napi-rs/napi-rs/issues/1124. let mut obj = ctx.env.create_object()?; let buf = ctx.env.create_buffer_with_data(self.code)?; obj.set_named_property("code", buf.into_raw())?; obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?; obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?; Ok(obj.into_unknown()) } } fn compile_attr<'i>( code: &'i str, config: &AttrConfig, #[allow(unused_variables)] visitor: &mut Option, ) -> Result, CompileError<'i, napi::Error>> { let warnings = if config.error_recovery { Some(Arc::new(RwLock::new(Vec::new()))) } else { None }; let res = { let filename = config.filename.clone().unwrap_or_default(); let mut attr = StyleAttribute::parse( &code, ParserOptions { filename, error_recovery: config.error_recovery, warnings: warnings.clone(), ..ParserOptions::default() }, )?; #[cfg(feature = "visitor")] if let Some(visitor) = visitor.as_mut() { attr.visit(visitor).unwrap(); } let targets = Targets { browsers: config.targets, include: Features::from_bits_truncate(config.include), exclude: Features::from_bits_truncate(config.exclude), }; attr.minify(MinifyOptions { targets, ..MinifyOptions::default() }); attr.to_css(PrinterOptions { minify: config.minify, source_map: None, project_root: None, targets, analyze_dependencies: if config.analyze_dependencies { Some(DependencyOptions::default()) } else { None }, pseudo_classes: None, })? }; Ok(AttrResult { code: res.code.into_bytes(), dependencies: res.dependencies, warnings: warnings.map_or(Vec::new(), |w| { Arc::try_unwrap(w) .unwrap() .into_inner() .unwrap() .into_iter() .map(|w| w.into()) .collect() }), }) } enum CompileError<'i, E: std::error::Error> { ParseError(Error>), MinifyError(Error), PrinterError(Error), SourceMapError(parcel_sourcemap::SourceMapError), BundleError(Error>), PatternError(PatternParseError), #[cfg(feature = "visitor")] JsError(napi::Error), } impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { CompileError::ParseError(err) => err.kind.fmt(f), CompileError::MinifyError(err) => err.kind.fmt(f), CompileError::PrinterError(err) => err.kind.fmt(f), CompileError::BundleError(err) => err.kind.fmt(f), CompileError::PatternError(err) => err.fmt(f), CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this #[cfg(feature = "visitor")] CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f), } } } impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> { fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result { let reason = self.to_string(); let data = match &self { CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?, CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?, CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?, CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?, _ => env.get_null()?.into_unknown(), }; let (js_error, loc) = match self { CompileError::BundleError(Error { loc, kind: BundleErrorKind::ResolverError(e), }) => { // Add location info to existing JS error if available. (e.into_js_error(env)?, loc) } CompileError::ParseError(Error { loc, .. }) | CompileError::PrinterError(Error { loc, .. }) | CompileError::MinifyError(Error { loc, .. }) | CompileError::BundleError(Error { loc, .. }) => { // Generate an error with location information. let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; let reason = env.create_string_from_std(reason)?; let obj = syntax_error.new_instance(&[reason])?; (obj.into_unknown(), loc) } _ => return Ok(self.into()), }; if js_error.get_type()? == napi::ValueType::Object { let mut obj: JsObject = unsafe { js_error.cast() }; if let Some(loc) = loc { let line = env.create_int32((loc.line + 1) as i32)?; let col = env.create_int32(loc.column as i32)?; let filename = env.create_string_from_std(loc.filename)?; obj.set_named_property("fileName", filename)?; if let Some(code) = code { let source = env.create_string(code)?; obj.set_named_property("source", source)?; } let mut loc = env.create_object()?; loc.set_named_property("line", line)?; loc.set_named_property("column", col)?; obj.set_named_property("loc", loc)?; } obj.set_named_property("data", data)?; Ok(obj.into_unknown().into()) } else { Ok(js_error.into()) } } } trait IntoJsError { fn into_js_error(self, env: Env) -> napi::Result; } impl IntoJsError for std::io::Error { fn into_js_error(self, env: Env) -> napi::Result { let reason = self.to_string(); let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; let reason = env.create_string_from_std(reason)?; let obj = syntax_error.new_instance(&[reason])?; Ok(obj.into_unknown()) } } impl IntoJsError for napi::Error { fn into_js_error(self, env: Env) -> napi::Result { unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) } } } impl<'i, E: std::error::Error> From>> for CompileError<'i, E> { fn from(e: Error>) -> CompileError<'i, E> { CompileError::ParseError(e) } } impl<'i, E: std::error::Error> From> for CompileError<'i, E> { fn from(err: Error) -> CompileError<'i, E> { CompileError::MinifyError(err) } } impl<'i, E: std::error::Error> From> for CompileError<'i, E> { fn from(err: Error) -> CompileError<'i, E> { CompileError::PrinterError(err) } } impl<'i, E: std::error::Error> From for CompileError<'i, E> { fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> { CompileError::SourceMapError(e) } } impl<'i, E: std::error::Error> From>> for CompileError<'i, E> { fn from(e: Error>) -> CompileError<'i, E> { CompileError::BundleError(e) } } impl<'i, E: std::error::Error> From> for napi::Error { fn from(e: CompileError<'i, E>) -> napi::Error { match e { CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()), CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()), #[cfg(feature = "visitor")] CompileError::JsError(e) => e, _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()), } } } #[derive(Serialize)] struct Warning<'i> { message: String, #[serde(flatten)] data: ParserError<'i>, loc: Option, } impl<'i> From>> for Warning<'i> { fn from(mut e: Error>) -> Self { // Convert to 1-based line numbers. if let Some(loc) = &mut e.loc { loc.line += 1; } Warning { message: e.kind.to_string(), data: e.kind, loc: e.loc, } } } ================================================ FILE: napi/src/threadsafe_function.rs ================================================ // Fork of threadsafe_function from napi-rs that allows calling JS function manually rather than // only returning args. This enables us to use the return value of the function. #![allow(clippy::single_component_path_imports)] use std::convert::Into; use std::ffi::CString; use std::marker::PhantomData; use std::os::raw::c_void; use std::ptr; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use napi::{check_status, sys, Env, Result, Status}; use napi::{JsError, JsFunction, NapiValue}; /// ThreadSafeFunction Context object /// the `value` is the value passed to `call` method pub struct ThreadSafeCallContext { pub env: Env, pub value: T, pub callback: Option, } #[repr(u8)] pub enum ThreadsafeFunctionCallMode { NonBlocking, Blocking, } impl From for sys::napi_threadsafe_function_call_mode { fn from(value: ThreadsafeFunctionCallMode) -> Self { match value { ThreadsafeFunctionCallMode::Blocking => sys::ThreadsafeFunctionCallMode::blocking, ThreadsafeFunctionCallMode::NonBlocking => sys::ThreadsafeFunctionCallMode::nonblocking, } } } /// Communicate with the addon's main thread by invoking a JavaScript function from other threads. /// /// ## Example /// An example of using `ThreadsafeFunction`: /// /// ```rust /// #[macro_use] /// extern crate napi_derive; /// /// use std::thread; /// /// use napi::{ /// threadsafe_function::{ /// ThreadSafeCallContext, ThreadsafeFunctionCallMode, ThreadsafeFunctionReleaseMode, /// }, /// CallContext, Error, JsFunction, JsNumber, JsUndefined, Result, Status, /// }; /// /// #[js_function(1)] /// pub fn test_threadsafe_function(ctx: CallContext) -> Result { /// let func = ctx.get::(0)?; /// /// let tsfn = /// ctx /// .env /// .create_threadsafe_function(&func, 0, |ctx: ThreadSafeCallContext>| { /// ctx.value /// .iter() /// .map(|v| ctx.env.create_uint32(*v)) /// .collect::>>() /// })?; /// /// let tsfn_cloned = tsfn.clone(); /// /// thread::spawn(move || { /// let output: Vec = vec![0, 1, 2, 3]; /// // It's okay to call a threadsafe function multiple times. /// tsfn.call(Ok(output.clone()), ThreadsafeFunctionCallMode::Blocking); /// }); /// /// thread::spawn(move || { /// let output: Vec = vec![3, 2, 1, 0]; /// // It's okay to call a threadsafe function multiple times. /// tsfn_cloned.call(Ok(output.clone()), ThreadsafeFunctionCallMode::NonBlocking); /// }); /// /// ctx.env.get_undefined() /// } /// ``` pub struct ThreadsafeFunction { raw_tsfn: sys::napi_threadsafe_function, aborted: Arc, ref_count: Arc, _phantom: PhantomData, } impl Clone for ThreadsafeFunction { fn clone(&self) -> Self { if !self.aborted.load(Ordering::Acquire) { let acquire_status = unsafe { sys::napi_acquire_threadsafe_function(self.raw_tsfn) }; debug_assert!( acquire_status == sys::Status::napi_ok, "Acquire threadsafe function failed in clone" ); } Self { raw_tsfn: self.raw_tsfn, aborted: Arc::clone(&self.aborted), ref_count: Arc::clone(&self.ref_count), _phantom: PhantomData, } } } unsafe impl Send for ThreadsafeFunction {} unsafe impl Sync for ThreadsafeFunction {} impl ThreadsafeFunction { /// See [napi_create_threadsafe_function](https://nodejs.org/api/n-api.html#n_api_napi_create_threadsafe_function) /// for more information. pub(crate) fn create) -> Result<()>>( env: sys::napi_env, func: sys::napi_value, max_queue_size: usize, callback: R, ) -> Result { let mut async_resource_name = ptr::null_mut(); let s = "napi_rs_threadsafe_function"; let len = s.len(); let s = CString::new(s)?; check_status!(unsafe { sys::napi_create_string_utf8(env, s.as_ptr(), len, &mut async_resource_name) })?; let initial_thread_count = 1usize; let mut raw_tsfn = ptr::null_mut(); let ptr = Box::into_raw(Box::new(callback)) as *mut c_void; check_status!(unsafe { sys::napi_create_threadsafe_function( env, func, ptr::null_mut(), async_resource_name, max_queue_size, initial_thread_count, ptr, Some(thread_finalize_cb::), ptr, Some(call_js_cb::), &mut raw_tsfn, ) })?; let aborted = Arc::new(AtomicBool::new(false)); let aborted_ptr = Arc::into_raw(aborted.clone()) as *mut c_void; check_status!(unsafe { sys::napi_add_env_cleanup_hook(env, Some(cleanup_cb), aborted_ptr) })?; Ok(ThreadsafeFunction { raw_tsfn, aborted, ref_count: Arc::new(AtomicUsize::new(initial_thread_count)), _phantom: PhantomData, }) } } impl ThreadsafeFunction { /// See [napi_call_threadsafe_function](https://nodejs.org/api/n-api.html#n_api_napi_call_threadsafe_function) /// for more information. pub fn call(&self, value: T, mode: ThreadsafeFunctionCallMode) -> Status { if self.aborted.load(Ordering::Acquire) { return Status::Closing; } unsafe { sys::napi_call_threadsafe_function(self.raw_tsfn, Box::into_raw(Box::new(value)) as *mut _, mode.into()) } .into() } } impl Drop for ThreadsafeFunction { fn drop(&mut self) { if !self.aborted.load(Ordering::Acquire) && self.ref_count.load(Ordering::Acquire) > 0usize { let release_status = unsafe { sys::napi_release_threadsafe_function(self.raw_tsfn, sys::ThreadsafeFunctionReleaseMode::release) }; assert!( release_status == sys::Status::napi_ok, "Threadsafe Function release failed" ); } } } unsafe extern "C" fn cleanup_cb(cleanup_data: *mut c_void) { let aborted = Arc::::from_raw(cleanup_data.cast()); aborted.store(true, Ordering::SeqCst); } unsafe extern "C" fn thread_finalize_cb( _raw_env: sys::napi_env, finalize_data: *mut c_void, _finalize_hint: *mut c_void, ) where R: 'static + Send + FnMut(ThreadSafeCallContext) -> Result<()>, { // cleanup drop(Box::::from_raw(finalize_data.cast())); } unsafe extern "C" fn call_js_cb( raw_env: sys::napi_env, js_callback: sys::napi_value, context: *mut c_void, data: *mut c_void, ) where R: 'static + Send + FnMut(ThreadSafeCallContext) -> Result<()>, { // env and/or callback can be null when shutting down if raw_env.is_null() { return; } let ctx: &mut R = &mut *context.cast::(); let val: Result = Ok(*Box::::from_raw(data.cast())); let mut recv = ptr::null_mut(); sys::napi_get_undefined(raw_env, &mut recv); let ret = val.and_then(|v| { (ctx)(ThreadSafeCallContext { env: Env::from_raw(raw_env), value: v, callback: if js_callback.is_null() { None } else { Some(JsFunction::from_raw(raw_env, js_callback).unwrap()) // TODO: unwrap }, }) }); let status = match ret { Ok(()) => sys::Status::napi_ok, Err(e) => sys::napi_fatal_exception(raw_env, JsError::from(e).into_value(raw_env)), }; if status == sys::Status::napi_ok { return; } if status == sys::Status::napi_pending_exception { let mut error_result = ptr::null_mut(); assert_eq!( sys::napi_get_and_clear_last_exception(raw_env, &mut error_result), sys::Status::napi_ok ); // When shutting down, napi_fatal_exception sometimes returns another exception let stat = sys::napi_fatal_exception(raw_env, error_result); assert!(stat == sys::Status::napi_ok || stat == sys::Status::napi_pending_exception); } else { let error_code: Status = status.into(); let error_code_string = format!("{:?}", error_code); let mut error_code_value = ptr::null_mut(); assert_eq!( sys::napi_create_string_utf8( raw_env, error_code_string.as_ptr() as *const _, error_code_string.len(), &mut error_code_value, ), sys::Status::napi_ok, ); let error_msg = "Call JavaScript callback failed in thread safe function"; let mut error_msg_value = ptr::null_mut(); assert_eq!( sys::napi_create_string_utf8( raw_env, error_msg.as_ptr() as *const _, error_msg.len(), &mut error_msg_value, ), sys::Status::napi_ok, ); let mut error_value = ptr::null_mut(); assert_eq!( sys::napi_create_error(raw_env, error_code_value, error_msg_value, &mut error_value), sys::Status::napi_ok, ); assert_eq!(sys::napi_fatal_exception(raw_env, error_value), sys::Status::napi_ok); } } ================================================ FILE: napi/src/transformer.rs ================================================ use std::{ marker::PhantomData, ops::{Index, IndexMut}, }; use lightningcss::{ media_query::MediaFeatureValue, properties::{ custom::{Token, TokenList, TokenOrValue}, Property, }, rules::{CssRule, CssRuleList}, stylesheet::ParserOptions, traits::ParseWithOptions, values::{ ident::Ident, length::{Length, LengthValue}, string::CowArcStr, }, visitor::{Visit, VisitTypes, Visitor}, }; use lightningcss::{stylesheet::StyleSheet, traits::IntoOwned}; use napi::{Env, JsFunction, JsObject, JsUnknown, Ref, ValueType}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use crate::{at_rule_parser::AtRule, utils::get_named_property}; pub struct JsVisitor { env: Env, visit_stylesheet: VisitorsRef, visit_rule: VisitorsRef, rule_map: VisitorsRef, property_map: VisitorsRef, visit_declaration: VisitorsRef, visit_length: Option>, visit_angle: Option>, visit_ratio: Option>, visit_resolution: Option>, visit_time: Option>, visit_color: Option>, visit_image: VisitorsRef, visit_url: Option>, visit_media_query: VisitorsRef, visit_supports_condition: VisitorsRef, visit_custom_ident: Option>, visit_dashed_ident: Option>, visit_selector: Option>, visit_token: VisitorsRef, token_map: VisitorsRef, visit_function: VisitorsRef, function_map: VisitorsRef, visit_variable: VisitorsRef, visit_env: VisitorsRef, env_map: VisitorsRef, types: VisitTypes, } // This is so that the visitor can work with bundleAsync. // We ensure that we only call JsVisitor from the main JS thread. unsafe impl Send for JsVisitor {} #[derive(PartialEq, Eq, Clone, Copy)] enum VisitStage { Enter, Exit, } type VisitorsRef = Visitors>; struct Visitors { enter: Option, exit: Option, } impl Visitors { fn new(enter: Option, exit: Option) -> Self { Self { enter, exit } } fn for_stage(&self, stage: VisitStage) -> Option<&T> { match stage { VisitStage::Enter => self.enter.as_ref(), VisitStage::Exit => self.exit.as_ref(), } } } impl Visitors> { fn get(&self, env: &Env) -> Visitors { Visitors { enter: self.enter.as_ref().and_then(|p| env.get_reference_value_unchecked(p).ok()), exit: self.exit.as_ref().and_then(|p| env.get_reference_value_unchecked(p).ok()), } } } impl Visitors { fn named(&self, stage: VisitStage, name: &str) -> Option { self .for_stage(stage) .and_then(|m| get_named_property::(m, name).ok()) } fn custom(&self, stage: VisitStage, obj: &str, name: &str) -> Option { self .for_stage(stage) .and_then(|m| m.get_named_property::(obj).ok()) .and_then(|v| { match v.get_type() { Ok(ValueType::Function) => return v.try_into().ok(), Ok(ValueType::Object) => { let o: napi::Result = v.try_into(); if let Ok(o) = o { return get_named_property::(&o, name).ok(); } } _ => {} } None }) } } impl Drop for JsVisitor { fn drop(&mut self) { macro_rules! drop { ($id: ident) => { if let Some(v) = &mut self.$id { drop(v.unref(self.env)); } }; } macro_rules! drop_tuple { ($id: ident) => { if let Some(v) = &mut self.$id.enter { drop(v.unref(self.env)); } if let Some(v) = &mut self.$id.exit { drop(v.unref(self.env)); } }; } drop_tuple!(visit_stylesheet); drop_tuple!(visit_rule); drop_tuple!(rule_map); drop_tuple!(visit_declaration); drop_tuple!(property_map); drop!(visit_length); drop!(visit_angle); drop!(visit_ratio); drop!(visit_resolution); drop!(visit_time); drop!(visit_color); drop_tuple!(visit_image); drop!(visit_url); drop_tuple!(visit_media_query); drop_tuple!(visit_supports_condition); drop_tuple!(visit_variable); drop_tuple!(visit_env); drop_tuple!(env_map); drop!(visit_custom_ident); drop!(visit_dashed_ident); drop_tuple!(visit_function); drop_tuple!(function_map); drop!(visit_selector); drop_tuple!(visit_token); drop_tuple!(token_map); } } impl JsVisitor { pub fn new(env: Env, visitor: JsObject) -> Self { let mut types = VisitTypes::empty(); macro_rules! get { ($name: literal, $( $t: ident )|+) => {{ let res: Option = get_named_property(&visitor, $name).ok(); if res.is_some() { types |= $( VisitTypes::$t )|+; } // We must create a reference so that the garbage collector doesn't destroy // the function before we try to call it (in the async bundle case). res.and_then(|res| env.create_reference(res).ok()) }}; } macro_rules! map { ($name: literal, $( $t: ident )|+) => {{ let obj: Option = get_named_property(&visitor, $name).ok(); if obj.is_some() { types |= $( VisitTypes::$t )|+; } obj.and_then(|obj| env.create_reference(obj).ok()) }}; } Self { env, visit_stylesheet: VisitorsRef::new(get!("StyleSheet", RULES), get!("StyleSheetExit", RULES)), visit_rule: VisitorsRef::new(get!("Rule", RULES), get!("RuleExit", RULES)), rule_map: VisitorsRef::new(map!("Rule", RULES), get!("RuleExit", RULES)), visit_declaration: VisitorsRef::new(get!("Declaration", PROPERTIES), get!("DeclarationExit", PROPERTIES)), property_map: VisitorsRef::new(map!("Declaration", PROPERTIES), map!("DeclarationExit", PROPERTIES)), visit_length: get!("Length", LENGTHS), visit_angle: get!("Angle", ANGLES), visit_ratio: get!("Ratio", RATIOS), visit_resolution: get!("Resolution", RESOLUTIONS), visit_time: get!("Time", TIMES), visit_color: get!("Color", COLORS), visit_image: VisitorsRef::new(get!("Image", IMAGES), get!("ImageExit", IMAGES)), visit_url: get!("Url", URLS), visit_media_query: VisitorsRef::new( get!("MediaQuery", MEDIA_QUERIES), get!("MediaQueryExit", MEDIA_QUERIES), ), visit_supports_condition: VisitorsRef::new( get!("SupportsCondition", SUPPORTS_CONDITIONS), get!("SupportsConditionExit", SUPPORTS_CONDITIONS), ), visit_variable: VisitorsRef::new(get!("Variable", TOKENS), get!("VariableExit", TOKENS)), visit_env: VisitorsRef::new( get!("EnvironmentVariable", TOKENS | MEDIA_QUERIES | ENVIRONMENT_VARIABLES), get!( "EnvironmentVariableExit", TOKENS | MEDIA_QUERIES | ENVIRONMENT_VARIABLES ), ), env_map: VisitorsRef::new( map!("EnvironmentVariable", TOKENS | MEDIA_QUERIES | ENVIRONMENT_VARIABLES), map!( "EnvironmentVariableExit", TOKENS | MEDIA_QUERIES | ENVIRONMENT_VARIABLES ), ), visit_custom_ident: get!("CustomIdent", CUSTOM_IDENTS), visit_dashed_ident: get!("DashedIdent", DASHED_IDENTS), visit_function: VisitorsRef::new(get!("Function", TOKENS), get!("FunctionExit", TOKENS)), function_map: VisitorsRef::new(map!("Function", TOKENS), map!("FunctionExit", TOKENS)), visit_selector: get!("Selector", SELECTORS), visit_token: VisitorsRef::new(get!("Token", TOKENS), None), token_map: VisitorsRef::new(map!("Token", TOKENS), None), types, } } } impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { type Error = napi::Error; fn visit_types(&self) -> VisitTypes { self.types } fn visit_stylesheet<'o>(&mut self, stylesheet: &mut StyleSheet<'i, 'o, AtRule<'i>>) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::RULES) { let env = self.env; let visit_stylesheet = self.visit_stylesheet.get::(&env); if let Some(visit) = visit_stylesheet.for_stage(VisitStage::Enter) { call_visitor(&env, stylesheet, visit)? } stylesheet.visit_children(self)?; if let Some(visit) = visit_stylesheet.for_stage(VisitStage::Exit) { call_visitor(&env, stylesheet, visit)? } Ok(()) } else { stylesheet.visit_children(self) } } fn visit_rule_list( &mut self, rules: &mut lightningcss::rules::CssRuleList<'i, AtRule<'i>>, ) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::RULES) { let env = self.env; let rule_map = self.rule_map.get::(&env); let visit_rule = self.visit_rule.get::(&env); visit_list( rules, |value, stage| { // Use a more specific visitor function if available, but fall back to visit_rule. let name = match value { CssRule::Media(..) => "media", CssRule::Import(..) => "import", CssRule::Style(..) => "style", CssRule::Keyframes(..) => "keyframes", CssRule::FontFace(..) => "font-face", CssRule::FontPaletteValues(..) => "font-palette-values", CssRule::FontFeatureValues(..) => "font-feature-values", CssRule::Page(..) => "page", CssRule::Supports(..) => "supports", CssRule::CounterStyle(..) => "counter-style", CssRule::Namespace(..) => "namespace", CssRule::CustomMedia(..) => "custom-media", CssRule::LayerBlock(..) => "layer-block", CssRule::LayerStatement(..) => "layer-statement", CssRule::Property(..) => "property", CssRule::Container(..) => "container", CssRule::Scope(..) => "scope", CssRule::MozDocument(..) => "moz-document", CssRule::Nesting(..) => "nesting", CssRule::NestedDeclarations(..) => "nested-declarations", CssRule::Viewport(..) => "viewport", CssRule::StartingStyle(..) => "starting-style", CssRule::ViewTransition(..) => "view-transition", CssRule::Unknown(v) => { let name = v.name.as_ref(); if let Some(visit) = rule_map.custom(stage, "unknown", name) { let js_value = env.to_js_value(v)?; let res = visit.call(None, &[js_value])?; return env.from_js_value(res).map(serde_detach::detach); } else { "unknown" } } CssRule::Custom(c) => { let name = c.name.as_ref(); if let Some(visit) = rule_map.custom(stage, "custom", name) { let js_value = env.to_js_value(c)?; let res = visit.call(None, &[js_value])?; return env.from_js_value(res).map(serde_detach::detach); } else { "custom" } } CssRule::Ignored => return Ok(None), }; if let Some(visit) = rule_map.named(stage, name).as_ref().or(visit_rule.for_stage(stage)) { let js_value = env.to_js_value(value)?; let res = visit.call(None, &[js_value])?; env.from_js_value(res).map(serde_detach::detach) } else { Ok(None) } }, |rule| rule.visit_children(self), )?; Ok(()) } else { rules.visit_children(self) } } fn visit_declaration_block( &mut self, decls: &mut lightningcss::declaration::DeclarationBlock<'i>, ) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::PROPERTIES) { let env = self.env; let property_map = self.property_map.get::(&env); let visit_declaration = self.visit_declaration.get::(&env); visit_declaration_list( &env, &mut decls.important_declarations, &visit_declaration, &property_map, |property| property.visit_children(self), )?; visit_declaration_list( &env, &mut decls.declarations, &visit_declaration, &property_map, |property| property.visit_children(self), )?; Ok(()) } else { decls.visit_children(self) } } fn visit_length(&mut self, length: &mut LengthValue) -> Result<(), Self::Error> { visit(&self.env, length, &self.visit_length) } fn visit_angle(&mut self, angle: &mut lightningcss::values::angle::Angle) -> Result<(), Self::Error> { visit(&self.env, angle, &self.visit_angle) } fn visit_ratio(&mut self, ratio: &mut lightningcss::values::ratio::Ratio) -> Result<(), Self::Error> { visit(&self.env, ratio, &self.visit_ratio) } fn visit_resolution( &mut self, resolution: &mut lightningcss::values::resolution::Resolution, ) -> Result<(), Self::Error> { visit(&self.env, resolution, &self.visit_resolution) } fn visit_time(&mut self, time: &mut lightningcss::values::time::Time) -> Result<(), Self::Error> { visit(&self.env, time, &self.visit_time) } fn visit_color(&mut self, color: &mut lightningcss::values::color::CssColor) -> Result<(), Self::Error> { visit(&self.env, color, &self.visit_color) } fn visit_image(&mut self, image: &mut lightningcss::values::image::Image<'i>) -> Result<(), Self::Error> { visit(&self.env, image, &self.visit_image.enter)?; image.visit_children(self)?; visit(&self.env, image, &self.visit_image.exit) } fn visit_url(&mut self, url: &mut lightningcss::values::url::Url<'i>) -> Result<(), Self::Error> { visit(&self.env, url, &self.visit_url) } fn visit_media_list(&mut self, media: &mut lightningcss::media_query::MediaList<'i>) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::MEDIA_QUERIES) { let env = self.env; let visit_media_query = self.visit_media_query.get::(&env); visit_list( &mut media.media_queries, |value, stage| { if let Some(visit) = visit_media_query.for_stage(stage) { let js_value = env.to_js_value(value)?; let res = visit.call(None, &[js_value])?; env.from_js_value(res).map(serde_detach::detach) } else { Ok(None) } }, |q| q.visit_children(self), )?; Ok(()) } else { media.visit_children(self) } } fn visit_media_feature_value(&mut self, value: &mut MediaFeatureValue<'i>) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::ENVIRONMENT_VARIABLES) && matches!(value, MediaFeatureValue::Env(_)) { let env_map = self.env_map.get::(&self.env); let visit_env = self.visit_env.get::(&self.env); let call = |stage: VisitStage, value: &mut MediaFeatureValue, env: &Env| -> napi::Result<()> { let env_var = if let MediaFeatureValue::Env(env) = value { env } else { return Ok(()); }; let visit_type = env_map.named(stage, env_var.name.name()); let visit = visit_env.for_stage(stage); let new_value: Option = if let Some(visit) = visit_type.as_ref().or(visit) { let js_value = env.to_js_value(env_var)?; let res = visit.call(None, &[js_value])?; env.from_js_value(res).map(serde_detach::detach)? } else { None }; match new_value { None => return Ok(()), Some(TokenOrValue::Length(l)) => *value = MediaFeatureValue::Length(Length::Value(l)), Some(TokenOrValue::Resolution(r)) => *value = MediaFeatureValue::Resolution(r), Some(TokenOrValue::Token(Token::Number { value: n, .. })) => *value = MediaFeatureValue::Number(n), Some(TokenOrValue::Token(Token::Ident(ident))) => *value = MediaFeatureValue::Ident(Ident(ident)), // TODO: ratio _ => { return Err(napi::Error::new( napi::Status::InvalidArg, format!("invalid environment value in media query: {:?}", new_value), )) } } Ok(()) }; call(VisitStage::Enter, value, &self.env)?; value.visit_children(self)?; call(VisitStage::Exit, value, &self.env)?; return Ok(()); } value.visit_children(self) } fn visit_supports_condition( &mut self, condition: &mut lightningcss::rules::supports::SupportsCondition<'i>, ) -> Result<(), Self::Error> { visit(&self.env, condition, &self.visit_supports_condition.enter)?; condition.visit_children(self)?; visit(&self.env, condition, &self.visit_supports_condition.exit) } fn visit_custom_ident( &mut self, ident: &mut lightningcss::values::ident::CustomIdent, ) -> Result<(), Self::Error> { visit(&self.env, ident, &self.visit_custom_ident) } fn visit_dashed_ident( &mut self, ident: &mut lightningcss::values::ident::DashedIdent, ) -> Result<(), Self::Error> { visit(&self.env, ident, &self.visit_dashed_ident) } fn visit_selector_list( &mut self, selectors: &mut lightningcss::selector::SelectorList<'i>, ) -> Result<(), Self::Error> { if let Some(visit) = self .visit_selector .as_ref() .and_then(|v| self.env.get_reference_value_unchecked::(v).ok()) { map::<_, _, _, true>(&mut selectors.0, |value| { let js_value = self.env.to_js_value(value)?; let res = visit.call(None, &[js_value])?; self.env.from_js_value(res).map(serde_detach::detach) })?; } Ok(()) } fn visit_token_list( &mut self, tokens: &mut lightningcss::properties::custom::TokenList<'i>, ) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::TOKENS) { let env = self.env; let visit_token = self.visit_token.get::(&env); let token_map = self.token_map.get::(&env); let visit_function = self.visit_function.get::(&env); let function_map = self.function_map.get::(&env); let visit_variable = self.visit_variable.get::(&env); let visit_env = self.visit_env.get::(&env); let env_map = self.env_map.get::(&env); visit_list( &mut tokens.0, |value, stage| { let (visit_type, visit) = match value { TokenOrValue::Function(f) => ( function_map.named(stage, f.name.0.as_ref()), visit_function.for_stage(stage), ), TokenOrValue::Var(_) => (None, visit_variable.for_stage(stage)), TokenOrValue::Env(e) => (env_map.named(stage, e.name.name()), visit_env.for_stage(stage)), TokenOrValue::Token(t) => { let name = match t { Token::Ident(_) => Some("ident"), Token::AtKeyword(_) => Some("at-keyword"), Token::Hash(_) => Some("hash"), Token::IDHash(_) => Some("id-hash"), Token::String(_) => Some("string"), Token::Number { .. } => Some("number"), Token::Percentage { .. } => Some("percentage"), Token::Dimension { .. } => Some("dimension"), _ => None, }; let visit = if let Some(name) = name { token_map.named(stage, name) } else { None }; (visit, visit_token.for_stage(stage)) } _ => return Ok(None), }; if let Some(visit) = visit_type.as_ref().or(visit) { let js_value = match value { TokenOrValue::Function(f) => env.to_js_value(f)?, TokenOrValue::Var(v) => env.to_js_value(v)?, TokenOrValue::Env(v) => env.to_js_value(v)?, TokenOrValue::Token(t) => env.to_js_value(t)?, _ => unreachable!(), }; let res = visit.call(None, &[js_value])?; let res: Option = env.from_js_value(res).map(serde_detach::detach)?; Ok(res.map(|r| r.0)) } else { Ok(None) } }, |value| value.visit_children(self), )?; Ok(()) } else { tokens.visit_children(self) } } } fn visit>( env: &Env, value: &mut V, visit: &Option>, ) -> napi::Result<()> { if let Some(visit) = visit .as_ref() .and_then(|v| env.get_reference_value_unchecked::(v).ok()) { call_visitor(env, value, &visit)?; } Ok(()) } fn call_visitor>( env: &Env, value: &mut V, visit: &JsFunction, ) -> napi::Result<()> { let js_value = env.to_js_value(value)?; let res = visit.call(None, &[js_value])?; let new_value: Option = env.from_js_value(res).map(serde_detach::detach)?; match new_value { Some(new_value) => *value = new_value, None => {} } Ok(()) } fn visit_declaration_list<'i, C: FnMut(&mut Property<'i>) -> napi::Result<()>>( env: &Env, list: &mut Vec>, visit_declaration: &Visitors, property_map: &Visitors, visit_children: C, ) -> napi::Result<()> { visit_list( list, |value, stage| { // Use a specific property visitor if available, or fall back to Property visitor. let visit = match value { Property::Custom(v) => { if let Some(visit) = property_map.custom(stage, "custom", v.name.as_ref()) { let js_value = env.to_js_value(v)?; let res = visit.call(None, &[js_value])?; return env.from_js_value(res).map(serde_detach::detach); } else { None } } _ => property_map.named(stage, value.property_id().name()), }; if let Some(visit) = visit.as_ref().or(visit_declaration.for_stage(stage)) { let js_value = env.to_js_value(value)?; let res = visit.call(None, &[js_value])?; env.from_js_value(res).map(serde_detach::detach) } else { Ok(None) } }, visit_children, ) } fn visit_list< V, L: List, F: Fn(&mut V, VisitStage) -> napi::Result>>, C: FnMut(&mut V) -> napi::Result<()>, >( list: &mut L, visit: F, mut visit_children: C, ) -> napi::Result<()> { map(list, |value| { let mut new_value: Option> = visit(value, VisitStage::Enter)?; match &mut new_value { Some(ValueOrVec::Value(v)) => { visit_children(v)?; if let Some(val) = visit(v, VisitStage::Exit)? { new_value = Some(val); } } Some(ValueOrVec::Vec(v)) => { map(v, |value| { visit_children(value)?; visit(value, VisitStage::Exit) })?; } None => { visit_children(value)?; if let Some(val) = visit(value, VisitStage::Exit)? { new_value = Some(val); } } } Ok(new_value) }) } fn map, F: FnMut(&mut V) -> napi::Result>>, const IS_VEC: bool>( list: &mut L, mut f: F, ) -> napi::Result<()> { let mut i = 0; while i < list.len() { let value = &mut list[i]; let new_value = f(value)?; match new_value { Some(ValueOrVec::Value(v)) => { list[i] = v; i += 1; } Some(ValueOrVec::Vec(vec)) => { if vec.is_empty() { list.remove(i); } else { let len = vec.len(); list.replace(i, vec); i += len; } } None => { i += 1; } } } Ok(()) } #[derive(serde::Serialize)] #[serde(untagged)] enum ValueOrVec { Value(V), Vec(Vec), } // Manually implemented deserialize for better error messages. // https://github.com/serde-rs/serde/issues/773 impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de> for ValueOrVec { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { use serde::Deserializer; let content = serde_content::Value::deserialize(deserializer)?; let de = serde_content::Deserializer::new(content.clone()).coerce_numbers(); // Try to deserialize as a sequence first. let mut was_seq = false; let res = de.deserialize_seq(SeqVisitor { was_seq: &mut was_seq, phantom: PhantomData, }); if was_seq { // Allow fallback if we know the value is also a list (e.g. selector). if res.is_ok() || !IS_VEC { return res.map_err(|e| serde::de::Error::custom(e.to_string())).map(ValueOrVec::Vec); } } // If it wasn't a sequence, try a value. let de = serde_content::Deserializer::new(content).coerce_numbers(); return V::deserialize(de) .map_err(|e| serde::de::Error::custom(e.to_string())) .map(ValueOrVec::Value); struct SeqVisitor<'a, V> { was_seq: &'a mut bool, phantom: PhantomData, } impl<'a, 'de, V: serde::Deserialize<'de>> serde::de::Visitor<'de> for SeqVisitor<'a, V> { type Value = Vec; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a sequence") } fn visit_seq(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { *self.was_seq = true; let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or(1)); while let Some(v) = seq.next_element()? { vec.push(v); } Ok(vec) } } } } struct TokensOrRaw<'i>(ValueOrVec>); impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(serde::Deserialize)] struct Raw<'i> { #[serde(borrow)] raw: CowArcStr<'i>, } let content = serde_content::Value::deserialize(deserializer)?; let de = serde_content::Deserializer::new(content.clone()).coerce_numbers(); if let Ok(res) = Raw::deserialize(de) { let res = TokenList::parse_string_with_options(res.raw.as_ref(), ParserOptions::default()) .map_err(|_| serde::de::Error::custom("Could not parse value"))?; return Ok(TokensOrRaw(ValueOrVec::Vec(res.into_owned().0))); } let de = serde_content::Deserializer::new(content).coerce_numbers(); Ok(TokensOrRaw( ValueOrVec::deserialize(de).map_err(|e| serde::de::Error::custom(e.to_string()))?, )) } } trait List: Index + IndexMut { fn len(&self) -> usize; fn remove(&mut self, i: usize); fn replace(&mut self, i: usize, items: Vec); } impl List for Vec { fn len(&self) -> usize { Vec::len(self) } fn remove(&mut self, i: usize) { Vec::remove(self, i); } fn replace(&mut self, i: usize, items: Vec) { self.splice(i..i + 1, items); } } impl> List for SmallVec { fn len(&self) -> usize { SmallVec::len(self) } fn remove(&mut self, i: usize) { SmallVec::remove(self, i); } fn replace(&mut self, i: usize, items: Vec) { let len = items.len(); let mut iter = items.into_iter(); self[i] = iter.next().unwrap(); if len > 1 { self.insert_many(i + 1, iter); } } } impl<'i, R> List> for CssRuleList<'i, R> { fn len(&self) -> usize { self.0.len() } fn remove(&mut self, i: usize) { self[i] = CssRule::Ignored; } fn replace(&mut self, i: usize, items: Vec>) { self.0.replace(i, items) } } ================================================ FILE: napi/src/utils.rs ================================================ use napi::{Error, JsObject, JsUnknown, Result}; // Workaround for https://github.com/napi-rs/napi-rs/issues/1641 pub fn get_named_property>(obj: &JsObject, property: &str) -> Result { let unknown = obj.get_named_property::(property)?; T::try_from(unknown) } ================================================ FILE: node/Cargo.toml ================================================ [package] authors = ["Devon Govett "] name = "lightningcss_node" version = "0.1.0" edition = "2021" publish = false [lib] crate-type = ["cdylib"] [dependencies] lightningcss-napi = { version = "0.4.8", path = "../napi", features = [ "bundler", "visitor", ] } napi = { version = "2.15.4", default-features = false, features = [ "compat-mode", ] } napi-derive = "2" [target.'cfg(target_os = "macos")'.dependencies] jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"] } [target.'cfg(not(target_arch = "wasm32"))'.build-dependencies] napi-build = "1" ================================================ FILE: node/ast.d.ts ================================================ /* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, * and run json-schema-to-typescript to regenerate this file. */ export type String = string; /** * A CSS rule. */ export type Rule = | { type: "media"; value: MediaRule; } | { type: "import"; value: ImportRule; } | { type: "style"; value: StyleRule; } | { type: "keyframes"; value: KeyframesRule; } | { type: "font-face"; value: FontFaceRule; } | { type: "font-palette-values"; value: FontPaletteValuesRule; } | { type: "font-feature-values"; value: FontFeatureValuesRule; } | { type: "page"; value: PageRule; } | { type: "supports"; value: SupportsRule; } | { type: "counter-style"; value: CounterStyleRule; } | { type: "namespace"; value: NamespaceRule; } | { type: "moz-document"; value: MozDocumentRule; } | { type: "nesting"; value: NestingRule; } | { type: "nested-declarations"; value: NestedDeclarationsRule; } | { type: "viewport"; value: ViewportRule; } | { type: "custom-media"; value: CustomMediaRule; } | { type: "layer-statement"; value: LayerStatementRule; } | { type: "layer-block"; value: LayerBlockRule; } | { type: "property"; value: PropertyRule; } | { type: "container"; value: ContainerRule; } | { type: "scope"; value: ScopeRule; } | { type: "starting-style"; value: StartingStyleRule; } | { type: "view-transition"; value: ViewTransitionRule; } | { type: "ignored"; } | { type: "unknown"; value: UnknownAtRule; } | { type: "custom"; value: DefaultAtRule; }; /** * Represents a media condition. */ export type MediaCondition = | { type: "feature"; value: QueryFeatureFor_MediaFeatureId; } | { type: "not"; value: MediaCondition; } | { /** * The conditions for the operator. */ conditions: MediaCondition[]; /** * The operator for the conditions. */ operator: Operator; type: "operation"; } | { type: "unknown"; value: TokenOrValue[]; }; /** * A generic media feature or container feature. */ export type QueryFeatureFor_MediaFeatureId = | { /** * The name of the feature. */ name: MediaFeatureNameFor_MediaFeatureId; type: "plain"; /** * The feature value. */ value: MediaFeatureValue; } | { /** * The name of the feature. */ name: MediaFeatureNameFor_MediaFeatureId; type: "boolean"; } | { /** * The name of the feature. */ name: MediaFeatureNameFor_MediaFeatureId; /** * A comparator. */ operator: MediaFeatureComparison; type: "range"; /** * The feature value. */ value: MediaFeatureValue; } | { /** * The end value. */ end: MediaFeatureValue; /** * A comparator for the end value. */ endOperator: MediaFeatureComparison; /** * The name of the feature. */ name: MediaFeatureNameFor_MediaFeatureId; /** * A start value. */ start: MediaFeatureValue; /** * A comparator for the start value. */ startOperator: MediaFeatureComparison; type: "interval"; }; /** * A media feature name. */ export type MediaFeatureNameFor_MediaFeatureId = MediaFeatureId | String | String; /** * A media query feature identifier. */ export type MediaFeatureId = | "width" | "height" | "aspect-ratio" | "orientation" | "overflow-block" | "overflow-inline" | "horizontal-viewport-segments" | "vertical-viewport-segments" | "display-mode" | "resolution" | "scan" | "grid" | "update" | "environment-blending" | "color" | "color-index" | "monochrome" | "color-gamut" | "dynamic-range" | "inverted-colors" | "pointer" | "hover" | "any-pointer" | "any-hover" | "nav-controls" | "video-color-gamut" | "video-dynamic-range" | "scripting" | "prefers-reduced-motion" | "prefers-reduced-transparency" | "prefers-contrast" | "forced-colors" | "prefers-color-scheme" | "prefers-reduced-data" | "device-width" | "device-height" | "device-aspect-ratio" | "-webkit-device-pixel-ratio" | "-moz-device-pixel-ratio"; /** * [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query. * * See [MediaFeature](MediaFeature). */ export type MediaFeatureValue = | { type: "length"; value: Length; } | { type: "number"; value: number; } | { type: "integer"; value: number; } | { type: "boolean"; value: boolean; } | { type: "resolution"; value: Resolution; } | { type: "ratio"; value: Ratio; } | { type: "ident"; value: String; } | { type: "env"; value: EnvironmentVariable; }; /** * A CSS [``](https://www.w3.org/TR/css-values-4/#lengths) value, with support for `calc()`. */ export type Length = | { type: "value"; value: LengthValue; } | { type: "calc"; value: CalcFor_Length; }; export type LengthUnit = | "px" | "in" | "cm" | "mm" | "q" | "pt" | "pc" | "em" | "rem" | "ex" | "rex" | "ch" | "rch" | "cap" | "rcap" | "ic" | "ric" | "lh" | "rlh" | "vw" | "lvw" | "svw" | "dvw" | "cqw" | "vh" | "lvh" | "svh" | "dvh" | "cqh" | "vi" | "svi" | "lvi" | "dvi" | "cqi" | "vb" | "svb" | "lvb" | "dvb" | "cqb" | "vmin" | "svmin" | "lvmin" | "dvmin" | "cqmin" | "vmax" | "svmax" | "lvmax" | "dvmax" | "cqmax"; /** * A mathematical expression used within the [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function. * * This type supports generic value types. Values such as [Length](super::length::Length), [Percentage](super::percentage::Percentage), [Time](super::time::Time), and [Angle](super::angle::Angle) support `calc()` expressions. */ export type CalcFor_Length = | { type: "value"; value: Length; } | { type: "number"; value: number; } | { type: "sum"; /** * @minItems 2 * @maxItems 2 */ value: [CalcFor_Length, CalcFor_Length]; } | { type: "product"; /** * @minItems 2 * @maxItems 2 */ value: [number, CalcFor_Length]; } | { type: "function"; value: MathFunctionFor_Length; }; /** * A CSS [math function](https://www.w3.org/TR/css-values-4/#math-function). * * Math functions may be used in most properties and values that accept numeric values, including lengths, percentages, angles, times, etc. */ export type MathFunctionFor_Length = | { type: "calc"; value: CalcFor_Length; } | { type: "min"; value: CalcFor_Length[]; } | { type: "max"; value: CalcFor_Length[]; } | { type: "clamp"; /** * @minItems 3 * @maxItems 3 */ value: [CalcFor_Length, CalcFor_Length, CalcFor_Length]; } | { type: "round"; /** * @minItems 3 * @maxItems 3 */ value: [RoundingStrategy, CalcFor_Length, CalcFor_Length]; } | { type: "rem"; /** * @minItems 2 * @maxItems 2 */ value: [CalcFor_Length, CalcFor_Length]; } | { type: "mod"; /** * @minItems 2 * @maxItems 2 */ value: [CalcFor_Length, CalcFor_Length]; } | { type: "abs"; value: CalcFor_Length; } | { type: "sign"; value: CalcFor_Length; } | { type: "hypot"; value: CalcFor_Length[]; }; /** * A [rounding strategy](https://www.w3.org/TR/css-values-4/#typedef-rounding-strategy), as used in the `round()` function. */ export type RoundingStrategy = "nearest" | "up" | "down" | "to-zero"; /** * A CSS [``](https://www.w3.org/TR/css-values-4/#resolution) value. */ export type Resolution = | { type: "dpi"; value: number; } | { type: "dpcm"; value: number; } | { type: "dppx"; value: number; }; /** * A CSS [``](https://www.w3.org/TR/css-values-4/#ratios) value, representing the ratio of two numeric values. * * @minItems 2 * @maxItems 2 */ export type Ratio = [number, number]; /** * A raw CSS token, or a parsed value. */ export type TokenOrValue = | { type: "token"; value: Token; } | { type: "color"; value: CssColor; } | { type: "unresolved-color"; value: UnresolvedColor; } | { type: "url"; value: Url; } | { type: "var"; value: Variable; } | { type: "env"; value: EnvironmentVariable; } | { type: "function"; value: Function; } | { type: "length"; value: LengthValue; } | { type: "angle"; value: Angle; } | { type: "time"; value: Time; } | { type: "resolution"; value: Resolution; } | { type: "dashed-ident"; value: String; } | { type: "animation-name"; value: AnimationName; }; /** * A raw CSS token. */ export type Token = | { type: "ident"; value: String; } | { type: "at-keyword"; value: String; } | { type: "hash"; value: String; } | { type: "id-hash"; value: String; } | { type: "string"; value: String; } | { type: "unquoted-url"; value: String; } | { type: "delim"; value: string; } | { type: "number"; /** * The value as a float */ value: number; } | { type: "percentage"; /** * The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0. */ value: number; } | { type: "dimension"; /** * The unit, e.g. "px" in `12px` */ unit: String; /** * The value as a float */ value: number; } | { type: "white-space"; value: String; } | { type: "comment"; value: String; } | { type: "colon"; } | { type: "semicolon"; } | { type: "comma"; } | { type: "include-match"; } | { type: "dash-match"; } | { type: "prefix-match"; } | { type: "suffix-match"; } | { type: "substring-match"; } | { type: "cdo"; } | { type: "cdc"; } | { type: "function"; value: String; } | { type: "parenthesis-block"; } | { type: "square-bracket-block"; } | { type: "curly-bracket-block"; } | { type: "bad-url"; value: String; } | { type: "bad-string"; value: String; } | { type: "close-parenthesis"; } | { type: "close-square-bracket"; } | { type: "close-curly-bracket"; }; /** * A CSS [``](https://www.w3.org/TR/css-color-4/#color-type) value. * * CSS supports many different color spaces to represent colors. The most common values are stored as RGBA using a single byte per component. Less common values are stored using a `Box` to reduce the amount of memory used per color. * * Each color space is represented as a struct that implements the `From` and `Into` traits for all other color spaces, so it is possible to convert between color spaces easily. In addition, colors support [interpolation](#method.interpolate) as in the `color-mix()` function. */ export type CssColor = CurrentColor | RGBColor | LABColor | PredefinedColor | FloatColor | LightDark | SystemColor; export type CurrentColor = { type: "currentcolor"; }; export type RGBColor = { /** * The alpha component. */ alpha: number; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "rgb"; }; /** * A color in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions. */ export type LABColor = | { /** * The a component. */ a: number; /** * The alpha component. */ alpha: number; /** * The b component. */ b: number; /** * The lightness component. */ l: number; type: "lab"; } | { /** * The alpha component. */ alpha: number; /** * The chroma component. */ c: number; /** * The hue component. */ h: number; /** * The lightness component. */ l: number; type: "lch"; } | { /** * The a component. */ a: number; /** * The alpha component. */ alpha: number; /** * The b component. */ b: number; /** * The lightness component. */ l: number; type: "oklab"; } | { /** * The alpha component. */ alpha: number; /** * The chroma component. */ c: number; /** * The hue component. */ h: number; /** * The lightness component. */ l: number; type: "oklch"; }; /** * A color in a predefined color space, e.g. `display-p3`. */ export type PredefinedColor = | { /** * The alpha component. */ alpha: number; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "srgb"; } | { /** * The alpha component. */ alpha: number; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "srgb-linear"; } | { /** * The alpha component. */ alpha: number; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "display-p3"; } | { /** * The alpha component. */ alpha: number; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "a98-rgb"; } | { /** * The alpha component. */ alpha: number; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "prophoto-rgb"; } | { /** * The alpha component. */ alpha: number; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "rec2020"; } | { /** * The alpha component. */ alpha: number; type: "xyz-d50"; /** * The x component. */ x: number; /** * The y component. */ y: number; /** * The z component. */ z: number; } | { /** * The alpha component. */ alpha: number; type: "xyz-d65"; /** * The x component. */ x: number; /** * The y component. */ y: number; /** * The z component. */ z: number; }; /** * A floating point representation of color types that are usually stored as RGBA. These are used when there are any `none` components, which are represented as NaN. */ export type FloatColor = | { /** * The alpha component. */ alpha: number; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "rgb"; } | { /** * The alpha component. */ alpha: number; /** * The hue component. */ h: number; /** * The lightness component. */ l: number; /** * The saturation component. */ s: number; type: "hsl"; } | { /** * The alpha component. */ alpha: number; /** * The blackness component. */ b: number; /** * The hue component. */ h: number; type: "hwb"; /** * The whiteness component. */ w: number; }; export type LightDark = { dark: CssColor; light: CssColor; type: "light-dark"; }; /** * A CSS [system color](https://drafts.csswg.org/css-color/#css-system-colors) keyword. */ export type SystemColor = | "accentcolor" | "accentcolortext" | "activetext" | "buttonborder" | "buttonface" | "buttontext" | "canvas" | "canvastext" | "field" | "fieldtext" | "graytext" | "highlight" | "highlighttext" | "linktext" | "mark" | "marktext" | "selecteditem" | "selecteditemtext" | "visitedtext" | "activeborder" | "activecaption" | "appworkspace" | "background" | "buttonhighlight" | "buttonshadow" | "captiontext" | "inactiveborder" | "inactivecaption" | "inactivecaptiontext" | "infobackground" | "infotext" | "menu" | "menutext" | "scrollbar" | "threeddarkshadow" | "threedface" | "threedhighlight" | "threedlightshadow" | "threedshadow" | "window" | "windowframe" | "windowtext"; /** * A color value with an unresolved alpha value (e.g. a variable). These can be converted from the modern slash syntax to older comma syntax. This can only be done when the only unresolved component is the alpha since variables can resolve to multiple tokens. */ export type UnresolvedColor = | { /** * The unresolved alpha component. */ alpha: TokenOrValue[]; /** * The blue component. */ b: number; /** * The green component. */ g: number; /** * The red component. */ r: number; type: "rgb"; } | { /** * The unresolved alpha component. */ alpha: TokenOrValue[]; /** * The hue component. */ h: number; /** * The lightness component. */ l: number; /** * The saturation component. */ s: number; type: "hsl"; } | { /** * The dark value. */ dark: TokenOrValue[]; /** * The light value. */ light: TokenOrValue[]; type: "light-dark"; }; /** * Defines where the class names referenced in the `composes` property are located. * * See [Composes](Composes). */ export type Specifier = | { type: "global"; } | { type: "file"; value: String; } | { type: "source-index"; value: number; }; /** * A CSS [``](https://www.w3.org/TR/css-values-4/#angles) value. * * Angles may be explicit or computed by `calc()`, but are always stored and serialized as their computed value. */ export type Angle = | { type: "deg"; value: number; } | { type: "rad"; value: number; } | { type: "grad"; value: number; } | { type: "turn"; value: number; }; /** * A CSS [`
Time to minify Bootstrap 4 (~10,000 lines). See the readme for more benchmarks.

Live in the future

Lightning CSS lets you use modern CSS features and future syntax today. Features such as CSS nesting, custom media queries, high gamut color spaces, logical properties, and new selector features are automatically converted to more compatible syntax based on your browser targets.

Lightning CSS also automatically adds vendor prefixes for your browser targets, so you can keep your source code clean and repetition free.

Learn more →

Target Browsers

last 2 versions

Input

.foo {
  color: oklab(59.686% 0.1009 0.1192);
}

Output

.foo {
  color: #c65d07;
  color: color(display-p3 .724144 .386777 .148795);
  color: lab(52.2319% 40.1449 59.9171);
}

Crush it!

Lightning CSS is not only fast when it comes to build time. It produces smaller output, so your website loads faster too.

The Lightning CSS minifier combines longhand properties into shorthands, removes unnecessary vendor prefixes, merges compatible adjacent rules, removes unnecessary default values, reduces calc() expressions, shortens colors, minifies gradients, and much more.

Details →

Output size after minifying Bootstrap 4 (~10,000 lines). See the readme for more benchmarks.

CSS modules

Lightning CSS supports CSS modules, which locally scope classes, ids, @keyframes, CSS variables, and more. This ensures that there are no unintended name clashes between different CSS files.

Lightning CSS generates a mapping of the original names to scoped names, which can be used from your JavaScript. This also enables unused classes and variables to be tree-shaken.

Documentation →

.heading {
  composes: typography from './typography.css';
  color: gray;
}
.EgL3uq_heading {
  color: gray;
}
{
  "heading": {
    "name": "EgL3uq_heading",
    "composes": [{
      "type": "dependency",
      "name": "typography",
      "specifier": "./typography.css"
    }]
  }
}

Browser grade

Lightning CSS is written in Rust, using the cssparser and selectors crates created by Mozilla and used by Firefox. These provide a solid CSS-parsing foundation on top of which Lightning CSS implements support for all specific CSS rules and properties.

Lightning CSS fully parses every CSS rule, property, and value just as a browser would. This reduces duplicate work for transformers, leading to improved performance and minification.

Custom transforms →

Background([Background {
  image: Url(Url { url: "img.png" }),
  color: CssColor(RGBA(RGBA { red: 0, green: 0, blue: 0, alpha: 0 })),
  position: Position {
    x: Length(Dimension(Px(20.0))),
    y: Length(Dimension(Px(10.0))),
  },
  repeat: BackgroundRepeat {
    x: Repeat,
    y: Repeat,
  },
  size: Explicit {
    width: LengthPercentage(Dimension(Px(50.0))),
    height: LengthPercentage(Dimension(Px(100.0))),
  },
  attachment: Scroll,
  origin: PaddingBox,
  clip: BorderBox,
}])
Copyright © 2024 Devon Govett and Parcel Contributors.
================================================ FILE: website/minification.html ================================================ ================================================ FILE: website/pages/bundling.md ================================================ # Bundling Lightning CSS supports bundling dependencies referenced by CSS `@import` rules into a single output file. When calling the Lightning CSS API, use the `bundle` or `bundleAsync` function instead of `transform`. When using the CLI, enable the `--bundle` flag. This API requires filesystem access, so it does not accept `code` directly via the API. Instead, the `filename` option is used to read the entry file directly. ```js import { bundle } from 'lightningcss'; let { code, map } = bundle({ filename: 'style.css', minify: true }); ``` ## Dependencies CSS files can contain dependencies referenced by `@import` syntax, as well as references to classes in other files via [CSS modules](css-modules.html). ### @import The [`@import`](https://developer.mozilla.org/en-US/docs/Web/CSS/@import) at-rule can be used to inline another CSS file into the same CSS bundle as the containing file. This means that at runtime a separate network request will not be needed to load the dependency. Referenced files should be relative to the containing CSS file. ```css @import 'other.css'; ``` `@import` rules must appear before all other rules in a stylesheet except `@charset` and `@layer` statement rules. Later import rules will cause an error to be emitted. ### CSS modules Dependencies are also bundled when referencing another file via [CSS modules composition](css-modules.html#dependencies) or [external variables](css-modules.html#local-css-variables). See the linked CSS modules documentation for more details. ## Conditional imports The `@import` rule can be conditional by appending a media query or `supports()` query. Lightning CSS will preserve this behavior by wrapping the inlined rules in `@media` and `@supports` rules as needed. ```css /* a.css */ @import "b.css" print; @import "c.css" supports(display: grid); .a { color: red } ``` ```css /* b.css */ .b { color: green } ``` ```css /* c.css */ .c { display: grid } ``` compiles to: ```css @media print { .b { color: green } } @supports (display: grid) { .c { display: grid } } .a { color: red } ```
**Note**: There are currently two cases where combining conditional rules is unsupported: 1. Importing the same CSS file with only a media query, and again with only a supports query. This would require duplicating all rules in the file. 2. Importing a file with a negated media type (e.g. `not print`) within another file with a negated media type.
## Cascade layers Imported CSS rules can also be placed into a CSS cascade layer, allowing you to control the order they apply. Nested imports will be placed into nested layers. ```css /* a.css */ @import "b.css" layer(foo); .a { color: red } ``` ```css /* b.css */ @import "c.css" layer(bar); .b { color: green } ``` ```css /* c.css */ .c { color: green } ``` compiles to: ```css @layer foo.bar { .c { color: green } } @layer foo { .b { color: green } } .a { color: red } ```
**Note**: There are two unsupported layer combinations that will currently emit a compiler error: 1. Importing the same CSS file with different layer names. This would require duplicating all imported rules multiple times. 2. Nested anonymous layers.
## Bundling order When `@import` rules are processed in browsers, if the same file appears more than once, the _last_ instance applies. This is the opposite from behavior in other languages like JavaScript. Lightning CSS follows this behavior when bundling so that the output behaves the same as if it were not bundled. ```css /* index.css */ @import "a.css"; @import "b.css"; @import "a.css"; ``` ```css /* a.css */ body { background: green } ``` ```css /* b.css */ body { background: red } ``` compiles to: ```css body { background: green } ``` ## Custom resolvers The `bundleAsync` API is an asynchronous version of `bundle`, which also accepts a custom `resolver` object. This allows you to provide custom JavaScript functions for resolving `@import` specifiers to file paths, and reading files from the file system (or another source). The `read` and `resolve` functions are both optional, and may either return a string synchronously, or a Promise for asynchronous resolution. `resolve` may also return a `{external: string}` object to mark an `@import` as external. This will preserve the `@import` in the output instead of bundling it. The string provided to the `external` property represents the target URL to import, which may be the original specifier or a different value. Note that using a custom resolver can slow down bundling significantly, especially when reading files asynchronously. Use `readFileSync` rather than `readFile` if possible for better performance, or omit either of the methods if you don't need to override the default behavior. ```js import { bundleAsync } from 'lightningcss'; let { code, map } = await bundleAsync({ filename: 'style.css', minify: true, resolver: { read(filePath) { return fs.readFileSync(filePath, 'utf8'); }, resolve(specifier, from) { if (/^https?:/.test(specifier)) { return {external: specifier}; } return path.resolve(path.dirname(from), specifier); } } }); ```
**Note:** External imports must be placed before all bundled imports in the source code. CSS does not support interleaving `@import` rules with other rules, so this is required to preserve the behavior of the source code. ```css @import "bundled.css"; @import "https://example.com/external.css"; /* ❌ */ ``` ```css @import "https://example.com/external.css"; /* ✅ */ @import "bundled.css"; ```
================================================ FILE: website/pages/css-modules.md ================================================ # CSS modules By default, CSS identifiers are global. If two files define the same class names, ids, custom properties, `@keyframes`, etc., they will potentially clash and overwrite each other. To solve this, Lightning CSS supports [CSS modules](https://github.com/css-modules/css-modules). CSS modules treat the classes defined in each file as unique. Each class name or identifier is renamed to include a unique hash, and a mapping is exported to JavaScript to allow referencing them. To enable CSS modules, provide the `cssModules` option when calling the Lightning CSS API. When using the CLI, enable the `--css-modules` flag. ```js import {transform} from 'lightningcss'; let {code, map, exports} = transform({ // ... cssModules: true, code: Buffer.from(` .logo { background: skyblue; } `), }); ``` This returns an `exports` object in addition to the compiled code and source map. Each property in the `exports` object maps from the original name in the source CSS to the compiled (i.e. hashed) name. You can use this mapping in your JavaScript or template files to reference the compiled classes and identifiers. The exports object for the above example might look like this: ```js { logo: { name: '8h19c6_logo', isReferenced: false, composes: [] } } ``` ## Class composition Style rules in CSS modules can reference other classes with the `composes` property. This causes the referenced class to be applied whenever the composed class is used, effectively providing a form of style mixins. ```css .bg-indigo { background: indigo; } .indigo-white { composes: bg-indigo; color: white; } ``` In the above example, whenever the `indigo-white` class is applied, the `bg-indigo` class will be applied as well. This is indicated in the `exports` object returned by Lightning CSS as follows: ```js { 'bg-indigo': { name: '8h19c6_bg-indigo', isReferenced: true, composes: [] }, 'indigo-white': { name: '8h19c6_indigo-white', isReferenced: false, composes: [{ type: 'local', name: '8h19c6_bg-indigo' }] } } ``` Multiple classes can be composed at once by separating them with spaces. ```css .logo { composes: bg-indigo padding-large; } ``` ### Dependencies You can also reference class names defined in a different CSS file using the `from` keyword: ```css .logo { composes: bg-indigo from './colors.module.css'; } ``` This outputs an exports object with the dependency information. It is the caller's responsibility to resolve this dependency and apply the target class name when using the `transform` API. When using the `bundle` API, this is handled automatically. ```js { logo: { name: '8h19c6_logo', isReferenced: false, composes: [{ type: 'dependency', name: 'bg-indigo', specifier: './colors.module.css' }] } } ``` ### Global composition Global (i.e. non-hashed) classes can also be composed using the `global` keyword: ```css .search { composes: search-widget from global; } ``` ## Global exceptions Within a CSS module, all class and id selectors are local by default. You can also opt out of this behavior for a single selector using the `:global` pseudo class. ```css .foo :global(.bar) { color: red; } .foo .bar { color: green; } ``` compiles to: ```css .EgL3uq_foo .bar { color: red; } .EgL3uq_foo .EgL3uq_bar { color: #ff0; } ``` ## Local CSS variables By default, class names, id selectors, and the names of `@keyframes`, `@counter-style`, and CSS grid lines and areas are scoped to the module they are defined in. Scoping for CSS variables and other [``](https://www.w3.org/TR/css-values-4/#dashed-idents) names can also be enabled using the `dashedIdents` option when calling the Lightning CSS API. When using the CLI, enable the `--css-modules-dashed-idents` flag. ```js let {code, map, exports} = transform({ // ... cssModules: { dashedIdents: true, }, }); ``` When enabled, CSS variables will be renamed so they don't conflict with variable names defined in other files. Referencing a variable uses the standard `var()` syntax, which Lightning CSS will update to match the locally scoped variable name. ```css :root { --accent-color: hotpink; } .button { background: var(--accent-color); } ``` becomes: ```css :root { --EgL3uq_accent-color: hotpink; } .EgL3uq_button { background: var(--EgL3uq_accent-color); } ``` You can also reference variables defined in other files using the `from` keyword: ```css .button { background: var(--accent-color from './vars.module.css'); } ``` Global variables may be referenced using the `from global` syntax. ```css .button { color: var(--color from global); } ``` The same syntax also applies to other CSS values that use the [``](https://www.w3.org/TR/css-values-4/#dashed-idents) syntax. For example, the [@font-palette-values](https://drafts.csswg.org/css-fonts-4/#font-palette-values) rule and [font-palette](https://drafts.csswg.org/css-fonts-4/#propdef-font-palette) property use the `` syntax to define and refer to custom font color palettes, and will be scoped and referenced the same way as CSS variables. ## Custom naming patterns By default, Lightning CSS prepends the hash of the filename to each class name and identifier in a CSS file. You can configure this naming pattern using the `pattern` when calling the Lightning CSS API. When using the CLI, provide the `--css-modules-pattern` option. A pattern is a string with placeholders that will be filled in by Lightning CSS. This allows you to add custom prefixes or adjust the naming convention for scoped classes. ```js let {code, map, exports} = transform({ // ... cssModules: { pattern: 'my-company-[name]-[hash]-[local]', }, }); ``` The following placeholders are currently supported: - `[name]` - The base name of the file, without the extension. - `[hash]` - A hash of the full file path. - `[content-hash]` - A hash of the file contents. - `[local]` - The original class name or identifier.
### CSS Grid **Note:** CSS grid line names can be ambiguous due to automatic postfixing done by the browser, which generates line names ending with `-start` and `-end` for each grid template area. When using CSS grid, your `"pattern"` configuration must end with the `[local]` placeholder so that these references work correctly. ```js let { code, map, exports } = transform({ // ... cssModules: { // ❌ [local] must be at the end so that // auto-generated grid line names work pattern: '[local]-[hash]' // ✅ do this instead pattern: '[hash]-[local]' } }); ``` ```css .grid { grid-template-areas: 'nav main'; } .nav { grid-column-start: nav-start; } ```
### Pure mode Just like the `pure` option of the `css-loader` for webpack, Lightning CSS also has a `pure` option that enforces usage of one or more id or class selectors for each rule. ```js let {code, map, exports} = transform({ // ... cssModules: { pure: true, }, }); ``` If you enable this option, Lightning CSS will throw an error for CSS rules that don't have at least one id or class selector, like `div`. This is useful because selectors like `div` are not scoped and affects all elements on the page. ## Turning off feature scoping Scoping of grid, animations, and custom identifiers can be turned off. By default all of these are scoped. ```js let {code, map, exports} = transform({ // ... cssModules: { animation: true, grid: true, customIdents: true, }, }); ``` ## Unsupported features Lightning CSS does not currently implement all CSS modules features available in other implementations. Some of these may be added in the future. - Non-function syntax for the `:local` and `:global` pseudo classes. - The `@value` rule – superseded by standard CSS variables. - The `:import` and `:export` ICSS rules. ================================================ FILE: website/pages/docs.md ================================================ # Getting Started Lightning CSS can be used as a library from JavaScript or Rust, or from a standalone CLI. It can also be wrapped as a plugin in other build tools, and it is built into [Parcel](https://parceljs.org) out of the box. ## From Node First, install Lightning CSS using a package manager such as npm or Yarn. ```shell npm install --save-dev lightningcss ``` Once installed, import the module and call one of the Lightning CSS APIs. The `transform` function compiles a CSS stylesheet from a [Node Buffer](https://nodejs.org/api/buffer.html). This example minifies the input CSS, and outputs the compiled code and a source map. ```js import { transform } from 'lightningcss'; let { code, map } = transform({ filename: 'style.css', code: Buffer.from('.foo { color: red }'), minify: true, sourceMap: true }); ``` See [Transpilation](transpilation.html) for details about syntax lowering and vendor prefixing CSS for your browser targets, and the draft syntax support in Lightning CSS. You can also use the `bundle` API to process `@import` rules and inline them – see [Bundling](bundling.html) for details. The [TypeScript definitions](https://github.com/parcel-bundler/lightningcss/blob/master/node/index.d.ts) also include documentation for all API options. ## From Rust Lightning CSS can also be used as a Rust library to parse, transform, and minify CSS. See the Rust API docs on [docs.rs](https://docs.rs/lightningcss). ## With Parcel [Parcel](https://parceljs.org) includes Lightning CSS as the default CSS transformer. You should also add a `browserslist` property to your `package.json`, which defines the target browsers that your CSS will be compiled for. While Lightning CSS handles the most commonly used PostCSS plugins like `autoprefixer`, `postcss-preset-env`, and CSS modules, you may still need PostCSS for more custom plugins like TailwindCSS. If that's the case, your PostCSS config will be picked up automatically. You can remove the plugins listed above from your PostCSS config, and they'll be handled by Lightning CSS. You can also configure Lightning CSS in the `package.json` in the root of your project. Currently, three options are supported: [drafts](transpilation.html#draft-syntax), which can be used to enable CSS nesting and custom media queries, [pseudoClasses](transpilation.html#pseudo-class-replacement), which allows replacing some pseudo classes like `:focus-visible` with normal classes that can be applied via JavaScript (e.g. polyfills), and [cssModules](css-modules.html), which enables CSS modules globally rather than only for files ending in `.module.css`, or accepts an options object. ```json { "@parcel/transformer-css": { "cssModules": true, "drafts": { "nesting": true, "customMedia": true }, "pseudoClasses": { "focusVisible": "focus-ring" } } } ``` See the [Parcel docs](https://parceljs.org/languages/css) for more details. ## From Deno or in browser The `lightningcss-wasm` package can be used in Deno or directly in browsers. This uses a WebAssembly build of Lightning CSS. Use `TextEncoder` and `TextDecoder` convert code from a string to a typed array and back. ```js import init, { transform } from 'https://esm.run/lightningcss-wasm'; await init(); let {code, map} = transform({ filename: 'style.css', code: new TextEncoder().encode('.foo { color: red }'), minify: true, }); console.log(new TextDecoder().decode(code)); ``` Note that the `bundle` and visitor APIs are not currently available in the WASM build. ## With webpack [css-minimizer-webpack-plugin](https://webpack.js.org/plugins/css-minimizer-webpack-plugin/) has built in support for Lightning CSS. To use it, first install Lightning CSS in your project with a package manager like npm or Yarn: ```shell npm install --save-dev lightningcss css-minimizer-webpack-plugin browserslist ``` Next, configure `css-minifier-webpack-plugin` to use Lightning CSS as the minifier. You can provide options using the `minimizerOptions` object. See [Transpilation](transpilation.html) for details. ```js // webpack.config.js const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const lightningcss = require('lightningcss'); const browserslist = require('browserslist'); module.exports = { optimization: { minimize: true, minimizer: [ new CssMinimizerPlugin({ minify: CssMinimizerPlugin.lightningCssMinify, minimizerOptions: { targets: lightningcss.browserslistToTargets(browserslist('>= 0.25%')) }, }), ], }, }; ``` ## With Vite Vite supports Lightning CSS out of the box. First, install Lightning CSS into your project: ```shell npm install --save-dev lightningcss ``` Then, set `'lightningcss'` as CSS [transformer](https://vitejs.dev/config/shared-options.html#css-transformer) and [minifier](https://vitejs.dev/config/build-options.html#build-cssminify) in your Vite config. You can also configure Lightning CSS options such as targets and css modules via the [css.lightningcss](https://vitejs.dev/config/shared-options.html#css-lightningcss) option in your Vite config. ```js // vite.config.ts import browserslist from 'browserslist'; import {browserslistToTargets} from 'lightningcss'; export default { css: { transformer: 'lightningcss', lightningcss: { targets: browserslistToTargets(browserslist('>= 0.25%')) } }, build: { cssMinify: 'lightningcss' } }; ``` ## From the CLI Lightning CSS includes a standalone CLI that can be used to compile, minify, and bundle CSS files. It can be used when you only need to compile CSS, and don't need more advanced functionality from a larger build tool such as code splitting and support for other languages. To use the CLI, install the `lightningcss-cli` package with an npm compatible package manager: ```shell npm install --save-dev lightningcss-cli ``` Then, you can run the `lightningcss` command via `npx`, `yarn`, or by setting up a script in your package.json. ```json { "scripts": { "build": "lightningcss --minify --bundle --targets \">= 0.25%\" input.css -o output.css" } } ``` To see all of the available options, use the `--help` argument: ```shell npx lightningcss-cli --help ``` ## Error recovery By default, Lightning CSS is strict, and will error when parsing an invalid rule or declaration. However, sometimes you may encounter a third party library that you can't easily modify, which unintentionally contains invalid syntax, or IE-specific hacks. In these cases, you can enable the `errorRecovery` option (or `--error-recovery` CLI flag). This will skip over invalid rules and declarations, omitting them in the output, and producing a warning instead of an error. You should also open an issue or PR to fix the issue in the library if possible. ## Source maps Lightning CSS supports generating source maps when compiling, minifying, and bundling your source code to make debugging easier. Use the `sourceMap` option to enable it when using the API, or the `--sourcemap` CLI flag. If the input CSS came from another compiler such as Sass or Less, you can also pass an input source map to Lightning CSS using the `inputSourceMap` API option. This will map compiled locations back to their location in the original source code. Finally, the `projectRoot` option can be used to make file paths in source maps relative to a root directory. This makes build stable between machines. ================================================ FILE: website/pages/minification.md ================================================ # Minification Lightning CSS can optimize your CSS to make it smaller, which can help improve the loading performance of your website. When using the Lightning CSS API, enable the `minify` option, or when using the CLI, use the `--minify` flag. ```js import { transform } from 'lightningcss'; let { code, map } = transform({ // ... minify: true }); ``` ## Optimizations The Lightning CSS minifier includes many optimizations to generate the smallest possible output for all rules, properties, and values in your stylesheet. Lightning CSS does not perform any optimizations that change the behavior of your CSS unless it can prove that it is safe to do so. For example, only adjacent style rules are merged to avoid changing the order and potentially breaking the styles. ### Shorthands Lightning CSS will combine longhand properties into shorthands when all of the constituent longhand properties are defined. For example: ```css .foo { padding-top: 1px; padding-left: 2px; padding-bottom: 3px; padding-right: 4px; } ``` minifies to: ```css .foo{padding:1px 4px 3px 2px} ``` This is supported across most shorthand properties defined in the CSS spec. ### Merge adjacent rules Lightning CSS will merge adjacent style rules with the same selectors or declarations. ```css .a { color: red; } .b { color: red; } .c { color: green; } .c { padding: 10px; } ``` becomes: ```css .a,.b{color:red}.c{color:green;padding:10px} ``` In addition to style rules, Lightning CSS will also merge adjacent `@media`, `@supports`, and `@container` rules with identical queries, and adjacent `@layer` rules with the same layer name. Lightning CSS will not merge rules that are not adjacent, e.g. if another rule is between rules with the same declarations or selectors. This is because changing the order of the rules could cause the behavior of the compiled CSS to differ from the input CSS. ### Remove prefixes Lightning CSS will remove vendor prefixed properties that are not needed according to your configured browser targets. This is more likely to affect precompiled libraries that include unused prefixes rather than your own code. For example, when compiling for modern browsers, prefixed versions of the `transition` property will be removed, since the unprefixed version is supported by all browsers. ```css .button { -webkit-transition: background 200ms; -moz-transition: background 200ms; transition: background 200ms; } ``` becomes: ```css .button{transition:background .2s} ``` See [Transpilation](transpilation.html) for more on how to configure browser targets. ### Reduce calc Lightning CSS will reduce `calc()` and other math expressions to constant values where possible. When different units are used, the terms are reduced as much as possible. ```css .foo { width: calc(100px * 2); height: calc(((75.37% - 63.5px) - 900px) + (2 * 100px)); } ``` minifies to: ```css .foo{width:200px;height:calc(75.37% - 763.5px)} ``` Note that `calc()` expressions with variables are currently left unmodified by Lightning CSS. ### Minify colors Lightning CSS will minify colors to the smallest format possible without changing the color gamut. For example, named colors as well as `rgb()` and `hsl()` colors are converted to hex notation, using hex alpha notation when supported by your browser targets. ```css .foo { color: rgba(255, 255, 0, 0.8) } ``` minifies to: ```css .foo{color:#ff0c} ``` Note that only colors in the RGB gamut (including HSL and HWB) are converted to hex. Colors in other color spaces such as LAB or P3 are preserved. In addition to static colors, Lightning CSS also supports many color functions such as `color-mix()` and relative colors. When all components are known, Lightning CSS precomputes the result of these functions and outputs a static color. This both reduces the size and makes the syntax compatible with more browser targets. ```css .foo { color: rgb(from rebeccapurple r calc(g * 2) b); background: color-mix(in hsl, hsl(120deg 10% 20%), hsl(30deg 30% 40%)); } ``` minifies to: ```css .foo{color:#669;background:#545c3d} ``` Note that these conversions cannot be performed when any of the components include CSS variables. ### Normalizing values Lightning CSS parses all properties and values according to the CSS specification, filling in defaults where appropriate. When minifying, it omits default values where possible since the browser will fill those in when parsing. ```css .foo { background: 0% 0% / auto repeat scroll padding-box border-box red; } ``` minifies to: ```css .foo{background:red} ``` In addition to removing defaults, Lightning CSS also omits quotes, whitespace, and optional delimiters where possible. It also converts values to shorter equivalents where possible. ```css .foo { font-weight: bold; background-position: center center; background-image: url("logo.png"); } ``` minifies to: ```css .foo{background-image:url(logo.png);background-position:50%;font-weight:700} ``` ### CSS grid templates Lightning CSS will minify the `grid-template-areas` property to remove unnecessary whitespace and placeholders in template strings. ```css .foo { grid-template-areas: "head head" "nav main" "foot ...."; } ``` minifies to: ```css .foo{grid-template-areas:"head head""nav main""foot."} ``` ### Reduce transforms Lightning CSS will reduce CSS transform functions to shorter equivalents where possible. ```css .foo { transform: translate(0, 50px); } ``` minifies to: ```css .foo{transform:translateY(50px)} ``` In addition, the `matrix()` and `matrix3d()` functions are converted to their equivalent transforms when shorter: ```css .foo { transform: matrix3d(1, 0, 0, 0, 0, 0.707106, 0.707106, 0, 0, -0.707106, 0.707106, 0, 100, 100, 10, 1); } ``` minifies to: ```css .foo{transform:translate3d(100px,100px,10px)rotateX(45deg)} ``` When a matrix would be shorter, individual transform functions are converted to a single matrix instead: ```css .foo { transform: translate(100px, 200px) rotate(45deg) skew(10deg) scale(2); } ``` minifies to: ```css .foo{transform:matrix(1.41421,1.41421,-1.16485,1.66358,100,200)} ``` ## Unused symbols If you know that certain class names, ids, `@keyframes` rules, CSS variables, or other CSS identifiers are unused (for example as part of a larger full project analysis), you can use the `unusedSymbols` option to remove them. ```js let { code, map } = transform({ // ... minify: true, unusedSymbols: ['foo', 'fade-in', '--color'] }); ``` With this configuration, the following CSS: ```css :root { --color: red; } .foo { color: var(--color); } @keyframes fade-in { from { opacity: 0 } to { opacity: 1 } } .bar { color: green; } ``` minifies to: ```css .bar{color:green} ``` ================================================ FILE: website/pages/transforms.md ================================================ # Custom transforms The Lightning CSS visitor API can be used to implement custom transform plugins in JavaScript. It is designed to enable custom non-standard extensions to CSS, making your code easier to author while shipping standard CSS to the browser. You can implement extensions such as custom shorthand properties or additional at-rules (e.g. mixins), build time transforms (e.g. convert units, inline constants, etc.), CSS rule analysis, and much more. Custom transforms have a build time cost: it can be around 2x slower to compile with a JS visitor than without. This means visitors should generally be used to implement custom, non-standard CSS extensions. Common standard transforms such as compiling modern standard CSS features (and draft specs) for older browsers should be done in Rust as part of Lightning CSS itself. Please open an issue if there's a feature we don't handle yet. ## Visitors Custom transforms are implemented by passing a `visitor` object to the Lightning CSS Node API. A visitor includes one or more functions which are called for specific value types such as `Rule`, `Property`, or `Length`. In general, you should try to be as specific as possible about the types of values you want to handle. This way, Lightning CSS needs to call into JS as infrequently as possible, with the smallest objects possible, which improves performance. See the [TypeScript definitions](https://github.com/parcel-bundler/lightningcss/blob/eb49015cf887ae720b80a2856ccbdf61bf940ef1/node/index.d.ts#L184-L214) for a full list of available visitor functions. Visitors can return a new value to update it. Each visitor accepts a different type of value, and usually expects the same type in return. This example multiplies all lengths by 2: ```js import { transform } from 'lightningcss'; let res = transform({ filename: 'test.css', minify: true, code: Buffer.from(` .foo { width: 12px; } `), visitor: { Length(length) { return { unit: length.unit, value: length.value * 2 } } } }); assert.equal(res.code.toString(), '.foo{width:24px}'); ``` Some visitor functions accept an array as a return value, enabling you to replace one value with multiple, or remove a value by returning an empty array. You can also provide an object instead of a function to further reduce the number of times a visitor is called. For example, when providing a `Property` visitor, you can use an object with keys for specific property names. This improves performance by only calling your visitor function when needed. This example adds `-webkit-overflow-scrolling: touch` before any `overflow` properties. ```js let res = transform({ filename: 'test.css', minify: true, code: Buffer.from(` .foo { overflow: auto; } `), visitor: { Property: { overflow(property) { return [{ property: 'custom', value: { name: '-webkit-overflow-scrolling', value: [{ type: 'token', value: { type: 'ident', value: 'touch' } }] } }, property]; }, } } }); assert.equal(res.code.toString(), '.foo{-webkit-overflow-scrolling:touch;overflow:auto}'); ``` ## Value types The Lightning CSS AST is very detailed – each CSS property has a specific value type with all parts fully normalized. For example, a shorthand property such as `background` includes values for all of its sub-properties such as `background-color`, `background-image`, `background-position`, etc. This makes it both easier and faster for custom transforms to correctly handle all value types without reimplementing parsing. See the [TypeScript definitions](https://github.com/parcel-bundler/lightningcss/blob/master/node/ast.d.ts) for full documentation of all values. Known property values can be either _parsed_ or _unparsed_. Parsed values are fully expanded following the CSS specification. Unparsed values could not be parsed according to the grammar, and are stored as raw CSS tokens. This may occur because the value is invalid, or because it included unknown values such as CSS variables. Each property visitor function will need to handle both types of values. ```js transform({ code: Buffer.from(` .foo { width: 12px } .bar { width: var(--w) } `), visitor: { Property: { width(v) { if (v.property === 'unparsed') { // Handle unparsed value, e.g. `var(--w)` } else { // Handle parsed value, e.g. `12px` } } } } }); ``` Unknown properties, including custom properties, have the property type "custom". These values are also stored as raw CSS tokens. To visit custom properties, use the `custom` visitor function, or an object to filter by name. For example, to handle a custom `size` property and expand it to `width` and `height`, the following transform might be used. ```js let res = transform({ minify: true, code: Buffer.from(` .foo { size: 12px; } `), visitor: { Property: { custom: { size(property) { // Handle the size property when the value is a length. if (property.value[0].type === 'length') { let value = { type: 'length-percentage', value: { type: 'dimension', value: property.value[0].value } }; return [ { property: 'width', value }, { property: 'height', value } ]; } } } } } }); assert.equal(res.code.toString(), '.foo{width:12px;height:12px}'); ``` ## Raw values The Lightning CSS AST is very detailed, which is really useful when you need to transform it. However, it can be tedious to construct a full AST from scratch when returning entirely new values from a visitor. That's when raw values come in handy. You can return a `raw` property containing a string of CSS syntax from visitors that return declarations (i.e. properties) and tokens, and Lightning CSS will parse it for you and put it into the AST. This example implements a custom `color` function, which returns a raw CSS color value as a string, rather than constructing the whole AST. ```js let res = transform({ minify: true, code: Buffer.from(` .foo { color: color('red'); } `), visitor: { Function: { color() { return { raw: 'rgb(255, 0, 0)' }; } } } }); assert.equal(res.code.toString(), '.foo{color:red}'); ``` ## Entry and exit visitors By default, visitors are called when traversing downward through the tree (a pre-order traversal). This means each node is visited before its children. Sometimes it is useful to process a node after its children instead (a post-order traversal). This can be done by using an `Exit` visitor function, such as `FunctionExit`. For example, if you had a function visitor to double a length argument, and a visitor to replace an environment variable with a value, you could use an exit visitor to process the function after its arguments. ```js let res = transform({ filename: 'test.css', minify: true, code: Buffer.from(` .foo { padding: double(env(--branding-padding)); } `), visitor: { FunctionExit: { // This will run after the EnvironmentVariable visitor, below. double(f) { if (f.arguments[0].type === 'length') { return { type: 'length', value: { unit: f.arguments[0].value.unit, value: f.arguments[0].value.value * 2 } }; } } }, EnvironmentVariable: { // This will run before the FunctionExit visitor, above. '--branding-padding': () => ({ type: 'length', value: { unit: 'px', value: 20 } }) } } }); assert.equal(res.code.toString(), '.foo{padding:40px}'); ``` ## Composing visitors Multiple visitors can be combined into one using the `composeVisitors` function. This lets you reuse visitors between projects by publishing them as plugins. The AST is visited in a single pass, running the functions from each visitor object as if they were written together. ```js import { transform, composeVisitors } from 'lightningcss'; let environmentVisitor = { EnvironmentVariable: { '--branding-padding': () => ({ type: 'length', value: { unit: 'px', value: 20 } }) } }; let doubleFunctionVisitor = { FunctionExit: { double(f) { if (f.arguments[0].type === 'length') { return { type: 'length', value: { unit: f.arguments[0].value.unit, value: f.arguments[0].value.value * 2 } }; } } } }; let res = transform({ filename: 'test.css', minify: true, code: Buffer.from(` .foo { padding: double(env(--branding-padding)); } `), visitor: composeVisitors([environmentVisitor, doubleFunctionVisitor]) }); assert.equal(res.code.toString(), '.foo{padding:40px}'); ``` Each visitor object has the opportunity to visit every value once. If a visitor returns a new value, that value is visited by the other visitor objects but not again by the original visitor that created it. If other visitors subsequently modify the value, the previous visitors will not revisit the value. This is to avoid infinite loops. ## Unknown at-rules By default, unknown at-rules are stored in the AST as raw tokens. This allows you to interpret them however you like by writing a custom visitor. The following example allows declaring static variables using named at-rules, and inlines them when an `at-keyword` token is seen: ```js let declared = new Map(); let res = transform({ filename: 'test.css', minify: true, code: Buffer.from(` @blue #056ef0; .menu_link { background: @blue; } `), visitor: { Rule: { unknown(rule) { declared.set(rule.name, rule.prelude); return []; } }, Token: { 'at-keyword'(token) { return declared.get(token.value); } } } }); assert.equal(res.code.toString(), '.menu_link{background:#056ef0}'); ``` ## Custom at-rules Raw tokens as stored in unknown at-rules are fine for simple cases, but in more complex cases, you may wish to interpret a custom at-rule body as a standard CSS declaration list or rule list. However, by default, Lightning CSS does not know how unknown rules should be parsed. You can define their syntax using the `customAtRules` option. The syntax of the at-rule prelude can be defined with a [CSS syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), which Lightning CSS will interpret and use to validate the input CSS. This uses the same syntax as the [@property](https://developer.mozilla.org/en-US/docs/Web/CSS/@property) rule. The body syntax is defined using one of the following options: * `"declaration-list"` – A list of CSS declarations (property value pairs), as in a style rule or other at-rules like `@font-face`. * `"rule-list"` – A list of nested CSS rules, including style rules and at rules. Directly nested declarations with CSS nesting are not allowed. This matches how rules like `@keyframes` are parsed. * `"style-block"` – A list of CSS declarations and/or nested rules. This matches the behavior of rules like `@media` and `@supports` which support directly nested declarations when inside a style rule. Note that the [nesting](transpilation.html#nesting) and [targets](transpilation.html#browser-targets) options must be defined for nesting to be compiled. This example defines two custom at-rules. `@mixin` defines a reusable style block, supporting both directly nested declarations and nested rules. A visitor function registers the mixin in a map and removes the custom rule. `@apply` looks up the requested mixin in the map and returns the nested rules, which are inlined into the parent. ```js let mixins = new Map(); let res = transform({ filename: 'test.css', minify: true, targets: { chrome: 100 << 16 }, code: Buffer.from(` @mixin color { color: red; &.bar { color: yellow; } } .foo { @apply color; } `), customAtRules: { mixin: { prelude: '', body: 'style-block' }, apply: { prelude: '' } }, visitor: { Rule: { custom: { mixin(rule) { mixins.set(rule.prelude.value, rule.body.value); return []; }, apply(rule) { return mixins.get(rule.prelude.value); } } } } }); assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}'); ``` ## Dependencies Visitors can emit dependencies so the caller (e.g. bundler) knows to re-run the transformation or invalidate a cache when those files change. These are returned as part of the result's `dependencies` property (along with other dependencies when the `analyzeDependencies` option is enabled). By passing a function to the `visitor` option instead of an object, you get access to the `addDependency` function. This accepts a dependency object with `type: 'file'` or `type: 'glob'`. File dependencies invalidate the transformation whenever the `filePath` changes (created, updated, or deleted). Glob dependencies invalidate whenever any file matched by the glob changes. `composeVisitors` also supports function visitors. By default, Lightning CSS does not do anything with these dependencies except return them to the caller. It's the caller's responsibility to implement file watching and cache invalidation accordingly. ```js let res = transform({ filename: 'test.css', code: Buffer.from(` @dep "foo.js"; @glob "**/*.json"; .foo { width: 32px; } `), visitor: ({addDependency}) => ({ Rule: { unknown: { dep(rule) { let file = rule.prelude[0].value.value; addDependency({ type: 'file', filePath: file }); return []; }, glob(rule) { let glob = rule.prelude[0].value.value; addDependency({ type: 'glob', glob }); return []; } } } }) }); assert.equal(res.dependencies, [ { type: 'file', filePath: 'foo.js' }, { type: 'glob', filePath: '**/*.json' } ]); ``` ## Examples For examples of visitors that perform a variety of real world tasks, see the Lightning CSS [visitor tests](https://github.com/parcel-bundler/lightningcss/blob/master/node/test/visitor.test.mjs). ## Publishing a plugin Visitor plugins can be published to npm in order to share them with others. Plugin packages simply consist of an exported visitor object, which users can compose with other plugins via the `composeVisitors` function as described above. ```js // lightningcss-plugin-double-function export default { FunctionExit: { double(f) { // ... } } }; ``` Plugins can also export a function in order to accept options. ```js // lightningcss-plugin-env export default (values) => ({ EnvironmentVariable(env) { return values[env.name]; } }); ``` Plugin package names should start with `lightningcss-plugin-` and be descriptive about what they do, e.g. `lightningcss-plugin-double-function`. In addition, they should include the `lightningcss-plugin` keyword in their package.json so people can find them on npm. ```json { "name": "lightningcss-plugin-double-function", "keywords": ["lightningcss-plugin"], "main": "plugin.mjs" } ``` ## Using plugins To use a published visitor plugin, install the package from npm, import it, and use the `composeVisitors` function as described above. ```js import { transform, composeVisitors } from 'lightningcss'; import environmentVisitor from 'lightningcss-plugin-environment'; import doubleFunctionVisitor from 'lightningcss-plugin-double-function'; let res = transform({ filename: 'test.css', minify: true, code: Buffer.from(` .foo { padding: double(env(--branding-padding)); } `), visitor: composeVisitors([ environmentVisitor({ '--branding-padding': { type: 'length', value: { unit: 'px', value: 20 } } }), doubleFunctionVisitor ]) }); assert.equal(res.code.toString(), '.foo{padding:40px}'); ``` ================================================ FILE: website/pages/transpilation.md ================================================ # Transpilation Lightning CSS includes support for transpiling modern CSS syntax to support older browsers, including vendor prefixing and syntax lowering. ## Browser targets By default Lightning CSS does not perform any transpilation of CSS syntax for older browsers. This means that if you write your code using modern syntax or without vendor prefixes, that’s what Lightning CSS will output. You can declare your app’s supported browsers using the `targets` option. When this is declared, Lightning CSS will transpile your code accordingly to ensure compatibility with your supported browsers. Targets are defined using an object that specifies the minimum version of each browser you want to support. The easiest way to build a targets object is to use [browserslist](https://browserslist.dev). This lets you use a query that automatically updates over time as new browser versions are released, market share changes, etc. The following example will return a targets object listing browsers with >= 0.25% market share. ```js import browserslist from 'browserslist'; import { transform, browserslistToTargets } from 'lightningcss'; // Call this once per build. let targets = browserslistToTargets(browserslist('>= 0.25%')); // Use `targets` for each file you transform. let { code, map } = transform({ // ... targets }); ``` For the best performance, you should call browserslist once for your whole build process, and reuse the same `targets` object when calling `transform` for each file. Under the hood, `targets` are represented using an object that maps browser names to minimum versions. Version numbers are represented using a single 24-bit number, with one semver component (major, minor, patch) per byte. For example, this targets object would represent Safari 13.2.0. ```js let targets = { safari: (13 << 16) | (2 << 8) }; ``` ### CLI When using the CLI, targets can be provided by passing a [browserslist](https://browserslist.dev) query to the `--targets` option. Alternatively, if the `--browserslist` option is provided, then `lightningcss` finds browserslist configuration, selects queries by environment and loads the resulting queries as targets. Configuration discovery and targets resolution is modeled after the original `browserslist` Node package. The configuration is resolved in the following order: - If a `BROWSERSLIST` environment variable is present, then load targets from its value. - If a `BROWSERSLIST_CONFIG` environment variable is present, then load the browserslist configuration from the file at the provided path. - If none of the above apply, then find, parse and use targets from the first `browserslist`, `.browserslistrc`, or `package.json` configuration file in any parent directory. Browserslist configuration files may contain sections denoted by square brackets. Use these to specify different targets for different environments. Targets which are not placed in a section are added to `defaults` and used if no section matches the environment. ```ini # Defaults, applied when no other section matches the provided environment. firefox ESR # Targets applied only to the staging environment. [staging] samsung >= 4 ``` When using parsed configuration from `browserslist`, `.browserslistrc`, or `package.json` configuration files, the environment is determined by: - the `BROWSERSLIST_ENV` environment variable if present - the `NODE_ENV` environment variable if present - otherwise `"production"` is used. If no targets are found for the resulting environment, then the `defaults` configuration section is used. ### Feature flags In most cases, setting the `targets` option and letting Lightning CSS automatically compile your CSS works great. However, in other cases you might need a little more control over exactly what features are compiled and which are not. That's where the `include` and `exclude` options come in. The `include` and `exclude` options allow you to explicitly turn on or off certain features. These override the defaults based on the provided browser targets. For example, you might want to only compile colors, and handle auto prefixing or other features with another tool. Or you may want to handle everything except vendor prefixing with Lightning CSS. These options make that possible. The `include` and `exclude` options are configured using the `Features` enum, which can be imported from `lightningcss`. You can bitwise OR multiple flags together to turn them on or off. ```js import { transform, Features } from 'lightningcss'; let { code, map } = transform({ // ... targets, // Always compile colors and CSS nesting, regardless of browser targets. include: Features.Colors | Features.Nesting, // Never add any vendor prefixes, regardless of targets. exclude: Features.VendorPrefixes }); ``` Here is a full list of available flags, described in the sections below:
* `Nesting` * `NotSelectorList` * `DirSelector` * `LangSelectorList` * `IsSelector` * `TextDecorationThicknessPercent` * `MediaIntervalSyntax` * `MediaRangeSyntax` * `CustomMediaQueries` * `ClampFunction` * `ColorFunction` * `OklabColors` * `LabColors` * `P3Colors` * `HexAlphaColors` * `SpaceSeparatedColorNotation` * `LightDark` * `FontFamilySystemUi` * `DoublePositionGradients` * `VendorPrefixes` * `LogicalProperties` * `Selectors` – shorthand for `Nesting | NotSelectorList | DirSelector | LangSelectorList | IsSelector` * `MediaQueries` – shorthand for `MediaIntervalSyntax | MediaRangeSyntax | CustomMediaQueries` * `Colors` – shorthand for `ColorFunction | OklabColors | LabColors | P3Colors | HexAlphaColors | SpaceSeparatedColorNotation | LightDark`
## Vendor prefixing Based on your configured browser targets, Lightning CSS automatically adds vendor prefixed fallbacks for many CSS features. For example, when using the [`image-set()`](https://developer.mozilla.org/en-US/docs/Web/CSS/image/image-set()) function, Lightning CSS will output a fallback `-webkit-image-set()` value as well, since Chrome does not yet support the unprefixed value. ```css .logo { background: image-set(url(logo.png) 2x, url(logo.png) 1x); } ``` compiles to: ```css .logo { background: -webkit-image-set(url(logo.png) 2x, url(logo.png) 1x); background: image-set("logo.png" 2x, "logo.png"); } ``` In addition, if your CSS source code (or more likely a library) includes unnecessary vendor prefixes, Lightning CSS will automatically remove them to reduce bundle sizes. For example, when compiling for modern browsers, prefixed versions of the `transition` property will be removed, since the unprefixed version is supported by all browsers. ```css .button { -webkit-transition: background 200ms; -moz-transition: background 200ms; transition: background 200ms; } ``` becomes: ```css .button { transition: background .2s; } ``` ## Syntax lowering Lightning CSS automatically compiles many modern CSS syntax features to more compatible output that is supported in your target browsers. ### Nesting The [CSS Nesting](https://drafts.csswg.org/css-nesting/) spec enables style rules to be nested, with the selectors of the child rules extending the parent selector in some way. This is very commonly supported by CSS pre-processors like Sass, but with this spec, it will eventually be supported natively in browsers. Lightning CSS compiles this syntax to un-nested style rules that are supported in all browsers today. ```css .foo { color: blue; .bar { color: red; } } ``` is equivalent to: ```css .foo { color: blue; } .foo .bar { color: red; } ``` [Conditional rules](https://drafts.csswg.org/css-nesting/#conditionals) such as `@media` may also be nested within a style rule, without repeating the selector. For example: ```css .foo { display: grid; @media (orientation: landscape) { grid-auto-flow: column; } } ``` is equivalent to: ```css .foo { display: grid; } @media (orientation: landscape) { .foo { grid-auto-flow: column; } } ``` ### Color mix The [`color-mix()`](https://drafts.csswg.org/css-color-5/#color-mix) function allows you to mix two colors by the specified amount in a certain color space. Lightning CSS will evaluate this function statically when all components are known (i.e. not variables). ```css .foo { color: color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40%)); } ``` compiles to: ```css .foo { color: #706a43; } ``` ### Relative colors Relative colors allow you to modify the components of a color using math functions. In addition, you can convert colors between color spaces. Lightning CSS performs these calculations statically when all components are known (i.e. not variables). This example lightens `slateblue` by 10% in the LCH color space. ```css .foo { color: lch(from slateblue calc(l * 1.1) c h); } ``` compiles to: ```css .foo { color: lch(49.0282% 65.7776 296.794); } ``` ### LAB colors Lightning CSS will convert [`lab()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lab()), [`lch()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch()), [`oklab()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab), and [`oklch()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) colors to fallback values for unsupported browsers when needed. These functions allow you to define colors in higher gamut color spaces, making it possible to use colors that cannot be represented by RGB. ```css .foo { color: lab(40% 56.6 39); } ``` compiles to: ```css .foo { color: #b32323; color: color(display-p3 .643308 .192455 .167712); color: lab(40% 56.6 39); } ``` As shown above, a `display-p3` fallback is included in addition to RGB when a target browser supports the P3 color space. This preserves high color gamut colors when possible. ### Color function Lightning CSS converts the [`color()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color()) function to RGB when needed for compatibility with older browsers. This allows you to use predefined color spaces such as `display-p3`, `xyz`, and `a98-rgb`. ```css .foo { color: color(a98-rgb 0.44091 0.49971 0.37408); } ``` compiles to: ```css .foo { color: #6a805d; color: color(a98-rgb .44091 .49971 .37408); } ``` ### HWB colors Lightning CSS converts [`hwb()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb) colors to RGB. ```css .foo { color: hwb(194 0% 0%); } ``` compiles to: ```css .foo { color: #00c4ff; } ``` ### Color notation Space separated color notation is converted to hex when needed. Hex colors with alpha are also converted to `rgba()` when unsupported by all targets. ```css .foo { color: #7bffff80; background: rgb(123 255 255); } ``` compiles to: ```css .foo { color: rgba(123, 255, 255, .5); background: #7bffff; } ``` ### light-dark() color function The [`light-dark()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark) function allows you to specify a light mode and dark mode color in a single declaration, without needing to write a separate media query rule. In addition, it uses the [`color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) property to control which theme to use, which allows you to set it programmatically. The `color-scheme` property also inherits so themes can be nested and the nearest ancestor color scheme applies. Lightning CSS converts the `light-dark()` function to use CSS variable fallback when your browser targets don't support it natively. For this to work, you must set the `color-scheme` property on an ancestor element. The following example shows how you can support both operating system and programmatic overrides for the color scheme. ```css html { color-scheme: light dark; } html[data-theme=light] { color-scheme: light; } html[data-theme=dark] { color-scheme: dark; } button { background: light-dark(#aaa, #444); } ``` compiles to: ```css html { --lightningcss-light: initial; --lightningcss-dark: ; color-scheme: light dark; } @media (prefers-color-scheme: dark) { html { --lightningcss-light: ; --lightningcss-dark: initial; } } html[data-theme="light"] { --lightningcss-light: initial; --lightningcss-dark: ; color-scheme: light; } html[data-theme="dark"] { --lightningcss-light: ; --lightningcss-dark: initial; color-scheme: dark; } button { background: var(--lightningcss-light, #aaa) var(--lightningcss-dark, #444); } ``` ### Logical properties CSS [logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties) allow you to define values in terms of writing direction, so that UIs mirror in right-to-left languages. Lightning CSS will compile these to use the `:dir()` selector when unsupported. If the `:dir()` selector is unsupported, it is compiled as described [below](#%3Adir()-selector). ```css .foo { border-start-start-radius: 20px } ``` compiles to: ```css .foo:dir(ltr) { border-top-left-radius: 20px; } .foo:dir(rtl) { border-top-right-radius: 20px; } ``` ### :dir() selector The [`:dir()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:dir) selector matches elements based on the writing direction. Lightning CSS compiles this to use the `:lang()` selector when unsupported, which approximates this behavior as closely as possible. ```css a:dir(rtl) { color:red } ``` compiles to: ```css a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) { color: red; } ``` If multiple arguments to `:lang()` are unsupported, it is compiled as described [below](#%3Alang()-selector). ### :lang() selector The [`:lang()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:lang) selector matches elements based on their language. Some browsers do not support multiple arguments to this function, so Lightning CSS compiles them to use `:is()` when needed. ```css a:lang(en, fr) { color:red } ``` compiles to: ```css a:is(:lang(en), :lang(fr)) { color: red; } ``` When the `:is()` selector is unsupported, it is compiled as described [below](#%3Ais()-selector). ### :is() selector The [`:is()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:is) matches when one of its arguments matches. Lightning CSS falls back to the `:-webkit-any` and `:-moz-any` prefixed selectors. ```css p:is(:first-child, .lead) { margin-top: 0; } ``` compiles to: ```css p:-webkit-any(:first-child, .lead) { margin-top: 0; } p:-moz-any(:first-child, .lead) { margin-top: 0; } p:is(:first-child, .lead) { margin-top: 0; } ```
**Note**: The prefixed versions of these selectors do not support complex selectors (e.g. selectors with combinators). Lightning CSS will only output prefixes if the arguments are simple selectors. Complex selectors in `:is()` are not currently compiled.
### :not() selector The [`:not()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:not) selector can accept multiple arguments, and matches if none of the arguments match. Some older browsers only support a single argument, so Lightning CSS compiles this when needed. The `:is` selector is used to ensure the specificity remains the same, with fallback to `-webkit-any` and `-moz-any` as needed (described above). ```css p:not(:first-child, .lead) { margin-top: 1em; } ``` compiles to: ```css p:not(:is(:first-child, .lead)) { margin-top: 1em; } ``` ### Math functions Lightning CSS simplifies [math functions](https://w3c.github.io/csswg-drafts/css-values/#math) including `clamp()`, `round()`, `rem()`, `mod()`, `abs()`, and `sign()`, [trigonometric functions](https://w3c.github.io/csswg-drafts/css-values/#trig-funcs) including `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, and `atan2()`, and [exponential functions](https://w3c.github.io/csswg-drafts/css-values/#exponent-funcs) including `pow()`, `log()`, `sqrt()`, `exp()`, and `hypot()` when all arguments are known (i.e. not variables). In addition, the numeric constants `e`, `pi`, `infinity`, `-infinity`, and `NaN` are supported in all calculations. ```css .foo { width: round(calc(100px * sin(pi / 4)), 5px); } ``` compiles to: ```css .foo { width: 70px; } ``` ### Media query ranges [Media query range syntax](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#syntax_improvements_in_level_4) allows defining media queries using comparison operators to create ranges and intervals. Lightning CSS compiles this to the corresponding `min` and `max` media features when needed. ```css @media (480px <= width <= 768px) { .foo { color: red } } ``` compiles to: ```css @media (min-width: 480px) and (max-width: 768px) { .foo { color: red } } ``` ### Shorthands Lightning CSS compiles the following shorthands to corresponding longhands when the shorthand is not supported in all target browsers: * Alignment shorthands: [place-items](https://developer.mozilla.org/en-US/docs/Web/CSS/place-items), [place-content](https://developer.mozilla.org/en-US/docs/Web/CSS/place-content), [place-self](https://developer.mozilla.org/en-US/docs/Web/CSS/place-self) * [Overflow shorthand](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow) with multiple values (e.g. `overflow: hidden auto`) * [text-decoration](https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration) with thickness, style, color, etc. * Two value [display](https://developer.mozilla.org/en-US/docs/Web/CSS/display) syntax (e.g. `display: inline flex`) ### Double position gradients CSS gradients support using two positions in a color stop to repeat the color at two subsequent positions. When unsupported, Lightning CSS compiles it. ```css .foo { background: linear-gradient(green, red 30% 40%, pink); } ``` compiles to: ```css .foo { background: linear-gradient(green, red 30%, red 40%, pink); } ``` ### system-ui font The `system-ui` font allows you to use the operating system default font. When unsupported, Lightning CSS compiles it to a font stack that works across major platforms. ```css .foo { font-family: system-ui; } ``` compiles to: ```css .foo { font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue; } ``` ## Draft syntax Lightning CSS can also be configured to compile several draft specs that are not yet available natively in any browser. Because these are drafts and the syntax can still change, they must be enabled manually in your project. ### Custom media queries Support for [custom media queries](https://drafts.csswg.org/mediaqueries-5/#custom-mq) is included in the Media Queries Level 5 draft spec. This allows you to define media queries that are reused in multiple places within a CSS file. Lightning CSS will perform this substitution ahead of time when this feature is enabled. For example: ```css @custom-media --modern (color), (hover); @media (--modern) and (width > 1024px) { .a { color: green; } } ``` is equivalent to: ```css @media ((color) or (hover)) and (width > 1024px) { .a { color: green; } } ``` Because custom media queries are a draft, they are not enabled by default. To use them, enable the `customMedia` option under `drafts` when calling the Lightning CSS API. When using the CLI, enable the `--custom-media` flag. ```js let { code, map } = transform({ // ... drafts: { customMedia: true } }); ``` ## Pseudo class replacement Lightning CSS supports replacing CSS pseudo classes such as `:focus-visible` with normal CSS classes that can be applied using JavaScript. This makes it possible to polyfill these pseudo classes for older browsers. ```js let { code, map } = transform({ // ... pseudoClasses: { focusVisible: 'focus-visible' } }); ``` The above configuration will result in the `:focus-visible` pseudo class in all selectors being replaced with the `.focus-visible` class. This enables you to use a JavaScript [polyfill](https://github.com/WICG/focus-visible), which will apply the `.focus-visible` class as appropriate. The following pseudo classes may be configured as shown above: * `hover` – corresponds to the `:hover` pseudo class * `active` – corresponds to the `:active` pseudo class * `focus` – corresponds to the `:focus` pseudo class * `focusVisible` – corresponds to the `:focus-visible` pseudo class * `focusWithin` – corresponds to the `:focus-within` pseudo class ## Non-standard syntax For compatibility with other tools, Lightning CSS supports parsing some non-standard CSS syntax. This must be enabled by turning on a flag under the `nonStandard` option. ```js let { code, map } = transform({ // ... nonStandard: { deepSelectorCombinator: true } }); ``` Currently the following features are supported: * `deepSelectorCombinator` – enables parsing the Vue/Angular `>>>` and `/deep/` selector combinators. ================================================ FILE: website/playground/index.html ================================================ ⚡️ Lightning CSS Playground

Lightning CSS Playground

================================================ FILE: website/playground/playground.js ================================================ import * as localWasm from '../../wasm'; import { EditorView, basicSetup } from 'codemirror'; import { javascript } from '@codemirror/lang-javascript'; import { css } from '@codemirror/lang-css'; import { oneDark } from '@codemirror/theme-one-dark'; import { syntaxTree } from '@codemirror/language'; import { linter, lintGutter } from '@codemirror/lint' import { Compartment } from '@codemirror/state' const linterCompartment = new Compartment; const visitorLinterCompartment = new Compartment; let wasm; let editor, visitorEditor, outputEditor, modulesEditor, depsEditor; let enc = new TextEncoder(); let dec = new TextDecoder(); let inputs = document.querySelectorAll('input[type=number]'); async function loadVersions() { const { versions } = await fetch('https://data.jsdelivr.com/v1/package/npm/lightningcss-wasm').then(r => r.json()); versions .map(v => { const option = document.createElement('option'); option.value = v; option.textContent = v; return option; }) .forEach(o => { version.appendChild(o); }) } async function loadWasm() { if (version.value === 'local') { wasm = localWasm; } else { wasm = await new Function('version', 'return import(`https://esm.sh/lightningcss-wasm@${version}?bundle`)')(version.value); } await wasm.default(); } function loadPlaygroundState() { const hash = window.location.hash.slice(1); try { return JSON.parse(decodeURIComponent(hash)); } catch { return { minify: minify.checked, visitorEnabled: visitorEnabled.checked, targets: getTargets(), include: 0, exclude: 0, source: `@custom-media --modern (color), (hover); .foo { background: yellow; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; -webkit-transition: background 200ms; -moz-transition: background 200ms; transition: background 200ms; &.bar { color: green; } } @media (--modern) and (width > 1024px) { .a { color: green; } }`, version: version.value, visitor: `{ Color(color) { if (color.type === 'rgb') { color.g = 0; return color; } } }` }; } } function reflectPlaygroundState(playgroundState) { if (typeof playgroundState.minify !== 'undefined') { minify.checked = playgroundState.minify; } if (typeof playgroundState.cssModules !== 'undefined') { cssModules.checked = playgroundState.cssModules; compiledModules.hidden = !playgroundState.cssModules; } if (typeof playgroundState.analyzeDependencies !== 'undefined') { analyzeDependencies.checked = playgroundState.analyzeDependencies; compiledDependencies.hidden = !playgroundState.analyzeDependencies; } if (typeof playgroundState.customMedia !== 'undefined') { customMedia.checked = playgroundState.customMedia; } if (typeof playgroundState.visitorEnabled !== 'undefined') { visitorEnabled.checked = playgroundState.visitorEnabled; } if (playgroundState.targets) { const { targets } = playgroundState; for (let input of inputs) { let value = targets[input.id]; input.value = value == null ? '' : value >> 16; } } updateFeatures(sidebar.elements.include, playgroundState.include); updateFeatures(sidebar.elements.exclude, playgroundState.exclude); if (playgroundState.include) { include.parentElement.open = true; } if (playgroundState.exclude) { exclude.parentElement.open = true; } if (playgroundState.unusedSymbols) { unusedSymbols.value = playgroundState.unusedSymbols.join('\n'); } if (playgroundState.version) { version.value = playgroundState.version; } } function savePlaygroundState() { let data = new FormData(sidebar); const playgroundState = { minify: minify.checked, customMedia: customMedia.checked, cssModules: cssModules.checked, analyzeDependencies: analyzeDependencies.checked, targets: getTargets(), include: getFeatures(data.getAll('include')), exclude: getFeatures(data.getAll('exclude')), source: editor.state.doc.toString(), visitorEnabled: visitorEnabled.checked, visitor: visitorEditor.state.doc.toString(), unusedSymbols: unusedSymbols.value.split('\n').map(v => v.trim()).filter(Boolean), version: version.value, }; const hash = encodeURIComponent(JSON.stringify(playgroundState)); if ( typeof URL === 'function' && typeof history === 'object' && typeof history.replaceState === 'function' ) { const url = new URL(location.href); url.hash = hash; history.replaceState(null, null, url); } else { location.hash = hash; } } function getTargets() { let targets = {}; for (let input of inputs) { if (input.value !== '') { targets[input.id] = input.valueAsNumber << 16; } } return targets; } function getFeatures(vals) { let features = 0; for (let name of vals) { features |= wasm.Features[name]; } return features; } function updateFeatures(elements, include) { for (let checkbox of elements) { let feature = wasm.Features[checkbox.value]; checkbox.checked = (include & feature) === feature; checkbox.indeterminate = !checkbox.checked && (include & feature); } } function update() { const { transform } = wasm; const targets = getTargets(); let data = new FormData(sidebar); let include = getFeatures(data.getAll('include')); let exclude = getFeatures(data.getAll('exclude')); try { let res = transform({ filename: 'test.css', code: enc.encode(editor.state.doc.toString()), minify: minify.checked, targets: Object.keys(targets).length === 0 ? null : targets, include, exclude, drafts: { customMedia: customMedia.checked }, cssModules: cssModules.checked, analyzeDependencies: analyzeDependencies.checked, unusedSymbols: unusedSymbols.value.split('\n').map(v => v.trim()).filter(Boolean), visitor: visitorEnabled.checked ? (0, eval)('(' + visitorEditor.state.doc.toString() + ')') : undefined, }); let update = outputEditor.state.update({ changes: { from: 0, to: outputEditor.state.doc.length, insert: dec.decode(res.code) } }); outputEditor.update([update]); if (res.exports) { let update = modulesEditor.state.update({ changes: { from: 0, to: modulesEditor.state.doc.length, insert: '// CSS module exports\n' + JSON.stringify(res.exports, false, 2) } }); modulesEditor.update([update]); } if (res.dependencies) { let update = depsEditor.state.update({ changes: { from: 0, to: depsEditor.state.doc.length, insert: '// Dependencies\n' + JSON.stringify(res.dependencies, false, 2) } }); depsEditor.update([update]); } compiledModules.hidden = !cssModules.checked; compiledDependencies.hidden = !analyzeDependencies.checked; visitor.hidden = !visitorEnabled.checked; source.dataset.expanded = visitor.hidden; compiled.dataset.expanded = compiledModules.hidden && compiledDependencies.hidden; compiledModules.dataset.expanded = compiledDependencies.hidden; compiledDependencies.dataset.expanded = compiledModules.hidden; editor.dispatch({ effects: linterCompartment.reconfigure(createCssLinter(null, res.warnings)) }); visitorEditor.dispatch({ effects: visitorLinterCompartment.reconfigure(createVisitorLinter(null)) }); } catch (e) { let update = outputEditor.state.update({ changes: { from: 0, to: outputEditor.state.doc.length, insert: `/* ERROR: ${e.message} */` } }); outputEditor.update([update]); editor.dispatch({ effects: linterCompartment.reconfigure(createCssLinter(e)) }); visitorEditor.dispatch({ effects: visitorLinterCompartment.reconfigure(createVisitorLinter(e)) }); } savePlaygroundState(); } function createCssLinter(lastError, warnings) { return linter(view => { let diagnostics = []; if (lastError && lastError.loc) { let l = view.state.doc.line(lastError.loc.line); let loc = l.from + lastError.loc.column - 1; let node = syntaxTree(view.state).resolveInner(loc, 1); diagnostics.push( { from: node.from, to: node.to, message: lastError.message, severity: 'error' } ); } if (warnings) { for (let warning of warnings) { let l = view.state.doc.line(warning.loc.line); let loc = l.from + warning.loc.column - 1; let node = syntaxTree(view.state).resolveInner(loc, 1); diagnostics.push({ from: node.from, to: node.to, message: warning.message, severity: 'warning' }); } } return diagnostics; }, { delay: 0 }); } function createVisitorLinter(lastError) { return linter(view => { if (lastError && !lastError.loc) { // Firefox has lineNumber and columnNumber, Safari has line and column. let line = lastError.lineNumber ?? lastError.line; let column = lastError.columnNumber ?? lastError.column; if (lastError.column != null) { column--; } if (line == null) { // Chrome. let match = lastError.stack.match(/(?:(?:eval.*:)|(?:eval:))(?\d+):(?\d+)/); if (match) { line = Number(match.groups.line); column = Number(match.groups.column); // Chrome's column numbers are off by the amount of leading whitespace in the line. let l = view.state.doc.line(line); let m = l.text.match(/^\s*/); if (m) { column += m[0].length; } } } if (line != null) { let l = view.state.doc.line(line); let loc = l.from + column; let node = syntaxTree(view.state).resolveInner(loc, -1); return [ { from: node.from, to: node.to, message: lastError.message, severity: 'error' } ]; } } return []; }, { delay: 0 }); } function renderFeatures(parent, name) { for (let feature in wasm.Features) { let label = document.createElement('label'); let checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.name = name; checkbox.value = feature; checkbox.oninput = () => { let data = new FormData(sidebar); let flags = getFeatures(data.getAll(name)); let f = wasm.Features[feature]; if (checkbox.checked) { flags |= f; } else { flags &= ~f; } updateFeatures(sidebar.elements[name], flags); }; label.appendChild(checkbox); label.appendChild(document.createTextNode(' ' + feature)) parent.appendChild(label); } } async function main() { await loadWasm(); renderFeatures(include, 'include'); renderFeatures(exclude, 'exclude'); let state = loadPlaygroundState(); reflectPlaygroundState(state); editor = new EditorView({ extensions: [lintGutter(), basicSetup, css(), oneDark, linterCompartment.of(createCssLinter())], parent: source, doc: state.source, dispatch(tr) { editor.update([tr]); if (tr.docChanged) { update(); } } }); visitorEditor = new EditorView({ extensions: [lintGutter(), basicSetup, javascript(), oneDark, visitorLinterCompartment.of(createVisitorLinter())], parent: visitor, doc: state.visitor, dispatch(tr) { visitorEditor.update([tr]); if (tr.docChanged) { update(); } } }); outputEditor = new EditorView({ extensions: [basicSetup, css(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping], parent: compiled, }); modulesEditor = new EditorView({ extensions: [basicSetup, javascript(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping], parent: compiledModules, }); depsEditor = new EditorView({ extensions: [basicSetup, javascript(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping], parent: compiledDependencies, }); update(); sidebar.oninput = update; await loadVersions(); version.onchange = async () => { await loadWasm(); update(); }; } main(); ================================================ FILE: website/synthwave.css ================================================ /* * Based on Synthwave '84 Theme originally by Robb Owen [@Robb0wen] for Visual Studio Code * Originally ported for PrismJS by Marc Backes [@themarcba] */ pre[class*="language-"] { color: lab(64% 103 0); text-shadow: 0 0 10px lab(64% 103 0 / .5); background: rgb(255 255 255 / .05); display: block; padding: 20px; border-radius: 8px; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; font-size: 13px; text-align: left; white-space: pre-wrap; word-spacing: normal; word-break: normal; word-wrap: break-word; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } /* Code blocks */ pre[class*="language-"] { padding: 1em; margin: .5em 0; overflow: auto; } /* Inline code */ :not(pre) > code[class*="language-"] { padding: .1em; border-radius: .3em; white-space: normal; } .token.comment, .token.block-comment, .token.prolog, .token.doctype, .token.cdata { color: #8e8e8e; text-shadow: none; } .token.punctuation { color: #ccc; } .token.tag, .token.attr-name, .token.namespace, .token.number, .token.unit, .token.hexcode, .token.deleted, .token.function { color: lch(65% 85 35); text-shadow: 0 0 10px lch(65% 85 35 / .5); } .token.property, .token.selector { color: lch(85% 58 205); text-shadow: 0 0 10px lch(85% 58 205 / .5); } .token.function-name { color: #6196cc; } .token.boolean, .token.selector .token.id { color: #fdfdfd; text-shadow: 0 0 2px #001716, 0 0 3px #03edf975, 0 0 5px #03edf975, 0 0 8px #03edf975; } .token.class-name { color: #fff5f6; text-shadow: 0 0 2px #000, 0 0 10px #fc1f2c75, 0 0 5px #fc1f2c75, 0 0 25px #fc1f2c75; } .token.constant, .token.symbol { color: lab(64% 103 0); text-shadow: 0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3; } .token.important, .token.atrule, .token.keyword, .token.selector .token.class, .token.builtin { color: #f4eee4; text-shadow: 0 0 2px #393a33, 0 0 8px #f39f0575, 0 0 2px #f39f0575; } .token.string, .token.string-property, .token.char, .token.attr-value, .token.regex, .token.variable, .token.url { color: lch(85% 82.34 80.104); text-shadow: 0 0 10px lch(85% 82.34 80.104 / .5); } .token.operator, .token.entity { color: #67cdcc; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } .token.inserted { color: green; } ================================================ FILE: website/transforms.html ================================================ ================================================ FILE: website/transpilation.html ================================================