mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
feat(napi): add Relay transform plugin (#25503)
## What Ports the Relay `graphql` tagged template transform (babel-plugin-relay / swc_relay) as an individual napi transform, per #24914: - **`crates/oxc_relay`** — the core transform as its own crate (modeled on `oxc_react_compiler`): replaces `graphql` tagged templates with references to relay-compiler artifacts. Definition names are extracted textually like `swc_relay` (equivalent to `\b(fragment|mutation|query|subscription)\s+(\w+)` with `#` comments stripped; hand-rolled scanner, no regex dependency). - **`napi/transform-relay`** (`oxc-transform-relay`) — standalone napi package copied from the `napi/transform-react` template: parse → semantic → relay traverse → codegen. Only `graphql` tags are rewritten; TypeScript/JSX syntax is preserved untouched, so the output composes with any downstream toolchain. ## Options | option | default | behavior | |---|---|---| | `artifactDirectory` | unset | when set, the import path is computed lexically relative to the transformed file (babel-plugin-relay `getRelativeImportPath` semantics — swc emits absolute paths instead); unset → `./__generated__/` next to the file | | `language` | `javascript` | `typescript` imports `Name.graphql.ts`, otherwise `Name.graphql.js` | | `eagerEsModules` | `true` | hoisted default imports (babel-plugin-relay ≥ v17 parity); `false` emits inline `require()` (`@swc/plugin-relay` / Next.js behavior) | A `graphql` tag with no extractable definition name produces an error diagnostic (babel throws; swc silently skips). Not supported, documented in both READMEs: dev-mode artifact hash validation, `jsModuleFormat: "haste"`, `isDevVariableName`, swc's Next.js-specific `projects`/`pagesDir`, and GraphQL document validation. closes #24914 --- Implemented with Claude Code.
This commit is contained in:
@@ -77,6 +77,12 @@ pnpm-lock.yaml
|
||||
/tasks/benchmark/benches/react_compiler.rs @Boshen
|
||||
/.github/workflows/release_napi_transform_react.yml @Boshen
|
||||
|
||||
# Relay transform (@Boshen)
|
||||
|
||||
/crates/oxc_relay @Boshen
|
||||
/napi/transform-relay @Boshen
|
||||
/.github/workflows/release_napi_transform_relay.yml @Boshen
|
||||
|
||||
# Core infrastructure crates (@overlookmotel)
|
||||
|
||||
/crates/oxc_allocator @overlookmotel
|
||||
|
||||
@@ -37,6 +37,7 @@ A-transformer:
|
||||
"tasks/transform_conformance/**",
|
||||
"napi/transform/**",
|
||||
"napi/transform-react/**",
|
||||
"napi/transform-relay/**",
|
||||
]
|
||||
|
||||
A-linter:
|
||||
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
id: filter
|
||||
with:
|
||||
exclude: oxc_linter,oxc_language_server
|
||||
paths: napi/parser/,napi/minify/,napi/transform/,napi/transform-react/,npm/oxc-types/,npm/runtime/,tasks/e2e/,pnpm-lock.yaml,package.json
|
||||
paths: napi/parser/,napi/minify/,napi/transform/,napi/transform-react/,napi/transform-relay/,npm/oxc-types/,npm/runtime/,tasks/e2e/,pnpm-lock.yaml,package.json
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
- uses: oxc-project/setup-node@d59b270414aa7d0eed947eed4eb5b0b8a675f01d # v1.4.0
|
||||
@@ -226,8 +226,8 @@ jobs:
|
||||
RUN_RAW_RANGE_TESTS: "true"
|
||||
RUN_RAW_TOKENS_TESTS: "true"
|
||||
run: |
|
||||
pnpm --workspace-concurrency=1 --filter "./napi/parser" --filter "./napi/minify" --filter "./napi/transform" --filter "./napi/transform-react" build-test
|
||||
pnpm --workspace-concurrency=1 --filter "./napi/parser" --filter "./napi/minify" --filter "./napi/transform" --filter "./napi/transform-react" test
|
||||
pnpm --workspace-concurrency=1 --filter "./napi/parser" --filter "./napi/minify" --filter "./napi/transform" --filter "./napi/transform-react" --filter "./napi/transform-relay" build-test
|
||||
pnpm --workspace-concurrency=1 --filter "./napi/parser" --filter "./napi/minify" --filter "./napi/transform" --filter "./napi/transform-react" --filter "./napi/transform-relay" test
|
||||
- if: steps.filter.outputs.changed == 'true'
|
||||
name: Run e2e tests
|
||||
working-directory: tasks/e2e
|
||||
@@ -347,7 +347,7 @@ jobs:
|
||||
id: filter
|
||||
with:
|
||||
exclude: oxc_linter,oxc_language_server
|
||||
paths: napi/parser/,napi/minify/,napi/transform/,napi/transform-react/,npm/oxc-types/,npm/runtime/,tasks/e2e/,pnpm-lock.yaml,package.json
|
||||
paths: napi/parser/,napi/minify/,napi/transform/,napi/transform-react/,napi/transform-relay/,npm/oxc-types/,npm/runtime/,tasks/e2e/,pnpm-lock.yaml,package.json
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
- uses: oxc-project/setup-node@d59b270414aa7d0eed947eed4eb5b0b8a675f01d # v1.4.0
|
||||
@@ -377,8 +377,8 @@ jobs:
|
||||
env:
|
||||
RUN_RAW_RANGE_TESTS: "true"
|
||||
run: |
|
||||
pnpm --workspace-concurrency=1 --filter "./napi/parser" --filter "./napi/minify" --filter "./napi/transform" --filter "./napi/transform-react" build-test
|
||||
pnpm --workspace-concurrency=1 --filter "./napi/parser" --filter "./napi/minify" --filter "./napi/transform" --filter "./napi/transform-react" test
|
||||
pnpm --workspace-concurrency=1 --filter "./napi/parser" --filter "./napi/minify" --filter "./napi/transform" --filter "./napi/transform-react" --filter "./napi/transform-relay" build-test
|
||||
pnpm --workspace-concurrency=1 --filter "./napi/parser" --filter "./napi/minify" --filter "./napi/transform" --filter "./napi/transform-react" --filter "./napi/transform-relay" test
|
||||
- if: steps.filter.outputs.changed == 'true'
|
||||
name: Run e2e tests
|
||||
working-directory: ${{ env.DEV_DRIVE_WORKSPACE }}/tasks/e2e
|
||||
|
||||
@@ -64,6 +64,7 @@ jobs:
|
||||
pnpm --filter oxc-parser run build-dev
|
||||
pnpm --filter oxc-transform run build-dev
|
||||
pnpm --filter oxc-transform-react run build-dev
|
||||
pnpm --filter oxc-transform-relay run build-dev
|
||||
|
||||
- name: Update Cargo.lock
|
||||
run: cargo check
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Release NAPI Relay Transform
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- napi/transform-relay/package.json
|
||||
- .github/workflows/release_napi_transform_relay.yml
|
||||
- .github/workflows/reusable_release_napi.yml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.repository == 'oxc-project/oxc'
|
||||
name: Release NAPI Relay Transform
|
||||
uses: ./.github/workflows/reusable_release_napi.yml
|
||||
with:
|
||||
name: transform-relay
|
||||
permissions:
|
||||
id-token: write # for `pnpm publish --provenance`
|
||||
Generated
+29
@@ -2525,6 +2525,21 @@ dependencies = [
|
||||
"unicode-id-start",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_relay"
|
||||
version = "0.144.0"
|
||||
dependencies = [
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_codegen",
|
||||
"oxc_diagnostics",
|
||||
"oxc_parser",
|
||||
"oxc_semantic",
|
||||
"oxc_span",
|
||||
"oxc_str",
|
||||
"oxc_traverse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_resolver"
|
||||
version = "11.24.2"
|
||||
@@ -2746,6 +2761,20 @@ dependencies = [
|
||||
"oxc_sourcemap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_transform_relay_napi"
|
||||
version = "0.144.0"
|
||||
dependencies = [
|
||||
"mimalloc-safe",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"oxc",
|
||||
"oxc_napi",
|
||||
"oxc_relay",
|
||||
"oxc_sourcemap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_transformer"
|
||||
version = "0.144.0"
|
||||
|
||||
+9
-1
@@ -146,12 +146,14 @@ oxc_parser = { version = "0.144.0", path = "crates/oxc_parser", features = [
|
||||
oxc_parser_napi = { version = "0.144.0", path = "napi/parser" } # Node.js parser binding
|
||||
oxc_react_compiler = { version = "0.143.0", path = "crates/oxc_react_compiler" } # React Compiler integration (experimental)
|
||||
oxc_regular_expression = { version = "0.144.0", path = "crates/oxc_regular_expression" } # Regex parser
|
||||
oxc_relay = { version = "0.144.0", path = "crates/oxc_relay" } # Relay graphql tagged template transform
|
||||
oxc_semantic = { version = "0.144.0", path = "crates/oxc_semantic" } # Semantic analysis
|
||||
oxc_span = { version = "0.144.0", path = "crates/oxc_span" } # Source positions
|
||||
oxc_str = { version = "0.144.0", path = "crates/oxc_str" } # String types
|
||||
oxc_syntax = { version = "0.144.0", path = "crates/oxc_syntax" } # Syntax utilities
|
||||
oxc_transform_napi = { version = "0.144.0", path = "napi/transform" } # Node.js transformer binding
|
||||
oxc_transform_react_napi = { version = "0.143.0", path = "napi/transform-react" } # Node.js React Compiler binding
|
||||
oxc_transform_relay_napi = { version = "0.144.0", path = "napi/transform-relay" } # Node.js Relay transform binding
|
||||
oxc_transformer = { version = "0.144.0", path = "crates/oxc_transformer" } # Code transformation
|
||||
oxc_transformer_plugins = { version = "0.144.0", path = "crates/oxc_transformer_plugins" } # Transformer plugins
|
||||
oxc_traverse = { version = "0.144.0", path = "crates/oxc_traverse" } # AST traversal
|
||||
@@ -278,7 +280,13 @@ walkdir = "2.5.0" # Directory traversal
|
||||
windows-sys = { version = "0.61.2" } # Windows API bindings
|
||||
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["oxc_transform_napi", "oxc_transform_react_napi", "oxc_parser_napi", "oxc_minify_napi"]
|
||||
ignored = [
|
||||
"oxc_transform_napi",
|
||||
"oxc_transform_react_napi",
|
||||
"oxc_transform_relay_napi",
|
||||
"oxc_parser_napi",
|
||||
"oxc_minify_napi",
|
||||
]
|
||||
|
||||
# `oxc_allocator` is both dogfooded here (workspace path) and published to crates.io.
|
||||
# External workspace-dependencies (e.g. `oxc-css-parser`) pull it from the registry,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this package will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0).
|
||||
@@ -0,0 +1,39 @@
|
||||
# oxc_relay
|
||||
#
|
||||
# Rust port of the Relay `graphql` tagged template transform
|
||||
# (`babel-plugin-relay` / `swc_relay`), backing the `oxc-transform-relay`
|
||||
# Node.js binding in `napi/transform-relay`.
|
||||
|
||||
[package]
|
||||
name = "oxc_relay"
|
||||
version = "0.144.0"
|
||||
publish = false
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "Relay graphql tagged template transform"
|
||||
include = ["src/**/*", "README.md", "CHANGELOG.md"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
oxc_allocator = { workspace = true }
|
||||
oxc_ast = { workspace = true }
|
||||
oxc_diagnostics = { workspace = true }
|
||||
oxc_semantic = { workspace = true }
|
||||
oxc_span = { workspace = true }
|
||||
oxc_str = { workspace = true }
|
||||
oxc_traverse = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
oxc_codegen = { workspace = true }
|
||||
oxc_parser = { workspace = true }
|
||||
@@ -0,0 +1,42 @@
|
||||
# oxc_relay
|
||||
|
||||
Rust port of the Relay `graphql` tagged template transform
|
||||
([babel-plugin-relay](https://github.com/facebook/relay/tree/main/packages/babel-plugin-relay) /
|
||||
[swc_relay](https://github.com/swc-project/plugins/tree/main/packages/relay)).
|
||||
|
||||
Replaces `graphql` tagged template expressions with references to the artifact
|
||||
files generated by `relay-compiler`:
|
||||
|
||||
```js
|
||||
const data = graphql`
|
||||
query FooQuery {
|
||||
id
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
becomes (with `eager_es_modules: true`, the default):
|
||||
|
||||
```js
|
||||
import _FooQuery from "./__generated__/FooQuery.graphql.js";
|
||||
const data = _FooQuery;
|
||||
```
|
||||
|
||||
or (with `eager_es_modules: false`):
|
||||
|
||||
```js
|
||||
const data = require("./__generated__/FooQuery.graphql.js");
|
||||
```
|
||||
|
||||
This crate backs the [`oxc-transform-relay`](https://www.npmjs.com/package/oxc-transform-relay)
|
||||
Node.js package in `napi/transform-relay`.
|
||||
|
||||
## Limitations
|
||||
|
||||
Compared to `babel-plugin-relay`:
|
||||
|
||||
- No development-mode artifact hash validation (`console.error` on stale
|
||||
artifacts) — like `swc_relay`, the transform is production-shaped.
|
||||
- No `jsModuleFormat: "haste"` and no `isDevVariableName`.
|
||||
- The definition name is extracted textually (like `swc_relay`), not by parsing
|
||||
the GraphQL document, so documents are not validated.
|
||||
@@ -0,0 +1,356 @@
|
||||
//! Relay `graphql` tagged template transform.
|
||||
//!
|
||||
//! Replaces `graphql` tagged template expressions with references to the
|
||||
//! artifact files generated by `relay-compiler`:
|
||||
//!
|
||||
//! ```js
|
||||
//! const data = graphql`query FooQuery { id }`;
|
||||
//! ```
|
||||
//!
|
||||
//! becomes (with `eager_es_modules: true`, the default):
|
||||
//!
|
||||
//! ```js
|
||||
//! import _FooQuery from "./__generated__/FooQuery.graphql.js";
|
||||
//! const data = _FooQuery;
|
||||
//! ```
|
||||
//!
|
||||
//! or (with `eager_es_modules: false`):
|
||||
//!
|
||||
//! ```js
|
||||
//! const data = require("./__generated__/FooQuery.graphql.js");
|
||||
//! ```
|
||||
//!
|
||||
//! Port of [babel-plugin-relay] and [swc_relay].
|
||||
//!
|
||||
//! Not supported compared to `babel-plugin-relay`: development-mode artifact
|
||||
//! hash validation, `jsModuleFormat: "haste"`, `isDevVariableName`, and GraphQL
|
||||
//! document validation (the definition name is extracted textually, like
|
||||
//! `swc_relay`, rather than by parsing the document).
|
||||
//!
|
||||
//! [babel-plugin-relay]: https://github.com/facebook/relay/tree/main/packages/babel-plugin-relay
|
||||
//! [swc_relay]: https://github.com/swc-project/plugins/tree/main/packages/relay
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use oxc_allocator::{Allocator, ArenaVec};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_diagnostics::{Diagnostics, OxcDiagnostic};
|
||||
use oxc_semantic::{ReferenceFlags, Scoping, SymbolFlags};
|
||||
use oxc_span::SPAN;
|
||||
use oxc_str::{Str, static_ident};
|
||||
use oxc_traverse::{BoundIdentifier, Traverse, traverse_mut};
|
||||
|
||||
type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, ()>;
|
||||
|
||||
/// The language `relay-compiler` emits artifacts in, which determines the
|
||||
/// artifact file extension (`.graphql.ts` for TypeScript, `.graphql.js`
|
||||
/// otherwise).
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum RelayLanguage {
|
||||
/// `Name.graphql.ts` artifacts.
|
||||
Typescript,
|
||||
/// `Name.graphql.js` artifacts.
|
||||
#[default]
|
||||
Javascript,
|
||||
/// `Name.graphql.js` artifacts.
|
||||
Flow,
|
||||
}
|
||||
|
||||
/// Options for the Relay transform.
|
||||
///
|
||||
/// Mirrors the options of `babel-plugin-relay` / `@swc/plugin-relay`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RelayOptions {
|
||||
/// Directory `relay-compiler` emits all artifacts to (its
|
||||
/// `artifactDirectory` setting). When set, artifacts are imported via a
|
||||
/// relative path from the file being transformed to this directory; the
|
||||
/// path is computed lexically, so both must either be absolute or relative
|
||||
/// to the same base directory. When unset, artifacts are imported from the
|
||||
/// `__generated__` directory next to the file being transformed.
|
||||
pub artifact_directory: Option<PathBuf>,
|
||||
|
||||
/// Artifact language, determining the imported file extension.
|
||||
///
|
||||
/// Defaults to [`RelayLanguage::Javascript`].
|
||||
pub language: RelayLanguage,
|
||||
|
||||
/// Emit a hoisted default import per `graphql` tag instead of an inline
|
||||
/// `require()` call.
|
||||
///
|
||||
/// Defaults to `true`, matching `babel-plugin-relay` since Relay v17.
|
||||
/// `@swc/plugin-relay` and Next.js default to `false`.
|
||||
pub eager_es_modules: bool,
|
||||
}
|
||||
|
||||
impl Default for RelayOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
artifact_directory: None,
|
||||
language: RelayLanguage::default(),
|
||||
eager_es_modules: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of [`Relay::build`].
|
||||
#[must_use]
|
||||
pub struct RelayReturn {
|
||||
/// Scoping returned from the traversal.
|
||||
pub scoping: Scoping,
|
||||
/// Errors for `graphql` tags no definition name could be extracted from.
|
||||
pub diagnostics: Diagnostics,
|
||||
}
|
||||
|
||||
/// Relay `graphql` tagged template transform.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let ret = Relay::new(RelayOptions::default(), Path::new("src/Foo.jsx"))
|
||||
/// .build(&allocator, &mut program, scoping);
|
||||
/// ```
|
||||
pub struct Relay<'a> {
|
||||
options: RelayOptions,
|
||||
/// Lexical relative path from the transformed file's directory to
|
||||
/// `artifact_directory`, precomputed with POSIX separators.
|
||||
/// `None` when `artifact_directory` is unset.
|
||||
relative_artifact_dir: Option<String>,
|
||||
/// One entry per transformed `graphql` tag in eager mode, in source order:
|
||||
/// the default import binding and the artifact import path.
|
||||
imports: Vec<(BoundIdentifier<'a>, Str<'a>)>,
|
||||
diagnostics: Diagnostics,
|
||||
}
|
||||
|
||||
impl<'a> Relay<'a> {
|
||||
/// Create a new Relay transform for the file at `source_path`.
|
||||
///
|
||||
/// `source_path` is only used lexically to compute artifact import paths;
|
||||
/// the filesystem is never accessed.
|
||||
pub fn new(options: RelayOptions, source_path: &Path) -> Self {
|
||||
let relative_artifact_dir = options.artifact_directory.as_ref().map(|artifact_dir| {
|
||||
let source_dir = source_path.parent().unwrap_or_else(|| Path::new(""));
|
||||
relative_path(&source_dir.to_string_lossy(), &artifact_dir.to_string_lossy())
|
||||
});
|
||||
Self { options, relative_artifact_dir, imports: vec![], diagnostics: Diagnostics::new() }
|
||||
}
|
||||
|
||||
/// Run the transform on `program`.
|
||||
pub fn build(
|
||||
mut self,
|
||||
allocator: &'a Allocator,
|
||||
program: &mut Program<'a>,
|
||||
scoping: Scoping,
|
||||
) -> RelayReturn {
|
||||
let scoping = traverse_mut(&mut self, allocator, program, scoping, ());
|
||||
RelayReturn { scoping, diagnostics: self.diagnostics }
|
||||
}
|
||||
|
||||
/// Import path for the artifact of the GraphQL definition `name`.
|
||||
fn artifact_import_path(&self, name: &str) -> String {
|
||||
let extension = match self.options.language {
|
||||
RelayLanguage::Typescript => "ts",
|
||||
RelayLanguage::Javascript | RelayLanguage::Flow => "js",
|
||||
};
|
||||
match self.relative_artifact_dir.as_deref() {
|
||||
None => format!("./__generated__/{name}.graphql.{extension}"),
|
||||
Some(".") => format!("./{name}.graphql.{extension}"),
|
||||
// Like `babel-plugin-relay`, paths already starting with `.`
|
||||
// (i.e. `..`) are used as-is; everything else gets a `./` prefix.
|
||||
Some(dir) if dir.starts_with('.') => format!("{dir}/{name}.graphql.{extension}"),
|
||||
Some(dir) => format!("./{dir}/{name}.graphql.{extension}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Traverse<'a, ()> for Relay<'a> {
|
||||
#[inline] // Fast path: most expressions are not tagged templates.
|
||||
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
let Expression::TaggedTemplateExpression(tagged) = expr else { return };
|
||||
let Expression::Identifier(tag) = &tagged.tag else { return };
|
||||
if tag.name != "graphql" {
|
||||
return;
|
||||
}
|
||||
|
||||
let span = tagged.span;
|
||||
// `Str::as_str` returns `&'a str` borrowed from the arena, not from
|
||||
// `tagged`, so `name` stays valid while `*expr` is replaced below.
|
||||
let Some(name) = extract_definition_name(&tagged.quasi) else {
|
||||
self.diagnostics.push(
|
||||
OxcDiagnostic::error(
|
||||
"`graphql` tagged template must contain a named GraphQL query, mutation, subscription, or fragment",
|
||||
)
|
||||
.with_label(span),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let source = Str::from_str_in(&self.artifact_import_path(name), ctx);
|
||||
*expr = if self.options.eager_es_modules {
|
||||
let binding = ctx.generate_uid_in_root_scope(name, SymbolFlags::Import);
|
||||
let reference = binding.create_spanned_read_expression(span, ctx);
|
||||
self.imports.push((binding, source));
|
||||
reference
|
||||
} else {
|
||||
let callee =
|
||||
ctx.create_unbound_ident_expr(SPAN, static_ident!("require"), ReferenceFlags::Read);
|
||||
let argument = Argument::new_string_literal(SPAN, source, None, ctx);
|
||||
Expression::new_call_expression(span, callee, None, [argument], false, ctx)
|
||||
};
|
||||
}
|
||||
|
||||
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
if self.imports.is_empty() {
|
||||
return;
|
||||
}
|
||||
// `import <uid> from "<artifact path>";`, one per `graphql` tag.
|
||||
let imports = self.imports.drain(..).map(|(binding, source)| {
|
||||
let local = binding.create_binding_identifier(ctx);
|
||||
let specifier =
|
||||
ImportDeclarationSpecifier::new_import_default_specifier(SPAN, local, ctx);
|
||||
let specifiers = Some(ArenaVec::from_value_in(specifier, ctx));
|
||||
let source = StringLiteral::new(SPAN, source, None, ctx);
|
||||
Statement::new_import_declaration(
|
||||
SPAN,
|
||||
specifiers,
|
||||
source,
|
||||
None,
|
||||
None,
|
||||
ImportOrExportKind::Value,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
program.body.splice(0..0, imports);
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the first GraphQL definition name across the template's quasis.
|
||||
fn extract_definition_name<'a>(template: &TemplateLiteral<'a>) -> Option<&'a str> {
|
||||
template.quasis.iter().find_map(|quasi| find_definition_name(quasi.value.raw.as_str()))
|
||||
}
|
||||
|
||||
const KEYWORDS: [&str; 4] = ["fragment", "mutation", "query", "subscription"];
|
||||
|
||||
fn is_word_byte(byte: u8) -> bool {
|
||||
byte == b'_' || byte.is_ascii_alphanumeric()
|
||||
}
|
||||
|
||||
/// Find the first GraphQL definition name in `text`.
|
||||
///
|
||||
/// Equivalent to stripping `#` line comments and matching
|
||||
/// `\b(fragment|mutation|query|subscription)\s+(\w+)`, like `swc_relay`,
|
||||
/// without allocating. Byte scanning is UTF-8 safe: slices are only taken at
|
||||
/// ASCII word-run boundaries, which are always character boundaries.
|
||||
fn find_definition_name(text: &str) -> Option<&str> {
|
||||
let bytes = text.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'#' {
|
||||
// Comment: skip to the end of the line.
|
||||
while i < bytes.len() && bytes[i] != b'\n' {
|
||||
i += 1;
|
||||
}
|
||||
} else if is_word_byte(bytes[i]) {
|
||||
// Consuming maximal word runs guarantees a word boundary on both
|
||||
// sides of the keyword.
|
||||
let start = i;
|
||||
while i < bytes.len() && is_word_byte(bytes[i]) {
|
||||
i += 1;
|
||||
}
|
||||
if !KEYWORDS.contains(&&text[start..i]) {
|
||||
continue;
|
||||
}
|
||||
// Consume the whitespace (and comments) separating the keyword
|
||||
// from the name; at least one whitespace character is required.
|
||||
let mut separated = false;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'#' {
|
||||
while i < bytes.len() && bytes[i] != b'\n' {
|
||||
i += 1;
|
||||
}
|
||||
} else if bytes[i].is_ascii_whitespace() {
|
||||
separated = true;
|
||||
i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if separated && i < bytes.len() && is_word_byte(bytes[i]) {
|
||||
let name_start = i;
|
||||
while i < bytes.len() && is_word_byte(bytes[i]) {
|
||||
i += 1;
|
||||
}
|
||||
return Some(&text[name_start..i]);
|
||||
}
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Lexical relative path from directory `from` to directory `to`, with POSIX
|
||||
/// separators.
|
||||
///
|
||||
/// Pure string computation — the filesystem is never accessed, so both paths
|
||||
/// must either be absolute or relative to the same base directory. Accepts `/`
|
||||
/// and `\` as input separators.
|
||||
fn relative_path(from: &str, to: &str) -> String {
|
||||
let from = normalize(from);
|
||||
let to = normalize(to);
|
||||
let is_windows = from.first().is_some_and(|component| is_windows_drive(component))
|
||||
&& to.first().is_some_and(|component| is_windows_drive(component));
|
||||
let common = from
|
||||
.iter()
|
||||
.zip(&to)
|
||||
.take_while(|(a, b)| if is_windows { a.eq_ignore_ascii_case(b) } else { a == b })
|
||||
.count();
|
||||
let mut components = vec![".."; from.len() - common];
|
||||
components.extend_from_slice(&to[common..]);
|
||||
if components.is_empty() { ".".to_string() } else { components.join("/") }
|
||||
}
|
||||
|
||||
fn is_windows_drive(component: &str) -> bool {
|
||||
matches!(component.as_bytes(), [drive, b':'] if drive.is_ascii_alphabetic())
|
||||
}
|
||||
|
||||
/// Split a path into components, dropping `.` and folding `..`.
|
||||
fn normalize(path: &str) -> Vec<&str> {
|
||||
let mut components: Vec<&str> = vec![];
|
||||
for component in path.split(['/', '\\']) {
|
||||
match component {
|
||||
"" | "." => {}
|
||||
".." => {
|
||||
if components.last().is_none_or(|&last| last == "..") {
|
||||
components.push("..");
|
||||
} else {
|
||||
components.pop();
|
||||
}
|
||||
}
|
||||
_ => components.push(component),
|
||||
}
|
||||
}
|
||||
components
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{find_definition_name, relative_path};
|
||||
|
||||
#[test]
|
||||
fn definition_name() {
|
||||
assert_eq!(find_definition_name("query FooQuery { id }"), Some("FooQuery"));
|
||||
assert_eq!(find_definition_name("fragment Foo_bar on User { id }"), Some("Foo_bar"));
|
||||
assert_eq!(find_definition_name("# query Hidden\nquery Real { id }"), Some("Real"));
|
||||
assert_eq!(find_definition_name("query { id }"), None);
|
||||
assert_eq!(find_definition_name("myquery Foo { id }"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_paths() {
|
||||
assert_eq!(relative_path("/a/b", "/a/b"), ".");
|
||||
assert_eq!(relative_path("/a/b/c", "/a/d/e"), "../../d/e");
|
||||
assert_eq!(relative_path("src/pages", "src/__generated__"), "../__generated__");
|
||||
assert_eq!(relative_path("C:\\a\\b", "C:\\a\\c"), "../c");
|
||||
assert_eq!(relative_path("C:\\Repo\\src", "c:\\repo\\src\\__generated__"), "__generated__");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::path::Path;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_codegen::{Codegen, CodegenOptions};
|
||||
use oxc_parser::Parser;
|
||||
use oxc_relay::{Relay, RelayLanguage, RelayOptions};
|
||||
use oxc_semantic::SemanticBuilder;
|
||||
use oxc_span::SourceType;
|
||||
|
||||
fn transform(source_path: &str, source_text: &str, options: RelayOptions) -> (String, bool) {
|
||||
let source_type = SourceType::from_path(Path::new(source_path)).unwrap();
|
||||
let allocator = Allocator::default();
|
||||
let ret = Parser::new(&allocator, source_text, source_type).parse();
|
||||
assert!(ret.diagnostics.is_empty(), "parse errors for source {source_text}");
|
||||
let mut program = ret.program;
|
||||
let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
|
||||
let ret = Relay::new(options, Path::new(source_path)).build(&allocator, &mut program, scoping);
|
||||
let code = Codegen::new()
|
||||
.with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() })
|
||||
.build(&program)
|
||||
.code;
|
||||
(code, ret.diagnostics.has_errors())
|
||||
}
|
||||
|
||||
fn codegen(source_path: &str, source_text: &str) -> String {
|
||||
let source_type = SourceType::from_path(Path::new(source_path)).unwrap();
|
||||
let allocator = Allocator::default();
|
||||
let ret = Parser::new(&allocator, source_text, source_type).parse();
|
||||
assert!(ret.diagnostics.is_empty(), "parse errors for expected {source_text}");
|
||||
Codegen::new()
|
||||
.with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() })
|
||||
.build(&ret.program)
|
||||
.code
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_with_path(source_path: &str, source_text: &str, options: RelayOptions, expected: &str) {
|
||||
let (code, has_errors) = transform(source_path, source_text, options);
|
||||
assert!(!has_errors, "unexpected diagnostics for source {source_text}");
|
||||
assert_eq!(code, codegen(source_path, expected), "for source {source_text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transforms_graphql_in_tsx() {
|
||||
test_with_path(
|
||||
"component.tsx",
|
||||
"interface Props { id: string }
|
||||
const style = css`color: red`;
|
||||
const data = graphql`query FooQuery { id }`;
|
||||
export const App = (props: Props) => <div>{data}</div>;",
|
||||
RelayOptions::default(),
|
||||
"import _FooQuery from './__generated__/FooQuery.graphql.js';
|
||||
interface Props { id: string }
|
||||
const style = css`color: red`;
|
||||
const data = _FooQuery;
|
||||
export const App = (props: Props) => <div>{data}</div>;",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_relay_options() {
|
||||
test_with_path(
|
||||
"project/src/pages/foo.ts",
|
||||
"const data = graphql`fragment Foo_item on Item { id }`;",
|
||||
RelayOptions {
|
||||
artifact_directory: Some("project/src/__generated__".into()),
|
||||
language: RelayLanguage::Typescript,
|
||||
eager_es_modules: false,
|
||||
},
|
||||
"const data = require('../__generated__/Foo_item.graphql.ts');",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_unnamed_documents() {
|
||||
let source = "const data = graphql`{ id }`;";
|
||||
let (code, has_errors) = transform("test.js", source, RelayOptions::default());
|
||||
assert!(has_errors);
|
||||
assert_eq!(code, codegen("test.js", source));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this package will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0).
|
||||
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "oxc_transform_relay_napi"
|
||||
version = "0.144.0"
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
include = ["/src", "build.rs"]
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
description.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
oxc = { workspace = true, features = ["full"] }
|
||||
oxc_napi = { workspace = true }
|
||||
oxc_relay = { workspace = true }
|
||||
oxc_sourcemap = { workspace = true, features = ["napi"] }
|
||||
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
mimalloc-safe = { workspace = true, optional = true, features = ["skip_collect_on_exit"] }
|
||||
|
||||
[target.'cfg(all(target_os = "linux", not(target_arch = "arm"), not(target_arch = "aarch64")))'.dependencies]
|
||||
mimalloc-safe = { workspace = true, optional = true, features = [
|
||||
"skip_collect_on_exit",
|
||||
"local_dynamic_tls",
|
||||
] }
|
||||
|
||||
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies]
|
||||
mimalloc-safe = { workspace = true, optional = true, features = [
|
||||
"skip_collect_on_exit",
|
||||
"local_dynamic_tls",
|
||||
"no_opt_arch",
|
||||
] }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
allocator = ["dep:mimalloc-safe"]
|
||||
@@ -0,0 +1,48 @@
|
||||
# Oxc Relay Transform
|
||||
|
||||
Native Node.js bindings for Oxc's Rust port of the Relay transform
|
||||
([babel-plugin-relay](https://github.com/facebook/relay/tree/main/packages/babel-plugin-relay) /
|
||||
[@swc/plugin-relay](https://github.com/swc-project/plugins/tree/main/packages/relay)).
|
||||
|
||||
The API follows `oxc-transform`: pass a filename, source text, and optional
|
||||
options to either `transformSync` or `transform`. `graphql` tagged template
|
||||
expressions are replaced with references to the artifact files generated by
|
||||
`relay-compiler`; everything else — including TypeScript and JSX syntax — is
|
||||
preserved untouched, so the output composes with any downstream toolchain.
|
||||
|
||||
```javascript
|
||||
import { transformSync } from "oxc-transform-relay";
|
||||
|
||||
const result = transformSync("src/Component.tsx", "const data = graphql`query FooQuery { id }`;");
|
||||
|
||||
console.log(result.code);
|
||||
// import _FooQuery from "./__generated__/FooQuery.graphql.js";
|
||||
// const data = _FooQuery;
|
||||
```
|
||||
|
||||
`errors` contains every diagnostic reported by parsing and the transform; when
|
||||
it is non-empty, `code` is empty.
|
||||
|
||||
## Options
|
||||
|
||||
- `artifactDirectory` — directory `relay-compiler` emits all artifacts to (its
|
||||
`artifactDirectory` setting). When set, artifacts are imported via a relative
|
||||
path computed lexically from the file being transformed, so pass the filename
|
||||
and directory either both absolute or both relative to the same base
|
||||
directory. When unset, artifacts are imported from the `__generated__`
|
||||
directory next to the file being transformed.
|
||||
- `language` — `"typescript"`, `"javascript"` (default), or `"flow"`. Artifacts
|
||||
are imported as `Name.graphql.ts` for `"typescript"` and `Name.graphql.js`
|
||||
otherwise.
|
||||
- `eagerEsModules` — emit a hoisted default import per `graphql` tag (default,
|
||||
matching `babel-plugin-relay` since Relay v17) instead of an inline
|
||||
`require()` call (`@swc/plugin-relay` and Next.js behavior).
|
||||
- `lang`, `sourceType`, `sourcemap` — configure the surrounding Oxc
|
||||
parse/codegen pipeline, as in `oxc-transform`.
|
||||
|
||||
## Limitations
|
||||
|
||||
Compared to `babel-plugin-relay`: no development-mode artifact hash validation,
|
||||
no `jsModuleFormat: "haste"`, no `isDevVariableName`, and the GraphQL definition
|
||||
name is extracted textually (like `@swc/plugin-relay`) rather than by parsing
|
||||
the document, so documents are not validated.
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@oxc-transform-relay/binding-wasm32-wasi'
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
export interface Comment {
|
||||
type: 'Line' | 'Block'
|
||||
value: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface ErrorLabel {
|
||||
message: string | null
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface OxcError {
|
||||
severity: Severity
|
||||
message: string
|
||||
labels: Array<ErrorLabel>
|
||||
helpMessage: string | null
|
||||
codeframe: string | null
|
||||
}
|
||||
|
||||
export declare const enum Severity {
|
||||
Error = 'Error',
|
||||
Warning = 'Warning',
|
||||
Advice = 'Advice'
|
||||
}
|
||||
export interface SourceMap {
|
||||
file?: string
|
||||
mappings: string
|
||||
names: Array<string>
|
||||
sourceRoot?: string
|
||||
sources: Array<string>
|
||||
sourcesContent?: Array<string>
|
||||
version: number
|
||||
x_google_ignoreList?: Array<number>
|
||||
}
|
||||
/**
|
||||
* Apply the Relay `graphql` tagged template transform asynchronously.
|
||||
*
|
||||
* This uses a worker-pool thread and can be slower than `transformSync` for a
|
||||
* single small module.
|
||||
*/
|
||||
export declare function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): Promise<TransformResult>
|
||||
|
||||
/**
|
||||
* Options for the Relay transform.
|
||||
*
|
||||
* `lang`, `sourceType`, and `sourcemap` configure the surrounding Oxc
|
||||
* parse/codegen pipeline; the remaining fields mirror the options of
|
||||
* `babel-plugin-relay` / `@swc/plugin-relay`.
|
||||
*/
|
||||
export interface TransformOptions {
|
||||
/** Treat the source as `js`, `jsx`, `ts`, `tsx`, or `dts`. */
|
||||
lang?: 'js' | 'jsx' | 'ts' | 'tsx' | 'dts'
|
||||
/** Treat the source as script, module, CommonJS, or infer it from syntax. */
|
||||
sourceType?: 'script' | 'module' | 'commonjs' | 'unambiguous'
|
||||
/**
|
||||
* Generate a source map.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
sourcemap?: boolean
|
||||
/**
|
||||
* Directory `relay-compiler` emits all artifacts to (its
|
||||
* `artifactDirectory` setting). When set, artifacts are imported via a
|
||||
* relative path from the file being transformed to this directory; the
|
||||
* path is computed lexically, so both must either be absolute or relative
|
||||
* to the same base directory. When unset, artifacts are imported from the
|
||||
* `__generated__` directory next to the file being transformed.
|
||||
*/
|
||||
artifactDirectory?: string
|
||||
/**
|
||||
* Artifact language, determining the imported file extension:
|
||||
* `Name.graphql.ts` for `typescript`, `Name.graphql.js` otherwise.
|
||||
*
|
||||
* @default 'javascript'
|
||||
*/
|
||||
language?: 'typescript' | 'javascript' | 'flow'
|
||||
/**
|
||||
* Emit a hoisted default import per `graphql` tag instead of an inline
|
||||
* `require()` call.
|
||||
*
|
||||
* Defaults to `true`, matching `babel-plugin-relay` since Relay v17.
|
||||
* `@swc/plugin-relay` and Next.js default to `false`.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
eagerEsModules?: boolean
|
||||
}
|
||||
|
||||
/** Result returned by the Relay transform. */
|
||||
export interface TransformResult {
|
||||
/**
|
||||
* Transformed code.
|
||||
*
|
||||
* This is empty when parsing, semantic analysis, option validation, or
|
||||
* the Relay transform reports an error.
|
||||
*/
|
||||
code: string
|
||||
/** Source map, populated when `sourcemap` is `true`. */
|
||||
map?: SourceMap
|
||||
/** Parse, semantic, option validation, and Relay transform diagnostics. */
|
||||
errors: Array<OxcError>
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the Relay `graphql` tagged template transform synchronously.
|
||||
*
|
||||
* Only `graphql` tags are rewritten; TypeScript and JSX syntax are preserved
|
||||
* untouched, so the output composes with any downstream toolchain.
|
||||
*/
|
||||
export declare function transformSync(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult
|
||||
@@ -0,0 +1,717 @@
|
||||
// prettier-ignore
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
import { createRequire } from 'module'
|
||||
const require = createRequire(import.meta.url)
|
||||
const __dirname = new URL('.', import.meta.url).pathname
|
||||
|
||||
const { readFileSync } = require('fs')
|
||||
let nativeBinding = null
|
||||
const loadErrors = []
|
||||
|
||||
const isMusl = () => {
|
||||
let musl = false
|
||||
if (process.platform === 'linux') {
|
||||
musl = isMuslFromFilesystem()
|
||||
if (musl === null) {
|
||||
musl = isMuslFromReport()
|
||||
}
|
||||
if (musl === null) {
|
||||
musl = isMuslFromChildProcess()
|
||||
}
|
||||
}
|
||||
return musl
|
||||
}
|
||||
|
||||
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
|
||||
|
||||
const isMuslFromFilesystem = () => {
|
||||
try {
|
||||
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const isMuslFromReport = () => {
|
||||
let report = null
|
||||
if (process.report && typeof process.report.getReport === 'function') {
|
||||
process.report.excludeNetwork = true
|
||||
report = process.report.getReport()
|
||||
}
|
||||
if (!report) {
|
||||
return null
|
||||
}
|
||||
if (report.header && report.header.glibcVersionRuntime) {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(report.sharedObjects)) {
|
||||
if (report.sharedObjects.some(isFileMusl)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isMuslFromChildProcess = () => {
|
||||
try {
|
||||
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
|
||||
} catch (e) {
|
||||
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function requireNative() {
|
||||
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
||||
try {
|
||||
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
|
||||
} catch (err) {
|
||||
loadErrors.push(err)
|
||||
}
|
||||
} else if (process.platform === 'android') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./transform-relay.android-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-android-arm64')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-android-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./transform-relay.android-arm-eabi.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-android-arm-eabi')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-android-arm-eabi/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
if (process.arch === 'x64') {
|
||||
if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) {
|
||||
try {
|
||||
return require('./transform-relay.win32-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-win32-x64-gnu')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-win32-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./transform-relay.win32-x64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-win32-x64-msvc')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-win32-x64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ia32') {
|
||||
try {
|
||||
return require('./transform-relay.win32-ia32-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-win32-ia32-msvc')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-win32-ia32-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./transform-relay.win32-arm64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-win32-arm64-msvc')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-win32-arm64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
try {
|
||||
return require('./transform-relay.darwin-universal.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-darwin-universal')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-darwin-universal/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./transform-relay.darwin-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-darwin-x64')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-darwin-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./transform-relay.darwin-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-darwin-arm64')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-darwin-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'freebsd') {
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./transform-relay.freebsd-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-freebsd-x64')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-freebsd-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./transform-relay.freebsd-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-freebsd-arm64')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-freebsd-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
if (process.arch === 'x64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./transform-relay.linux-x64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-x64-musl')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-x64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./transform-relay.linux-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-x64-gnu')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./transform-relay.linux-arm64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-arm64-musl')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-arm64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./transform-relay.linux-arm64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-arm64-gnu')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-arm64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./transform-relay.linux-arm-musleabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-arm-musleabihf')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-arm-musleabihf/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./transform-relay.linux-arm-gnueabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-arm-gnueabihf')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-arm-gnueabihf/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'loong64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./transform-relay.linux-loong64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-loong64-musl')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-loong64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./transform-relay.linux-loong64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-loong64-gnu')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-loong64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'riscv64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./transform-relay.linux-riscv64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-riscv64-musl')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-riscv64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./transform-relay.linux-riscv64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-riscv64-gnu')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-riscv64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ppc64') {
|
||||
try {
|
||||
return require('./transform-relay.linux-ppc64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-ppc64-gnu')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-ppc64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 's390x') {
|
||||
try {
|
||||
return require('./transform-relay.linux-s390x-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-linux-s390x-gnu')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-linux-s390x-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'openharmony') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./transform-relay.openharmony-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-openharmony-arm64')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-openharmony-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./transform-relay.openharmony-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-openharmony-x64')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-openharmony-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./transform-relay.openharmony-arm.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@oxc-transform-relay/binding-openharmony-arm')
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-openharmony-arm/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
|
||||
}
|
||||
}
|
||||
|
||||
function createLoadErrorChain(errors) {
|
||||
return errors.reduce((previous, current) => {
|
||||
let message
|
||||
try {
|
||||
message =
|
||||
current && typeof current.message === 'string'
|
||||
? current.message
|
||||
: String(current)
|
||||
} catch {
|
||||
message = 'Unknown error'
|
||||
}
|
||||
const error = new Error(message)
|
||||
error.cause = previous
|
||||
return error
|
||||
}, null)
|
||||
}
|
||||
|
||||
// NAPI_RS_FORCE_WASI is a tri-state flag:
|
||||
// unset / any other value → native binding preferred, WASI is only a fallback
|
||||
// 'true' → prefer WASI, but retain native as a lazy fallback
|
||||
// 'error' → require WASI without initializing a native fallback
|
||||
// Treating any non-empty string as truthy (the historical behavior) meant
|
||||
// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered
|
||||
// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file.
|
||||
//
|
||||
// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict
|
||||
// WASI loading. It never crosses into another flavor or falls back to native.
|
||||
const __napiWasiFlavors = ["wasm32-wasi"]
|
||||
const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR
|
||||
const __napiWasiFlavorRequested =
|
||||
typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0
|
||||
if (
|
||||
__napiWasiFlavorRequested &&
|
||||
__napiWasiFlavors.indexOf(__napiWasiFlavor) === -1
|
||||
) {
|
||||
throw new Error(
|
||||
'Unsupported WASI flavor "' +
|
||||
__napiWasiFlavor +
|
||||
'". Available flavors: ' +
|
||||
__napiWasiFlavors.join(', '),
|
||||
)
|
||||
}
|
||||
const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error'
|
||||
const forceWasi =
|
||||
process.env.NAPI_RS_FORCE_WASI === 'true' ||
|
||||
forceWasiError ||
|
||||
__napiWasiFlavorRequested
|
||||
|
||||
if (!forceWasi) {
|
||||
nativeBinding = requireNative()
|
||||
}
|
||||
|
||||
if (!nativeBinding || forceWasi) {
|
||||
let wasiBinding = null
|
||||
let wasiBindingLoaded = false
|
||||
const wasiBindingErrors = []
|
||||
const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => {
|
||||
try {
|
||||
require.resolve(specifier)
|
||||
} catch (resolveError) {
|
||||
if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') {
|
||||
throw resolveError
|
||||
}
|
||||
if (isPackage) {
|
||||
try {
|
||||
require.resolve(specifier + '/package.json')
|
||||
} catch (packageError) {
|
||||
if (packageError && packageError.code === 'MODULE_NOT_FOUND') {
|
||||
return resolveError
|
||||
}
|
||||
// An exports restriction proves the package exists even when its
|
||||
// package.json is not public. Preserve the root resolution failure.
|
||||
throw resolveError
|
||||
}
|
||||
// The package exists but its main/export target is broken.
|
||||
throw resolveError
|
||||
}
|
||||
return resolveError
|
||||
}
|
||||
if (localArtifacts) {
|
||||
let artifactError = null
|
||||
for (let i = 0; i < localArtifacts.length; i++) {
|
||||
try {
|
||||
require.resolve(localArtifacts[i])
|
||||
return null
|
||||
} catch (resolveError) {
|
||||
if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') {
|
||||
throw resolveError
|
||||
}
|
||||
artifactError = resolveError
|
||||
}
|
||||
}
|
||||
return artifactError
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) {
|
||||
let candidateError = null
|
||||
let candidateFailed = false
|
||||
try {
|
||||
candidateError = __napiWasiResolveCandidate('./transform-relay.wasi.cjs', false, ["./transform-relay.wasm32-wasi.debug.wasm","./transform-relay.wasm32-wasi.wasm"])
|
||||
candidateFailed = candidateError !== null
|
||||
if (!candidateFailed) {
|
||||
wasiBinding = require('./transform-relay.wasi.cjs')
|
||||
nativeBinding = wasiBinding
|
||||
wasiBindingLoaded = true
|
||||
}
|
||||
} catch (err) {
|
||||
candidateError = err
|
||||
candidateFailed = true
|
||||
}
|
||||
if (candidateFailed) {
|
||||
wasiBindingErrors.push(candidateError)
|
||||
loadErrors.push(candidateError)
|
||||
}
|
||||
}
|
||||
if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) {
|
||||
let candidateError = null
|
||||
let candidateFailed = false
|
||||
try {
|
||||
candidateError = __napiWasiResolveCandidate('@oxc-transform-relay/binding-wasm32-wasi', true, undefined)
|
||||
candidateFailed = candidateError !== null
|
||||
if (!candidateFailed) {
|
||||
if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
const bindingPackageVersion = require('@oxc-transform-relay/binding-wasm32-wasi/package.json').version
|
||||
if (bindingPackageVersion !== '0.144.0') {
|
||||
throw new Error(`WASI binding package version mismatch, expected 0.144.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
}
|
||||
wasiBinding = require('@oxc-transform-relay/binding-wasm32-wasi')
|
||||
nativeBinding = wasiBinding
|
||||
wasiBindingLoaded = true
|
||||
}
|
||||
} catch (err) {
|
||||
candidateError = err
|
||||
candidateFailed = true
|
||||
}
|
||||
if (candidateFailed) {
|
||||
wasiBindingErrors.push(candidateError)
|
||||
loadErrors.push(candidateError)
|
||||
}
|
||||
}
|
||||
if (
|
||||
!wasiBindingLoaded &&
|
||||
forceWasi &&
|
||||
!forceWasiError &&
|
||||
!__napiWasiFlavorRequested
|
||||
) {
|
||||
nativeBinding = requireNative()
|
||||
}
|
||||
if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) {
|
||||
const error = new Error(
|
||||
__napiWasiFlavorRequested
|
||||
? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found'
|
||||
: 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error',
|
||||
)
|
||||
error.cause = createLoadErrorChain(wasiBindingErrors)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (!nativeBinding && globalThis.process?.versions?.["webcontainer"]) {
|
||||
try {
|
||||
nativeBinding = require('./webcontainer-fallback.cjs');
|
||||
} catch (err) {
|
||||
loadErrors.push(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadErrors.length > 0) {
|
||||
const error = new Error(
|
||||
`Cannot find native binding. ` +
|
||||
`npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +
|
||||
'Please try `npm i` again after removing both package-lock.json and node_modules directory.',
|
||||
)
|
||||
// assign instead of the `new Error(message, { cause })` options form,
|
||||
// which Node < 16.9 silently ignores
|
||||
error.cause = createLoadErrorChain(loadErrors)
|
||||
throw error
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { Severity, transform, transformSync } = nativeBinding
|
||||
export { Severity }
|
||||
export { transform }
|
||||
export { transformSync }
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"name": "oxc-transform-relay",
|
||||
"version": "0.144.0",
|
||||
"description": "Oxc Relay Transform Node API",
|
||||
"keywords": [
|
||||
"graphql",
|
||||
"javascript",
|
||||
"oxc",
|
||||
"relay",
|
||||
"transform",
|
||||
"typescript"
|
||||
],
|
||||
"homepage": "https://oxc.rs",
|
||||
"bugs": "https://github.com/oxc-project/oxc/issues",
|
||||
"license": "MIT",
|
||||
"author": "Boshen and oxc contributors",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/oxc-project/oxc.git",
|
||||
"directory": "napi/transform-relay"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"files": [
|
||||
"browser.js",
|
||||
"index.d.ts",
|
||||
"index.js",
|
||||
"webcontainer-fallback.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "index.js",
|
||||
"browser": "browser.js",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"build-dev": "napi build --esm --platform",
|
||||
"build-test": "pnpm run build-dev --profile coverage",
|
||||
"build": "pnpm run build-dev --features allocator --release",
|
||||
"postbuild": "publint",
|
||||
"postbuild-dev": "node scripts/patch.js",
|
||||
"build-wasm": "pnpm run build-wasm-dev --release",
|
||||
"build-wasm-dev": "pnpm run build-dev --target wasm32-wasip1-threads --dts transform-relay.wasi.d.cts",
|
||||
"test": "vitest run --dir ./test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emnapi/core": "catalog:",
|
||||
"@emnapi/runtime": "catalog:",
|
||||
"@napi-rs/cli": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"napi": {
|
||||
"binaryName": "transform-relay",
|
||||
"packageName": "@oxc-transform-relay/binding",
|
||||
"targets": [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-linux-android",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-unknown-linux-ohos",
|
||||
"armv7-linux-androideabi",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"armv7-unknown-linux-musleabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"powerpc64le-unknown-linux-gnu",
|
||||
"riscv64gc-unknown-linux-gnu",
|
||||
"riscv64gc-unknown-linux-musl",
|
||||
"s390x-unknown-linux-gnu",
|
||||
"wasm32-wasip1-threads",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-freebsd",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl"
|
||||
],
|
||||
"wasm": {
|
||||
"browser": {
|
||||
"fs": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import fs from "node:fs";
|
||||
import { join as pathJoin } from "node:path";
|
||||
|
||||
const packageDir = pathJoin(import.meta.dirname, "..");
|
||||
const path = pathJoin(packageDir, "index.js");
|
||||
|
||||
let data = fs.readFileSync(path, "utf-8");
|
||||
data = data.replace(
|
||||
"\nif (!nativeBinding) {",
|
||||
(source) =>
|
||||
`
|
||||
if (!nativeBinding && globalThis.process?.versions?.["webcontainer"]) {
|
||||
try {
|
||||
nativeBinding = require('./webcontainer-fallback.cjs');
|
||||
} catch (err) {
|
||||
loadErrors.push(err)
|
||||
}
|
||||
}
|
||||
` + source,
|
||||
);
|
||||
fs.writeFileSync(path, data);
|
||||
|
||||
const workerPath = pathJoin(packageDir, "wasi-worker-browser.mjs");
|
||||
const worker = fs.readFileSync(workerPath, "utf-8").replaceAll(/[ \t]+$/gmu, "");
|
||||
fs.writeFileSync(workerPath, worker);
|
||||
@@ -0,0 +1,162 @@
|
||||
#![expect(clippy::needless_pass_by_value)]
|
||||
|
||||
#[cfg(all(
|
||||
feature = "allocator",
|
||||
not(any(
|
||||
target_arch = "arm",
|
||||
target_os = "android",
|
||||
target_os = "freebsd",
|
||||
target_os = "windows",
|
||||
target_family = "wasm"
|
||||
))
|
||||
))]
|
||||
#[global_allocator]
|
||||
static ALLOC: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc;
|
||||
|
||||
mod options;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use napi::{Task, bindgen_prelude::AsyncTask};
|
||||
use napi_derive::napi;
|
||||
|
||||
use oxc::{
|
||||
allocator::Allocator,
|
||||
codegen::{Codegen, CodegenOptions},
|
||||
diagnostics::Diagnostics,
|
||||
parser::Parser,
|
||||
semantic::SemanticBuilder,
|
||||
};
|
||||
use oxc_napi::{OxcError, get_source_type};
|
||||
use oxc_relay::Relay;
|
||||
use oxc_sourcemap::napi::SourceMap;
|
||||
|
||||
pub use crate::options::*;
|
||||
|
||||
/// Result returned by the Relay transform.
|
||||
#[derive(Default)]
|
||||
#[napi(object)]
|
||||
pub struct TransformResult {
|
||||
/// Transformed code.
|
||||
///
|
||||
/// This is empty when parsing, semantic analysis, option validation, or
|
||||
/// the Relay transform reports an error.
|
||||
pub code: String,
|
||||
|
||||
/// Source map, populated when `sourcemap` is `true`.
|
||||
pub map: Option<SourceMap>,
|
||||
|
||||
/// Parse, semantic, option validation, and Relay transform diagnostics.
|
||||
pub errors: Vec<OxcError>,
|
||||
}
|
||||
|
||||
fn transform_impl(
|
||||
filename: &str,
|
||||
source_text: &str,
|
||||
options: Option<TransformOptions>,
|
||||
) -> TransformResult {
|
||||
let source_type = get_source_type(
|
||||
filename,
|
||||
options.as_ref().and_then(|options| options.lang.as_deref()),
|
||||
options.as_ref().and_then(|options| options.source_type.as_deref()),
|
||||
);
|
||||
let sourcemap = options.as_ref().and_then(|options| options.sourcemap).unwrap_or(false);
|
||||
|
||||
let relay_options = match options.unwrap_or_default().resolve() {
|
||||
Ok(options) => options,
|
||||
Err(error) => {
|
||||
return TransformResult {
|
||||
errors: OxcError::from_diagnostics(filename, source_text, [error]),
|
||||
..TransformResult::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let allocator = Allocator::default();
|
||||
let parser_return = Parser::new(&allocator, source_text, source_type).parse();
|
||||
let mut diagnostics = parser_return.diagnostics;
|
||||
let mut program = parser_return.program;
|
||||
if diagnostics.has_errors() {
|
||||
return error_result(filename, source_text, diagnostics);
|
||||
}
|
||||
|
||||
let semantic_return = SemanticBuilder::new().build(&program);
|
||||
if !semantic_return.diagnostics.is_empty() {
|
||||
diagnostics.extend(semantic_return.diagnostics);
|
||||
return error_result(filename, source_text, diagnostics);
|
||||
}
|
||||
|
||||
let scoping = semantic_return.semantic.into_scoping();
|
||||
let relay_return =
|
||||
Relay::new(relay_options, Path::new(filename)).build(&allocator, &mut program, scoping);
|
||||
let relay_has_errors = relay_return.diagnostics.has_errors();
|
||||
diagnostics.extend(relay_return.diagnostics);
|
||||
if relay_has_errors {
|
||||
return error_result(filename, source_text, diagnostics);
|
||||
}
|
||||
|
||||
let codegen_return = Codegen::new()
|
||||
.with_options(CodegenOptions {
|
||||
source_map_path: sourcemap.then(|| Path::new(filename).to_path_buf()),
|
||||
..CodegenOptions::default()
|
||||
})
|
||||
.build(&program);
|
||||
TransformResult {
|
||||
code: codegen_return.code,
|
||||
map: codegen_return.map.map(SourceMap::from),
|
||||
errors: OxcError::from_diagnostics(filename, source_text, diagnostics),
|
||||
}
|
||||
}
|
||||
|
||||
fn error_result(filename: &str, source_text: &str, diagnostics: Diagnostics) -> TransformResult {
|
||||
TransformResult {
|
||||
errors: OxcError::from_diagnostics(filename, source_text, diagnostics),
|
||||
..TransformResult::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the Relay `graphql` tagged template transform synchronously.
|
||||
///
|
||||
/// Only `graphql` tags are rewritten; TypeScript and JSX syntax are preserved
|
||||
/// untouched, so the output composes with any downstream toolchain.
|
||||
#[napi]
|
||||
pub fn transform_sync(
|
||||
filename: String,
|
||||
source_text: String,
|
||||
options: Option<TransformOptions>,
|
||||
) -> TransformResult {
|
||||
transform_impl(&filename, &source_text, options)
|
||||
}
|
||||
|
||||
pub struct TransformTask {
|
||||
filename: String,
|
||||
source_text: String,
|
||||
options: Option<TransformOptions>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for TransformTask {
|
||||
type JsValue = TransformResult;
|
||||
type Output = TransformResult;
|
||||
|
||||
fn compute(&mut self) -> napi::Result<Self::Output> {
|
||||
Ok(transform_impl(&self.filename, &self.source_text, self.options.take()))
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _: napi::Env, result: Self::Output) -> napi::Result<Self::JsValue> {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the Relay `graphql` tagged template transform asynchronously.
|
||||
///
|
||||
/// This uses a worker-pool thread and can be slower than `transformSync` for a
|
||||
/// single small module.
|
||||
#[napi]
|
||||
pub fn transform(
|
||||
filename: String,
|
||||
source_text: String,
|
||||
options: Option<TransformOptions>,
|
||||
) -> AsyncTask<TransformTask> {
|
||||
AsyncTask::new(TransformTask { filename, source_text, options })
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use napi_derive::napi;
|
||||
|
||||
use oxc::diagnostics::OxcDiagnostic;
|
||||
use oxc_relay::{RelayLanguage, RelayOptions};
|
||||
|
||||
/// Options for the Relay transform.
|
||||
///
|
||||
/// `lang`, `sourceType`, and `sourcemap` configure the surrounding Oxc
|
||||
/// parse/codegen pipeline; the remaining fields mirror the options of
|
||||
/// `babel-plugin-relay` / `@swc/plugin-relay`.
|
||||
#[napi(object)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TransformOptions {
|
||||
/// Treat the source as `js`, `jsx`, `ts`, `tsx`, or `dts`.
|
||||
#[napi(ts_type = "'js' | 'jsx' | 'ts' | 'tsx' | 'dts'")]
|
||||
pub lang: Option<String>,
|
||||
|
||||
/// Treat the source as script, module, CommonJS, or infer it from syntax.
|
||||
#[napi(ts_type = "'script' | 'module' | 'commonjs' | 'unambiguous'")]
|
||||
pub source_type: Option<String>,
|
||||
|
||||
/// Generate a source map.
|
||||
///
|
||||
/// @default false
|
||||
pub sourcemap: Option<bool>,
|
||||
|
||||
/// Directory `relay-compiler` emits all artifacts to (its
|
||||
/// `artifactDirectory` setting). When set, artifacts are imported via a
|
||||
/// relative path from the file being transformed to this directory; the
|
||||
/// path is computed lexically, so both must either be absolute or relative
|
||||
/// to the same base directory. When unset, artifacts are imported from the
|
||||
/// `__generated__` directory next to the file being transformed.
|
||||
pub artifact_directory: Option<String>,
|
||||
|
||||
/// Artifact language, determining the imported file extension:
|
||||
/// `Name.graphql.ts` for `typescript`, `Name.graphql.js` otherwise.
|
||||
///
|
||||
/// @default 'javascript'
|
||||
#[napi(ts_type = "'typescript' | 'javascript' | 'flow'")]
|
||||
pub language: Option<String>,
|
||||
|
||||
/// Emit a hoisted default import per `graphql` tag instead of an inline
|
||||
/// `require()` call.
|
||||
///
|
||||
/// Defaults to `true`, matching `babel-plugin-relay` since Relay v17.
|
||||
/// `@swc/plugin-relay` and Next.js default to `false`.
|
||||
///
|
||||
/// @default true
|
||||
pub eager_es_modules: Option<bool>,
|
||||
}
|
||||
|
||||
impl TransformOptions {
|
||||
pub(crate) fn resolve(self) -> Result<RelayOptions, OxcDiagnostic> {
|
||||
let language = match self.language.as_deref() {
|
||||
None => RelayLanguage::default(),
|
||||
Some("typescript") => RelayLanguage::Typescript,
|
||||
Some("javascript") => RelayLanguage::Javascript,
|
||||
Some("flow") => RelayLanguage::Flow,
|
||||
Some(value) => {
|
||||
return Err(OxcDiagnostic::error(format!("Invalid `language` option: `{value}`.")));
|
||||
}
|
||||
};
|
||||
Ok(RelayOptions {
|
||||
artifact_directory: self.artifact_directory.map(PathBuf::from),
|
||||
language,
|
||||
eager_es_modules: self.eager_es_modules.unwrap_or(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { transform, transformSync } from "../index";
|
||||
|
||||
const fixture = "const data = graphql`query FooQuery { id }`;\n";
|
||||
|
||||
describe("transformSync", () => {
|
||||
it("hoists an ES import by default", () => {
|
||||
const result = transformSync("foo.js", fixture);
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"import _FooQuery from "./__generated__/FooQuery.graphql.js";
|
||||
const data = _FooQuery;
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("supports Relay options", () => {
|
||||
const result = transformSync("project/src/pages/Foo.tsx", fixture, {
|
||||
artifactDirectory: "project/src/__generated__",
|
||||
language: "typescript",
|
||||
eagerEsModules: false,
|
||||
});
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.code).toContain('require("../__generated__/FooQuery.graphql.ts")');
|
||||
});
|
||||
|
||||
it("reports transform and option errors", () => {
|
||||
const unnamed = transformSync("foo.js", "const data = graphql`{ id }`;");
|
||||
|
||||
expect(unnamed.code).toBe("");
|
||||
expect(unnamed.errors[0].message).toContain("named GraphQL");
|
||||
|
||||
// @ts-expect-error Testing runtime validation.
|
||||
const invalidOption = transformSync("foo.js", fixture, { language: "elm" });
|
||||
expect(invalidOption.code).toBe("");
|
||||
expect(invalidOption.errors[0].message).toContain("language");
|
||||
});
|
||||
});
|
||||
|
||||
describe("transform", () => {
|
||||
it("transforms asynchronously", async () => {
|
||||
const result = await transform("foo.js", fixture, { eagerEsModules: false });
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.code).toContain('require("./__generated__/FooQuery.graphql.js")');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,606 @@
|
||||
import {
|
||||
emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,
|
||||
emnapiTSFNPlugin as __emnapiTSFNPlugin,
|
||||
createOnMessage as __wasmCreateOnMessageForFsProxy,
|
||||
instantiateNapiModule as __emnapiInstantiateNapiModule,
|
||||
WASI as __WASI,
|
||||
} from '@napi-rs/wasm-runtime'
|
||||
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
|
||||
|
||||
|
||||
|
||||
const __wasi = new __WASI({
|
||||
version: 'preview1',
|
||||
})
|
||||
|
||||
const __wasmUrl = new URL('./transform-relay.wasm32-wasi.wasm', import.meta.url).href
|
||||
const __wasmResponse = await globalThis.fetch(__wasmUrl)
|
||||
if (!__wasmResponse.ok) {
|
||||
throw new Error(
|
||||
'Failed to fetch WASI module ' +
|
||||
__wasmUrl +
|
||||
': ' +
|
||||
__wasmResponse.status +
|
||||
' ' +
|
||||
(__wasmResponse.statusText || 'Unknown Status'),
|
||||
)
|
||||
}
|
||||
const __wasmFile = await __wasmResponse.arrayBuffer()
|
||||
|
||||
const __sharedMemory = new WebAssembly.Memory({
|
||||
initial: 4000,
|
||||
maximum: 65536,
|
||||
shared: true,
|
||||
})
|
||||
const __asyncWorkPoolSize = 4
|
||||
const __workerPoolSize = Math.max(
|
||||
2,
|
||||
globalThis.navigator?.hardwareConcurrency ?? 4,
|
||||
)
|
||||
|
||||
let __emnapiContext
|
||||
|
||||
const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose')
|
||||
const __wasiWorkers = new Set()
|
||||
let __napiInstance
|
||||
let __emnapiContextDestroyed = false
|
||||
let __emnapiContextDestroyPromise
|
||||
let __emnapiWasmEnvCleanupPrepared = false
|
||||
let __emnapiWasmEnvCleanupRan = false
|
||||
let __emnapiWasmEnvCleanupDrained = false
|
||||
let __emnapiWasmEnvCleanupDrainPromise
|
||||
let __wasiDisposed = false
|
||||
let __wasiDisposePromise
|
||||
let __completeWasiDisposal = function() {}
|
||||
// Overridden by loader flavors that have a last-resort reclaim for a rollback
|
||||
// that stopped short of destroying the context. See
|
||||
// `__rollbackWasiInitialization`.
|
||||
let __retainWasiRollbackForRetry = function() {}
|
||||
|
||||
function __isThenable(value) {
|
||||
return (
|
||||
value !== null &&
|
||||
(typeof value === 'object' || typeof value === 'function') &&
|
||||
typeof value.then === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
function __createCleanupError(errors, message) {
|
||||
if (errors.length === 1) {
|
||||
return errors[0]
|
||||
}
|
||||
const __AggregateError = globalThis.AggregateError
|
||||
if (typeof __AggregateError === 'function') {
|
||||
return new __AggregateError(errors, message)
|
||||
}
|
||||
const error = new Error(message)
|
||||
error.errors = errors
|
||||
return error
|
||||
}
|
||||
|
||||
function __attachCleanupErrors(error, cleanupErrors) {
|
||||
if (cleanupErrors.length === 0) {
|
||||
return error
|
||||
}
|
||||
const cleanupError = __createCleanupError(
|
||||
cleanupErrors,
|
||||
'WASI binding cleanup failed',
|
||||
)
|
||||
try {
|
||||
if (
|
||||
error &&
|
||||
(typeof error === 'object' || typeof error === 'function')
|
||||
) {
|
||||
if (error.cause === undefined) {
|
||||
error.cause = cleanupError
|
||||
if (error.cause === cleanupError) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
if (Array.isArray(error.cleanupErrors)) {
|
||||
error.cleanupErrors.push(cleanupError)
|
||||
return error
|
||||
} else {
|
||||
const attachedCleanupErrors = [cleanupError]
|
||||
error.cleanupErrors = attachedCleanupErrors
|
||||
if (error.cleanupErrors === attachedCleanupErrors) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
const aggregate = __createCleanupError(
|
||||
[error, cleanupError],
|
||||
'WASI binding initialization and cleanup failed',
|
||||
)
|
||||
try {
|
||||
aggregate.cause = error
|
||||
} catch {}
|
||||
return aggregate
|
||||
}
|
||||
|
||||
function __prepareWasmEnvCleanup() {
|
||||
if (__emnapiWasmEnvCleanupPrepared) {
|
||||
return
|
||||
}
|
||||
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
|
||||
if (typeof prepare === 'function') {
|
||||
prepare()
|
||||
__emnapiWasmEnvCleanupRan = true
|
||||
}
|
||||
__emnapiWasmEnvCleanupPrepared = true
|
||||
}
|
||||
|
||||
// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch
|
||||
// on, so the drain turns below interleave with that dispatch instead of racing
|
||||
// ahead of it on a faster queue.
|
||||
const __scheduleMacrotask = (function () {
|
||||
if (typeof setImmediate === 'function') {
|
||||
return function (callback) {
|
||||
setImmediate(callback)
|
||||
}
|
||||
}
|
||||
const __MessageChannel = globalThis.MessageChannel
|
||||
if (typeof __MessageChannel === 'function') {
|
||||
return function (callback) {
|
||||
const channel = new __MessageChannel()
|
||||
channel.port1.onmessage = function () {
|
||||
channel.port1.onmessage = null
|
||||
try {
|
||||
channel.port1.close()
|
||||
} catch {}
|
||||
try {
|
||||
channel.port2.close()
|
||||
} catch {}
|
||||
callback()
|
||||
}
|
||||
channel.port2.postMessage(null)
|
||||
}
|
||||
}
|
||||
return function (callback) {
|
||||
setTimeout(callback, 0)
|
||||
}
|
||||
})()
|
||||
|
||||
// Turns to wait for while the addon still reports queued settlements. Reaching
|
||||
// zero is the only success. A counter still nonzero at this bound rejects the
|
||||
// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than
|
||||
// destroying the context over a still-queued settlement — the wait stays
|
||||
// bounded either way.
|
||||
const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128
|
||||
// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall
|
||||
// back to the number of turns @emnapi/core needs to coalesce and dispatch a
|
||||
// call made on this thread (two), plus a margin.
|
||||
const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4
|
||||
|
||||
/**
|
||||
* `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the
|
||||
* tasks it cancelled: `napi_call_threadsafe_function` appends to the
|
||||
* threadsafe-function queue, and @emnapi/core dispatches that queue from a
|
||||
* macrotask — two coalescing turns later, even for a call made on this very
|
||||
* thread. `Context.destroy()` then runs the threadsafe function's cleanup hook,
|
||||
* which drains the queue with a null env and *discards* whatever is still in it.
|
||||
*
|
||||
* So destroying without yielding first strands exactly the promises the barrier
|
||||
* exists to settle. Yield real event-loop turns until the addon reports the
|
||||
* queue empty; microtask checkpoints cannot help, no number of them lets a
|
||||
* macrotask run.
|
||||
*
|
||||
* Returns nothing when there is nothing to wait for, which keeps disposal
|
||||
* synchronous in the common case.
|
||||
*
|
||||
* The "already drained" flag is set only once a wait has actually finished.
|
||||
* Scheduling a macrotask can fail — a host-provided or patched `setImmediate`
|
||||
* that throws is enough — and a disposal that rejects stays retryable, so
|
||||
* marking the drain complete up front would make the retry skip it and destroy
|
||||
* the context with the barrier's settlements still queued.
|
||||
*
|
||||
* A wait that runs out of turns with the counter still nonzero rejects with
|
||||
* `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point
|
||||
* "finished" is indistinguishable from the stranding above, and destroying
|
||||
* would discard the very settlement the wait was for. The rejection leaves the
|
||||
* flag unset and disposal retryable.
|
||||
*/
|
||||
function __drainWasmEnvCleanup() {
|
||||
if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) {
|
||||
return
|
||||
}
|
||||
if (__emnapiWasmEnvCleanupDrainPromise) {
|
||||
return __emnapiWasmEnvCleanupDrainPromise
|
||||
}
|
||||
const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending
|
||||
const observable = typeof pending === 'function'
|
||||
if (observable) {
|
||||
let queued
|
||||
try {
|
||||
queued = pending()
|
||||
} catch {
|
||||
__emnapiWasmEnvCleanupDrained = true
|
||||
return
|
||||
}
|
||||
if (!queued) {
|
||||
__emnapiWasmEnvCleanupDrained = true
|
||||
return
|
||||
}
|
||||
}
|
||||
const limit = observable
|
||||
? __WASM_ENV_CLEANUP_DRAIN_TURNS
|
||||
: __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS
|
||||
const drainPromise = (async () => {
|
||||
let queued = 0
|
||||
for (let turn = 0; turn < limit; turn++) {
|
||||
await new Promise((resolve) => {
|
||||
__scheduleMacrotask(resolve)
|
||||
})
|
||||
if (!observable) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
queued = pending()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!queued) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!observable) {
|
||||
// Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the
|
||||
// contract — there is nothing to consult, so finishing the turns is
|
||||
// finishing the drain.
|
||||
return
|
||||
}
|
||||
// The counter is still nonzero after every turn the bound allows. The wait
|
||||
// stays bounded — but claiming success here would be indistinguishable from
|
||||
// the stranding this drain exists to prevent: disposal would go on to
|
||||
// destroy the context, whose cleanup hook discards the still-queued
|
||||
// settlement with a null env, and the promise it was for hangs forever.
|
||||
// Reject instead, as a retryable cleanup failure: the drained flag stays
|
||||
// unset, dispose() (and the rollback) decline to destroy, and a later
|
||||
// dispose() runs the drain again — by which time the queue has usually been
|
||||
// delivered. A counter that is somehow stuck nonzero therefore costs each
|
||||
// attempt at most another bounded wait and a rejection, never a stranded
|
||||
// promise; the process-exit teardown still reclaims the context.
|
||||
const drainError = new Error(
|
||||
'the wasm environment still reports ' +
|
||||
queued +
|
||||
' queued settlement(s) after ' +
|
||||
limit +
|
||||
' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again',
|
||||
)
|
||||
drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING'
|
||||
throw drainError
|
||||
})().then(
|
||||
(value) => {
|
||||
// Set only when the wait actually finished AND the queue was seen empty
|
||||
// (or is unobservable): a drain that timed out with settlements still
|
||||
// queued rejects above and must stay repeatable.
|
||||
__emnapiWasmEnvCleanupDrained = true
|
||||
__emnapiWasmEnvCleanupDrainPromise = undefined
|
||||
return value
|
||||
},
|
||||
(error) => {
|
||||
__emnapiWasmEnvCleanupDrainPromise = undefined
|
||||
throw error
|
||||
},
|
||||
)
|
||||
__emnapiWasmEnvCleanupDrainPromise = drainPromise
|
||||
return drainPromise
|
||||
}
|
||||
|
||||
function __destroyEmnapiContext() {
|
||||
if (__emnapiContextDestroyed || __emnapiContext === undefined) {
|
||||
__emnapiContextDestroyed = true
|
||||
return
|
||||
}
|
||||
if (__emnapiContextDestroyPromise) {
|
||||
return __emnapiContextDestroyPromise
|
||||
}
|
||||
|
||||
__prepareWasmEnvCleanup()
|
||||
const result = __emnapiContext.destroy()
|
||||
if (!__isThenable(result)) {
|
||||
__emnapiContextDestroyed = true
|
||||
return
|
||||
}
|
||||
|
||||
const destroyPromise = Promise.resolve(result).then(
|
||||
(value) => {
|
||||
__emnapiContextDestroyed = true
|
||||
return value
|
||||
},
|
||||
(error) => {
|
||||
__emnapiContextDestroyPromise = undefined
|
||||
throw error
|
||||
},
|
||||
)
|
||||
__emnapiContextDestroyPromise = destroyPromise
|
||||
return destroyPromise
|
||||
}
|
||||
|
||||
function __terminateWasiWorkers() {
|
||||
const cleanupErrors = []
|
||||
const pending = []
|
||||
|
||||
for (const worker of __wasiWorkers) {
|
||||
let result
|
||||
try {
|
||||
result = worker.terminate()
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error)
|
||||
continue
|
||||
}
|
||||
if (__isThenable(result)) {
|
||||
pending.push(
|
||||
Promise.resolve(result).then(
|
||||
() => {
|
||||
__wasiWorkers.delete(worker)
|
||||
},
|
||||
(error) => {
|
||||
cleanupErrors.push(error)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
__wasiWorkers.delete(worker)
|
||||
}
|
||||
}
|
||||
|
||||
const finish = () => {
|
||||
if (cleanupErrors.length > 0) {
|
||||
throw __createCleanupError(
|
||||
cleanupErrors,
|
||||
'Failed to terminate WASI workers',
|
||||
)
|
||||
}
|
||||
}
|
||||
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
|
||||
}
|
||||
|
||||
function __finishWasiDisposal() {
|
||||
const workerResult = __terminateWasiWorkers()
|
||||
if (__isThenable(workerResult)) {
|
||||
return Promise.resolve(workerResult).then(__completeWasiDisposal)
|
||||
}
|
||||
return __completeWasiDisposal()
|
||||
}
|
||||
|
||||
function __continueWasiDisposal() {
|
||||
const destroyResult = __destroyEmnapiContext()
|
||||
if (__isThenable(destroyResult)) {
|
||||
return Promise.resolve(destroyResult).then(__finishWasiDisposal)
|
||||
}
|
||||
return __finishWasiDisposal()
|
||||
}
|
||||
|
||||
function __startWasiDisposal() {
|
||||
// Run the pre-teardown barrier, then let the settlements it queued actually
|
||||
// reach JavaScript, and only then destroy the environment. Doing these two
|
||||
// back to back is what strands them.
|
||||
__prepareWasmEnvCleanup()
|
||||
const drainResult = __drainWasmEnvCleanup()
|
||||
if (__isThenable(drainResult)) {
|
||||
return Promise.resolve(drainResult).then(__continueWasiDisposal)
|
||||
}
|
||||
return __continueWasiDisposal()
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes this generated WASI binding.
|
||||
*
|
||||
* Access this function with:
|
||||
* binding[Symbol.for('napi.rs.wasi.dispose')]()
|
||||
*/
|
||||
function __disposeWasiBinding() {
|
||||
if (__wasiDisposePromise) {
|
||||
return __wasiDisposePromise
|
||||
}
|
||||
if (__wasiDisposed) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
let resolveDispose
|
||||
let rejectDispose
|
||||
const disposePromise = new Promise((resolve, reject) => {
|
||||
resolveDispose = resolve
|
||||
rejectDispose = reject
|
||||
})
|
||||
__wasiDisposePromise = disposePromise
|
||||
|
||||
let result
|
||||
try {
|
||||
result = __startWasiDisposal()
|
||||
} catch (error) {
|
||||
__wasiDisposePromise = undefined
|
||||
rejectDispose(error)
|
||||
return disposePromise
|
||||
}
|
||||
|
||||
Promise.resolve(result).then(
|
||||
(value) => {
|
||||
__wasiDisposed = true
|
||||
resolveDispose(value)
|
||||
},
|
||||
(error) => {
|
||||
__wasiDisposePromise = undefined
|
||||
rejectDispose(error)
|
||||
},
|
||||
)
|
||||
return disposePromise
|
||||
}
|
||||
|
||||
function __publishWasiDispose(exports) {
|
||||
Object.defineProperty(exports, __wasiDisposeSymbol, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: __disposeWasiBinding,
|
||||
writable: false,
|
||||
})
|
||||
}
|
||||
|
||||
function __finishWasiInitializationRollback(cleanupErrors) {
|
||||
let workerResult
|
||||
try {
|
||||
workerResult = __terminateWasiWorkers()
|
||||
} catch (cleanupError) {
|
||||
cleanupErrors.push(cleanupError)
|
||||
return cleanupErrors
|
||||
}
|
||||
if (__isThenable(workerResult)) {
|
||||
return Promise.resolve(workerResult)
|
||||
.catch((cleanupError) => {
|
||||
cleanupErrors.push(cleanupError)
|
||||
})
|
||||
.then(() => cleanupErrors)
|
||||
}
|
||||
return cleanupErrors
|
||||
}
|
||||
|
||||
function __destroyContextForWasiRollback(cleanupErrors) {
|
||||
let destroyResult
|
||||
try {
|
||||
destroyResult = __destroyEmnapiContext()
|
||||
} catch (cleanupError) {
|
||||
cleanupErrors.push(cleanupError)
|
||||
return __finishWasiInitializationRollback(cleanupErrors)
|
||||
}
|
||||
if (__isThenable(destroyResult)) {
|
||||
return Promise.resolve(destroyResult)
|
||||
.catch((cleanupError) => {
|
||||
cleanupErrors.push(cleanupError)
|
||||
})
|
||||
.then(() => __finishWasiInitializationRollback(cleanupErrors))
|
||||
}
|
||||
return __finishWasiInitializationRollback(cleanupErrors)
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves a rollback that could not reach the queued settlements undestroyed, and
|
||||
* hands it to whatever this flavor has that can still reclaim it.
|
||||
*/
|
||||
function __retainFailedWasiRollback(cleanupErrors) {
|
||||
try {
|
||||
__retainWasiRollbackForRetry()
|
||||
} catch (cleanupError) {
|
||||
cleanupErrors.push(cleanupError)
|
||||
}
|
||||
return cleanupErrors
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization can fail *after* registration has already run, and registration
|
||||
* runs with a live environment: a module-init hook can start async work and then
|
||||
* return an error, and the promise it created may already have escaped into
|
||||
* JavaScript. The barrier cancels that work and *queues* the settlement, so this
|
||||
* path needs the same drain the ordinary disposal does — destroying without
|
||||
* yielding discards the queue with a null env and strands the promise.
|
||||
*
|
||||
* Stays synchronous when nothing is queued, which covers every failure before
|
||||
* `beforeInit`: there is no instance to run the barrier on, so nothing to drain.
|
||||
*
|
||||
* A barrier or drain that did *not* finish stops the rollback short of
|
||||
* destroying, which is what `dispose()` already does — a rejected drain there
|
||||
* never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the
|
||||
* two trades, and not because of what it saves:
|
||||
*
|
||||
* - It cannot deliver the settlements. `Context.destroy()` runs the
|
||||
* threadsafe function's cleanup hook, which drains the queue with a null env
|
||||
* and discards it, so a promise that already escaped into JavaScript hangs
|
||||
* forever with nothing left that could ever settle it.
|
||||
* - It saves less than it looks. `Context.destroy()` stops JavaScript calls
|
||||
* and runs cleanup hooks; it does not free the wasm instance or its Memory,
|
||||
* which this module's scope holds either way. What stopping short retains is
|
||||
* the emnapi context's bookkeeping and its un-run cleanup hooks.
|
||||
* - Retry is not theoretical. A rollback that records a cleanup error is
|
||||
* already kept in the process-wide registry above, so re-`require()`ing this
|
||||
* file replays it instead of re-instantiating — and the `6e15de6f` flag fix
|
||||
* means the replay drains again rather than skipping it. Destroying first is
|
||||
* what makes that retained record useless.
|
||||
*
|
||||
* The residual cost is honest: the CJS flavor hands the context to its
|
||||
* `process.on('exit')` teardown, so a process that never retries still reclaims
|
||||
* it on the way out. The ESM browser flavor has no equivalent — a module that
|
||||
* throws while evaluating is permanently errored, so re-importing rethrows
|
||||
* without re-running this file — and there the context stays until the realm
|
||||
* goes away. That is the deliberate choice: a hung promise is a silent liveness
|
||||
* bug with no upper bound, while the retained bookkeeping is bounded by the page.
|
||||
*/
|
||||
function __rollbackWasiInitialization() {
|
||||
const cleanupErrors = []
|
||||
let drainResult
|
||||
let settlementsUnreached = false
|
||||
try {
|
||||
__prepareWasmEnvCleanup()
|
||||
drainResult = __drainWasmEnvCleanup()
|
||||
} catch (cleanupError) {
|
||||
cleanupErrors.push(cleanupError)
|
||||
settlementsUnreached = true
|
||||
}
|
||||
if (__isThenable(drainResult)) {
|
||||
return Promise.resolve(drainResult).then(
|
||||
() => __destroyContextForWasiRollback(cleanupErrors),
|
||||
(cleanupError) => {
|
||||
cleanupErrors.push(cleanupError)
|
||||
return __retainFailedWasiRollback(cleanupErrors)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (settlementsUnreached) {
|
||||
return __retainFailedWasiRollback(cleanupErrors)
|
||||
}
|
||||
return __destroyContextForWasiRollback(cleanupErrors)
|
||||
}
|
||||
|
||||
let __wasiModule
|
||||
let __napiModule
|
||||
|
||||
try {
|
||||
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
|
||||
__emnapiContext.suppressDestroy()
|
||||
|
||||
;({
|
||||
instance: __napiInstance,
|
||||
module: __wasiModule,
|
||||
napiModule: __napiModule,
|
||||
} = await __emnapiInstantiateNapiModule(__wasmFile, {
|
||||
context: __emnapiContext,
|
||||
asyncWorkPoolSize: __asyncWorkPoolSize,
|
||||
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },
|
||||
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
|
||||
wasi: __wasi,
|
||||
onCreateWorker() {
|
||||
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
|
||||
type: 'module',
|
||||
})
|
||||
__wasiWorkers.add(worker)
|
||||
|
||||
|
||||
return worker
|
||||
},
|
||||
overwriteImports(importObject) {
|
||||
importObject.env = {
|
||||
...importObject.env,
|
||||
...importObject.napi,
|
||||
...importObject.emnapi,
|
||||
memory: __sharedMemory,
|
||||
}
|
||||
return importObject
|
||||
},
|
||||
beforeInit({ instance }) {
|
||||
__napiInstance = instance
|
||||
for (const name of Object.keys(instance.exports)) {
|
||||
if (name.startsWith('__napi_register__')) {
|
||||
instance.exports[name]()
|
||||
}
|
||||
}
|
||||
},
|
||||
}))
|
||||
__publishWasiDispose(__napiModule.exports)
|
||||
} catch (error) {
|
||||
const cleanupErrors = await __rollbackWasiInitialization()
|
||||
throw __attachCleanupErrors(error, cleanupErrors)
|
||||
}
|
||||
export default __napiModule.exports
|
||||
export const Severity = __napiModule.exports.Severity
|
||||
export const transform = __napiModule.exports.transform
|
||||
export const transformSync = __napiModule.exports.transformSync
|
||||
@@ -0,0 +1,926 @@
|
||||
// napi-rs-artifact-metadata:{"version":2,"rootEntry":"index.js","exports":["Severity","transform","transformSync"],"managedRootEntries":["browser.js","index.js","transform-relay.wasm","transform-relay.debug.wasm"]}
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
const __nodeFs = require('node:fs')
|
||||
const __nodePath = require('node:path')
|
||||
const { WASI: __nodeWASI } = require('node:wasi')
|
||||
const { Worker } = require('node:worker_threads')
|
||||
|
||||
const {
|
||||
emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin,
|
||||
emnapiTSFNPlugin: __emnapiTSFNPlugin,
|
||||
createOnMessage: __wasmCreateOnMessageForFsProxy,
|
||||
instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync,
|
||||
} = require('@napi-rs/wasm-runtime')
|
||||
const { createContext: __emnapiCreateContext } = require('@emnapi/runtime')
|
||||
|
||||
function __getWasiWorkerExecArgv() {
|
||||
const __workerExecArgv = []
|
||||
for (let __index = 0; __index < process.execArgv.length; __index += 1) {
|
||||
const __arg = process.execArgv[__index]
|
||||
if (
|
||||
__arg === '--input-type' ||
|
||||
__arg === '--eval' ||
|
||||
__arg === '-e' ||
|
||||
__arg === '--print' ||
|
||||
__arg === '-p'
|
||||
) {
|
||||
__index += 1
|
||||
continue
|
||||
}
|
||||
if (
|
||||
__arg.startsWith('--input-type=') ||
|
||||
__arg.startsWith('--eval=') ||
|
||||
__arg.startsWith('--print=')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
__workerExecArgv.push(__arg)
|
||||
}
|
||||
return __workerExecArgv
|
||||
}
|
||||
|
||||
function __isInvalidWasiWorkerExecArgv(errorMessage, argument) {
|
||||
const __equalsIndex = argument.indexOf('=')
|
||||
const __argumentName =
|
||||
__equalsIndex === -1 ? argument : argument.slice(0, __equalsIndex)
|
||||
return (
|
||||
errorMessage.includes(': ' + __argumentName + ',') ||
|
||||
errorMessage.includes(': ' + __argumentName + '=') ||
|
||||
errorMessage.endsWith(': ' + __argumentName) ||
|
||||
errorMessage.includes(', ' + __argumentName + ',') ||
|
||||
errorMessage.includes(', ' + __argumentName + '=') ||
|
||||
errorMessage.endsWith(', ' + __argumentName)
|
||||
)
|
||||
}
|
||||
|
||||
function __removeInvalidWasiWorkerExecArgv(execArgv, error) {
|
||||
if (typeof error.message !== 'string') {
|
||||
return
|
||||
}
|
||||
const __workerExecArgv = []
|
||||
let __removed = false
|
||||
for (let __index = 0; __index < execArgv.length; __index += 1) {
|
||||
const __arg = execArgv[__index]
|
||||
if (
|
||||
__arg.startsWith('-') &&
|
||||
__isInvalidWasiWorkerExecArgv(error.message, __arg)
|
||||
) {
|
||||
__removed = true
|
||||
if (
|
||||
!__arg.includes('=') &&
|
||||
__index + 1 < execArgv.length &&
|
||||
!execArgv[__index + 1].startsWith('-')
|
||||
) {
|
||||
__index += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
__workerExecArgv.push(__arg)
|
||||
}
|
||||
return __removed ? __workerExecArgv : undefined
|
||||
}
|
||||
|
||||
function __createWasiWorker(filename) {
|
||||
let __workerExecArgv = __getWasiWorkerExecArgv()
|
||||
while (true) {
|
||||
try {
|
||||
return new Worker(filename, {
|
||||
env: process.env,
|
||||
execArgv: __workerExecArgv,
|
||||
})
|
||||
} catch (error) {
|
||||
if (!error || error.code !== 'ERR_WORKER_INVALID_EXEC_ARGV') {
|
||||
throw error
|
||||
}
|
||||
const __nextWorkerExecArgv =
|
||||
__removeInvalidWasiWorkerExecArgv(__workerExecArgv, error)
|
||||
if (!__nextWorkerExecArgv) {
|
||||
throw error
|
||||
}
|
||||
__workerExecArgv = __nextWorkerExecArgv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const __rootDir = __nodePath.parse(process.cwd()).root
|
||||
|
||||
const __wasi = new __nodeWASI({
|
||||
version: 'preview1',
|
||||
env: process.env,
|
||||
preopens: {
|
||||
[__rootDir]: __rootDir,
|
||||
}
|
||||
})
|
||||
|
||||
const __sharedMemory = new WebAssembly.Memory({
|
||||
initial: 4000,
|
||||
maximum: 65536,
|
||||
shared: true,
|
||||
})
|
||||
|
||||
let __wasmFilePath = __nodePath.join(__dirname, 'transform-relay.wasm32-wasi.wasm')
|
||||
const __wasmDebugFilePath = __nodePath.join(__dirname, 'transform-relay.wasm32-wasi.debug.wasm')
|
||||
|
||||
if (__nodeFs.existsSync(__wasmDebugFilePath)) {
|
||||
__wasmFilePath = __wasmDebugFilePath
|
||||
} else if (!__nodeFs.existsSync(__wasmFilePath)) {
|
||||
const __wasiPackageEntry = require.resolve('@oxc-transform-relay/binding-wasm32-wasi')
|
||||
const __packagedWasmFilePath = __nodePath.join(
|
||||
__nodePath.dirname(__wasiPackageEntry),
|
||||
'transform-relay.wasm32-wasi.wasm',
|
||||
)
|
||||
if (!__nodeFs.existsSync(__packagedWasmFilePath)) {
|
||||
throw new Error(
|
||||
'@oxc-transform-relay/binding-wasm32-wasi is installed but is missing transform-relay.wasm32-wasi.wasm.',
|
||||
)
|
||||
}
|
||||
__wasmFilePath = __packagedWasmFilePath
|
||||
}
|
||||
|
||||
const __wasmFile = __nodeFs.readFileSync(__wasmFilePath)
|
||||
let __emnapiContext
|
||||
|
||||
const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose')
|
||||
const __wasiWorkers = new Set()
|
||||
let __napiInstance
|
||||
let __emnapiContextDestroyed = false
|
||||
let __emnapiContextDestroyPromise
|
||||
let __emnapiWasmEnvCleanupPrepared = false
|
||||
let __emnapiWasmEnvCleanupRan = false
|
||||
let __emnapiWasmEnvCleanupDrained = false
|
||||
let __emnapiWasmEnvCleanupDrainPromise
|
||||
let __wasiDisposed = false
|
||||
let __wasiDisposePromise
|
||||
let __completeWasiDisposal = function() {}
|
||||
// Overridden by loader flavors that have a last-resort reclaim for a rollback
|
||||
// that stopped short of destroying the context. See
|
||||
// `__rollbackWasiInitialization`.
|
||||
let __retainWasiRollbackForRetry = function() {}
|
||||
|
||||
function __isThenable(value) {
|
||||
return (
|
||||
value !== null &&
|
||||
(typeof value === 'object' || typeof value === 'function') &&
|
||||
typeof value.then === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
function __createCleanupError(errors, message) {
|
||||
if (errors.length === 1) {
|
||||
return errors[0]
|
||||
}
|
||||
const __AggregateError = globalThis.AggregateError
|
||||
if (typeof __AggregateError === 'function') {
|
||||
return new __AggregateError(errors, message)
|
||||
}
|
||||
const error = new Error(message)
|
||||
error.errors = errors
|
||||
return error
|
||||
}
|
||||
|
||||
function __attachCleanupErrors(error, cleanupErrors) {
|
||||
if (cleanupErrors.length === 0) {
|
||||
return error
|
||||
}
|
||||
const cleanupError = __createCleanupError(
|
||||
cleanupErrors,
|
||||
'WASI binding cleanup failed',
|
||||
)
|
||||
try {
|
||||
if (
|
||||
error &&
|
||||
(typeof error === 'object' || typeof error === 'function')
|
||||
) {
|
||||
if (error.cause === undefined) {
|
||||
error.cause = cleanupError
|
||||
if (error.cause === cleanupError) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
if (Array.isArray(error.cleanupErrors)) {
|
||||
error.cleanupErrors.push(cleanupError)
|
||||
return error
|
||||
} else {
|
||||
const attachedCleanupErrors = [cleanupError]
|
||||
error.cleanupErrors = attachedCleanupErrors
|
||||
if (error.cleanupErrors === attachedCleanupErrors) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
const aggregate = __createCleanupError(
|
||||
[error, cleanupError],
|
||||
'WASI binding initialization and cleanup failed',
|
||||
)
|
||||
try {
|
||||
aggregate.cause = error
|
||||
} catch {}
|
||||
return aggregate
|
||||
}
|
||||
|
||||
function __prepareWasmEnvCleanup() {
|
||||
if (__emnapiWasmEnvCleanupPrepared) {
|
||||
return
|
||||
}
|
||||
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
|
||||
if (typeof prepare === 'function') {
|
||||
prepare()
|
||||
__emnapiWasmEnvCleanupRan = true
|
||||
}
|
||||
__emnapiWasmEnvCleanupPrepared = true
|
||||
}
|
||||
|
||||
// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch
|
||||
// on, so the drain turns below interleave with that dispatch instead of racing
|
||||
// ahead of it on a faster queue.
|
||||
const __scheduleMacrotask = (function () {
|
||||
if (typeof setImmediate === 'function') {
|
||||
return function (callback) {
|
||||
setImmediate(callback)
|
||||
}
|
||||
}
|
||||
const __MessageChannel = globalThis.MessageChannel
|
||||
if (typeof __MessageChannel === 'function') {
|
||||
return function (callback) {
|
||||
const channel = new __MessageChannel()
|
||||
channel.port1.onmessage = function () {
|
||||
channel.port1.onmessage = null
|
||||
try {
|
||||
channel.port1.close()
|
||||
} catch {}
|
||||
try {
|
||||
channel.port2.close()
|
||||
} catch {}
|
||||
callback()
|
||||
}
|
||||
channel.port2.postMessage(null)
|
||||
}
|
||||
}
|
||||
return function (callback) {
|
||||
setTimeout(callback, 0)
|
||||
}
|
||||
})()
|
||||
|
||||
// Turns to wait for while the addon still reports queued settlements. Reaching
|
||||
// zero is the only success. A counter still nonzero at this bound rejects the
|
||||
// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than
|
||||
// destroying the context over a still-queued settlement — the wait stays
|
||||
// bounded either way.
|
||||
const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128
|
||||
// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall
|
||||
// back to the number of turns @emnapi/core needs to coalesce and dispatch a
|
||||
// call made on this thread (two), plus a margin.
|
||||
const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4
|
||||
|
||||
/**
|
||||
* `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the
|
||||
* tasks it cancelled: `napi_call_threadsafe_function` appends to the
|
||||
* threadsafe-function queue, and @emnapi/core dispatches that queue from a
|
||||
* macrotask — two coalescing turns later, even for a call made on this very
|
||||
* thread. `Context.destroy()` then runs the threadsafe function's cleanup hook,
|
||||
* which drains the queue with a null env and *discards* whatever is still in it.
|
||||
*
|
||||
* So destroying without yielding first strands exactly the promises the barrier
|
||||
* exists to settle. Yield real event-loop turns until the addon reports the
|
||||
* queue empty; microtask checkpoints cannot help, no number of them lets a
|
||||
* macrotask run.
|
||||
*
|
||||
* Returns nothing when there is nothing to wait for, which keeps disposal
|
||||
* synchronous in the common case.
|
||||
*
|
||||
* The "already drained" flag is set only once a wait has actually finished.
|
||||
* Scheduling a macrotask can fail — a host-provided or patched `setImmediate`
|
||||
* that throws is enough — and a disposal that rejects stays retryable, so
|
||||
* marking the drain complete up front would make the retry skip it and destroy
|
||||
* the context with the barrier's settlements still queued.
|
||||
*
|
||||
* A wait that runs out of turns with the counter still nonzero rejects with
|
||||
* `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point
|
||||
* "finished" is indistinguishable from the stranding above, and destroying
|
||||
* would discard the very settlement the wait was for. The rejection leaves the
|
||||
* flag unset and disposal retryable.
|
||||
*/
|
||||
function __drainWasmEnvCleanup() {
|
||||
if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) {
|
||||
return
|
||||
}
|
||||
if (__emnapiWasmEnvCleanupDrainPromise) {
|
||||
return __emnapiWasmEnvCleanupDrainPromise
|
||||
}
|
||||
const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending
|
||||
const observable = typeof pending === 'function'
|
||||
if (observable) {
|
||||
let queued
|
||||
try {
|
||||
queued = pending()
|
||||
} catch {
|
||||
__emnapiWasmEnvCleanupDrained = true
|
||||
return
|
||||
}
|
||||
if (!queued) {
|
||||
__emnapiWasmEnvCleanupDrained = true
|
||||
return
|
||||
}
|
||||
}
|
||||
const limit = observable
|
||||
? __WASM_ENV_CLEANUP_DRAIN_TURNS
|
||||
: __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS
|
||||
const drainPromise = (async () => {
|
||||
let queued = 0
|
||||
for (let turn = 0; turn < limit; turn++) {
|
||||
await new Promise((resolve) => {
|
||||
__scheduleMacrotask(resolve)
|
||||
})
|
||||
if (!observable) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
queued = pending()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!queued) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!observable) {
|
||||
// Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the
|
||||
// contract — there is nothing to consult, so finishing the turns is
|
||||
// finishing the drain.
|
||||
return
|
||||
}
|
||||
// The counter is still nonzero after every turn the bound allows. The wait
|
||||
// stays bounded — but claiming success here would be indistinguishable from
|
||||
// the stranding this drain exists to prevent: disposal would go on to
|
||||
// destroy the context, whose cleanup hook discards the still-queued
|
||||
// settlement with a null env, and the promise it was for hangs forever.
|
||||
// Reject instead, as a retryable cleanup failure: the drained flag stays
|
||||
// unset, dispose() (and the rollback) decline to destroy, and a later
|
||||
// dispose() runs the drain again — by which time the queue has usually been
|
||||
// delivered. A counter that is somehow stuck nonzero therefore costs each
|
||||
// attempt at most another bounded wait and a rejection, never a stranded
|
||||
// promise; the process-exit teardown still reclaims the context.
|
||||
const drainError = new Error(
|
||||
'the wasm environment still reports ' +
|
||||
queued +
|
||||
' queued settlement(s) after ' +
|
||||
limit +
|
||||
' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again',
|
||||
)
|
||||
drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING'
|
||||
throw drainError
|
||||
})().then(
|
||||
(value) => {
|
||||
// Set only when the wait actually finished AND the queue was seen empty
|
||||
// (or is unobservable): a drain that timed out with settlements still
|
||||
// queued rejects above and must stay repeatable.
|
||||
__emnapiWasmEnvCleanupDrained = true
|
||||
__emnapiWasmEnvCleanupDrainPromise = undefined
|
||||
return value
|
||||
},
|
||||
(error) => {
|
||||
__emnapiWasmEnvCleanupDrainPromise = undefined
|
||||
throw error
|
||||
},
|
||||
)
|
||||
__emnapiWasmEnvCleanupDrainPromise = drainPromise
|
||||
return drainPromise
|
||||
}
|
||||
|
||||
function __destroyEmnapiContext() {
|
||||
if (__emnapiContextDestroyed || __emnapiContext === undefined) {
|
||||
__emnapiContextDestroyed = true
|
||||
return
|
||||
}
|
||||
if (__emnapiContextDestroyPromise) {
|
||||
return __emnapiContextDestroyPromise
|
||||
}
|
||||
|
||||
__prepareWasmEnvCleanup()
|
||||
const result = __emnapiContext.destroy()
|
||||
if (!__isThenable(result)) {
|
||||
__emnapiContextDestroyed = true
|
||||
return
|
||||
}
|
||||
|
||||
const destroyPromise = Promise.resolve(result).then(
|
||||
(value) => {
|
||||
__emnapiContextDestroyed = true
|
||||
return value
|
||||
},
|
||||
(error) => {
|
||||
__emnapiContextDestroyPromise = undefined
|
||||
throw error
|
||||
},
|
||||
)
|
||||
__emnapiContextDestroyPromise = destroyPromise
|
||||
return destroyPromise
|
||||
}
|
||||
|
||||
function __terminateWasiWorkers() {
|
||||
const cleanupErrors = []
|
||||
const pending = []
|
||||
|
||||
for (const worker of __wasiWorkers) {
|
||||
let result
|
||||
try {
|
||||
result = worker.terminate()
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error)
|
||||
continue
|
||||
}
|
||||
if (__isThenable(result)) {
|
||||
pending.push(
|
||||
Promise.resolve(result).then(
|
||||
() => {
|
||||
__wasiWorkers.delete(worker)
|
||||
},
|
||||
(error) => {
|
||||
cleanupErrors.push(error)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
__wasiWorkers.delete(worker)
|
||||
}
|
||||
}
|
||||
|
||||
const finish = () => {
|
||||
if (cleanupErrors.length > 0) {
|
||||
throw __createCleanupError(
|
||||
cleanupErrors,
|
||||
'Failed to terminate WASI workers',
|
||||
)
|
||||
}
|
||||
}
|
||||
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
|
||||
}
|
||||
|
||||
function __finishWasiDisposal() {
|
||||
const workerResult = __terminateWasiWorkers()
|
||||
if (__isThenable(workerResult)) {
|
||||
return Promise.resolve(workerResult).then(__completeWasiDisposal)
|
||||
}
|
||||
return __completeWasiDisposal()
|
||||
}
|
||||
|
||||
function __continueWasiDisposal() {
|
||||
const destroyResult = __destroyEmnapiContext()
|
||||
if (__isThenable(destroyResult)) {
|
||||
return Promise.resolve(destroyResult).then(__finishWasiDisposal)
|
||||
}
|
||||
return __finishWasiDisposal()
|
||||
}
|
||||
|
||||
function __startWasiDisposal() {
|
||||
// Run the pre-teardown barrier, then let the settlements it queued actually
|
||||
// reach JavaScript, and only then destroy the environment. Doing these two
|
||||
// back to back is what strands them.
|
||||
__prepareWasmEnvCleanup()
|
||||
const drainResult = __drainWasmEnvCleanup()
|
||||
if (__isThenable(drainResult)) {
|
||||
return Promise.resolve(drainResult).then(__continueWasiDisposal)
|
||||
}
|
||||
return __continueWasiDisposal()
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes this generated WASI binding.
|
||||
*
|
||||
* Access this function with:
|
||||
* binding[Symbol.for('napi.rs.wasi.dispose')]()
|
||||
*/
|
||||
function __disposeWasiBinding() {
|
||||
if (__wasiDisposePromise) {
|
||||
return __wasiDisposePromise
|
||||
}
|
||||
if (__wasiDisposed) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
let resolveDispose
|
||||
let rejectDispose
|
||||
const disposePromise = new Promise((resolve, reject) => {
|
||||
resolveDispose = resolve
|
||||
rejectDispose = reject
|
||||
})
|
||||
__wasiDisposePromise = disposePromise
|
||||
|
||||
let result
|
||||
try {
|
||||
result = __startWasiDisposal()
|
||||
} catch (error) {
|
||||
__wasiDisposePromise = undefined
|
||||
rejectDispose(error)
|
||||
return disposePromise
|
||||
}
|
||||
|
||||
Promise.resolve(result).then(
|
||||
(value) => {
|
||||
__wasiDisposed = true
|
||||
resolveDispose(value)
|
||||
},
|
||||
(error) => {
|
||||
__wasiDisposePromise = undefined
|
||||
rejectDispose(error)
|
||||
},
|
||||
)
|
||||
return disposePromise
|
||||
}
|
||||
|
||||
function __publishWasiDispose(exports) {
|
||||
Object.defineProperty(exports, __wasiDisposeSymbol, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: __disposeWasiBinding,
|
||||
writable: false,
|
||||
})
|
||||
}
|
||||
|
||||
function __finishWasiInitializationRollback(cleanupErrors) {
|
||||
let workerResult
|
||||
try {
|
||||
workerResult = __terminateWasiWorkers()
|
||||
} catch (cleanupError) {
|
||||
cleanupErrors.push(cleanupError)
|
||||
return cleanupErrors
|
||||
}
|
||||
if (__isThenable(workerResult)) {
|
||||
return Promise.resolve(workerResult)
|
||||
.catch((cleanupError) => {
|
||||
cleanupErrors.push(cleanupError)
|
||||
})
|
||||
.then(() => cleanupErrors)
|
||||
}
|
||||
return cleanupErrors
|
||||
}
|
||||
|
||||
function __destroyContextForWasiRollback(cleanupErrors) {
|
||||
let destroyResult
|
||||
try {
|
||||
destroyResult = __destroyEmnapiContext()
|
||||
} catch (cleanupError) {
|
||||
cleanupErrors.push(cleanupError)
|
||||
return __finishWasiInitializationRollback(cleanupErrors)
|
||||
}
|
||||
if (__isThenable(destroyResult)) {
|
||||
return Promise.resolve(destroyResult)
|
||||
.catch((cleanupError) => {
|
||||
cleanupErrors.push(cleanupError)
|
||||
})
|
||||
.then(() => __finishWasiInitializationRollback(cleanupErrors))
|
||||
}
|
||||
return __finishWasiInitializationRollback(cleanupErrors)
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves a rollback that could not reach the queued settlements undestroyed, and
|
||||
* hands it to whatever this flavor has that can still reclaim it.
|
||||
*/
|
||||
function __retainFailedWasiRollback(cleanupErrors) {
|
||||
try {
|
||||
__retainWasiRollbackForRetry()
|
||||
} catch (cleanupError) {
|
||||
cleanupErrors.push(cleanupError)
|
||||
}
|
||||
return cleanupErrors
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization can fail *after* registration has already run, and registration
|
||||
* runs with a live environment: a module-init hook can start async work and then
|
||||
* return an error, and the promise it created may already have escaped into
|
||||
* JavaScript. The barrier cancels that work and *queues* the settlement, so this
|
||||
* path needs the same drain the ordinary disposal does — destroying without
|
||||
* yielding discards the queue with a null env and strands the promise.
|
||||
*
|
||||
* Stays synchronous when nothing is queued, which covers every failure before
|
||||
* `beforeInit`: there is no instance to run the barrier on, so nothing to drain.
|
||||
*
|
||||
* A barrier or drain that did *not* finish stops the rollback short of
|
||||
* destroying, which is what `dispose()` already does — a rejected drain there
|
||||
* never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the
|
||||
* two trades, and not because of what it saves:
|
||||
*
|
||||
* - It cannot deliver the settlements. `Context.destroy()` runs the
|
||||
* threadsafe function's cleanup hook, which drains the queue with a null env
|
||||
* and discards it, so a promise that already escaped into JavaScript hangs
|
||||
* forever with nothing left that could ever settle it.
|
||||
* - It saves less than it looks. `Context.destroy()` stops JavaScript calls
|
||||
* and runs cleanup hooks; it does not free the wasm instance or its Memory,
|
||||
* which this module's scope holds either way. What stopping short retains is
|
||||
* the emnapi context's bookkeeping and its un-run cleanup hooks.
|
||||
* - Retry is not theoretical. A rollback that records a cleanup error is
|
||||
* already kept in the process-wide registry above, so re-`require()`ing this
|
||||
* file replays it instead of re-instantiating — and the `6e15de6f` flag fix
|
||||
* means the replay drains again rather than skipping it. Destroying first is
|
||||
* what makes that retained record useless.
|
||||
*
|
||||
* The residual cost is honest: the CJS flavor hands the context to its
|
||||
* `process.on('exit')` teardown, so a process that never retries still reclaims
|
||||
* it on the way out. The ESM browser flavor has no equivalent — a module that
|
||||
* throws while evaluating is permanently errored, so re-importing rethrows
|
||||
* without re-running this file — and there the context stays until the realm
|
||||
* goes away. That is the deliberate choice: a hung promise is a silent liveness
|
||||
* bug with no upper bound, while the retained bookkeeping is bounded by the page.
|
||||
*/
|
||||
function __rollbackWasiInitialization() {
|
||||
const cleanupErrors = []
|
||||
let drainResult
|
||||
let settlementsUnreached = false
|
||||
try {
|
||||
__prepareWasmEnvCleanup()
|
||||
drainResult = __drainWasmEnvCleanup()
|
||||
} catch (cleanupError) {
|
||||
cleanupErrors.push(cleanupError)
|
||||
settlementsUnreached = true
|
||||
}
|
||||
if (__isThenable(drainResult)) {
|
||||
return Promise.resolve(drainResult).then(
|
||||
() => __destroyContextForWasiRollback(cleanupErrors),
|
||||
(cleanupError) => {
|
||||
cleanupErrors.push(cleanupError)
|
||||
return __retainFailedWasiRollback(cleanupErrors)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (settlementsUnreached) {
|
||||
return __retainFailedWasiRollback(cleanupErrors)
|
||||
}
|
||||
return __destroyContextForWasiRollback(cleanupErrors)
|
||||
}
|
||||
|
||||
const __wasiRollbackRegistrySymbol = Symbol.for('napi.rs.wasi.rollback.registry.v1')
|
||||
const __wasiRollbackRegistryKey =
|
||||
typeof __filename === 'string' ? __filename : __wasmFilePath
|
||||
|
||||
function __getWasiRollbackRegistry() {
|
||||
const existing = process[__wasiRollbackRegistrySymbol]
|
||||
if (existing !== undefined) {
|
||||
if (!(existing instanceof Map)) {
|
||||
throw new TypeError(
|
||||
'The process-wide NAPI-RS WASI rollback registry is invalid',
|
||||
)
|
||||
}
|
||||
return existing
|
||||
}
|
||||
const registry = new Map()
|
||||
Object.defineProperty(process, __wasiRollbackRegistrySymbol, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: registry,
|
||||
writable: false,
|
||||
})
|
||||
return registry
|
||||
}
|
||||
|
||||
const __wasiRollbackRegistry = __getWasiRollbackRegistry()
|
||||
|
||||
function __completeWasiInitializationRollback(record, cleanupErrors) {
|
||||
try {
|
||||
if (cleanupErrors.length === 0) {
|
||||
if (
|
||||
__wasiRollbackRegistry.get(__wasiRollbackRegistryKey) === record
|
||||
) {
|
||||
__wasiRollbackRegistry.delete(__wasiRollbackRegistryKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
record.error = __attachCleanupErrors(record.error, cleanupErrors)
|
||||
} catch (cleanupError) {
|
||||
try {
|
||||
record.error = __createCleanupError(
|
||||
[record.error, cleanupError],
|
||||
'WASI binding initialization and cleanup failed',
|
||||
)
|
||||
} catch {}
|
||||
} finally {
|
||||
record.active = false
|
||||
record.promise = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function __runWasiInitializationRollback(record) {
|
||||
if (record.active) {
|
||||
return
|
||||
}
|
||||
record.active = true
|
||||
|
||||
let rollbackResult
|
||||
try {
|
||||
rollbackResult = record.rollback()
|
||||
} catch (cleanupError) {
|
||||
__completeWasiInitializationRollback(record, [cleanupError])
|
||||
return
|
||||
}
|
||||
|
||||
if (!__isThenable(rollbackResult)) {
|
||||
__completeWasiInitializationRollback(record, rollbackResult)
|
||||
return
|
||||
}
|
||||
|
||||
record.promise = Promise.resolve(rollbackResult).then(
|
||||
(cleanupErrors) => {
|
||||
__completeWasiInitializationRollback(record, cleanupErrors)
|
||||
},
|
||||
(cleanupError) => {
|
||||
__completeWasiInitializationRollback(record, [cleanupError])
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const __pendingWasiRollback = __wasiRollbackRegistry.get(
|
||||
__wasiRollbackRegistryKey,
|
||||
)
|
||||
if (__pendingWasiRollback !== undefined) {
|
||||
__runWasiInitializationRollback(__pendingWasiRollback)
|
||||
throw __pendingWasiRollback.error
|
||||
}
|
||||
|
||||
let __wasiModule
|
||||
let __napiModule
|
||||
let __wasiExitListenerRegistered = false
|
||||
|
||||
function __removeWasiExitListener() {
|
||||
if (
|
||||
__wasiExitListenerRegistered &&
|
||||
typeof process.removeListener === 'function'
|
||||
) {
|
||||
process.removeListener('exit', __disposeWasiBindingAtExit)
|
||||
}
|
||||
__wasiExitListenerRegistered = false
|
||||
}
|
||||
|
||||
function __disposeWasiBindingAtExit() {
|
||||
__wasiExitListenerRegistered = false
|
||||
// An 'exit' handler cannot yield, so it cannot wait for queued promise
|
||||
// settlements the way __startWasiDisposal does — the process is leaving and
|
||||
// those promises have no observer left anyway. Run the synchronous teardown
|
||||
// directly. Every step is idempotent, which also makes this the synchronous
|
||||
// finish for a disposal that is still waiting for its drain.
|
||||
try {
|
||||
__destroyEmnapiContext()
|
||||
} catch {}
|
||||
try {
|
||||
const workerResult = __terminateWasiWorkers()
|
||||
if (__isThenable(workerResult)) {
|
||||
void Promise.resolve(workerResult).catch(() => {})
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function __registerWasiExitListener() {
|
||||
if (
|
||||
!__wasiExitListenerRegistered &&
|
||||
typeof process.once === 'function'
|
||||
) {
|
||||
process.once('exit', __disposeWasiBindingAtExit)
|
||||
__wasiExitListenerRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
__completeWasiDisposal = __removeWasiExitListener
|
||||
// A rollback that could not reach the queued settlements keeps the context so
|
||||
// the registry replay above can retry it. Nothing forces that replay to happen,
|
||||
// so hand the context to the same synchronous teardown a successful load uses:
|
||||
// a process that exits without ever retrying still runs the cleanup hooks. The
|
||||
// handler cannot yield, so it does not settle anything — but by then the process
|
||||
// is leaving and those promises have no observer left anyway.
|
||||
__retainWasiRollbackForRetry = __registerWasiExitListener
|
||||
|
||||
function __captureEmnapiAutoDestroyListener() {
|
||||
if (
|
||||
typeof process.prependListener !== 'function' ||
|
||||
typeof process.removeListener !== 'function'
|
||||
) {
|
||||
return
|
||||
}
|
||||
let __autoDestroyListener
|
||||
const __captureListener = (__event, __listener) => {
|
||||
if (__event === 'beforeExit' && __autoDestroyListener === undefined) {
|
||||
__autoDestroyListener = __listener
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Run before existing newListener hooks so a hook that registers its own
|
||||
// beforeExit listener cannot be mistaken for emnapi's registration.
|
||||
process.prependListener('newListener', __captureListener)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
return () => {
|
||||
try {
|
||||
process.removeListener('newListener', __captureListener)
|
||||
} catch {}
|
||||
if (__autoDestroyListener !== undefined) {
|
||||
try {
|
||||
process.removeListener('beforeExit', __autoDestroyListener)
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
|
||||
try {
|
||||
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
|
||||
// emnapi 2.x still registers an unconditional once-listener for
|
||||
// beforeExit that auto-destroys the context, and suppressDestroy() only
|
||||
// neutralizes its callback without removing it. This loader owns cleanup
|
||||
// through its 'exit' listener, so emnapi's listener is captured and
|
||||
// removed; suppressDestroy() remains the safety net when removal fails.
|
||||
__emnapiContext.suppressDestroy()
|
||||
} finally {
|
||||
// Remove only the exact emnapi callback captured above.
|
||||
__finishAutoDestroyCapture?.()
|
||||
}
|
||||
|
||||
;({
|
||||
instance: __napiInstance,
|
||||
module: __wasiModule,
|
||||
napiModule: __napiModule,
|
||||
} = __emnapiInstantiateNapiModuleSync(__wasmFile, {
|
||||
context: __emnapiContext,
|
||||
asyncWorkPoolSize: (function() {
|
||||
const threadsSizeFromEnv = Number(process.env.NAPI_RS_ASYNC_WORK_POOL_SIZE ?? process.env.UV_THREADPOOL_SIZE)
|
||||
// NaN > 0 is false
|
||||
if (threadsSizeFromEnv > 0) {
|
||||
return threadsSizeFromEnv
|
||||
} else {
|
||||
return 4
|
||||
}
|
||||
})(),
|
||||
reuseWorker: true,
|
||||
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
|
||||
wasi: __wasi,
|
||||
onCreateWorker() {
|
||||
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
|
||||
__wasiWorkers.add(worker)
|
||||
worker.onmessage = ({ data }) => {
|
||||
__wasmCreateOnMessageForFsProxy(__nodeFs)(data)
|
||||
}
|
||||
|
||||
// The main thread of Node.js waits for all the active handles before exiting.
|
||||
// But Rust threads are never waited without `thread::join`.
|
||||
// So here we hack the code of Node.js to prevent the workers from being referenced (active).
|
||||
// According to https://github.com/nodejs/node/blob/19e0d472728c79d418b74bddff588bea70a403d0/lib/internal/worker.js#L415,
|
||||
// a worker is consist of two handles: kPublicPort and kHandle.
|
||||
{
|
||||
const kPublicPort = Object.getOwnPropertySymbols(worker).find(s =>
|
||||
s.toString().includes("kPublicPort")
|
||||
);
|
||||
if (kPublicPort) {
|
||||
worker[kPublicPort].ref = () => {};
|
||||
}
|
||||
|
||||
const kHandle = Object.getOwnPropertySymbols(worker).find(s =>
|
||||
s.toString().includes("kHandle")
|
||||
);
|
||||
if (kHandle) {
|
||||
worker[kHandle].ref = () => {};
|
||||
}
|
||||
|
||||
worker.unref();
|
||||
}
|
||||
return worker
|
||||
},
|
||||
overwriteImports(importObject) {
|
||||
importObject.env = {
|
||||
...importObject.env,
|
||||
...importObject.napi,
|
||||
...importObject.emnapi,
|
||||
memory: __sharedMemory,
|
||||
}
|
||||
return importObject
|
||||
},
|
||||
beforeInit({ instance }) {
|
||||
__napiInstance = instance
|
||||
for (const name of Object.keys(instance.exports)) {
|
||||
if (name.startsWith('__napi_register__')) {
|
||||
instance.exports[name]()
|
||||
}
|
||||
}
|
||||
},
|
||||
}))
|
||||
__publishWasiDispose(__napiModule.exports)
|
||||
__registerWasiExitListener()
|
||||
} catch (error) {
|
||||
const rollback = {
|
||||
active: false,
|
||||
error,
|
||||
promise: undefined,
|
||||
rollback: __rollbackWasiInitialization,
|
||||
}
|
||||
__wasiRollbackRegistry.set(__wasiRollbackRegistryKey, rollback)
|
||||
__runWasiInitializationRollback(rollback)
|
||||
throw rollback.error
|
||||
}
|
||||
module.exports = __napiModule.exports
|
||||
module.exports.Severity = __napiModule.exports.Severity
|
||||
module.exports.transform = __napiModule.exports.transform
|
||||
module.exports.transformSync = __napiModule.exports.transformSync
|
||||
@@ -0,0 +1,114 @@
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
export interface Comment {
|
||||
type: 'Line' | 'Block'
|
||||
value: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface ErrorLabel {
|
||||
message: string | null
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface OxcError {
|
||||
severity: Severity
|
||||
message: string
|
||||
labels: Array<ErrorLabel>
|
||||
helpMessage: string | null
|
||||
codeframe: string | null
|
||||
}
|
||||
|
||||
export declare const enum Severity {
|
||||
Error = 'Error',
|
||||
Warning = 'Warning',
|
||||
Advice = 'Advice'
|
||||
}
|
||||
export interface SourceMap {
|
||||
file?: string
|
||||
mappings: string
|
||||
names: Array<string>
|
||||
sourceRoot?: string
|
||||
sources: Array<string>
|
||||
sourcesContent?: Array<string>
|
||||
version: number
|
||||
x_google_ignoreList?: Array<number>
|
||||
}
|
||||
/**
|
||||
* Apply the Relay `graphql` tagged template transform asynchronously.
|
||||
*
|
||||
* This uses a worker-pool thread and can be slower than `transformSync` for a
|
||||
* single small module.
|
||||
*/
|
||||
export declare function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): Promise<TransformResult>
|
||||
|
||||
/**
|
||||
* Options for the Relay transform.
|
||||
*
|
||||
* `lang`, `sourceType`, and `sourcemap` configure the surrounding Oxc
|
||||
* parse/codegen pipeline; the remaining fields mirror the options of
|
||||
* `babel-plugin-relay` / `@swc/plugin-relay`.
|
||||
*/
|
||||
export interface TransformOptions {
|
||||
/** Treat the source as `js`, `jsx`, `ts`, `tsx`, or `dts`. */
|
||||
lang?: 'js' | 'jsx' | 'ts' | 'tsx' | 'dts'
|
||||
/** Treat the source as script, module, CommonJS, or infer it from syntax. */
|
||||
sourceType?: 'script' | 'module' | 'commonjs' | 'unambiguous'
|
||||
/**
|
||||
* Generate a source map.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
sourcemap?: boolean
|
||||
/**
|
||||
* Directory `relay-compiler` emits all artifacts to (its
|
||||
* `artifactDirectory` setting). When set, artifacts are imported via a
|
||||
* relative path from the file being transformed to this directory; the
|
||||
* path is computed lexically, so both must either be absolute or relative
|
||||
* to the same base directory. When unset, artifacts are imported from the
|
||||
* `__generated__` directory next to the file being transformed.
|
||||
*/
|
||||
artifactDirectory?: string
|
||||
/**
|
||||
* Artifact language, determining the imported file extension:
|
||||
* `Name.graphql.ts` for `typescript`, `Name.graphql.js` otherwise.
|
||||
*
|
||||
* @default 'javascript'
|
||||
*/
|
||||
language?: 'typescript' | 'javascript' | 'flow'
|
||||
/**
|
||||
* Emit a hoisted default import per `graphql` tag instead of an inline
|
||||
* `require()` call.
|
||||
*
|
||||
* Defaults to `true`, matching `babel-plugin-relay` since Relay v17.
|
||||
* `@swc/plugin-relay` and Next.js default to `false`.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
eagerEsModules?: boolean
|
||||
}
|
||||
|
||||
/** Result returned by the Relay transform. */
|
||||
export interface TransformResult {
|
||||
/**
|
||||
* Transformed code.
|
||||
*
|
||||
* This is empty when parsing, semantic analysis, option validation, or
|
||||
* the Relay transform reports an error.
|
||||
*/
|
||||
code: string
|
||||
/** Source map, populated when `sourcemap` is `true`. */
|
||||
map?: SourceMap
|
||||
/** Parse, semantic, option validation, and Relay transform diagnostics. */
|
||||
errors: Array<OxcError>
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the Relay `graphql` tagged template transform synchronously.
|
||||
*
|
||||
* Only `graphql` tags are rewritten; TypeScript and JSX syntax are preserved
|
||||
* untouched, so the output composes with any downstream toolchain.
|
||||
*/
|
||||
export declare function transformSync(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"target": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
instantiateNapiModuleSync,
|
||||
MessageHandler,
|
||||
WASI,
|
||||
emnapiAsyncWorkPlugin,
|
||||
emnapiTSFNPlugin,
|
||||
} from '@napi-rs/wasm-runtime'
|
||||
|
||||
const handler = new MessageHandler({
|
||||
onLoad({ wasmModule, wasmMemory }) {
|
||||
const wasi = new WASI({
|
||||
print: function () {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log.apply(console, arguments)
|
||||
},
|
||||
printErr: function() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error.apply(console, arguments)
|
||||
|
||||
},
|
||||
})
|
||||
return instantiateNapiModuleSync(wasmModule, {
|
||||
childThread: true,
|
||||
wasi,
|
||||
// The wasm links a "basic" emnapi archive (no C async-work /
|
||||
// threadsafe-function implementations), so every thread that
|
||||
// instantiates it must provide the JavaScript implementations
|
||||
// through the emnapi plugins.
|
||||
plugins: [emnapiAsyncWorkPlugin, emnapiTSFNPlugin],
|
||||
overwriteImports(importObject) {
|
||||
importObject.env = {
|
||||
...importObject.env,
|
||||
...importObject.napi,
|
||||
...importObject.emnapi,
|
||||
memory: wasmMemory,
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
globalThis.onmessage = function (e) {
|
||||
handler.handle(e)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { parse } from "node:path";
|
||||
import { WASI } from "node:wasi";
|
||||
import { parentPort, Worker } from "node:worker_threads";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const {
|
||||
instantiateNapiModuleSync,
|
||||
MessageHandler,
|
||||
getDefaultContext,
|
||||
emnapiAsyncWorkPlugin,
|
||||
emnapiTSFNPlugin,
|
||||
} = require("@napi-rs/wasm-runtime");
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.on("message", (data) => {
|
||||
globalThis.onmessage({ data });
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(globalThis, {
|
||||
self: globalThis,
|
||||
require,
|
||||
Worker,
|
||||
importScripts: function (f) {
|
||||
;(0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f);
|
||||
},
|
||||
postMessage: function (msg) {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(msg);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const emnapiContext = getDefaultContext();
|
||||
|
||||
const __rootDir = parse(process.cwd()).root;
|
||||
|
||||
const handler = new MessageHandler({
|
||||
onLoad({ wasmModule, wasmMemory }) {
|
||||
const wasi = new WASI({
|
||||
version: 'preview1',
|
||||
env: process.env,
|
||||
preopens: {
|
||||
[__rootDir]: __rootDir,
|
||||
},
|
||||
});
|
||||
|
||||
return instantiateNapiModuleSync(wasmModule, {
|
||||
childThread: true,
|
||||
wasi,
|
||||
context: emnapiContext,
|
||||
// The wasm links a "basic" emnapi archive (no C async-work /
|
||||
// threadsafe-function implementations), so every thread that
|
||||
// instantiates it must provide the JavaScript implementations
|
||||
// through the emnapi plugins.
|
||||
plugins: [emnapiAsyncWorkPlugin, emnapiTSFNPlugin],
|
||||
overwriteImports(importObject) {
|
||||
importObject.env = {
|
||||
...importObject.env,
|
||||
...importObject.napi,
|
||||
...importObject.emnapi,
|
||||
memory: wasmMemory
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
globalThis.onmessage = function (e) {
|
||||
handler.handle(e);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
const fs = require("node:fs");
|
||||
const childProcess = require("node:child_process");
|
||||
|
||||
const pkg = JSON.parse(
|
||||
fs.readFileSync(require.resolve("oxc-transform-relay/package.json"), "utf-8"),
|
||||
);
|
||||
const { version } = pkg;
|
||||
const baseDir = `/tmp/oxc-transform-relay-${version}`;
|
||||
const bindingEntry = `${baseDir}/node_modules/@oxc-transform-relay/binding-wasm32-wasi/transform-relay.wasi.cjs`;
|
||||
|
||||
if (!fs.existsSync(bindingEntry)) {
|
||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(baseDir, { recursive: true });
|
||||
const bindingPkg = `@oxc-transform-relay/binding-wasm32-wasi@${version}`;
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log(`[oxc-transform-relay] Downloading ${bindingPkg} on WebContainer...`);
|
||||
childProcess.execFileSync("pnpm", ["i", bindingPkg], {
|
||||
cwd: baseDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = require(bindingEntry);
|
||||
@@ -9,6 +9,7 @@ versioned_files = [
|
||||
"napi/parser/package.json",
|
||||
"napi/transform/package.json",
|
||||
"napi/transform-react/package.json",
|
||||
"napi/transform-relay/package.json",
|
||||
"napi/minify/package.json",
|
||||
"npm/oxc-types/package.json",
|
||||
"npm/runtime/package.json",
|
||||
|
||||
+9
-9
@@ -31,15 +31,15 @@
|
||||
"tasks/codegen_conformance/index.js",
|
||||
"tasks/codegen_conformance/index.d.ts",
|
||||
"napi/playground/playground.wasi-browser.js",
|
||||
"napi/{transform,transform-react,minify,playground}/index.js",
|
||||
"napi/{transform,transform-react,minify,playground}/index.d.ts",
|
||||
"napi/{parser,transform,transform-react,minify,playground}/**/index.d.ts",
|
||||
"napi/{parser,transform,transform-react,minify,playground}/**/*.wasi.d.cts",
|
||||
"napi/{parser,transform,transform-react,minify,playground}/**/*.wasi-browser.js",
|
||||
"napi/{parser,transform,transform-react,minify,playground}/**/*.wasi.cjs",
|
||||
"napi/{parser,transform,transform-react,minify,playground}/**/wasi-worker-browser.mjs",
|
||||
"napi/{parser,transform,transform-react,minify,playground}/**/wasi-worker.mjs",
|
||||
"napi/{parser,transform,transform-react,minify,playground}/**/browser.js",
|
||||
"napi/{transform,transform-react,transform-relay,minify,playground}/index.js",
|
||||
"napi/{transform,transform-react,transform-relay,minify,playground}/index.d.ts",
|
||||
"napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/index.d.ts",
|
||||
"napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/*.wasi.d.cts",
|
||||
"napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/*.wasi-browser.js",
|
||||
"napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/*.wasi.cjs",
|
||||
"napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/wasi-worker-browser.mjs",
|
||||
"napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/wasi-worker.mjs",
|
||||
"napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/browser.js",
|
||||
"npm/runtime/src",
|
||||
"npm/oxc-types/types.d.ts",
|
||||
"npm/oxfmt/configuration_schema.json",
|
||||
|
||||
Generated
+21
@@ -373,6 +373,27 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.10(@types/node@24.1.0)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(happy-dom@20.0.11)(vite@7.3.0(@types/node@24.1.0)(lightningcss@1.33.0)(terser@5.44.1)(tsx@4.23.9))
|
||||
|
||||
napi/transform-relay:
|
||||
devDependencies:
|
||||
'@emnapi/core':
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0-alpha.3
|
||||
'@emnapi/runtime':
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0-alpha.3
|
||||
'@napi-rs/cli':
|
||||
specifier: 'catalog:'
|
||||
version: 3.8.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 24.1.0
|
||||
publint:
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.23
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.10(@types/node@24.1.0)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(happy-dom@20.0.11)(vite@7.3.0(@types/node@24.1.0)(lightningcss@1.33.0)(terser@5.44.1)(tsx@4.23.9))
|
||||
|
||||
npm/oxc-types: {}
|
||||
|
||||
npm/oxfmt:
|
||||
|
||||
Reference in New Issue
Block a user