@@ -9,7 +9,15 @@ import {
9
9
import type { Configuration } from 'webpack'
10
10
import type { NodeLoaderOptions , WebpackAssetRelocatorLoader } from './types'
11
11
import { 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'
13
21
14
22
export interface NativeOptions {
15
23
/** @default 'node_natives' */
@@ -29,11 +37,15 @@ const TAG = '[vite-plugin-native]'
29
37
const loader1 = '@vercel/webpack-asset-relocator-loader'
30
38
const NativeExt = '.native.cjs'
31
39
const 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 @ ] (? ! .* : \/ \/ ) /
32
42
// `nativesMap` is placed in the global scope and can be effective for multiple builds.
33
43
const nativesMap = new Map < string , {
34
- built : boolean
44
+ status : 'built' | 'resolved'
35
45
nativeFilename : string
36
46
interopFilename : string
47
+ type : NativeRecordType
48
+ nodeFiles : string [ ]
37
49
} >
38
50
39
51
export default function native ( options : NativeOptions ) : Plugin {
@@ -49,64 +61,91 @@ export default function native(options: NativeOptions): Plugin {
49
61
const outDir = config . build ?. outDir ?? 'dist'
50
62
output = normalizePath ( path . join ( resolvedRoot , outDir , assetsDir ) )
51
63
52
- const natives = await getNatives ( resolvedRoot )
53
- options . natives ??= natives
64
+ const depsNativeRecord = await getDependenciesNatives ( resolvedRoot )
65
+ const depsNatives = [ ... depsNativeRecord . keys ( ) ]
54
66
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`.
57
72
}
58
73
59
- const aliases : Alias [ ] = [ ]
60
74
const withDistAssetBase = ( p : string ) => ( assetsDir && p ) ? `${ assetsDir } /${ p } ` : p
61
75
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 )
63
101
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 )
67
104
68
- aliases . push ( {
69
- find : native ,
70
- replacement : interopFilename ,
71
- customResolver ( source ) {
72
- const record = nativesMap . get ( native )
73
- if ( ! record ?. built ) {
74
105
// 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
+ )
77
110
78
111
// We did not immediately call the `webpackBundle()` build here
79
112
// because `build.emptyOutDir = true` will cause the built file to be removed.
80
113
114
+ const isDetected = detectedNativeRecord . has ( source )
115
+
81
116
// 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
+ } )
83
126
}
84
127
85
- return { id : source }
86
- } ,
87
- } )
128
+ return { id : interopFilename }
129
+ }
130
+ } ,
88
131
}
89
132
90
- const aliasKeys = aliases . map ( ( { find } ) => find as string )
91
-
92
- modifyAlias ( config , aliases )
133
+ modifyAlias ( config , [ alias ] )
93
134
// 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 ] )
98
136
} ,
99
137
async buildEnd ( error ) {
100
138
if ( error ) return
101
139
102
140
// Must be explicitly specify use Webpack.
103
141
if ( options . webpack ) {
104
142
for ( const [ native , info ] of nativesMap ) {
105
- if ( info . built ) continue
143
+ if ( info . status === ' built' ) continue
106
144
107
145
try {
108
146
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'
110
149
} catch ( error : any ) {
111
150
console . error ( `\n${ TAG } ` , error )
112
151
process . exit ( 1 )
@@ -148,7 +187,7 @@ async function webpackBundle(
148
187
webpackOpts : NonNullable < NativeOptions [ 'webpack' ] >
149
188
) {
150
189
webpackOpts [ loader1 ] ??= { }
151
- const { validate, webpack } = cjs . require ( 'webpack' ) as typeof import ( 'webpack' ) ;
190
+ const { validate, webpack } = cjs . require ( 'webpack' ) as typeof import ( 'webpack' )
152
191
const assetBase = webpackOpts [ loader1 ] . outputAssetBase ??= 'native_modules'
153
192
154
193
return new Promise < null > ( async ( resolve , reject ) => {
0 commit comments