Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,9 @@ jobs:
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: yarn wasm:build-release
- name: Optimize wasm
run: |
./binaryen-version_111/bin/wasm-opt wasm/lightningcss_node.wasm -Oz -o wasm/lightningcss_node.opt.wasm
mv wasm/lightningcss_node.opt.wasm wasm/lightningcss_node.wasm
export PATH="$PATH:./binaryen-version_111/bin"
yarn wasm:build-release
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo fmt
- run: cargo test --all-features

test-js:
runs-on: ubuntu-latest
steps:
Expand All @@ -32,3 +33,28 @@ jobs:
- 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
139 changes: 138 additions & 1 deletion node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,14 +420,151 @@ mod bundle {
}
}

#[cfg(target_arch = "wasm32")]
mod bundle {
use super::*;
use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref};
use std::cell::UnsafeCell;

#[js_function(1)]
pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
use transformer::JsVisitor;

let opts = ctx.get::<JsObject>(0)?;
let mut visitor = if let Ok(visitor) = opts.get_named_property::<JsObject>("visitor") {
Some(JsVisitor::new(*ctx.env, visitor))
} else {
None
};

let resolver = opts.get_named_property::<JsObject>("resolver")?;
let read = resolver.get_named_property::<JsFunction>("read")?;
let resolve = if resolver.has_named_property("resolve")? {
let resolve = resolver.get_named_property::<JsFunction>("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<Ref<()>>,
read: Ref<()>,
inputs: UnsafeCell<Vec<*mut String>>,
}

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<JsString> {
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".into()));
}

value = unsafe { JsUnknown::from_raw(env.raw(), result)? };
}

value.try_into()
}

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 mut source: JsUnknown = read.call(None, &[file])?;
let source = get_result(self.env, 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<PathBuf, Self::Error> {
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)?.into_utf8()?;
Ok(PathBuf::from_str(result.as_str()?).unwrap())
} else {
Ok(originating_file.with_file_name(specifier))
}
}
}
}

#[cfg_attr(not(target_arch = "wasm32"), module_exports)]
fn init(mut exports: JsObject) -> napi::Result<()> {
exports.create_named_method("transform", transform)?;
exports.create_named_method("transformStyleAttribute", transform_style_attribute)?;
exports.create_named_method("bundle", bundle::bundle)?;

#[cfg(not(target_arch = "wasm32"))]
{
exports.create_named_method("bundle", bundle::bundle)?;
exports.create_named_method("bundleAsync", bundle::bundle_async)?;
}

Expand Down
21 changes: 20 additions & 1 deletion node/test/bundle.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import path from 'path';
import fs from 'fs';
import { bundleAsync } from '../index.mjs';
import { test } from 'uvu';
import * as assert from 'uvu/assert';

let bundleAsync;
if (process.env.TEST_WASM === 'node') {
bundleAsync = (await import('../../wasm/wasm-node.mjs')).bundleAsync;
} else if (process.env.TEST_WASM === 'browser') {
let wasm = await import('../../wasm/index.mjs');
await wasm.default();
bundleAsync = function (options) {
if (!options.resolver?.read) {
options.resolver = {
...options.resolver,
read: (filePath) => fs.readFileSync(filePath, 'utf8')
};
}

return wasm.bundleAsync(options);
}
} else {
bundleAsync = (await import('../index.mjs')).bundleAsync;
}

test('resolver', async () => {
const inMemoryFs = new Map(Object.entries({
'foo.css': `
Expand Down
12 changes: 11 additions & 1 deletion node/test/composeVisitors.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { transform, composeVisitors } from '../index.mjs';

let transform, composeVisitors;
if (process.env.TEST_WASM === 'node') {
({transform, composeVisitors} = await import('../../wasm/wasm-node.mjs'));
} else if (process.env.TEST_WASM === 'browser') {
let wasm = await import('../../wasm/index.mjs');
await wasm.default();
({transform, composeVisitors} = wasm);
} else {
({transform, composeVisitors} = await import('../index.mjs'));
}

test('different types', () => {
let res = transform({
Expand Down
21 changes: 20 additions & 1 deletion node/test/customAtRules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@

import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { bundle, transform } from '../index.mjs';
import fs from 'fs';

let bundle, transform;
if (process.env.TEST_WASM === 'node') {
({ bundle, transform } = await import('../../wasm/wasm-node.mjs'));
} else if (process.env.TEST_WASM === 'browser') {
let wasm = await import('../../wasm/index.mjs');
await wasm.default();
transform = wasm.transform;
bundle = function(options) {
return wasm.bundle({
...options,
resolver: {
read: (filePath) => fs.readFileSync(filePath, 'utf8')
}
});
}
} else {
({bundle, transform} = await import('../index.mjs'));
}

test('declaration list', () => {
let definitions = new Map();
Expand Down
14 changes: 13 additions & 1 deletion node/test/transform.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { transform, Features } from 'lightningcss';
import { test } from 'uvu';
import * as assert from 'uvu/assert';

let transform, Features;
if (process.env.TEST_WASM === 'node') {
({transform, Features} = await import('../../wasm/wasm-node.mjs'));
} else if (process.env.TEST_WASM === 'browser') {
let wasm = await import('../../wasm/index.mjs');
await wasm.default();
({transform, Features} = wasm);
} else {
({transform, Features} = await import('../index.mjs'));
}

test('can enable non-standard syntax', () => {
let res = transform({
filename: 'test.css',
Expand Down Expand Up @@ -56,3 +66,5 @@ test('can disable prefixing', () => {

assert.equal(res.code.toString(), '.foo{user-select:none}');
});

test.run();
32 changes: 31 additions & 1 deletion node/test/visitor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,37 @@

import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { bundle, bundleAsync, transform, transformStyleAttribute } from '../index.mjs';
import fs from 'fs';

let bundle, bundleAsync, transform, transformStyleAttribute;
if (process.env.TEST_WASM === 'node') {
({ bundle, bundleAsync, transform, transformStyleAttribute } = await import('../../wasm/wasm-node.mjs'));
} else if (process.env.TEST_WASM === 'browser') {
let wasm = await import('../../wasm/index.mjs');
await wasm.default();
({ transform, transformStyleAttribute } = wasm);
bundle = function(options) {
return wasm.bundle({
...options,
resolver: {
read: (filePath) => fs.readFileSync(filePath, 'utf8')
}
});
}

bundleAsync = function (options) {
if (!options.resolver?.read) {
options.resolver = {
...options.resolver,
read: (filePath) => fs.readFileSync(filePath, 'utf8')
};
}

return wasm.bundleAsync(options);
}
} else {
({ bundle, bundleAsync, transform, transformStyleAttribute } = await import('../index.mjs'));
}

test('px to rem', () => {
// Similar to https://github.com/cuth/postcss-pxtorem
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@
"build": "node scripts/build.js && node scripts/build-flow.js",
"build-release": "node scripts/build.js --release && node scripts/build-flow.js",
"prepublishOnly": "node scripts/build-flow.js",
"wasm:build": "cargo build --target wasm32-unknown-unknown -p lightningcss_node && cp target/wasm32-unknown-unknown/debug/lightningcss_node.wasm wasm/. && node scripts/build-wasm.js",
"wasm:build-release": "cargo build --target wasm32-unknown-unknown -p lightningcss_node --release && cp target/wasm32-unknown-unknown/release/lightningcss_node.wasm wasm/. && node scripts/build-wasm.js",
"wasm:build": "cargo build --target wasm32-unknown-unknown -p lightningcss_node && wasm-opt target/wasm32-unknown-unknown/debug/lightningcss_node.wasm --asyncify --pass-arg=asyncify-imports@env.await_promise_sync -Oz -o wasm/lightningcss_node.wasm && node scripts/build-wasm.js",
"wasm:build-release": "cargo build --target wasm32-unknown-unknown -p lightningcss_node --release && wasm-opt target/wasm32-unknown-unknown/release/lightningcss_node.wasm --asyncify --pass-arg=asyncify-imports@env.await_promise_sync -Oz -o wasm/lightningcss_node.wasm && node scripts/build-wasm.js",
"website:start": "parcel 'website/*.html' website/playground/index.html",
"website:build": "yarn wasm:build-release && parcel build 'website/*.html' website/playground/index.html",
"build-ast": "cargo run --example schema --features jsonschema && node scripts/build-ast.js",
Expand Down
4 changes: 4 additions & 0 deletions scripts/build-wasm.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ let flags = fs.readFileSync(`${dir}/node/flags.js`, 'utf8');
flags = flags.replace('exports.Features =', 'export const Features =');
fs.writeFileSync(`${dir}/wasm/flags.js`, flags);

let composeVisitors = fs.readFileSync(`${dir}/node/composeVisitors.js`, 'utf8');
composeVisitors = composeVisitors.replace('module.exports = composeVisitors', 'export { composeVisitors }');
fs.writeFileSync(`${dir}/wasm/composeVisitors.js`, composeVisitors);

let dts = fs.readFileSync(`${dir}/node/index.d.ts`, 'utf8');
dts = dts.replace(/: Buffer/g, ': Uint8Array');
dts += `
Expand Down
1 change: 1 addition & 0 deletions wasm/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ package.json
README.md
browserslistToTargets.js
flags.js
composeVisitors.js
Loading