Skip to content

Commit 494c8e3

Browse files
committed
feat: add runtime fallback and webcontainer support
1 parent c610659 commit 494c8e3

File tree

8 files changed

+189
-6
lines changed

8 files changed

+189
-6
lines changed

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ This will check and prepare the napi binding packages for you automatically.
7676
#### Types
7777

7878
```ts
79+
// napi-postinstall
7980
export interface PackageJson {
8081
name: string
8182
version: string
@@ -85,16 +86,30 @@ export declare function checkAndPreparePackage(
8586
packageNameOrPackageJson: PackageJson | string,
8687
checkVersion?: boolean,
8788
): Promise<void>
89+
90+
// napi-postinstall/fallback
91+
declare function fallback<T = unknown>(
92+
packageJsonPath: string,
93+
checkVersion?: boolean,
94+
): T
95+
export = fallback
8896
```
8997

9098
#### Example
9199

92100
```js
93-
import { checkAndPreparePackage, isNpm } from 'napi-postinstall'
101+
// index.js
102+
const { checkAndPreparePackage, isNpm } = require('napi-postinstall')
94103

95104
if (isNpm()) {
96-
checkAndPreparePackage('unrs-resolver' /* <napi-package-name> */)
105+
void checkAndPreparePackage('unrs-resolver' /* <napi-package-name> */)
97106
}
107+
108+
// fallback.js
109+
module.exports = require('napi-postinstall/fallback')(
110+
require.resolve('../package.json') /* <napi-package-json-path> */,
111+
true /* <check-version> */,
112+
)
98113
```
99114

100115
## Sponsors and Backers

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
"types": "./lib/index.d.ts",
2020
"default": "./lib/index.js"
2121
},
22+
"./fallback": {
23+
"types": "./lib/fallback.d.ts",
24+
"default": "./lib/fallback.js"
25+
},
2226
"./package.json": "./package.json"
2327
},
2428
"files": [

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as path from 'node:path'
22

3-
import { PackageJson } from './types.js'
3+
import type { PackageJson } from './types.js'
44

55
export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/'
66

src/fallback.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { execFileSync } from 'node:child_process'
2+
import * as fs from 'node:fs'
3+
import * as os from 'node:os'
4+
import * as path from 'node:path'
5+
6+
import { WASM32_WASI } from './constants.js'
7+
import { errorMessage, getNapiInfoFromPackageJson } from './helpers.js'
8+
import type { PackageJson } from './types.js'
9+
10+
const EXECUTORS = {
11+
npm: 'npx',
12+
pnpm: 'pnpm',
13+
yarn: 'yarn',
14+
bun: 'bun',
15+
deno: (args: string[]) => ['deno', 'run', `npm:${args[0]}`, ...args.slice(1)],
16+
}
17+
18+
function constructCommand(
19+
value: string[] | string | ((args: string[]) => string[]),
20+
args: string[],
21+
) {
22+
const list =
23+
typeof value === 'function'
24+
? value(args)
25+
: // eslint-disable-next-line unicorn-x/prefer-spread
26+
([] as string[]).concat(value, args)
27+
return {
28+
command: list[0],
29+
args: list.slice(1),
30+
}
31+
}
32+
33+
/**
34+
* Fallback for webcontainer and docker environments like
35+
* @see https://github.com/un-ts/eslint-plugin-import-x/issues/337.
36+
*
37+
* @param packageJsonPath The absolute path to the package.json file.
38+
* @param checkVersion Wether to check version matching
39+
*/
40+
function fallback<T = unknown>(
41+
packageJsonPath: string,
42+
checkVersion?: boolean,
43+
) {
44+
const packageJson = require(packageJsonPath) as PackageJson
45+
46+
const { name, version: pkgVersion, optionalDependencies } = packageJson
47+
48+
const { napi, version = pkgVersion } = getNapiInfoFromPackageJson(
49+
packageJson,
50+
checkVersion,
51+
)
52+
53+
if (checkVersion && pkgVersion !== version) {
54+
throw new Error(
55+
errorMessage(
56+
`Inconsistent package versions found for \`${name}\` v${pkgVersion} vs \`${napi.packageName}\` v${version}.`,
57+
),
58+
)
59+
}
60+
61+
if (process.versions.webcontainer) {
62+
const bindingPkgName = `${napi.packageName}-${WASM32_WASI}`
63+
64+
if (!optionalDependencies?.[bindingPkgName]) {
65+
throw new Error(
66+
errorMessage(
67+
`\`${WASM32_WASI}\` target is not unavailable for \`${name}\` v${version}`,
68+
),
69+
)
70+
}
71+
72+
const baseDir = path.resolve(os.tmpdir(), `${name}-${version}`)
73+
74+
const bindingEntry = path.resolve(
75+
baseDir,
76+
`node_modules/${bindingPkgName}/${napi.binaryName}.wasi.cjs`,
77+
)
78+
79+
if (!fs.existsSync(bindingEntry)) {
80+
fs.rmSync(baseDir, { recursive: true, force: true })
81+
fs.mkdirSync(baseDir, { recursive: true })
82+
83+
const bindingPkg = `${bindingPkgName}@${version}`
84+
85+
console.log(
86+
errorMessage(`Downloading \`${bindingPkg}\` on WebContainer...`),
87+
)
88+
89+
execFileSync('pnpm', ['i', bindingPkg], {
90+
cwd: baseDir,
91+
stdio: 'inherit',
92+
})
93+
}
94+
95+
return require(bindingEntry) as T
96+
}
97+
98+
const userAgent = ((process.env.npm_config_user_agent || '').split('/')[0] ||
99+
'npm') as keyof typeof EXECUTORS
100+
101+
const executor = EXECUTORS[userAgent]
102+
103+
if (!executor) {
104+
throw new Error(
105+
errorMessage(
106+
`Unsupported package manager: ${userAgent}. Supported managers are: ${Object.keys(
107+
EXECUTORS,
108+
).join(', ')}.`,
109+
),
110+
)
111+
}
112+
113+
const { command, args } = constructCommand(executor, [
114+
'napi-postinstall',
115+
name,
116+
version,
117+
checkVersion ? '1' : '0',
118+
])
119+
120+
const pkgDir = path.dirname(packageJsonPath)
121+
122+
execFileSync(command, args, {
123+
cwd: pkgDir,
124+
stdio: 'inherit',
125+
})
126+
127+
// eslint-disable-next-line unicorn-x/prefer-string-replace-all
128+
process.env[`SKIP_${name.replace(/-/g, '_').toUpperCase()}_FALLBACK`] = '1'
129+
130+
const PKG_RESOLVED_PATH = require.resolve(pkgDir)
131+
132+
delete require.cache[PKG_RESOLVED_PATH]
133+
134+
return require(pkgDir) as T
135+
}
136+
137+
export = fallback

src/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as path from 'node:path'
44

55
import { DEFAULT_NPM_REGISTRY, LOG_PREFIX } from './constants.js'
66
import { parseTriple } from './target.js'
7-
import { NapiInfo, PackageJson } from './types.js'
7+
import type { NapiInfo, PackageJson } from './types.js'
88

99
export function getGlobalNpmRegistry() {
1010
try {

src/target.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// based on https://github.com/napi-rs/napi-rs/blob/2eb2ab619f9fb924453e21d2198fe67ea21b9680/cli/src/utils/target.ts
22

33
import { EABI, WASI, WASM32, WASM32_WASI } from './constants.js'
4-
import { NodeJSArch, Platform, Target } from './types.js'
4+
import type { NodeJSArch, Platform, Target } from './types.js'
55

66
const CpuToNodeArch: Record<string, NodeJSArch> = {
77
x86_64: 'x64',

test/fallback.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import unrsResolver = require('unrs-resolver')
2+
3+
import fallback = require('napi-postinstall/fallback')
4+
5+
describe('fallback', () => {
6+
afterEach(() => {
7+
delete process.env.SKIP_UNRS_RESOLVER_FALLBACK
8+
delete process.versions.webcontainer
9+
})
10+
11+
it('should resolve napi package successfully and set skip env after processing', () => {
12+
expect(fallback(require.resolve('unrs-resolver/package.json'))).toBe(
13+
unrsResolver,
14+
)
15+
expect(process.env.SKIP_UNRS_RESOLVER_FALLBACK).toBe('1')
16+
})
17+
18+
it('should support webcontainer', () => {
19+
process.versions.webcontainer = '1'
20+
const resolved = fallback<typeof unrsResolver>(
21+
require.resolve('unrs-resolver/package.json'),
22+
)
23+
expect(resolved).not.toBe(unrsResolver)
24+
expect(typeof resolved.sync).toBe('function')
25+
})
26+
})

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"esModuleInterop": false,
66
"rootDir": ".",
77
"paths": {
8-
"napi-postinstall": ["./src/index.ts"]
8+
"napi-postinstall": ["./src/index.ts"],
9+
"napi-postinstall/fallback": ["./src/fallback.ts"]
910
},
1011
"target": "ES2020"
1112
}

0 commit comments

Comments
 (0)