Skip to content

Commit 4fe3a4b

Browse files
authored
Enable custom resolvers to mark imports as external (parcel-bundler#880)
1 parent da5b491 commit 4fe3a4b

File tree

5 files changed

+229
-73
lines changed

5 files changed

+229
-73
lines changed

napi/src/lib.rs

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ pub fn transform_style_attribute(ctx: CallContext) -> napi::Result<JsUnknown> {
121121
mod bundle {
122122
use super::*;
123123
use crossbeam_channel::{self, Receiver, Sender};
124-
use lightningcss::bundler::FileProvider;
125-
use napi::{Env, JsFunction, JsString, NapiRaw};
124+
use lightningcss::bundler::{FileProvider, ResolveResult};
125+
use napi::{Env, JsBoolean, JsFunction, JsString, NapiRaw};
126126
use std::path::{Path, PathBuf};
127127
use std::str::FromStr;
128128
use std::sync::Mutex;
@@ -169,6 +169,7 @@ mod bundle {
169169
// Allocate a single channel per thread to communicate with the JS thread.
170170
thread_local! {
171171
static CHANNEL: (Sender<napi::Result<String>>, Receiver<napi::Result<String>>) = crossbeam_channel::unbounded();
172+
static RESOLVER_CHANNEL: (Sender<napi::Result<ResolveResult>>, Receiver<napi::Result<ResolveResult>>) = crossbeam_channel::unbounded();
172173
}
173174

174175
impl SourceProvider for JsSourceProvider {
@@ -203,32 +204,28 @@ mod bundle {
203204
}
204205
}
205206

206-
fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
207+
fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<ResolveResult, Self::Error> {
207208
if let Some(resolve) = &self.resolve {
208-
return CHANNEL.with(|channel| {
209+
return RESOLVER_CHANNEL.with(|channel| {
209210
let message = ResolveMessage {
210211
specifier: specifier.to_owned(),
211212
originating_file: originating_file.to_str().unwrap().to_owned(),
212213
tx: channel.0.clone(),
213214
};
214215

215216
resolve.call(message, ThreadsafeFunctionCallMode::Blocking);
216-
let result = channel.1.recv().unwrap();
217-
match result {
218-
Ok(result) => Ok(PathBuf::from_str(&result).unwrap()),
219-
Err(e) => Err(e),
220-
}
217+
channel.1.recv().unwrap()
221218
});
222219
}
223220

224-
Ok(originating_file.with_file_name(specifier))
221+
Ok(originating_file.with_file_name(specifier).into())
225222
}
226223
}
227224

228225
struct ResolveMessage {
229226
specifier: String,
230227
originating_file: String,
231-
tx: Sender<napi::Result<String>>,
228+
tx: Sender<napi::Result<ResolveResult>>,
232229
}
233230

234231
struct ReadMessage {
@@ -241,17 +238,20 @@ mod bundle {
241238
tx: Sender<napi::Result<String>>,
242239
}
243240

244-
fn await_promise(env: Env, result: JsUnknown, tx: Sender<napi::Result<String>>) -> napi::Result<()> {
241+
fn await_promise<T, Cb>(env: Env, result: JsUnknown, tx: Sender<napi::Result<T>>, parse: Cb) -> napi::Result<()>
242+
where
243+
T: 'static,
244+
Cb: 'static + Fn(JsUnknown) -> Result<T, napi::Error>,
245+
{
245246
// If the result is a promise, wait for it to resolve, and send the result to the channel.
246247
// Otherwise, send the result immediately.
247248
if result.is_promise()? {
248249
let result: JsObject = result.try_into()?;
249250
let then: JsFunction = get_named_property(&result, "then")?;
250251
let tx2 = tx.clone();
251252
let cb = env.create_function_from_closure("callback", move |ctx| {
252-
let res = ctx.get::<JsString>(0)?.into_utf8()?;
253-
let s = res.into_owned()?;
254-
tx.send(Ok(s)).unwrap();
253+
let res = parse(ctx.get::<JsUnknown>(0)?)?;
254+
tx.send(Ok(res)).unwrap();
255255
ctx.env.get_undefined()
256256
})?;
257257
let eb = env.create_function_from_closure("error_callback", move |ctx| {
@@ -261,10 +261,8 @@ mod bundle {
261261
})?;
262262
then.call(Some(&result), &[cb, eb])?;
263263
} else {
264-
let result: JsString = result.try_into()?;
265-
let utf8 = result.into_utf8()?;
266-
let s = utf8.into_owned()?;
267-
tx.send(Ok(s)).unwrap();
264+
let result = parse(result)?;
265+
tx.send(Ok(result)).unwrap();
268266
}
269267

270268
Ok(())
@@ -274,10 +272,12 @@ mod bundle {
274272
let specifier = ctx.env.create_string(&ctx.value.specifier)?;
275273
let originating_file = ctx.env.create_string(&ctx.value.originating_file)?;
276274
let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?;
277-
await_promise(ctx.env, result, ctx.value.tx)
275+
await_promise(ctx.env, result, ctx.value.tx, move |unknown| {
276+
ctx.env.from_js_value(unknown)
277+
})
278278
}
279279

280-
fn handle_error(tx: Sender<napi::Result<String>>, res: napi::Result<()>) -> napi::Result<()> {
280+
fn handle_error<T>(tx: Sender<napi::Result<T>>, res: napi::Result<()>) -> napi::Result<()> {
281281
match res {
282282
Ok(_) => Ok(()),
283283
Err(e) => {
@@ -295,7 +295,9 @@ mod bundle {
295295
fn read_on_js_thread(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
296296
let file = ctx.env.create_string(&ctx.value.file)?;
297297
let result = ctx.callback.unwrap().call(None, &[file])?;
298-
await_promise(ctx.env, result, ctx.value.tx)
298+
await_promise(ctx.env, result, ctx.value.tx, |unknown| {
299+
JsString::try_from(unknown)?.into_utf8()?.into_owned()
300+
})
299301
}
300302

301303
fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
@@ -421,10 +423,10 @@ mod bundle {
421423
#[cfg(target_arch = "wasm32")]
422424
mod bundle {
423425
use super::*;
426+
use lightningcss::bundler::ResolveResult;
424427
use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref};
425428
use std::cell::UnsafeCell;
426-
use std::path::{Path, PathBuf};
427-
use std::str::FromStr;
429+
use std::path::Path;
428430

429431
pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
430432
let opts = ctx.get::<JsObject>(0)?;
@@ -497,7 +499,7 @@ mod bundle {
497499
);
498500
}
499501

500-
fn get_result(env: Env, mut value: JsUnknown) -> napi::Result<JsString> {
502+
fn get_result(env: Env, mut value: JsUnknown) -> napi::Result<JsUnknown> {
501503
if value.is_promise()? {
502504
let mut result = std::ptr::null_mut();
503505
let mut error = std::ptr::null_mut();
@@ -513,7 +515,7 @@ mod bundle {
513515
value = unsafe { JsUnknown::from_raw(env.raw(), result)? };
514516
}
515517

516-
value.try_into()
518+
Ok(value)
517519
}
518520

519521
impl SourceProvider for JsSourceProvider {
@@ -523,7 +525,9 @@ mod bundle {
523525
let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?;
524526
let file = self.env.create_string(file.to_str().unwrap())?;
525527
let source: JsUnknown = read.call(None, &[file])?;
526-
let source = get_result(self.env, source)?.into_utf8()?.into_owned()?;
528+
let source = get_result(self.env, source)?;
529+
let source: JsString = source.try_into()?;
530+
let source = source.into_utf8()?.into_owned()?;
527531

528532
// cache the result
529533
let ptr = Box::into_raw(Box::new(source));
@@ -535,16 +539,17 @@ mod bundle {
535539
Ok(unsafe { &*ptr })
536540
}
537541

538-
fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
542+
fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<ResolveResult, Self::Error> {
539543
if let Some(resolve) = &self.resolve {
540544
let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?;
541545
let specifier = self.env.create_string(specifier)?;
542546
let originating_file = self.env.create_string(originating_file.to_str().unwrap())?;
543547
let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?;
544-
let result = get_result(self.env, result)?.into_utf8()?;
545-
Ok(PathBuf::from_str(result.as_str()?).unwrap())
548+
let result = get_result(self.env, result)?;
549+
let result = self.env.from_js_value(result)?;
550+
Ok(result)
546551
} else {
547-
Ok(originating_file.with_file_name(specifier))
552+
Ok(ResolveResult::File(originating_file.with_file_name(specifier)))
548553
}
549554
}
550555
}

node/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
44

55
use napi::{CallContext, JsObject, JsUnknown};
6-
use napi_derive::{js_function, module_exports};
6+
use napi_derive::js_function;
77

88
#[js_function(1)]
99
fn transform(ctx: CallContext) -> napi::Result<JsUnknown> {
@@ -26,7 +26,7 @@ pub fn bundle_async(ctx: CallContext) -> napi::Result<JsObject> {
2626
lightningcss_napi::bundle_async(ctx)
2727
}
2828

29-
#[cfg_attr(not(target_arch = "wasm32"), module_exports)]
29+
#[cfg_attr(not(target_arch = "wasm32"), napi_derive::module_exports)]
3030
fn init(mut exports: JsObject) -> napi::Result<()> {
3131
exports.create_named_method("transform", transform)?;
3232
exports.create_named_method("transformStyleAttribute", transform_style_attribute)?;
@@ -45,7 +45,6 @@ pub fn register_module() {
4545
unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> {
4646
use napi::{Env, JsObject, NapiValue};
4747

48-
let env = Env::from_raw(raw_env);
4948
let exports = JsObject::from_raw_unchecked(raw_env, raw_exports);
5049
init(exports)
5150
}

node/test/bundle.test.mjs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ test('resolve return non-string', async () => {
365365
}
366366

367367
if (!error) throw new Error(`\`testResolveReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
368-
assert.equal(error.message, 'expect String, got: Number');
368+
assert.equal(error.message, 'data did not match any variant of untagged enum ResolveResult');
369369
assert.equal(error.fileName, 'tests/testdata/foo.css');
370370
assert.equal(error.loc, {
371371
line: 1,
@@ -414,4 +414,29 @@ test('should support throwing in visitors', async () => {
414414
assert.equal(error.message, 'Some error');
415415
});
416416

417+
test('external import', async () => {
418+
const { code: buffer } = await bundleAsync(/** @type {import('../index').BundleAsyncOptions} */ ({
419+
filename: 'tests/testdata/has_external.css',
420+
resolver: {
421+
resolve(specifier, originatingFile) {
422+
if (specifier === './does_not_exist.css' || specifier.startsWith('https:')) {
423+
return {external: specifier};
424+
}
425+
return path.resolve(path.dirname(originatingFile), specifier);
426+
}
427+
}
428+
}));
429+
const code = buffer.toString('utf-8').trim();
430+
431+
const expected = `
432+
@import "https://fonts.googleapis.com/css2?family=Roboto&display=swap";
433+
@import "./does_not_exist.css";
434+
435+
.b {
436+
height: calc(100vh - 64px);
437+
}
438+
`.trim();
439+
if (code !== expected) throw new Error(`\`testResolver()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
440+
});
441+
417442
test.run();

0 commit comments

Comments
 (0)