-
Notifications
You must be signed in to change notification settings - Fork 194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom resolver #174
Comments
You should use Parcel for this eg via the api https://parceljs.org/features/parcel-api/. Parcel CSS is used under the hood, and you can hook into resolution via the plugin system. Standalone, Parcel CSS has very limited bundling support. |
@devongovett, thanks for the suggestion. I spent most of today trying to get things working with the Parcel API. I was kind of able to get a custom resolver working, but it turned out much more complicated than I expected. Parcel plugins seem to require being loaded from an NPM package, with some tricks available to load local plugins. However as a build system, Bazel is already capable of solving those orchestration challenges pretty easily, and Parcel's tight coupling with NPM and I'm wondering if there's a package under import { Parcel, getDefaultParcelConfig, getDefaultParcelResolver } from '@parcel/something';
import { Resolver } from '@parcel/plugin';
// Create a custom resolver dynamically.
function createResolver(context: any): Resolver {
return new Resolver({
async resolve() { /* ... */ }
});
}
const bundler = new Parcel({
entries: 'input.css',
// Pass in a config object directly, no need to automatically import anything.
config: {
...getDefaultParcelConfig(),
resolvers: [ createResolver(someUseCaseSpecificContext), getDefaultParcelResolver() ],
},
});
const bundled = await bundler.bundle();
await fs.writeFile('output.css', bundled); This is a bit idealized, and passing through the default configuration is a bit awkward, but I think this structure would remove a lot of the coupling between Parcel and Does an API at this level of abstraction exist in Parcel? Or is the Parcel API the closest thing? |
I think you're looking for something similar to #82, though perhaps rather than providing the file contents you only need to do some kind of custom mapping of the filenames? We could add a JS API for that, but I was worried that it would impact performance since we'd need to block on the JS thread for each dependency. When you use Parcel, the If you're willing to write Rust, you could do this today using a custom SourceProvider wrapping around |
Ah, I didn't realize this module was also published as a crate. I'm certainly open to writing Rust here and can explore that option. I see your concern with the performance, making a JS call for every import is definitely a hit and I understand why you would want to avoid it. My immediate thoughts are:
#82 does seem very similar. My use case is still on the filesystem, just at different paths, so a resolver alone would be good enough for me. However, I understand that providing file contents directly would probably be more flexible, and I'm fine making the |
I took a pass at a Rust implementation and it seems to mostly work, it's definitely much easier to integrate with Bazel than using the Parcel API since I no longer need to deal with getting the configuration file and resolver to load. The two pieces of feedback I have from this experiment are:
/* my_workspace/foo/bar.css */
/* Absolute import of `test.css` from the project root. */
@import 'my_workspace/test.css'; This results in a call to the I think the issue here is that the bundler is assuming that is ok to join the import specifier with the current file path, when that may not be true or have the intended effect. Ideally pub trait SourceProvider: Send + Sync {
fn read<'a>(&'a self, import_specifier: &str, imported_from_file: &Path) -> Result<&'a str>;
} This would also allow plugins to treat specifiers as non-file paths. For example, if someone wanted to implement Personally I would still love to see a JS binding for path resolution, though Rust is good enough as a workaround. Sorry that this issue is covering a lot of different topics, just trying to share my experience with configuring Parcel for an uncommon use case and the challenges I encounter. Hopefully this is useful to see where it can be improved. I'm happy to take a stab at implementing some of these suggestions if you think they are the right way to go here. |
The Parcel CSS Rust API has a massive surface area because it exposes structs for each individual CSS rule/property/value. The alpha status indicates that these structs are still changing as we work on improvements/features. However, the quality of the parsing/compiling/minifying itself (the parts exposed to JS) is considered stable, and it should be safe to use top-level APIs like StyleSheet and Bundler.
I think probably we'd have a separate method on the SourceProvider trait to resolve the path since
Happy to also consider that, but getting the Rust implementation going first would be a good start. |
…r` (parcel-bundler#174) This allows the `fs!` macro to be reused with other `SourceProvider` implementations on a per-test basis.
…r` (parcel-bundler#174) This allows the `fs!` macro to be reused with other `SourceProvider` implementations on a per-test basis.
…l-bundler#174) This allows each caller to define its own assertions and not be forced into asserting for `BundleErrorKind::UnsupportedLayerCombination`.
This adds a `resolve()` method to `SourceProvider` which is responsible for converting an `@import` specifier to a file path. The default `FileProvider` still uses the existing behavior of assuming the specifier to be a relative path and joining it with the originating file's path.
…r` (parcel-bundler#174) This allows the `fs!` macro to be reused with other `SourceProvider` implementations on a per-test basis.
…l-bundler#174) This allows each caller to define its own assertions and not be forced into asserting for `BundleErrorKind::UnsupportedLayerCombination`.
This adds a `resolve()` method to `SourceProvider` which is responsible for converting an `@import` specifier to a file path. The default `FileProvider` still uses the existing behavior of assuming the specifier to be a relative path and joining it with the originating file's path.
Thanks @devongovett! I made #177 with an initial attempt at adding a |
* Refactor bundle tests' `fs!` macro to drop dependency on `TestProvider` (#174) This allows the `fs!` macro to be reused with other `SourceProvider` implementations on a per-test basis. * Refactor `error_test()` to invoke a callback for its assertion (#174) This allows each caller to define its own assertions and not be forced into asserting for `BundleErrorKind::UnsupportedLayerCombination`. * Add `resolve()` method to `SourceProvider` (#174) This adds a `resolve()` method to `SourceProvider` which is responsible for converting an `@import` specifier to a file path. The default `FileProvider` still uses the existing behavior of assuming the specifier to be a relative path and joining it with the originating file's path.
I spent some time today trying to implement a JS resolver and wanted to document my findings so far (apologies for the wall of text, I just want to get this down somewhere before I forget). Again, I'm not that familiar with Rust or NAPI, so take everything here with a grain of salt. TL;DR:
I was hoping to get an API like: const { code } = css.bundle({
filename: 'styles.css',
resolver: {
async resolve(specifier: string, originatingFile: string): Promise<string> {
return path.join(specifier, originatingFile);
},
async read(filePath: string): Promise<string> {
return await fs.readFile(filePath, 'utf-8');
},
},
}); If either The first problem I ran into was that updating the I worked around this by just manually getting #[cfg(not(target_arch = "wasm32"))]
#[js_function(1)]
fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
let opts = ctx.get::<JsObject>(0)?;
let resolver = opts.get::<_, napi::JsObject>("resolver")?.unwrap();
let read = opts.get::<_, napi::JsFunction>("read")?.unwrap();
// ...
} I then tried to stick the After a lot more research, I eventually came to understand the
That second issue is particularly tricky to work around and was the reason the JS wasn't actually receiving the call, because the main thread was blocked and not able to process the JS execution queue. Since await new Promise((resolve) => setTimeout(resolve, 1_000)); to the end of the test case yielded the main thread to the JS event queue and gave Rust an opportunity to actually execute the queued JS and In theory, // Queue of paths to resolve.
const resolveQueue = new ThreadSafeQueue<[ threadId: number, specifier: string ]>();
// Queue of resolved paths.
const resolvedQueue = new ThreadSafeQueue<[ threadId: number, path: string ]>();
function bundle(entryPoint: string, sourceProvider: SourceProvider): string {
const stylesheet = parseStylesheet(entryPoint);
// Spawn a thread for each rule for simplicity.
const threads = stylesheet.rules.map((rule) => thread.spawn((threadId) => {
if (!rule.isImport()) return; // Ignore other rule types for simplicity.
// Queue a request for the main thread to resolve the given import.
resolveQueue.push([ threadId, rule.importUrl ]);
// Spin lock until the main thread has resolved the import.
let path: string;
while (true) {
const resolved = resolvedQueue.find(([ tid ]) => threadId === tid);
if (!resolved) continue;
path = resolved[1];
}
// Use resolved `path`...
}));
// Spin lock on the main thread until all background threads are complete.
while (!threads.every((thread) => thread.isDone())) {
if (resolveQueue.length === 0) continue; // Nothing to do.
// Take an item from the queue, call the JS `resolve()` function, and queue the result back.
const [ [ threadId, specifier ] ] = resolveQueue.splice(0, 1);
const path = sourceProvider.resolve(specifier); // Probably need a callback here since `ThreadSafeFunction` can't return directly.
resolvedQueue.push([ threadId, path ]);
}
// All threads completed, do something with the result...
} That's theoretically possible, but sounds like a mess to manage and I'm doubtful that something like Rayon has utilities to manage this kind of multithreading model. There might be a cleaner way to do this with other multithreading primitives. If you haven't noticed, I'm a frontend dev by trade, so I'm not used to thinking in the multithreading mindset or familiar with the modern tools in this space. I do think the easier option is to make If you want to keep introduce a separate @devongovett, I think the core question here is: How do you feel about making |
@dgp1130 thanks for your detailed analysis! Did you look into the blocking mode for napi's thread safe function API? I haven't used it before, but looks like there is As for getting the return value, I think it should be possible, but we might need help from napi-rs. In the underlying C API, the thread safe function actually calls a C callback on the main thread, and not the JS function directly. The C callback's responsibility is then to call the JS function however it needs. napi-rs handles this internally, and currently doesn't expose a way to get the return value, but I think it could be updated to do so. cc. @Brooooooklyn. We could make everything async, but as you noted, it would add a significant amount of complexity. Previously I have found that async I/O actually might be slower when you have multiple worker threads of your own anyway (under the hood, node is just maintaining a thread pool to do the I/O), but could be worth experimenting with. The issues about the main thread blocking until the rayon tasks are complete sound harder to address, so it might be necessary. In that case, we should introduce a new async version of the API rather than making a breaking change I guess. |
@devongovett, thanks for looking this over. I did use Looking at the C API for That's a good point about how Node async I/O is just another thread pool, I had always hoped there was some native API which passed those performance improvements down the whole stack, but I don't think it actually works that way. I can try an approach which introduces a new One other thing I was thinking about was that I noticed a lot of |
You'd need to marshal it back I guess. Here's the implementation in napi-rs: https://github.com/napi-rs/napi-rs/blob/main/crates/napi/src/threadsafe_function.rs#L400. Right now it passes a null pointer to the return value and doesn't do anything with it, but I guess it could and then attach it to the
There is a WASM build, but it does not currently support the |
) The two `SourceProvider` methods are now `async` which will give a hook for JavaScript to implement them and be called from the main thread. Everything is `async` now where possible and `bundle()` executes the `Future` synchronously to maintain its contract. Since it won't support custom JavaScript resolvers, there should never be a case where `bundle()` can't execute synchronously. Rayon doesn't seem to support `async` iterators, but also shouldn't be as necessary here. Removed it in order to call `async` APIs.
This is roughly equivalent to `bundle()`, except that is executes the `Future` *asynchronously*, returning a JS `Promise` holding the result. Errors are formatted a bit differently since they just use the `Display` trait to convert to strings, rather than the `throw()` function which converts them to a JS `SyntaxError` in `bundle()`. This can't be used in the async version because it must return a `napi::Result` which is immediately emitted to the user and no JS values are accessible. While it is possible to return the `CompileError` directly from the async block in a `napi::Result<Result<TransformResult, CompileError>>` and then call `throw()` in the callback with access to JS data, doing so causes lifetime issues with `fs` and isn't easily viable.
…#174) This adds a new `JsSourceProvider` which acts a `SourceProvider` which invokes associated JS implementations of the resolver if they exist or falls back to `FileProvider` when not given. This allows JS consumers to override one or both of these methods. If JS does *not* override either, they should not pay any significant performance penalty since all the Rust work will stay on the same thread. The JS implementations are invoked as thread-safe functions, pushing the arguments onto a queue and adding it to the event queue. Some time later, the main thread pulls from this queue and invokes the function. `napi-rs` doesn't seem to provide any means of receiving a JS return value in Rust, so instead arguments for both `read()` and `resolve()` include a callback function for JS to invoke with a result. `napi-rs` also requires Node async calling conventions, so the actual contract of these functions are: ```javascript await bundleAsync({ // Options... resolver: { read(err: any, file: string, cb: (string) => void): void { if (err) cb(err, null); // Propagate error. // Read file and invoke callback with result. fs.read(file, 'utf-8').then((res) => cb(null, res), (err) => cb(err, null)); }, resolve(err: any, specifier: string, originatingFile: string, cb: (string) => void): void { if (err) cb(err, null); // Propagate error. // Resolve and invoke callback with result. const resolved = path.resolve(originatingFile, '..', specifier); cb(null, resolved); }, }, }); ```
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that N-API sends an `error` as the first parameter and a `callback` as the last parameter, expecting the function to propagate errors and invoke the callback. This is unergonomic, so this shim presents a traditional async API on top of these implementation details. Also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
) This adds a new `JsSourceProvider` which acts a `SourceProvider` which invokes associated JS implementations of the resolver if they exist or falls back to `FileProvider` when not given. This allows JS consumers to override one or both of these methods. If JS does *not* override either, they should not pay any significant performance penalty since all the Rust work will stay on the same thread. The JS implementations are invoked as thread-safe functions, pushing the arguments onto a queue and adding it to the event queue. Some time later, the main thread pulls from this queue and invokes the function. `napi-rs` doesn't seem to provide any means of receiving a JS return value in Rust, so instead arguments for both `read()` and `resolve()` include a callback function for JS to invoke with a result. `napi-rs` also requires Node async calling conventions, so the actual contract of these functions are: ```javascript await bundleAsync({ // Options... resolver: { read(err: any, file: string, cb: (string) => void): void { if (err) cb(err, null); // Propagate error. // Read file and invoke callback with result. fs.read(file, 'utf-8').then((res) => cb(null, res), (err) => cb(err, null)); }, resolve(err: any, specifier: string, originatingFile: string, cb: (string) => void): void { if (err) cb(err, null); // Propagate error. // Resolve and invoke callback with result. const resolved = path.resolve(originatingFile, '..', specifier); cb(null, resolved); }, }, }); ```
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that N-API sends an `error` as the first parameter and a `callback` as the last parameter, expecting the function to propagate errors and invoke the callback. This is unergonomic, so this shim presents a traditional async API on top of these implementation details. Also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that `napi-rs` doesn't actually do anything with the return value of a threadsafe JS function, so any returned data would be dropped. Instead, Parcel adds a callback function which gets invoked by JS with the resulting value, following Node conventions of `callback(err, result)`. This is unergonimic as an API, so the JS shim exposes a `Promise`-based interface which gets converted to the callback behavior required by `napi-rs`. This also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that `napi-rs` doesn't actually do anything with the return value of a threadsafe JS function, so any returned data would be dropped. Instead, Parcel adds a callback function which gets invoked by JS with the resulting value, following Node conventions of `callback(err, result)`. This is unergonimic as an API, so the JS shim exposes a `Promise`-based interface which gets converted to the callback behavior required by `napi-rs`. This also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
) This adds a new `JsSourceProvider` which acts a `SourceProvider` which invokes associated JS implementations of the resolver if they exist or falls back to `FileProvider` when not given. This allows JS consumers to override one or both of these methods. If JS does *not* override either, they should not pay any significant performance penalty since all the Rust work will stay on the same thread. The JS implementations are invoked as thread-safe functions, pushing the arguments onto a queue and adding it to the event queue. Some time later, the main thread pulls from this queue and invokes the function. `napi-rs` doesn't seem to provide any means of receiving a JS return value in Rust, so instead arguments for both `read()` and `resolve()` include a callback function using Node calling conventions (`callback(err, result)`). This looks like: ```javascript await bundleAsync({ // Options... resolver: { read(file: string, cb: (string) => void): void { // Read file and invoke callback with result. fs.read(file, 'utf-8').then((res) => cb(null, res), (err) => cb(err, null)); }, resolve(specifier: string, originatingFile: string, cb: (string) => void): void { // Resolve and invoke callback with result. const resolved = path.resolve(originatingFile, '..', specifier); cb(null, resolved); }, }, }); ```
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that `napi-rs` doesn't actually do anything with the return value of a threadsafe JS function, so any returned data would be dropped. Instead, Parcel adds a callback function which gets invoked by JS with the resulting value, following Node conventions of `callback(err, result)`. This is unergonimic as an API, so the JS shim exposes a `Promise`-based interface which gets converted to the callback behavior required by `napi-rs`. This also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
) This adds a new `JsSourceProvider` which acts a `SourceProvider` which invokes associated JS implementations of the resolver if they exist or falls back to `FileProvider` when not given. This allows JS consumers to override one or both of these methods. If JS does *not* override either, they should not pay any significant performance penalty since all the Rust work will stay on the same thread. The JS implementations are invoked as thread-safe functions, pushing the arguments onto a queue and adding it to the event queue. Some time later, the main thread pulls from this queue and invokes the function. `napi-rs` doesn't seem to provide any means of receiving a JS return value in Rust, so instead arguments for both `read()` and `resolve()` include a callback function using Node calling conventions (`callback(err, result)`). This looks like: ```javascript await bundleAsync({ // Options... resolver: { read(file: string, cb: (string) => void): void { // Read file and invoke callback with result. fs.read(file, 'utf-8').then((res) => cb(null, res), (err) => cb(err, null)); }, resolve(specifier: string, originatingFile: string, cb: (string) => void): void { // Resolve and invoke callback with result. const resolved = path.resolve(originatingFile, '..', specifier); cb(null, resolved); }, }, }); ```
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that `napi-rs` doesn't actually do anything with the return value of a threadsafe JS function, so any returned data would be dropped. Instead, Parcel adds a callback function which gets invoked by JS with the resulting value, following Node conventions of `callback(err, result)`. This is unergonimic as an API, so the JS shim exposes a `Promise`-based interface which gets converted to the callback behavior required by `napi-rs`. This also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
) The two `SourceProvider` methods are now `async` which will give a hook for JavaScript to implement them and be called from the main thread. Everything is `async` now where possible and `bundle()` executes the `Future` synchronously to maintain its contract. Since it won't support custom JavaScript resolvers, there should never be a case where `bundle()` can't execute synchronously. Rayon doesn't seem to support `async` iterators, but also shouldn't be as necessary here. Removed it in order to call `async` APIs.
This is roughly equivalent to `bundle()`, except that is executes the `Future` *asynchronously*, returning a JS `Promise` holding the result. Errors are formatted a bit differently since they just use the `Display` trait to convert to strings, rather than the `throw()` function which converts them to a JS `SyntaxError` in `bundle()`. This can't be used in the async version because it must return a `napi::Result` which is immediately emitted to the user and no JS values are accessible. While it is possible to return the `CompileError` directly from the async block in a `napi::Result<Result<TransformResult, CompileError>>` and then call `throw()` in the callback with access to JS data, doing so causes lifetime issues with `fs` and isn't easily viable.
) This adds a new `JsSourceProvider` which acts a `SourceProvider` which invokes associated JS implementations of the resolver if they exist or falls back to `FileProvider` when not given. This allows JS consumers to override one or both of these methods. If JS does *not* override either, they should not pay any significant performance penalty since all the Rust work will stay on the same thread. The JS implementations are invoked as thread-safe functions, pushing the arguments onto a queue and adding it to the event queue. Some time later, the main thread pulls from this queue and invokes the function. `napi-rs` doesn't seem to provide any means of receiving a JS return value in Rust, so instead arguments for both `read()` and `resolve()` include a callback function using Node calling conventions (`callback(err, result)`). This looks like: ```javascript await bundleAsync({ // Options... resolver: { read(file: string, cb: (string) => void): void { // Read file and invoke callback with result. fs.read(file, 'utf-8').then((res) => cb(null, res), (err) => cb(err, null)); }, resolve(specifier: string, originatingFile: string, cb: (string) => void): void { // Resolve and invoke callback with result. const resolved = path.resolve(originatingFile, '..', specifier); cb(null, resolved); }, }, }); ```
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that `napi-rs` doesn't actually do anything with the return value of a threadsafe JS function, so any returned data would be dropped. Instead, Parcel adds a callback function which gets invoked by JS with the resulting value, following Node conventions of `callback(err, result)`. This is unergonimic as an API, so the JS shim exposes a `Promise`-based interface which gets converted to the callback behavior required by `napi-rs`. This also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
Proposed implementation of custom JS resolver in #196. Mostly works, but still a few kinks to work out. |
) The two `SourceProvider` methods are now `async` which will give a hook for JavaScript to implement them and be called from the main thread. Everything is `async` now where possible and `bundle()` executes the `Future` synchronously to maintain its contract. Since it won't support custom JavaScript resolvers, there should never be a case where `bundle()` can't execute synchronously. Rayon doesn't seem to support `async` iterators, but also shouldn't be as necessary here. Removed it in order to call `async` APIs.
This is roughly equivalent to `bundle()`, except that is executes the `Future` *asynchronously*, returning a JS `Promise` holding the result. Errors are formatted a bit differently since they just use the `Display` trait to convert to strings, rather than the `throw()` function which converts them to a JS `SyntaxError` in `bundle()`. This can't be used in the async version because it must return a `napi::Result` which is immediately emitted to the user and no JS values are accessible. While it is possible to return the `CompileError` directly from the async block in a `napi::Result<Result<TransformResult, CompileError>>` and then call `throw()` in the callback with access to JS data, doing so causes lifetime issues with `fs` and isn't easily viable.
) This adds a new `JsSourceProvider` which acts a `SourceProvider` which invokes associated JS implementations of the resolver if they exist or falls back to `FileProvider` when not given. This allows JS consumers to override one or both of these methods. If JS does *not* override either, they should not pay any significant performance penalty since all the Rust work will stay on the same thread. The JS implementations are invoked as thread-safe functions, pushing the arguments onto a queue and adding it to the event queue. Some time later, the main thread pulls from this queue and invokes the function. `napi-rs` doesn't seem to provide any means of receiving a JS return value in Rust, so instead arguments for both `read()` and `resolve()` include a callback function using Node calling conventions (`callback(err, result)`). This looks like: ```javascript await bundleAsync({ // Options... resolver: { read(file: string, cb: (string) => void): void { // Read file and invoke callback with result. fs.read(file, 'utf-8').then((res) => cb(null, res), (err) => cb(err, null)); }, resolve(specifier: string, originatingFile: string, cb: (string) => void): void { // Resolve and invoke callback with result. const resolved = path.resolve(originatingFile, '..', specifier); cb(null, resolved); }, }, }); ```
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that `napi-rs` doesn't actually do anything with the return value of a threadsafe JS function, so any returned data would be dropped. Instead, Parcel adds a callback function which gets invoked by JS with the resulting value, following Node conventions of `callback(err, result)`. This is unergonimic as an API, so the JS shim exposes a `Promise`-based interface which gets converted to the callback behavior required by `napi-rs`. This also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
) The two `SourceProvider` methods are now `async` which will give a hook for JavaScript to implement them and be called from the main thread. Everything is `async` now where possible and `bundle()` executes the `Future` synchronously to maintain its contract. Since it won't support custom JavaScript resolvers, there should never be a case where `bundle()` can't execute synchronously. Rayon doesn't seem to support `async` iterators, but also shouldn't be as necessary here. Removed it in order to call `async` APIs.
This is roughly equivalent to `bundle()`, except that is executes the `Future` *asynchronously*, returning a JS `Promise` holding the result. Errors are formatted a bit differently since they just use the `Display` trait to convert to strings, rather than the `throw()` function which converts them to a JS `SyntaxError` in `bundle()`. This can't be used in the async version because it must return a `napi::Result` which is immediately emitted to the user and no JS values are accessible. While it is possible to return the `CompileError` directly from the async block in a `napi::Result<Result<TransformResult, CompileError>>` and then call `throw()` in the callback with access to JS data, doing so causes lifetime issues with `fs` and isn't easily viable.
) This adds a new `JsSourceProvider` which acts a `SourceProvider` which invokes associated JS implementations of the resolver if they exist or falls back to `FileProvider` when not given. This allows JS consumers to override one or both of these methods. If JS does *not* override either, they should not pay any significant performance penalty since all the Rust work will stay on the same thread. The JS implementations are invoked as thread-safe functions, pushing the arguments onto a queue and adding it to the event queue. Some time later, the main thread pulls from this queue and invokes the function. `napi-rs` doesn't seem to provide any means of receiving a JS return value in Rust, so instead arguments for both `read()` and `resolve()` include a callback function using Node calling conventions (`callback(err, result)`). This looks like: ```javascript await bundleAsync({ // Options... resolver: { read(file: string, cb: (string) => void): void { // Read file and invoke callback with result. fs.read(file, 'utf-8').then((res) => cb(null, res), (err) => cb(err, null)); }, resolve(specifier: string, originatingFile: string, cb: (string) => void): void { // Resolve and invoke callback with result. const resolved = path.resolve(originatingFile, '..', specifier); cb(null, resolved); }, }, }); ```
…(...args) => Promise<Result>` (parcel-bundler#174) This hides the fact that `napi-rs` doesn't actually do anything with the return value of a threadsafe JS function, so any returned data would be dropped. Instead, Parcel adds a callback function which gets invoked by JS with the resulting value, following Node conventions of `callback(err, result)`. This is unergonimic as an API, so the JS shim exposes a `Promise`-based interface which gets converted to the callback behavior required by `napi-rs`. This also includes tests for all the common use cases and error conditions of custom JS resolvers.
This rejects immediately if the user attempts to pass a `resolver` property to the synchronous `bundle()` function. Since communciating across between background threads and the main thread is quite tricky without an asynchronous hook, `bundle()` does not support custom resolvers.
Hi, I'm trying to use
@parcel/css
to bundle CSS files, however there doesn't seem to be a way to provide a custom resolver for@import
statements, the package appears to directly use the real file system. This is a challenge for my use case where I'm hoping to use Parcel for CSS bundling in a Bazel workspace. Bazel is a build tool which puts generated files in different directories from the source code, meaning that@import './foo.css'
could resolve to./foo
or$project_root/dist/bin/path/to/dir/foo.css
, or even a few other paths. I would like to configure@parcel/css
to resolve these paths correctly, but AFAICT, this package doesn't have any direct support for that. See dgp1130/rules_prerender#46 and dgp1130/rules_prerender@7e995d6 for a little more info about the use case and my attempt to implement it.It does seem that custom resolvers are supported in
.parcelrc
, but I don't get the impression@parcel/css
has any understanding of that configuration file. I'm unclear how theparcel
package supports custom CSS resolvers if@parcel/css
doesn't, but maybe they don't compose each other in the way I think they do? If there is a more appropriate package or different level of abstraction I should be using which would support resolvers, please let me know and I'm happy to try that.If it does make sense to support custom resolvers in
@parcel/css
, I'm happy to put in the effort to contribute it. I think we would just need to updateBundleConfig
to accept aSourceResolver
from JS and then use that instead of theFileResolver
. All the inputs and outputs would be strings, so serializing into and out-of JS shouldn't be too much of an issue. I think it's possible, though I haven't done too much with Rust or WASM, so any pointers or suggestions here would be appreciated.Is this a contribution you would be interested in accepting or am I going about this problem in the wrong manner?
The text was updated successfully, but these errors were encountered: