Skip to content

Commit 95c387e

Browse files
authored
Merge pull request #11 from vite-plugin/v2.2.0
V2.2.0
2 parents 72f2555 + f593c71 commit 95c387e

8 files changed

Lines changed: 182 additions & 77 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.2.0 (2023-06-27)
2+
3+
- 64b881a feat: add `options.forceCopyIfUnbuilt`
4+
- 7c60957 feat: add `options.ignore`
5+
16
## 2.1.0 (2023-06-25)
27

38
- 867f085 test: v2.1.0

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vite-plugin-native",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"description": "Supports Node/Electron C/C++ native addons",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -20,12 +20,14 @@
2020
"build": "vite build",
2121
"types": "tsc --emitDeclarationOnly",
2222
"prepublishOnly": "npm run build && npm run test",
23+
"build:test": "vite build -c test/fixtures/vite.config.ts",
2324
"test": "vitest run"
2425
},
2526
"dependencies": {
2627
"@vercel/webpack-asset-relocator-loader": "1.7.3",
28+
"dependencies-tree": "~0.2.0",
2729
"fast-glob": "^3.3.2",
28-
"lib-esm": "^0.4.2",
30+
"lib-esm": "~0.4.2",
2931
"node-loader": "^2.0.0",
3032
"webpack": "^5.70.0"
3133
},

src/index.ts

Lines changed: 100 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import {
99
import type { Configuration } from 'webpack'
1010
import type { NodeLoaderOptions, WebpackAssetRelocatorLoader } from './types'
1111
import { COLOURS } from 'vite-plugin-utils/function'
12+
import { flatDependencies } from 'dependencies-tree'
1213
import {
13-
type NativeRecord,
14-
type NativeRecordType,
14+
type ResolvedNativeRecord,
1515
createCjs,
16+
copy,
1617
ensureDir,
1718
getInteropSnippet,
1819
getDependenciesNatives,
@@ -22,8 +23,15 @@ import {
2223
export interface NativeOptions {
2324
/** @default 'node_natives' */
2425
assetsDir?: string
25-
/** By default native modules are automatically detected if this option is not explicitly configure by the user. */
26+
/**
27+
* By default native modules are automatically detected if this option is not explicitly configure by the user.
28+
* @deprecated use `ignore` option instead
29+
*/
2630
natives?: string[] | ((natives: string[]) => string[])
31+
/** Ignore the specified native module. */
32+
ignore?: (name: string) => boolean | undefined
33+
/** Force copy *.node files to dist/node_modules path if Webpack can't bundle native modules correctly. */
34+
forceCopyIfUnbuilt?: true
2735
/** Enable and configure webpack. */
2836
webpack?: {
2937
config?: (config: Configuration) => Configuration | undefined | Promise<Configuration | undefined>
@@ -35,18 +43,13 @@ export interface NativeOptions {
3543
const cjs = createCjs(import.meta.url)
3644
const TAG = '[vite-plugin-native]'
3745
const loader1 = '@vercel/webpack-asset-relocator-loader'
46+
const outputAssetBase = 'native_modules'
3847
const NativeExt = '.native.cjs'
3948
const InteropExt = '.interop.mjs'
4049
// https://github.com/vitejs/vite/blob/v5.3.1/packages/vite/src/node/plugins/index.ts#L55
4150
const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/
4251
// `nativesMap` is placed in the global scope and can be effective for multiple builds.
43-
const nativesMap = new Map<string, {
44-
status: 'built' | 'resolved'
45-
nativeFilename: string
46-
interopFilename: string
47-
type: NativeRecordType
48-
nodeFiles: string[]
49-
}>
52+
const nativesMap = new Map<string, ResolvedNativeRecord>
5053

5154
export default function native(options: NativeOptions): Plugin {
5255
const assetsDir = options.assetsDir ??= 'node_natives'
@@ -61,21 +64,16 @@ export default function native(options: NativeOptions): Plugin {
6164
const outDir = config.build?.outDir ?? 'dist'
6265
output = normalizePath(path.join(resolvedRoot, outDir, assetsDir))
6366

64-
const depsNativeRecord = await getDependenciesNatives(resolvedRoot)
65-
const depsNatives = [...depsNativeRecord.keys()]
67+
let nativeRecord = await getDependenciesNatives(resolvedRoot)
6668

6769
if (options.natives) {
6870
const natives = Array.isArray(options.natives)
6971
? options.natives
70-
: options.natives(depsNatives)
72+
: options.natives([...nativeRecord.keys()])
7173
// TODO: bundle modules based on `natives`.
7274
}
7375

7476
const withDistAssetBase = (p: string) => (assetsDir && p) ? `${assetsDir}/${p}` : p
75-
76-
let detectedNativeRecord: NativeRecord = new Map
77-
let detectedNatives: string[] = []
78-
7977
const alias: Alias = {
8078
find: /(.*)/,
8179
// Keep `customResolver` receive original source.
@@ -85,67 +83,74 @@ export default function native(options: NativeOptions): Plugin {
8583
if (!importer) return
8684
if (!bareImportRE.test(source)) return
8785

88-
if (!depsNativeRecord.has(source)) {
89-
// Auto detection.
86+
if (!nativeRecord.has(source)) {
87+
// Dynamic deep detection.
9088
// e.g. serialport -> @serialport/bindings-cpp
91-
const nativeRecord = await resolveNativeRecord(source, importer)
92-
if (nativeRecord) {
93-
detectedNativeRecord = new Map([...detectedNativeRecord, ...nativeRecord])
94-
detectedNatives = [...depsNativeRecord.keys()]
95-
}
89+
nativeRecord = new Map([...nativeRecord, ...(await resolveNativeRecord(source, importer) ?? [])])
9690
}
9791

98-
if ([...depsNatives, ...detectedNatives].includes(source)) {
99-
const nativeFilename = path.join(output, source + NativeExt)
100-
const interopFilename = path.join(output, source + InteropExt)
92+
const nativeItem = nativeRecord.get(source)
93+
if (!nativeItem) return
10194

102-
if (!nativesMap.get(source)) {
103-
ensureDir(output)
95+
if (options.ignore?.(source) === false) {
96+
nativeItem.ignore = true
97+
return
98+
}
10499

105-
// Generate Vite and Webpack interop file.
106-
fs.writeFileSync(
107-
interopFilename,
108-
getInteropSnippet(source, `./${withDistAssetBase(source + NativeExt)}`),
109-
)
100+
const nativeFilename = path.join(output, source + NativeExt)
101+
const interopFilename = path.join(output, source + InteropExt)
110102

111-
// We did not immediately call the `webpackBundle()` build here
112-
// because `build.emptyOutDir = true` will cause the built file to be removed.
113-
114-
const isDetected = detectedNativeRecord.has(source)
115-
116-
// Collect modules that are explicitly used.
117-
nativesMap.set(source, {
118-
status: 'resolved',
119-
nativeFilename,
120-
interopFilename,
121-
type: isDetected ? 'detected' : 'dependencies',
122-
nodeFiles: isDetected
123-
? detectedNativeRecord.get(source)?.nativeFiles!
124-
: depsNativeRecord.get(source)?.nativeFiles!,
125-
})
126-
}
103+
if (!nativesMap.get(source)) {
104+
ensureDir(path.dirname(interopFilename))
127105

128-
return { id: interopFilename }
106+
// Generate Vite and Webpack interop file.
107+
fs.writeFileSync(
108+
interopFilename,
109+
getInteropSnippet(source, `./${withDistAssetBase(source + NativeExt)}`),
110+
)
111+
112+
// We did not immediately call the `webpackBundle()` build here
113+
// because `build.emptyOutDir = true` will cause the built file to be removed.
114+
115+
// Collect modules that are explicitly used.
116+
nativesMap.set(source, {
117+
status: 'resolved',
118+
nativeFilename,
119+
interopFilename,
120+
native: nativeItem,
121+
})
129122
}
123+
124+
return { id: interopFilename }
130125
},
131126
}
132127

133128
modifyAlias(config, [alias])
134129
// Run build are not necessary.
135-
modifyOptimizeDeps(config, [...depsNatives, ...detectedNatives])
130+
modifyOptimizeDeps(config, [...nativeRecord.keys()])
136131
},
137132
async buildEnd(error) {
138133
if (error) return
139134

140135
// Must be explicitly specify use Webpack.
141136
if (options.webpack) {
142-
for (const [native, info] of nativesMap) {
143-
if (info.status === 'built') continue
137+
for (const item of nativesMap) {
138+
const [name, native] = item
139+
if (native.status === 'built') continue
140+
if (native.native.ignore) continue
144141

145142
try {
146-
await webpackBundle(native, output, options.webpack)
147-
// TODO: force copy *.node files to dist/node_modules path if Webpack can't bundle it correctly.
148-
info.status = 'built'
143+
await webpackBundle(name, output, options.webpack)
144+
145+
if (options.forceCopyIfUnbuilt) {
146+
await forceCopyNativeFilesIfUnbuilt(
147+
native,
148+
output,
149+
options.webpack[loader1]?.outputAssetBase ?? outputAssetBase,
150+
)
151+
}
152+
153+
native.status = 'built'
149154
} catch (error: any) {
150155
console.error(`\n${TAG}`, error)
151156
process.exit(1)
@@ -188,7 +193,7 @@ async function webpackBundle(
188193
) {
189194
webpackOpts[loader1] ??= {}
190195
const { validate, webpack } = cjs.require('webpack') as typeof import('webpack')
191-
const assetBase = webpackOpts[loader1].outputAssetBase ??= 'native_modules'
196+
const assetBase = webpackOpts[loader1].outputAssetBase ??= outputAssetBase
192197

193198
return new Promise<null>(async (resolve, reject) => {
194199
let options: Configuration = {
@@ -258,3 +263,40 @@ async function webpackBundle(
258263
})
259264
})
260265
}
266+
267+
// Force copy *.node files to dist/node_modules path if Webpack can't bundle native modules correctly.
268+
async function forceCopyNativeFilesIfUnbuilt(
269+
resolvedNative: ResolvedNativeRecord,
270+
output: string,
271+
assetBase: string,
272+
) {
273+
const { nativeFilename } = resolvedNative
274+
const { name: nativeName, path: nativeRoot, nativeFiles } = resolvedNative.native
275+
const nativeOutput = path.posix.join(output, assetBase)
276+
const nativeNodeModules = path.posix.join(output, 'node_modules')
277+
const exists = nativeFiles
278+
// e.g. ['build/Release/better_sqlite3.node', 'build/Release/test_extension.node']
279+
.some((file) => fs.existsSync(path.join(nativeOutput, file)))
280+
281+
if (!exists) {
282+
const nativeDest = path.join(nativeNodeModules, nativeName)
283+
copy(nativeRoot, nativeDest)
284+
285+
const dependencies = await flatDependencies(nativeRoot)
286+
for (const dep of dependencies) {
287+
copy(dep.src, path.join(nativeNodeModules, dep.name))
288+
}
289+
290+
let relativePath = path.posix.relative(path.dirname(nativeFilename), nativeDest)
291+
if (!relativePath.startsWith('.')) {
292+
relativePath = `./${relativePath}`
293+
}
294+
fs.writeFileSync(
295+
path.join(nativeFilename),
296+
`
297+
// This is a native module that cannot be built correctly.
298+
module.exports = require("${relativePath}");
299+
`.trim(),
300+
)
301+
}
302+
}

src/utils.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@ import glob from 'fast-glob'
66
import _libEsm from 'lib-esm'
77
import { node_modules as findNodeModules } from 'vite-plugin-utils/function'
88

9-
export type NativeRecordType = 'dependencies' | 'detected'
10-
export type NativeRecord = Map<string, {
11-
type: NativeRecordType
9+
export interface ResolvedNativeRecord {
10+
status: 'built' | 'resolved'
11+
nativeFilename: string
12+
interopFilename: string
13+
native: NativeRecord
14+
}
15+
16+
export interface NativeRecord {
17+
name: string
18+
type: 'dependencies' | 'detected'
1219
path: string
1320
nativeFiles: string[]
14-
}>
21+
ignore?: true
22+
}
1523

1624
// @ts-ignore
1725
const libEsm: typeof import('lib-esm').default = _libEsm.default || _libEsm
@@ -39,10 +47,10 @@ export async function globNativeFiles(cwd: string) {
3947
return nativeFiles
4048
}
4149

42-
export async function getDependenciesNatives(root = process.cwd()): Promise<NativeRecord> {
50+
export async function getDependenciesNatives(root = process.cwd()): Promise<Map<string, NativeRecord>> {
4351
const node_modules_paths = findNodeModules(root)
4452
// Native modules of package.json
45-
const natives: NativeRecord = new Map
53+
const natives = new Map<string, NativeRecord>
4654

4755
for (const node_modules_path of node_modules_paths) {
4856
const pkgId = path.join(node_modules_path, '../package.json')
@@ -52,11 +60,14 @@ export async function getDependenciesNatives(root = process.cwd()): Promise<Nati
5260
const deps = Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.devDependencies || {}))
5361

5462
for (const dep of deps) {
63+
if (natives.has(dep)) continue
64+
5565
const depPath = path.join(node_modules_path, dep)
5666
const nativeFiles = await globNativeFiles(depPath)
5767

5868
if (nativeFiles.length) {
5969
natives.set(dep, {
70+
name: dep,
6071
type: 'dependencies',
6172
path: depPath,
6273
nativeFiles,
@@ -90,7 +101,7 @@ export function ensureDir(dir: string) {
90101
return dir
91102
}
92103

93-
export async function resolveNativeRecord(source: string, importer: string): Promise<NativeRecord | undefined> {
104+
export async function resolveNativeRecord(source: string, importer: string): Promise<Map<string, NativeRecord> | undefined> {
94105
let modulePath: string | undefined
95106
try {
96107
const modulePackageJson = cjs.require.resolve(`${source}/package.json`, {
@@ -100,11 +111,32 @@ export async function resolveNativeRecord(source: string, importer: string): Pro
100111
} catch { }
101112

102113
if (modulePath) {
103-
const nodeFiles = await globNativeFiles(modulePath)
104-
return new Map().set(source, {
105-
type: 'deep-dependencies',
106-
path: modulePath,
107-
nodeFiles,
108-
})
114+
const nativeFiles = await globNativeFiles(modulePath)
115+
if (nativeFiles.length) {
116+
return new Map<string, NativeRecord>().set(source, {
117+
name: source,
118+
type: 'detected',
119+
path: modulePath,
120+
nativeFiles,
121+
})
122+
}
123+
}
124+
}
125+
126+
function copyDir(srcDir: string, destDir: string) {
127+
fs.mkdirSync(destDir, { recursive: true })
128+
for (const file of fs.readdirSync(srcDir)) {
129+
const srcFile = path.resolve(srcDir, file)
130+
const destFile = path.resolve(destDir, file)
131+
copy(srcFile, destFile)
132+
}
133+
}
134+
135+
export function copy(src: string, dest: string) {
136+
const stat = fs.statSync(src)
137+
if (stat.isDirectory()) {
138+
copyDir(src, dest)
139+
} else {
140+
fs.copyFileSync(src, dest)
109141
}
110142
}

0 commit comments

Comments
 (0)