11import fs from 'fs' ;
2+ import { createRequire } from 'node:module' ;
23import path from 'path' ;
34import type { Plugin } from 'vite' ;
45
@@ -20,6 +21,8 @@ import { logger } from '@openzeppelin/ui-utils';
2021 * How it works:
2122 * 1. Define a mapping (`virtualFiles`) between a virtual module name (e.g., 'virtual:tailwind-config-content')
2223 * and the real file path relative to the monorepo root.
24+ * Entries prefixed with `npm:` are resolved via Node's module resolution from the
25+ * builder package directory, so they work with any pnpm node-linker strategy.
2326 * 2. The `resolveId` hook intercepts imports matching the 'virtual:' prefix and marks them
2427 * as resolved virtual modules using the null byte prefix ('\0').
2528 * 3. The `load` hook intercepts requests for these resolved virtual modules.
@@ -34,23 +37,24 @@ import { logger } from '@openzeppelin/ui-utils';
3437 * - Import the content in your application code: `import myConfig from 'virtual:my-config-content';`
3538 */
3639
37- // Define the virtual module IDs and their corresponding real file paths
38- // Paths are relative to the monorepo root
40+ // Define the virtual module IDs and their corresponding real file paths.
41+ // Paths are relative to the monorepo root unless prefixed with `npm:`,
42+ // which triggers Node module resolution from the builder package directory.
3943const VIRTUAL_MODULE_PREFIX = 'virtual:' ;
4044const RESOLVED_VIRTUAL_MODULE_PREFIX = '\0virtual:' ; // Null byte prefix for resolved IDs
45+ const NPM_RESOLVE_PREFIX = 'npm:' ;
4146
42- // Renamed map to reflect it handles more than just config
4347const virtualFiles : Record < string , string > = {
44- // Config files
48+ // Config files (relative to monorepo root)
4549 'tailwind-config-content' : 'tailwind.config.cjs' ,
4650 'postcss-config-content' : 'postcss.config.cjs' ,
4751 'components-json-content' : 'components.json' ,
48- // CSS files - now from npm package after migration
49- 'global-css-content' : 'node_modules/ @openzeppelin/ui-styles/global.css' ,
50- // Template-specific CSS (add template name if multiple templates have different styles.css )
52+ // npm packages (resolved via Node module resolution — works with any pnpm hoisting strategy)
53+ 'global-css-content' : 'npm: @openzeppelin/ui-styles/global.css' ,
54+ // Template-specific CSS (relative to monorepo root )
5155 'template-vite-styles-css-content' :
5256 'apps/builder/src/export/templates/typescript-react-vite/src/styles.css' ,
53- // Core Type Files (added )
57+ // Core Type Files (relative to monorepo root )
5458 'contract-schema-content' : 'packages/types/src/contracts/schema.ts' ,
5559} ;
5660
@@ -61,44 +65,48 @@ export function virtualContentLoaderPlugin(): Plugin {
6165 // Resolve the monorepo root directory relative to this plugin file
6266 // Assumes this file is in apps/builder/vite-plugins/
6367 const monorepoRoot = path . resolve ( __dirname , '../../..' ) ;
68+ const builderDir = path . join ( monorepoRoot , 'apps/builder' ) ;
69+ const builderRequire = createRequire ( path . join ( builderDir , 'package.json' ) ) ;
6470
6571 return {
66- name : 'virtual-content-loader' , // Renamed plugin for clarity
72+ name : 'virtual-content-loader' ,
6773
6874 resolveId ( id ) {
6975 if ( id . startsWith ( VIRTUAL_MODULE_PREFIX ) ) {
7076 const moduleName = id . substring ( VIRTUAL_MODULE_PREFIX . length ) ;
71- // Check the renamed map
7277 if ( virtualFiles [ moduleName ] ) {
73- // Prepend null byte to mark as resolved virtual module
7478 return RESOLVED_VIRTUAL_MODULE_PREFIX + moduleName ;
7579 }
7680 }
77- return null ; // Let other plugins handle it
81+ return null ;
7882 } ,
7983
8084 load ( id ) {
8185 if ( id . startsWith ( RESOLVED_VIRTUAL_MODULE_PREFIX ) ) {
8286 const moduleName = id . substring ( RESOLVED_VIRTUAL_MODULE_PREFIX . length ) ;
83- // Use the renamed map
8487 const fileName = virtualFiles [ moduleName ] ;
8588
8689 if ( fileName ) {
8790 try {
88- const filePath = path . resolve ( monorepoRoot , fileName ) ;
91+ let filePath : string ;
92+ if ( fileName . startsWith ( NPM_RESOLVE_PREFIX ) ) {
93+ // Use Node module resolution from the builder package so this works
94+ // with any pnpm node-linker strategy (hoisted or isolated).
95+ const specifier = fileName . substring ( NPM_RESOLVE_PREFIX . length ) ;
96+ filePath = builderRequire . resolve ( specifier ) ;
97+ } else {
98+ filePath = path . resolve ( monorepoRoot , fileName ) ;
99+ }
100+
89101 const content = fs . readFileSync ( filePath , 'utf-8' ) ;
90102
91- // Choose export format based on file type
92103 if ( fileName . endsWith ( '.css' ) ) {
93- // Export CSS content as a raw JS string literal using backticks
94104 const escapedContent = content
95- . replace ( / \\ / g, '\\' ) // Escape backslashes
96- . replace ( / ` / g, '\\`' ) // Escape backticks
97- . replace ( / \$ / g, '\\$' ) ; // Escape dollars (for template literals)
105+ . replace ( / \\ / g, '\\\\' )
106+ . replace ( / ` / g, '\\`' )
107+ . replace ( / \$ / g, '\\$' ) ;
98108 return `export default \`${ escapedContent } \`;` ;
99109 } else {
100- // For non-CSS (like .cjs, .json), try JSON.stringify
101- // This worked for postcss.config.cjs previously and might be safer for JS/JSON code
102110 return `export default ${ JSON . stringify ( content ) } ;` ;
103111 }
104112 } catch ( error : unknown ) {
@@ -111,7 +119,7 @@ export function virtualContentLoaderPlugin(): Plugin {
111119 }
112120 }
113121 }
114- return null ; // Let other plugins handle it
122+ return null ;
115123 } ,
116124 } ;
117125}
0 commit comments