1+ import { readFileSync , realpathSync } from "node:fs"
2+ import { basename , dirname , isAbsolute } from "node:path"
3+ import { fileURLToPath } from "node:url"
14import { type BunPlugin } from "bun"
25import * as coreRuntime from "./index.js"
36
@@ -45,6 +48,22 @@ const exactSpecifierFilter = (specifier: string): RegExp => {
4548 return new RegExp ( `^${ escapeRegExp ( specifier ) } $` )
4649}
4750
51+ const exactPathFilter = ( path : string ) : RegExp => {
52+ const variants = new Set < string > ( [ sourcePath ( path ) , normalizeSourcePath ( path ) ] )
53+
54+ for ( const variant of [ ...variants ] ) {
55+ if ( variant . startsWith ( "/var/" ) ) {
56+ variants . add ( `/private${ variant } ` )
57+ }
58+
59+ if ( variant . startsWith ( "/private/var/" ) ) {
60+ variants . add ( variant . slice ( "/private" . length ) )
61+ }
62+ }
63+
64+ return new RegExp ( `^(?:${ [ ...variants ] . map ( escapeRegExp ) . join ( "|" ) } )(?:[?#].*)?$` )
65+ }
66+
4867export const runtimeModuleIdForSpecifier = ( specifier : string ) : string => {
4968 return `${ RUNTIME_MODULE_PREFIX } ${ encodeURIComponent ( specifier ) } `
5069}
@@ -64,10 +83,41 @@ const sourcePath = (path: string): string => {
6483 return end === undefined ? path : path . slice ( 0 , end )
6584}
6685
86+ const normalizeSourcePath = ( path : string ) : string => {
87+ const cleanPath = sourcePath ( path )
88+
89+ try {
90+ return realpathSync ( cleanPath )
91+ } catch {
92+ return cleanPath
93+ }
94+ }
95+
6796const isNodeModulesPath = ( path : string ) : boolean => {
6897 return / (?: ^ | [ / \\ ] ) n o d e _ m o d u l e s (?: [ / \\ ] ) / . test ( path )
6998}
7099
100+ const nodeModulesPackageRootForPath = ( path : string ) : string | null => {
101+ let currentDir = dirname ( path )
102+
103+ while ( true ) {
104+ const parentDir = dirname ( currentDir )
105+ if ( parentDir === currentDir ) {
106+ return null
107+ }
108+
109+ if ( basename ( parentDir ) === "node_modules" ) {
110+ return currentDir
111+ }
112+
113+ if ( basename ( dirname ( parentDir ) ) === "node_modules" && basename ( parentDir ) . startsWith ( "@" ) ) {
114+ return currentDir
115+ }
116+
117+ currentDir = parentDir
118+ }
119+ }
120+
71121const resolveRuntimePluginRewriteOptions = (
72122 options : RuntimePluginRewriteOptions | undefined ,
73123) : Required < RuntimePluginRewriteOptions > => {
@@ -101,8 +151,6 @@ const runtimeLoaderForPath = (path: string): "js" | "ts" | "jsx" | "tsx" | null
101151 return null
102152}
103153
104- const runtimeSourceFilter = / \. (?: [ c m ] ? j s | [ c m ] ? t s | j s x | t s x ) (?: [ ? # ] .* ) ? $ /
105-
106154const resolveImportSpecifierPatterns = [
107155 / ( f r o m \s + [ " ' ] ) ( [ ^ " ' ] + ) ( [ " ' ] ) / g,
108156 / ( i m p o r t \s + [ " ' ] ) ( [ ^ " ' ] + ) ( [ " ' ] ) / g,
@@ -180,6 +228,42 @@ const resolveFromParent = (specifier: string, parent: string): string | null =>
180228 }
181229}
182230
231+ const resolveSourcePathFromSpecifier = ( specifier : string , importer : string ) : string | null => {
232+ if (
233+ specifier . startsWith ( "node:" ) ||
234+ specifier . startsWith ( "bun:" ) ||
235+ specifier . startsWith ( "http:" ) ||
236+ specifier . startsWith ( "https:" ) ||
237+ specifier . startsWith ( "data:" ) ||
238+ specifier . startsWith ( RUNTIME_MODULE_PREFIX )
239+ ) {
240+ return null
241+ }
242+
243+ if ( specifier . startsWith ( "file:" ) ) {
244+ return normalizeSourcePath ( fileURLToPath ( specifier ) )
245+ }
246+
247+ if ( isAbsolute ( specifier ) ) {
248+ return normalizeSourcePath ( specifier )
249+ }
250+
251+ const resolvedSpecifier = resolveFromParent ( specifier , importer )
252+ if ( ! resolvedSpecifier ) {
253+ return null
254+ }
255+
256+ if ( resolvedSpecifier . startsWith ( "file:" ) ) {
257+ return normalizeSourcePath ( fileURLToPath ( resolvedSpecifier ) )
258+ }
259+
260+ if ( isAbsolute ( resolvedSpecifier ) ) {
261+ return normalizeSourcePath ( resolvedSpecifier )
262+ }
263+
264+ return null
265+ }
266+
183267const rewriteImportsFromResolveParents = ( code : string , resolveParentsByRecency : string [ ] ) : string => {
184268 if ( resolveParentsByRecency . length === 0 ) {
185269 return code
@@ -230,6 +314,61 @@ export function createRuntimePlugin(input: CreateRuntimePluginOptions = {}): Bun
230314 name : "bun-plugin-opentui-runtime-modules" ,
231315 setup : ( build ) => {
232316 const resolveParentsByRecency : string [ ] = [ ]
317+ const installedRewriteLoaders = new Set < string > ( )
318+ const nodeModulesBareRewritePackageRoots = new Set < string > ( )
319+ const runtimeSpecifierRewriteNeededByPath = new Map < string , boolean > ( )
320+
321+ const installRewriteLoader = ( path : string ) : void => {
322+ const normalizedPath = normalizeSourcePath ( path )
323+ if ( installedRewriteLoaders . has ( normalizedPath ) ) {
324+ return
325+ }
326+
327+ installedRewriteLoaders . add ( normalizedPath )
328+
329+ build . onLoad ( { filter : exactPathFilter ( normalizedPath ) } , async ( args ) => {
330+ const path = normalizeSourcePath ( args . path )
331+ const nodeModulesPath = isNodeModulesPath ( path )
332+ const shouldRewriteRuntimeSpecifiers = ! nodeModulesPath || rewriteOptions . nodeModulesRuntimeSpecifiers
333+ const shouldRewriteBareSpecifiers = ! nodeModulesPath || rewriteOptions . nodeModulesBareSpecifiers
334+ const loader = runtimeLoaderForPath ( args . path )
335+
336+ if ( ! loader ) {
337+ throw new Error ( `Unable to determine runtime loader for path: ${ args . path } ` )
338+ }
339+
340+ const contents = await Bun . file ( path ) . text ( )
341+ const runtimeRewrittenContents = shouldRewriteRuntimeSpecifiers
342+ ? rewriteRuntimeSpecifiers ( contents , runtimeModuleIdsBySpecifier )
343+ : contents
344+
345+ if ( runtimeRewrittenContents !== contents && shouldRewriteBareSpecifiers ) {
346+ registerResolveParent ( resolveParentsByRecency , path )
347+ }
348+
349+ const transformedContents = shouldRewriteBareSpecifiers
350+ ? rewriteImportsFromResolveParents ( runtimeRewrittenContents , resolveParentsByRecency )
351+ : runtimeRewrittenContents
352+
353+ return {
354+ contents : transformedContents ,
355+ loader,
356+ }
357+ } )
358+ }
359+
360+ const needsRuntimeSpecifierRewrite = ( path : string ) : boolean => {
361+ const normalizedPath = normalizeSourcePath ( path )
362+ const cached = runtimeSpecifierRewriteNeededByPath . get ( normalizedPath )
363+ if ( cached !== undefined ) {
364+ return cached
365+ }
366+
367+ const contents = readFileSync ( normalizedPath , "utf8" )
368+ const needsRewrite = rewriteRuntimeSpecifiers ( contents , runtimeModuleIdsBySpecifier ) !== contents
369+ runtimeSpecifierRewriteNeededByPath . set ( normalizedPath , needsRewrite )
370+ return needsRewrite
371+ }
233372
234373 for ( const [ specifier , moduleEntry ] of runtimeModules . entries ( ) ) {
235374 const moduleId = runtimeModuleIdsBySpecifier . get ( specifier )
@@ -246,39 +385,43 @@ export function createRuntimePlugin(input: CreateRuntimePluginOptions = {}): Bun
246385 build . onResolve ( { filter : exactSpecifierFilter ( specifier ) } , ( ) => ( { path : moduleId } ) )
247386 }
248387
249- build . onLoad ( { filter : runtimeSourceFilter } , async ( args ) => {
250- const path = sourcePath ( args . path )
251- const nodeModulesPath = isNodeModulesPath ( path )
252- const shouldRewriteRuntimeSpecifiers = ! nodeModulesPath || rewriteOptions . nodeModulesRuntimeSpecifiers
253- const shouldRewriteBareSpecifiers = ! nodeModulesPath || rewriteOptions . nodeModulesBareSpecifiers
388+ build . onResolve ( { filter : / .* / } , ( args ) => {
389+ if ( runtimeModuleIdsBySpecifier . has ( args . path ) || args . path . startsWith ( RUNTIME_MODULE_PREFIX ) ) {
390+ return undefined
391+ }
254392
255- if ( ! shouldRewriteRuntimeSpecifiers && ! shouldRewriteBareSpecifiers ) {
393+ const path = resolveSourcePathFromSpecifier ( args . path , args . importer )
394+ if ( ! path || ! runtimeLoaderForPath ( path ) ) {
256395 return undefined
257396 }
258397
259- const loader = runtimeLoaderForPath ( args . path )
260- if ( ! loader ) {
261- throw new Error ( `Unable to determine runtime loader for path: ${ args . path } ` )
398+ const nodeModulesPath = isNodeModulesPath ( path )
399+
400+ if ( ! nodeModulesPath ) {
401+ installRewriteLoader ( path )
402+ return undefined
262403 }
263404
264- const file = Bun . file ( path )
265- const contents = await file . text ( )
266- const runtimeRewrittenContents = shouldRewriteRuntimeSpecifiers
267- ? rewriteRuntimeSpecifiers ( contents , runtimeModuleIdsBySpecifier )
268- : contents
405+ if ( ! rewriteOptions . nodeModulesRuntimeSpecifiers && ! rewriteOptions . nodeModulesBareSpecifiers ) {
406+ return undefined
407+ }
269408
270- if ( runtimeRewrittenContents !== contents && shouldRewriteBareSpecifiers ) {
271- registerResolveParent ( resolveParentsByRecency , path )
409+ const packageRoot = nodeModulesPackageRootForPath ( path )
410+ if ( rewriteOptions . nodeModulesBareSpecifiers && packageRoot && nodeModulesBareRewritePackageRoots . has ( packageRoot ) ) {
411+ installRewriteLoader ( path )
412+ return undefined
272413 }
273414
274- const transformedContents = shouldRewriteBareSpecifiers
275- ? rewriteImportsFromResolveParents ( runtimeRewrittenContents , resolveParentsByRecency )
276- : runtimeRewrittenContents
415+ if ( ! rewriteOptions . nodeModulesRuntimeSpecifiers || ! needsRuntimeSpecifierRewrite ( path ) ) {
416+ return undefined
417+ }
277418
278- return {
279- contents : transformedContents ,
280- loader,
419+ if ( rewriteOptions . nodeModulesBareSpecifiers && packageRoot ) {
420+ nodeModulesBareRewritePackageRoots . add ( packageRoot )
281421 }
422+
423+ installRewriteLoader ( path )
424+ return undefined
282425 } )
283426 } ,
284427 }
0 commit comments