diff --git a/.github/workflows/release_napi_resolver.yml b/.github/workflows/release_napi_resolver.yml new file mode 100644 index 000000000..a4ed194ec --- /dev/null +++ b/.github/workflows/release_napi_resolver.yml @@ -0,0 +1,180 @@ +name: Release NAPI Resolver + +on: + push: + branches: + - main + paths: + - npm/oxc-resolver/package.json # Please only commit this file, so we don't need to wait for test CI to pass. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Check version + runs-on: ubuntu-latest + outputs: + version: ${{ env.version }} + version_changed: ${{ steps.version.outputs.changed }} + steps: + - uses: actions/checkout@v4 + + - name: Check version changes + uses: EndBug/version-check@v2 + id: version + with: + static-checking: localIsNew + file-url: https://unpkg.com/oxc-resolver@latest/package.json + file-name: npm/oxc-resolver/package.json + + - name: Set version name + if: steps.version.outputs.changed == 'true' + run: | + echo "Version change found! New version: ${{ steps.version.outputs.version }} (${{ steps.version.outputs.version_type }})" + echo "version=${{ steps.version.outputs.version }}" >> $GITHUB_ENV + + build: + needs: check + if: needs.check.outputs.version_changed == 'true' + env: + version: ${{ needs.check.outputs.version }} + outputs: + version: ${{ env.version }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + code-target: win32-x64-msvc + + - os: windows-latest + target: aarch64-pc-windows-msvc + code-target: win32-arm64-msvc + + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + code-target: linux-x64-gnu + + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + code-target: linux-arm64-gnu + + - os: macos-latest + target: x86_64-apple-darwin + code-target: darwin-x64 + + - os: macos-latest + target: aarch64-apple-darwin + code-target: darwin-arm64 + + name: Package ${{ matrix.target }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Install cross + uses: taiki-e/install-action@cross + + - name: Add Rust Target + run: rustup target add ${{ matrix.target }} + + - name: Build with cross + run: cross build -p oxc_napi_resolver --release --target=${{ matrix.target }} + + - name: Move file on ${{ matrix.os }} + shell: bash + run: | + shopt -s extglob + ls target/${{ matrix.target }}/release/*.@(so|dll|dylib) + mv target/${{ matrix.target }}/release/*.@(so|dll|dylib) napi/resolver/resolver.${{ matrix.code-target }}.node + ls napi/resolver + + - name: Test + working-directory: napi/resolver + if: ${{ contains(matrix.target, 'x86') }} # Need docker for aarch64 + run: | + ls + node test.mjs + + # The binary is zipped to fix permission loss https://github.com/actions/upload-artifact#permission-loss + - name: Archive Binary + if: runner.os == 'Windows' + shell: bash + run: 7z a ${{ matrix.code-target }}.zip napi/resolver/resolver.${{ matrix.code-target }}.node + + # The binary is zipped to fix permission loss https://github.com/actions/upload-artifact#permission-loss + - name: Archive Binary + if: runner.os != 'Windows' + shell: bash + run: tar czf ${{ matrix.code-target }}.tar.gz napi/resolver/resolver.${{ matrix.code-target }}.node + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + if-no-files-found: error + name: binaries + path: | + *.zip + *.tar.gz + + publish: + name: Publish NAPI + runs-on: ubuntu-latest + permissions: + contents: write # for softprops/action-gh-release@v1 + id-token: write # for `npm publish --provenance` + needs: + - build + steps: + - uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + registry-url: 'https://registry.npmjs.org' + + - name: Download Artifacts + uses: actions/download-artifact@v3 + with: + name: binaries + + - name: Unzip + uses: montudor/action-zip@v1 + with: + args: unzip -qq *.zip -d . + + - name: Untar + shell: bash + run: ls *.gz | xargs -i tar xvf {} + + - name: Generate npm packages + shell: bash + run: | + ls + ls napi/resolver + node npm/oxc-resolver/scripts/generate-packages.mjs + cat npm/oxc-resolver/package.json + for package in npm/oxc-resolver* + do + ls $package + cat $package/package.json + echo '----' + done + + - name: Publish npm packages as latest + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + shell: bash + # NOTE: The trailing slash on $package/ changes it to publishing the directory + run: | + # publish subpackages first + for package in npm/oxc-resolver-* + do + npm publish $package/ --tag latest --provenance --access public + done + # publish root package last + npm publish npm/oxc-resolver/ --tag latest --provenance --access public diff --git a/.taplo.toml b/.taplo.toml index ae85dae49..7d326d040 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -1,4 +1,4 @@ -include = ["Cargo.toml", "crates/*/*.toml", "tasks/*/*.toml", "editor/*/*.toml"] +include = ["Cargo.toml", "crates/*/*.toml", "tasks/*/*.toml", "editor/*/*.toml", "napi/*/*.toml"] [formatting] align_entries = true diff --git a/Cargo.lock b/Cargo.lock index 92409927a..3c981097e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1740,6 +1740,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "oxc_napi_resolver" +version = "0.0.0" +dependencies = [ + "napi", + "napi-build", + "napi-derive", + "oxc_resolver", +] + [[package]] name = "oxc_parser" version = "0.3.0" diff --git a/napi/parser/README.md b/napi/parser/README.md index 4c2ac0a5d..07c9e263b 100644 --- a/napi/parser/README.md +++ b/napi/parser/README.md @@ -14,5 +14,5 @@ pnpm run build # Test ```bash -node test.mjs +pnpm test ``` diff --git a/napi/parser/package.json b/napi/parser/package.json index ea56e2d96..a86434b41 100644 --- a/napi/parser/package.json +++ b/napi/parser/package.json @@ -2,7 +2,8 @@ "name": "@oxc-parser/binding", "private": true, "scripts": { - "build": "napi build --platform --release" + "build": "napi build --platform --release", + "test": "node test.mjs" }, "devDependencies": { "@napi-rs/cli": "^2.15.2" diff --git a/napi/resolver/.gitignore b/napi/resolver/.gitignore new file mode 100644 index 000000000..391dfe772 --- /dev/null +++ b/napi/resolver/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +*.node diff --git a/napi/resolver/Cargo.toml b/napi/resolver/Cargo.toml new file mode 100644 index 000000000..a3f248c25 --- /dev/null +++ b/napi/resolver/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "oxc_napi_resolver" +version = "0.0.0" +publish = false +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +categories.workspace = true + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +oxc_resolver = { workspace = true } +napi = { version = "2", features = ["serde-json", "async"] } +napi-derive = { version = "2" } + +[build-dependencies] +napi-build = "2" + +[package.metadata.cargo-machete] +ignored = ["napi"] diff --git a/napi/resolver/README.md b/napi/resolver/README.md new file mode 100644 index 000000000..07c9e263b --- /dev/null +++ b/napi/resolver/README.md @@ -0,0 +1,18 @@ +# Installation + +```bash +corepack enable +``` + +# Build + +```bash +pnpm install +pnpm run build +``` + +# Test + +```bash +pnpm test +``` diff --git a/napi/resolver/build.rs b/napi/resolver/build.rs new file mode 100644 index 000000000..0f1b01002 --- /dev/null +++ b/napi/resolver/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/napi/resolver/index.d.ts b/napi/resolver/index.d.ts new file mode 100644 index 000000000..320e26ac3 --- /dev/null +++ b/napi/resolver/index.d.ts @@ -0,0 +1,14 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +export interface ResolveResult { + path?: string + error?: string +} +export function sync(path: string, request: string): ResolveResult +export class ResolverFactory { + constructor() + sync(path: string, request: string): ResolveResult +} diff --git a/napi/resolver/index.js b/napi/resolver/index.js new file mode 100644 index 000000000..90128b739 --- /dev/null +++ b/napi/resolver/index.js @@ -0,0 +1,258 @@ +/* tslint:disable */ +/* eslint-disable */ +/* prettier-ignore */ + +/* auto-generated by NAPI-RS */ + +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +const { platform, arch } = process + +let nativeBinding = null +let localFileExisted = false +let loadError = null + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim(); + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime + } +} + +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'resolver.android-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.android-arm64.node') + } else { + nativeBinding = require('@oxc-resolver/binding-android-arm64') + } + } catch (e) { + loadError = e + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'resolver.android-arm-eabi.node')) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.android-arm-eabi.node') + } else { + nativeBinding = require('@oxc-resolver/binding-android-arm-eabi') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'resolver.win32-x64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.win32-x64-msvc.node') + } else { + nativeBinding = require('@oxc-resolver/binding-win32-x64-msvc') + } + } catch (e) { + loadError = e + } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'resolver.win32-ia32-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.win32-ia32-msvc.node') + } else { + nativeBinding = require('@oxc-resolver/binding-win32-ia32-msvc') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'resolver.win32-arm64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.win32-arm64-msvc.node') + } else { + nativeBinding = require('@oxc-resolver/binding-win32-arm64-msvc') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'resolver.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.darwin-universal.node') + } else { + nativeBinding = require('@oxc-resolver/binding-darwin-universal') + } + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'resolver.darwin-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.darwin-x64.node') + } else { + nativeBinding = require('@oxc-resolver/binding-darwin-x64') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'resolver.darwin-arm64.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.darwin-arm64.node') + } else { + nativeBinding = require('@oxc-resolver/binding-darwin-arm64') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'resolver.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.freebsd-x64.node') + } else { + nativeBinding = require('@oxc-resolver/binding-freebsd-x64') + } + } catch (e) { + loadError = e + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'resolver.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.linux-x64-musl.node') + } else { + nativeBinding = require('@oxc-resolver/binding-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'resolver.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.linux-x64-gnu.node') + } else { + nativeBinding = require('@oxc-resolver/binding-linux-x64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'resolver.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.linux-arm64-musl.node') + } else { + nativeBinding = require('@oxc-resolver/binding-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'resolver.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.linux-arm64-gnu.node') + } else { + nativeBinding = require('@oxc-resolver/binding-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm': + localFileExisted = existsSync( + join(__dirname, 'resolver.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./resolver.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('@oxc-resolver/binding-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} + +if (!nativeBinding) { + if (loadError) { + throw loadError + } + throw new Error(`Failed to load native binding`) +} + +const { ResolverFactory, sync } = nativeBinding + +module.exports.ResolverFactory = ResolverFactory +module.exports.sync = sync diff --git a/napi/resolver/package.json b/napi/resolver/package.json new file mode 100644 index 000000000..0d32bd80e --- /dev/null +++ b/napi/resolver/package.json @@ -0,0 +1,29 @@ +{ + "name": "@oxc-resolver/binding", + "private": true, + "scripts": { + "build": "napi build --platform --release", + "test": "node test.mjs" + }, + "devDependencies": { + "@napi-rs/cli": "^2.15.2" + }, + "engines": { + "node": ">=14.*" + }, + "packageManager": "pnpm@8.2.0", + "napi": { + "name": "resolver", + "triples": { + "defaults": false, + "additional": [ + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "aarch64-apple-darwin" + ] + } + } +} diff --git a/napi/resolver/pnpm-lock.yaml b/napi/resolver/pnpm-lock.yaml new file mode 100644 index 000000000..9ccfb6c27 --- /dev/null +++ b/napi/resolver/pnpm-lock.yaml @@ -0,0 +1,14 @@ +lockfileVersion: '6.0' + +devDependencies: + '@napi-rs/cli': + specifier: ^2.15.2 + version: 2.15.2 + +packages: + + /@napi-rs/cli@2.15.2: + resolution: {integrity: sha512-80tBCtCnEhAmFtB9oPM0FL74uW7fAmtpeqjvERH7Q1z/aZzCAs/iNfE7U3ehpwg9Q07Ob2Eh/+1guyCdX/p24w==} + engines: {node: '>= 10'} + hasBin: true + dev: true diff --git a/napi/resolver/src/lib.rs b/napi/resolver/src/lib.rs new file mode 100644 index 000000000..3057997d9 --- /dev/null +++ b/napi/resolver/src/lib.rs @@ -0,0 +1,49 @@ +use std::path::{Path, PathBuf}; + +use napi_derive::napi; + +use oxc_resolver::{ResolveOptions, Resolver}; + +#[napi(object)] +pub struct ResolveResult { + pub path: Option, + pub error: Option, +} + +#[napi] +pub struct ResolverFactory { + resolver: Resolver, +} + +#[napi] +impl ResolverFactory { + #[napi(constructor)] + pub fn new() -> Self { + Self { resolver: Resolver::new(ResolveOptions::default()) } + } + + #[allow(clippy::needless_pass_by_value)] + #[napi] + pub fn sync(&self, path: String, request: String) -> ResolveResult { + let path = PathBuf::from(path); + resolve(&self.resolver, &path, &request) + } +} + +fn resolve(resolver: &Resolver, path: &Path, request: &str) -> ResolveResult { + match resolver.resolve(path, request) { + Ok(resolution) => ResolveResult { + path: Some(resolution.full_path().to_string_lossy().to_string()), + error: None, + }, + Err(err) => ResolveResult { path: None, error: Some(err.to_string()) }, + } +} + +#[allow(clippy::needless_pass_by_value)] +#[napi] +pub fn sync(path: String, request: String) -> ResolveResult { + let path = PathBuf::from(path); + let resolver = Resolver::new(ResolveOptions::default()); + resolve(&resolver, &path, &request) +} diff --git a/napi/resolver/test.mjs b/napi/resolver/test.mjs new file mode 100644 index 000000000..924d64c18 --- /dev/null +++ b/napi/resolver/test.mjs @@ -0,0 +1,14 @@ +import path from 'path'; +import resolve, { ResolverFactory } from './index.js'; +import assert from 'assert'; + +console.log(`Testing on ${process.platform}-${process.arch}`) + +const cwd = process.cwd(); + +// `resolve` +assert(resolve.sync(cwd, "./index.js").path, path.join(cwd, 'index.js')); + +// `ResolverFactory` +const resolver = new ResolverFactory(); +assert(resolver.sync(cwd, "./index.js").path, path.join(cwd, 'index.js')); diff --git a/npm/oxc-resolver/README.md b/npm/oxc-resolver/README.md new file mode 100644 index 000000000..a0297efa3 --- /dev/null +++ b/npm/oxc-resolver/README.md @@ -0,0 +1,18 @@ +# The JavaScript Oxidation Compiler + +See index.d.ts for `resolveSync` and `ResolverFactory` API. + +## ESM + +```javascript +import path from 'path'; +import resolve, { ResolverFactory } from './index.js'; +import assert from 'assert'; + +// `resolve` +assert(resolve.sync(process.cwd(), "./index.js").path, path.join(cwd, 'index.js')); + +// `ResolverFactory` +const resolver = new ResolverFactory(); +assert(resolver.sync(process.cwd(), "./index.js").path, path.join(cwd, 'index.js')); +``` diff --git a/npm/oxc-resolver/package.json b/npm/oxc-resolver/package.json new file mode 100644 index 000000000..13aafc7ea --- /dev/null +++ b/npm/oxc-resolver/package.json @@ -0,0 +1,19 @@ +{ + "name": "oxc-resolver", + "version": "0.0.1", + "description": "Oxc Resolver Node API", + "main": "index.js", + "files": [ + "index.d.ts", + "index.js" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/oxc-project/oxc.git", + "directory": "npm/oxc-resolver" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } +} diff --git a/npm/oxc-resolver/scripts/generate-packages.mjs b/npm/oxc-resolver/scripts/generate-packages.mjs new file mode 100644 index 000000000..c66e36b8a --- /dev/null +++ b/npm/oxc-resolver/scripts/generate-packages.mjs @@ -0,0 +1,110 @@ +// Code copied from [Rome](https://github.com/rome/tools/blob/main/npm/rome/scripts/generate-packages.mjs) + +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import * as fs from "node:fs"; + +const OXC_ROOT = resolve(fileURLToPath(import.meta.url), "../.."); +const PACKAGES_ROOT = resolve(OXC_ROOT, ".."); +const BINARY_ROOT = resolve(OXC_ROOT, "../../napi/resolver"); +const MANIFEST_PATH = resolve(OXC_ROOT, "package.json"); + +console.log('OXC_ROOT', OXC_ROOT); +console.log('PACKAGES_ROOT', PACKAGES_ROOT); +console.log('BINARY_ROOT', BINARY_ROOT); +console.log('MANIFEST_PATH', MANIFEST_PATH); + +const rootManifest = JSON.parse( + fs.readFileSync(MANIFEST_PATH).toString("utf-8") +); + +function package_name(target) { + return `@oxc-resolver/binding-${target}` +} + +function generateNativePackage(target) { + const binaryName = `resolver.${target}.node`; + + const packageRoot = resolve(PACKAGES_ROOT, `oxc-resolver-${target}`); + const binarySource = resolve(BINARY_ROOT, binaryName); + const binaryTarget = resolve(packageRoot, binaryName); + + // Remove the directory just in case it already exists (it's autogenerated + // so there shouldn't be anything important there anyway) + fs.rmSync(packageRoot, { recursive: true, force: true }); + + // Create the package directory + console.log(`Create directory ${packageRoot}`); + fs.mkdirSync(packageRoot); + + // Generate the package.json manifest + const { version, license, repository } = rootManifest; + + const [os, cpu, third] = target.split("-"); + const manifest = { + name: package_name(target), + version, + main: binaryName, + files: [binaryName], + os: [os], + cpu: [cpu], + license, + repository + }; + + if (cpu == "linux" && third == "gnu") { + manifest.libc = ["glibc"]; + } + + const manifestPath = resolve(packageRoot, "package.json"); + console.log(`Create manifest ${manifestPath}`); + fs.writeFileSync(manifestPath, JSON.stringify(manifest)); + + console.log(`Copy binary ${binaryTarget}`); + fs.copyFileSync(binarySource, binaryTarget); +} + +function writeManifest() { + const packageRoot = resolve(PACKAGES_ROOT, 'oxc-resolver'); + const manifestPath = resolve(packageRoot, "package.json"); + + console.log('packageRoot', packageRoot); + + const manifestData = JSON.parse( + fs.readFileSync(manifestPath).toString("utf-8") + ); + + const nativePackages = TARGETS.map((target) => [ + package_name(target), + rootManifest.version, + ]); + + manifestData["version"] = rootManifest.version; + manifestData["optionalDependencies"] = Object.fromEntries(nativePackages); + + console.log('manifestPath', manifestPath); + console.log('manifestData', manifestData); + + const content = JSON.stringify(manifestData); + fs.writeFileSync(manifestPath, content); + + let files = ["index.js", "index.d.ts"]; + for (const file of files) { + fs.copyFileSync(resolve(BINARY_ROOT, file), resolve(packageRoot, file)); + } +} + +const TARGETS = [ + "win32-x64-msvc", + "win32-arm64-msvc", + "linux-x64-gnu", + "linux-arm64-gnu", + "darwin-x64", + "darwin-arm64", +]; + +for (const target of TARGETS) { + generateNativePackage(target); +} + +writeManifest();