Skip to content

Commit 64b881a

Browse files
committed
feat: add options.forceCopyIfUnbuilt
1 parent 7c60957 commit 64b881a

2 files changed

Lines changed: 93 additions & 20 deletions

File tree

src/index.ts

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +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 ResolvedNativeRecord,
1415
createCjs,
16+
copy,
1517
ensureDir,
1618
getInteropSnippet,
1719
getDependenciesNatives,
@@ -28,6 +30,8 @@ export interface NativeOptions {
2830
natives?: string[] | ((natives: string[]) => string[])
2931
/** Ignore the specified native module. */
3032
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
3135
/** Enable and configure webpack. */
3236
webpack?: {
3337
config?: (config: Configuration) => Configuration | undefined | Promise<Configuration | undefined>
@@ -39,17 +43,13 @@ export interface NativeOptions {
3943
const cjs = createCjs(import.meta.url)
4044
const TAG = '[vite-plugin-native]'
4145
const loader1 = '@vercel/webpack-asset-relocator-loader'
46+
const outputAssetBase = 'native_modules'
4247
const NativeExt = '.native.cjs'
4348
const InteropExt = '.interop.mjs'
4449
// https://github.com/vitejs/vite/blob/v5.3.1/packages/vite/src/node/plugins/index.ts#L55
4550
const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/
4651
// `nativesMap` is placed in the global scope and can be effective for multiple builds.
47-
const nativesMap = new Map<string, {
48-
status: 'built' | 'resolved'
49-
nativeFilename: string
50-
interopFilename: string
51-
native: NativeRecord
52-
}>
52+
const nativesMap = new Map<string, ResolvedNativeRecord>
5353

5454
export default function native(options: NativeOptions): Plugin {
5555
const assetsDir = options.assetsDir ??= 'node_natives'
@@ -101,7 +101,7 @@ export default function native(options: NativeOptions): Plugin {
101101
const interopFilename = path.join(output, source + InteropExt)
102102

103103
if (!nativesMap.get(source)) {
104-
ensureDir(output)
104+
ensureDir(path.dirname(interopFilename))
105105

106106
// Generate Vite and Webpack interop file.
107107
fs.writeFileSync(
@@ -134,13 +134,23 @@ export default function native(options: NativeOptions): Plugin {
134134

135135
// Must be explicitly specify use Webpack.
136136
if (options.webpack) {
137-
for (const [native, info] of nativesMap) {
138-
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
139141

140142
try {
141-
await webpackBundle(native, output, options.webpack)
142-
// TODO: force copy *.node files to dist/node_modules path if Webpack can't bundle it correctly.
143-
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'
144154
} catch (error: any) {
145155
console.error(`\n${TAG}`, error)
146156
process.exit(1)
@@ -183,7 +193,7 @@ async function webpackBundle(
183193
) {
184194
webpackOpts[loader1] ??= {}
185195
const { validate, webpack } = cjs.require('webpack') as typeof import('webpack')
186-
const assetBase = webpackOpts[loader1].outputAssetBase ??= 'native_modules'
196+
const assetBase = webpackOpts[loader1].outputAssetBase ??= outputAssetBase
187197

188198
return new Promise<null>(async (resolve, reject) => {
189199
let options: Configuration = {
@@ -254,6 +264,39 @@ async function webpackBundle(
254264
})
255265
}
256266

257-
async function ensureNodeFiles() {
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)
258284

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+
}
259302
}

src/utils.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ 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 interface ResolvedNativeRecord {
10+
status: 'built' | 'resolved'
11+
nativeFilename: string
12+
interopFilename: string
13+
native: NativeRecord
14+
}
15+
916
export interface NativeRecord {
17+
name: string
1018
type: 'dependencies' | 'detected'
1119
path: string
1220
nativeFiles: string[]
@@ -59,6 +67,7 @@ export async function getDependenciesNatives(root = process.cwd()): Promise<Map<
5967

6068
if (nativeFiles.length) {
6169
natives.set(dep, {
70+
name: dep,
6271
type: 'dependencies',
6372
path: depPath,
6473
nativeFiles,
@@ -103,10 +112,31 @@ export async function resolveNativeRecord(source: string, importer: string): Pro
103112

104113
if (modulePath) {
105114
const nativeFiles = await globNativeFiles(modulePath)
106-
return new Map<string, NativeRecord>().set(source, {
107-
type: 'detected',
108-
path: modulePath,
109-
nativeFiles,
110-
})
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)
111141
}
112142
}

0 commit comments

Comments
 (0)