@@ -9,9 +9,11 @@ import {
99import type { Configuration } from 'webpack'
1010import type { NodeLoaderOptions , WebpackAssetRelocatorLoader } from './types'
1111import { COLOURS } from 'vite-plugin-utils/function'
12+ import { flatDependencies } from 'dependencies-tree'
1213import {
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 {
3943const cjs = createCjs ( import . meta. url )
4044const TAG = '[vite-plugin-native]'
4145const loader1 = '@vercel/webpack-asset-relocator-loader'
46+ const outputAssetBase = 'native_modules'
4247const NativeExt = '.native.cjs'
4348const InteropExt = '.interop.mjs'
4449// https://github.com/vitejs/vite/blob/v5.3.1/packages/vite/src/node/plugins/index.ts#L55
4550const bareImportRE = / ^ (? ! [ a - z A - 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
5454export 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}
0 commit comments