@@ -9,7 +9,15 @@ import {
99import type { Configuration } from 'webpack'
1010import type { NodeLoaderOptions , WebpackAssetRelocatorLoader } from './types'
1111import { COLOURS } from 'vite-plugin-utils/function'
12- import { createCjs , ensureDir , getInteropSnippet , getNatives } from './utils'
12+ import {
13+ type NativeRecord ,
14+ type NativeRecordType ,
15+ createCjs ,
16+ ensureDir ,
17+ getInteropSnippet ,
18+ getDependenciesNatives ,
19+ resolveNativeRecord ,
20+ } from './utils'
1321
1422export interface NativeOptions {
1523 /** @default 'node_natives' */
@@ -29,11 +37,15 @@ const TAG = '[vite-plugin-native]'
2937const loader1 = '@vercel/webpack-asset-relocator-loader'
3038const NativeExt = '.native.cjs'
3139const InteropExt = '.interop.mjs'
40+ // https://github.com/vitejs/vite/blob/v5.3.1/packages/vite/src/node/plugins/index.ts#L55
41+ const bareImportRE = / ^ (? ! [ a - z A - Z ] : ) [ \w @ ] (? ! .* : \/ \/ ) /
3242// `nativesMap` is placed in the global scope and can be effective for multiple builds.
3343const nativesMap = new Map < string , {
34- built : boolean
44+ status : 'built' | 'resolved'
3545 nativeFilename : string
3646 interopFilename : string
47+ type : NativeRecordType
48+ nodeFiles : string [ ]
3749} >
3850
3951export default function native ( options : NativeOptions ) : Plugin {
@@ -49,64 +61,91 @@ export default function native(options: NativeOptions): Plugin {
4961 const outDir = config . build ?. outDir ?? 'dist'
5062 output = normalizePath ( path . join ( resolvedRoot , outDir , assetsDir ) )
5163
52- const natives = await getNatives ( resolvedRoot )
53- options . natives ??= natives
64+ const depsNativeRecord = await getDependenciesNatives ( resolvedRoot )
65+ const depsNatives = [ ... depsNativeRecord . keys ( ) ]
5466
55- if ( typeof options . natives === 'function' ) {
56- options . natives = options . natives ( natives )
67+ if ( options . natives ) {
68+ const natives = Array . isArray ( options . natives )
69+ ? options . natives
70+ : options . natives ( depsNatives )
71+ // TODO: bundle modules based on `natives`.
5772 }
5873
59- const aliases : Alias [ ] = [ ]
6074 const withDistAssetBase = ( p : string ) => ( assetsDir && p ) ? `${ assetsDir } /${ p } ` : p
6175
62- options . natives . length && ensureDir ( output )
76+ let detectedNativeRecord : NativeRecord = new Map
77+ let detectedNatives : string [ ] = [ ]
78+
79+ const alias : Alias = {
80+ find : / ( .* ) / ,
81+ // Keep `customResolver` receive original source.
82+ // @see https://github.com/rollup/plugins/blob/alias-v5.1.0/packages/alias/src/index.ts#L92
83+ replacement : '$1' ,
84+ async customResolver ( source , importer ) {
85+ if ( ! importer ) return
86+ if ( ! bareImportRE . test ( source ) ) return
87+
88+ if ( ! depsNativeRecord . has ( source ) ) {
89+ // Auto detection.
90+ // 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+ }
96+ }
97+
98+ if ( [ ...depsNatives , ...detectedNatives ] . includes ( source ) ) {
99+ const nativeFilename = path . join ( output , source + NativeExt )
100+ const interopFilename = path . join ( output , source + InteropExt )
63101
64- for ( const native of options . natives ) {
65- const nativeFilename = path . join ( output , native + NativeExt )
66- const interopFilename = path . join ( output , native + InteropExt )
102+ if ( ! nativesMap . get ( source ) ) {
103+ ensureDir ( output )
67104
68- aliases . push ( {
69- find : native ,
70- replacement : interopFilename ,
71- customResolver ( source ) {
72- const record = nativesMap . get ( native )
73- if ( ! record ?. built ) {
74105 // Generate Vite and Webpack interop file.
75- const code = getInteropSnippet ( native , `./${ withDistAssetBase ( native + NativeExt ) } ` )
76- fs . writeFileSync ( interopFilename , code )
106+ fs . writeFileSync (
107+ interopFilename ,
108+ getInteropSnippet ( source , `./${ withDistAssetBase ( source + NativeExt ) } ` ) ,
109+ )
77110
78111 // We did not immediately call the `webpackBundle()` build here
79112 // because `build.emptyOutDir = true` will cause the built file to be removed.
80113
114+ const isDetected = detectedNativeRecord . has ( source )
115+
81116 // Collect modules that are explicitly used.
82- nativesMap . set ( native , { built : false , nativeFilename, interopFilename } )
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+ } )
83126 }
84127
85- return { id : source }
86- } ,
87- } )
128+ return { id : interopFilename }
129+ }
130+ } ,
88131 }
89132
90- const aliasKeys = aliases . map ( ( { find } ) => find as string )
91-
92- modifyAlias ( config , aliases )
133+ modifyAlias ( config , [ alias ] )
93134 // Run build are not necessary.
94- modifyOptimizeDeps ( config , aliasKeys )
95- } ,
96- resolveId ( ) {
97- // TODO: dynamic detect by bare moduleId. e.g. serialport
135+ modifyOptimizeDeps ( config , [ ...depsNatives , ...detectedNatives ] )
98136 } ,
99137 async buildEnd ( error ) {
100138 if ( error ) return
101139
102140 // Must be explicitly specify use Webpack.
103141 if ( options . webpack ) {
104142 for ( const [ native , info ] of nativesMap ) {
105- if ( info . built ) continue
143+ if ( info . status === ' built' ) continue
106144
107145 try {
108146 await webpackBundle ( native , output , options . webpack )
109- info . built = true
147+ // TODO: force copy *.node files to dist/node_modules path if Webpack can't bundle it correctly.
148+ info . status = 'built'
110149 } catch ( error : any ) {
111150 console . error ( `\n${ TAG } ` , error )
112151 process . exit ( 1 )
@@ -148,7 +187,7 @@ async function webpackBundle(
148187 webpackOpts : NonNullable < NativeOptions [ 'webpack' ] >
149188) {
150189 webpackOpts [ loader1 ] ??= { }
151- const { validate, webpack } = cjs . require ( 'webpack' ) as typeof import ( 'webpack' ) ;
190+ const { validate, webpack } = cjs . require ( 'webpack' ) as typeof import ( 'webpack' )
152191 const assetBase = webpackOpts [ loader1 ] . outputAssetBase ??= 'native_modules'
153192
154193 return new Promise < null > ( async ( resolve , reject ) => {
0 commit comments