|
| 1 | +import { builtinModules } from 'node:module'; |
| 2 | + |
| 3 | +import { |
| 4 | + parse, |
| 5 | + type ParseResult, |
| 6 | + Visitor, |
| 7 | + type CallExpression, |
| 8 | + type Expression, |
| 9 | + type StaticMemberExpression, |
| 10 | + type VariableDeclarator, |
| 11 | +} from 'oxc-parser'; |
| 12 | + |
| 13 | +import { createModuleEntryFileName } from './build-cjs-deps'; |
| 14 | + |
| 15 | +// Node.js built-in modules (without node: prefix) |
| 16 | +const nodeBuiltins = new Set(builtinModules); |
| 17 | + |
| 18 | +// TODO, analysis the optional peerDependencies in the dependencies tree to exclude them in the future |
| 19 | +const optionalCjsExternal = new Set<string>(['oxc-resolver', 'synckit']); |
| 20 | + |
| 21 | +/** |
| 22 | + * Check if a module specifier is a third-party package |
| 23 | + * (not a Node.js built-in, not a relative path) |
| 24 | + */ |
| 25 | +function isThirdPartyModule(specifier: string): boolean { |
| 26 | + // Filter out relative paths |
| 27 | + if (specifier.startsWith('./') || specifier.startsWith('../')) { |
| 28 | + return false; |
| 29 | + } |
| 30 | + // Filter out Node.js built-ins (with or without node: prefix) |
| 31 | + if (specifier.startsWith('node:')) { |
| 32 | + return false; |
| 33 | + } |
| 34 | + if (nodeBuiltins.has(specifier) || optionalCjsExternal.has(specifier)) { |
| 35 | + return false; |
| 36 | + } |
| 37 | + return true; |
| 38 | +} |
| 39 | + |
| 40 | +/** |
| 41 | + * Find and replace all third-party CJS requires with local entry files |
| 42 | + * Returns the modified source code and the set of third-party modules found |
| 43 | + */ |
| 44 | +export async function replaceThirdPartyCjsRequires( |
| 45 | + source: string, |
| 46 | + filePath: string, |
| 47 | + tsdownExternal: Set<string>, |
| 48 | +): Promise<{ code: string; modules: Set<string> }> { |
| 49 | + const ast = await parse(filePath, source, { |
| 50 | + lang: 'js', |
| 51 | + sourceType: 'module', |
| 52 | + }); |
| 53 | + |
| 54 | + const thirdPartyModules = new Set<string>(); |
| 55 | + |
| 56 | + // Find all createRequire patterns and their require calls |
| 57 | + const results = [ |
| 58 | + findCreateRequireInStaticImports(ast), |
| 59 | + findCreateRequireInGlobalModule(ast), |
| 60 | + ].filter((result): result is { requireVarName: string; calls: RequireCall[] } => Boolean(result)); |
| 61 | + |
| 62 | + // Collect all third-party require calls |
| 63 | + const replacements: RequireCall[] = []; |
| 64 | + for (const { calls } of results) { |
| 65 | + for (const call of calls) { |
| 66 | + if (isThirdPartyModule(call.module)) { |
| 67 | + const parts = call.module.split('/'); |
| 68 | + const moduleName = call.module.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]; |
| 69 | + if (!tsdownExternal.has(moduleName)) { |
| 70 | + thirdPartyModules.add(call.module); |
| 71 | + replacements.push(call); |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + // Sort by position descending (process from end to start to avoid offset issues) |
| 78 | + replacements.sort((a, b) => b.start - a.start); |
| 79 | + |
| 80 | + // Perform replacements |
| 81 | + let code = source; |
| 82 | + for (const { module, start, end } of replacements) { |
| 83 | + const newSpecifier = `"./${createModuleEntryFileName(module)}"`; |
| 84 | + code = code.slice(0, start) + newSpecifier + code.slice(end); |
| 85 | + } |
| 86 | + |
| 87 | + return { code, modules: thirdPartyModules }; |
| 88 | +} |
| 89 | + |
| 90 | +interface RequireCall { |
| 91 | + module: string; |
| 92 | + start: number; |
| 93 | + end: number; |
| 94 | +} |
| 95 | + |
| 96 | +/** |
| 97 | + * Find all calls to a specific require function and return the module specifiers with positions |
| 98 | + */ |
| 99 | +function findRequireCalls(ast: ParseResult, requireVarName: string): RequireCall[] { |
| 100 | + const calls: RequireCall[] = []; |
| 101 | + |
| 102 | + const visitor = new Visitor({ |
| 103 | + CallExpression(node: CallExpression) { |
| 104 | + // Check if callee is the require variable |
| 105 | + if (node.callee.type !== 'Identifier' || node.callee.name !== requireVarName) { |
| 106 | + return; |
| 107 | + } |
| 108 | + |
| 109 | + // Extract the first argument (module specifier) |
| 110 | + if (node.arguments.length === 0) { |
| 111 | + return; |
| 112 | + } |
| 113 | + const arg = node.arguments[0]; |
| 114 | + if (arg.type !== 'Literal') { |
| 115 | + return; |
| 116 | + } |
| 117 | + const value = (arg as { value: unknown; start: number; end: number }).value; |
| 118 | + if (typeof value === 'string') { |
| 119 | + calls.push({ |
| 120 | + module: value, |
| 121 | + start: arg.start, |
| 122 | + end: arg.end, |
| 123 | + }); |
| 124 | + } |
| 125 | + }, |
| 126 | + }); |
| 127 | + |
| 128 | + visitor.visit(ast.program); |
| 129 | + return calls; |
| 130 | +} |
| 131 | + |
| 132 | +/** |
| 133 | + * Find createRequire from static imports and return the require variable name + all require calls |
| 134 | + * Handles: `import { createRequire } from "node:module"` then `const require = createRequire(...)` |
| 135 | + */ |
| 136 | +function findCreateRequireInStaticImports( |
| 137 | + ast: ParseResult, |
| 138 | +): { requireVarName: string; calls: RequireCall[] } | undefined { |
| 139 | + // Find import from 'module' or 'node:module' |
| 140 | + const importFromModule = ast.module.staticImports.find((imt) => { |
| 141 | + const { value } = imt.moduleRequest; |
| 142 | + return value === 'node:module' || value === 'module'; |
| 143 | + }); |
| 144 | + if (!importFromModule) { |
| 145 | + return; |
| 146 | + } |
| 147 | + |
| 148 | + // Find the createRequire import entry |
| 149 | + const createRequireEntry = importFromModule.entries.find((entry) => { |
| 150 | + return entry.importName.name === 'createRequire'; |
| 151 | + }); |
| 152 | + if (!createRequireEntry) { |
| 153 | + return; |
| 154 | + } |
| 155 | + |
| 156 | + const createRequireLocalName = createRequireEntry.localName.value; |
| 157 | + |
| 158 | + // Find the variable that stores the result of createRequire(...) |
| 159 | + // e.g., `const __require = createRequire(import.meta.url)` |
| 160 | + let requireVarName: string | undefined; |
| 161 | + |
| 162 | + const varVisitor = new Visitor({ |
| 163 | + VariableDeclarator(node: VariableDeclarator) { |
| 164 | + if (!node.init || node.init.type !== 'CallExpression') { |
| 165 | + return; |
| 166 | + } |
| 167 | + const call = node.init as CallExpression; |
| 168 | + if (call.callee.type === 'Identifier' && call.callee.name === createRequireLocalName) { |
| 169 | + if (node.id.type === 'Identifier') { |
| 170 | + requireVarName = node.id.name; |
| 171 | + } |
| 172 | + } |
| 173 | + }, |
| 174 | + }); |
| 175 | + varVisitor.visit(ast.program); |
| 176 | + |
| 177 | + if (!requireVarName) { |
| 178 | + return; |
| 179 | + } |
| 180 | + |
| 181 | + // Find all calls to the require variable |
| 182 | + const calls = findRequireCalls(ast, requireVarName); |
| 183 | + |
| 184 | + return { requireVarName, calls }; |
| 185 | +} |
| 186 | + |
| 187 | +// Helper to check if an expression is `process` or `globalThis.process` |
| 188 | +function isProcessExpression(expr: Expression): boolean { |
| 189 | + // Check for `process` |
| 190 | + if (expr.type === 'Identifier' && expr.name === 'process') { |
| 191 | + return true; |
| 192 | + } |
| 193 | + // Check for `globalThis.process` |
| 194 | + if (expr.type === 'MemberExpression' && !expr.computed) { |
| 195 | + const memberExpr = expr as StaticMemberExpression; |
| 196 | + return ( |
| 197 | + memberExpr.object.type === 'Identifier' && |
| 198 | + memberExpr.object.name === 'globalThis' && |
| 199 | + memberExpr.property.name === 'process' |
| 200 | + ); |
| 201 | + } |
| 202 | + return false; |
| 203 | +} |
| 204 | + |
| 205 | +// Helper to check if a CallExpression is `[process|globalThis.process].getBuiltinModule("module")` |
| 206 | +function isGetBuiltinModuleCall(expr: Expression): boolean { |
| 207 | + if (expr.type !== 'CallExpression') { |
| 208 | + return false; |
| 209 | + } |
| 210 | + const call = expr as CallExpression; |
| 211 | + |
| 212 | + // Check callee is a member expression with property `getBuiltinModule` |
| 213 | + if (call.callee.type !== 'MemberExpression' || call.callee.computed) { |
| 214 | + return false; |
| 215 | + } |
| 216 | + const callee = call.callee as StaticMemberExpression; |
| 217 | + if (callee.property.name !== 'getBuiltinModule') { |
| 218 | + return false; |
| 219 | + } |
| 220 | + |
| 221 | + // Check the object is `process` or `globalThis.process` |
| 222 | + if (!isProcessExpression(callee.object)) { |
| 223 | + return false; |
| 224 | + } |
| 225 | + |
| 226 | + // Check argument is "module" or "node:module" |
| 227 | + if (call.arguments.length === 0) { |
| 228 | + return false; |
| 229 | + } |
| 230 | + const arg = call.arguments[0]; |
| 231 | + if (arg.type !== 'Literal') { |
| 232 | + return false; |
| 233 | + } |
| 234 | + const value = (arg as { value: unknown }).value; |
| 235 | + return value === 'module' || value === 'node:module'; |
| 236 | +} |
| 237 | + |
| 238 | +/** |
| 239 | + * Find createRequire from getBuiltinModule and return the require variable name + all require calls |
| 240 | + * Handles: `const require = globalThis.process.getBuiltinModule("module").createRequire(import.meta.url)` |
| 241 | + * Or: `const require = process.getBuiltinModule("module").createRequire(import.meta.url)` |
| 242 | + */ |
| 243 | +function findCreateRequireInGlobalModule( |
| 244 | + ast: ParseResult, |
| 245 | +): { requireVarName: string; calls: RequireCall[] } | undefined { |
| 246 | + let requireVarName: string | undefined; |
| 247 | + |
| 248 | + const visitor = new Visitor({ |
| 249 | + VariableDeclarator(node: VariableDeclarator) { |
| 250 | + if (!node.init || node.init.type !== 'CallExpression') { |
| 251 | + return; |
| 252 | + } |
| 253 | + |
| 254 | + const call = node.init as CallExpression; |
| 255 | + |
| 256 | + // Check if callee is a MemberExpression with property `createRequire` |
| 257 | + if (call.callee.type !== 'MemberExpression' || call.callee.computed) { |
| 258 | + return; |
| 259 | + } |
| 260 | + const callee = call.callee as StaticMemberExpression; |
| 261 | + if (callee.property.name !== 'createRequire') { |
| 262 | + return; |
| 263 | + } |
| 264 | + |
| 265 | + // Check if the object is a getBuiltinModule("module") call |
| 266 | + if (!isGetBuiltinModuleCall(callee.object)) { |
| 267 | + return; |
| 268 | + } |
| 269 | + |
| 270 | + // Extract variable name |
| 271 | + if (node.id.type === 'Identifier') { |
| 272 | + requireVarName = node.id.name; |
| 273 | + } |
| 274 | + }, |
| 275 | + }); |
| 276 | + |
| 277 | + visitor.visit(ast.program); |
| 278 | + |
| 279 | + if (!requireVarName) { |
| 280 | + return; |
| 281 | + } |
| 282 | + |
| 283 | + // Find all calls to the require variable |
| 284 | + const calls = findRequireCalls(ast, requireVarName); |
| 285 | + |
| 286 | + return { requireVarName, calls }; |
| 287 | +} |
0 commit comments