@@ -9,10 +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 NativeRecordType ,
14+ type ResolvedNativeRecord ,
1515 createCjs ,
16+ copy ,
1617 ensureDir ,
1718 getInteropSnippet ,
1819 getDependenciesNatives ,
@@ -22,8 +23,15 @@ import {
2223export 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 {
3543const cjs = createCjs ( import . meta. url )
3644const TAG = '[vite-plugin-native]'
3745const loader1 = '@vercel/webpack-asset-relocator-loader'
46+ const outputAssetBase = 'native_modules'
3847const NativeExt = '.native.cjs'
3948const InteropExt = '.interop.mjs'
4049// https://github.com/vitejs/vite/blob/v5.3.1/packages/vite/src/node/plugins/index.ts#L55
4150const bareImportRE = / ^ (? ! [ a - z A - 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
5154export 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+ }
0 commit comments