Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 139 additions & 20 deletions src/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import nodeGypBuild from 'node-gyp-build';
import mapboxPregyp from '@mapbox/node-pre-gyp';
import { Job } from './node-file-trace';
import { fileURLToPath, pathToFileURL, URL } from 'url';
import { AssetLocation } from './types';

// Note: these should be deprecated over time as they ship in Acorn core
const acorn = Parser.extend(
Expand Down Expand Up @@ -253,6 +254,7 @@ export interface AnalyzeResult {
deps: Set<string>;
imports: Set<string>;
isESM: boolean;
assetsLocation?: Map<string, AssetLocation[]>;
}

export default async function analyze(
Expand All @@ -264,6 +266,47 @@ export default async function analyze(
const deps = new Set<string>();
const imports = new Set<string>();

// Location tracking (optional, enabled via job.analysis.trackLocations)
const assetsLocation = job.analysis.trackLocations
? new Map<string, AssetLocation[]>()
: undefined;

// Helper function to convert AST position to line/column/character
function getLocationFromPosition(
pos: { line: number; column: number },
code: string,
): { line: number; column: number; character: number } {
// Calculate character offset from line and column
const lines = code.split('\n');
let character = 0;
for (let i = 0; i < pos.line - 1 && i < lines.length; i++) {
character += lines[i].length + 1; // +1 for newline
}
character += pos.column;
return {
line: pos.line,
column: pos.column,
character,
};
}

// Helper function to track location of an asset/dep/import
function trackLocation(
type: string,
value: string,
start: { line: number; column: number },
end: { line: number; column: number },
) {
if (!assetsLocation) return;
const existing = assetsLocation.get(value) || [];
existing.push({
start: getLocationFromPosition(start, code),
end: getLocationFromPosition(end, code),
type,
});
assetsLocation.set(value, existing);
}

const dir = path.dirname(id);
// if (typeof options.production === 'boolean' && staticProcess.env.NODE_ENV === UNKNOWN)
// staticProcess.env.NODE_ENV = options.production ? 'production' : 'dev';
Expand Down Expand Up @@ -320,6 +363,7 @@ export default async function analyze(
ast = acorn.parse(code, {
ecmaVersion: 'latest',
allowReturnOutsideFunction: true,
locations: true,
});
isESM = false;
} catch (e: any) {
Expand All @@ -337,6 +381,7 @@ export default async function analyze(
ecmaVersion: 'latest',
sourceType: 'module',
allowAwaitOutsideFunction: true,
locations: true,
});
isESM = true;
} catch (e: any) {
Expand Down Expand Up @@ -431,6 +476,15 @@ export default async function analyze(
if (decl.type === 'ImportDeclaration') {
const source = String(decl.source.value);
deps.add(source);
// Track location
if (decl.source.loc) {
trackLocation(
'import',
source,
decl.source.loc.start,
decl.source.loc.end,
);
}
const staticModule =
staticModules[source.startsWith('node:') ? source.slice(5) : source];
if (staticModule) {
Expand All @@ -455,7 +509,19 @@ export default async function analyze(
decl.type === 'ExportNamedDeclaration' ||
decl.type === 'ExportAllDeclaration'
) {
if (decl.source) deps.add(String(decl.source.value));
if (decl.source) {
const source = String(decl.source.value);
deps.add(source);
// Track location
if (decl.source.loc) {
trackLocation(
'import',
source,
decl.source.loc.start,
decl.source.loc.end,
);
}
}
}
}
}
Expand Down Expand Up @@ -532,34 +598,59 @@ export default async function analyze(
});
}

async function processRequireArg(expression: Node, isImport = false) {
async function processRequireArg(
expression: Node,
isImport = false,
callNode?: Node,
) {
if (expression.type === 'ConditionalExpression') {
await processRequireArg(expression.consequent, isImport);
await processRequireArg(expression.alternate, isImport);
await processRequireArg(expression.consequent, isImport, callNode);
await processRequireArg(expression.alternate, isImport, callNode);
return;
}
if (expression.type === 'LogicalExpression') {
await processRequireArg(expression.left, isImport);
await processRequireArg(expression.right, isImport);
await processRequireArg(expression.left, isImport, callNode);
await processRequireArg(expression.right, isImport, callNode);
return;
}

let computed = await computePureStaticValue(expression, true);
if (!computed) return;

function add(value: string) {
function add(
value: string,
loc?: {
start: { line: number; column: number };
end: { line: number; column: number };
},
) {
(isImport ? imports : deps).add(value);
// Track location if available
if (loc && callNode && callNode.callee) {
const callee = callNode.callee;
const type =
callee.type === 'Identifier'
? `require.${callee.name}`
: callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier'
? `${callee.object.name}.${(callee.property as any).name}`
: isImport
? 'import'
: 'require';
trackLocation(type, value, loc.start, loc.end);
}
}

const argLoc = expression.loc;
if ('value' in computed && typeof computed.value === 'string') {
if (!computed.wildcards) add(computed.value);
if (!computed.wildcards) add(computed.value, argLoc);
else if (computed.wildcards.length >= 1)
emitWildcardRequire(computed.value);
} else {
if ('ifTrue' in computed && typeof computed.ifTrue === 'string')
add(computed.ifTrue);
add(computed.ifTrue, argLoc);
if ('else' in computed && typeof computed.else === 'string')
add(computed.else);
add(computed.else, argLoc);
}
}

Expand Down Expand Up @@ -658,7 +749,7 @@ export default async function analyze(
staticChildNode = node;
await backtrack(parent, this);
} else if (node.type === 'ImportExpression') {
await processRequireArg(node.source, true);
await processRequireArg(node.source, true, node);
return;
}
// Call expression cases and asset triggers
Expand All @@ -678,7 +769,7 @@ export default async function analyze(
knownBindings.require &&
knownBindings.require.shadowDepth === 0
) {
await processRequireArg(node.arguments[0]);
await processRequireArg(node.arguments[0], false, node);
return;
}
} else if (
Expand All @@ -692,7 +783,7 @@ export default async function analyze(
node.callee.property.name === 'require' &&
node.arguments.length
) {
await processRequireArg(node.arguments[0]);
await processRequireArg(node.arguments[0], false, node);
return;
} else if (
(!isESM || job.mixedModules) &&
Expand All @@ -706,7 +797,7 @@ export default async function analyze(
node.callee.property.name === 'resolve' &&
node.arguments.length
) {
await processRequireArg(node.arguments[0]);
await processRequireArg(node.arguments[0], false, node);
return;
}

Expand Down Expand Up @@ -745,7 +836,7 @@ export default async function analyze(
(!knownBindings.require ||
knownBindings.require.shadowDepth === 0)
) {
await processRequireArg(node.arguments[0]);
await processRequireArg(node.arguments[0], false, node);
}
break;
// require('bindings')(...)
Expand Down Expand Up @@ -828,9 +919,19 @@ export default async function analyze(
) {
const bindingInfo = nbind(arg.value);
if (bindingInfo && bindingInfo.path) {
deps.add(
path.relative(dir, bindingInfo.path).replace(/\\/g, '/'),
);
const depPath = path
.relative(dir, bindingInfo.path)
.replace(/\\/g, '/');
deps.add(depPath);
// Track location
if (node.loc) {
trackLocation(
'nbind.init',
depPath,
node.loc.start,
node.loc.end,
);
}
return this.skip();
}
}
Expand All @@ -845,7 +946,7 @@ export default async function analyze(
node.arguments[0].value === 'view engine' &&
!definedExpressEngines
) {
await processRequireArg(node.arguments[1]);
await processRequireArg(node.arguments[1], false, node);
return this.skip();
}
break;
Expand Down Expand Up @@ -939,10 +1040,28 @@ export default async function analyze(
: './' + srcPath;

imports.add(relativeSrcPath);
// Track location
if (node.arguments[0].loc) {
trackLocation(
'module.createRequire',
relativeSrcPath,
node.arguments[0].loc.start,
node.arguments[0].loc.end,
);
}
}
} else {
// It's a bare specifier, so just add into the imports
imports.add(pathOrSpecifier);
// Track location
if (node.arguments[0].loc) {
trackLocation(
'module.createRequire',
pathOrSpecifier,
node.arguments[0].loc.start,
node.arguments[0].loc.end,
);
}
}
}
break;
Expand Down Expand Up @@ -1150,7 +1269,7 @@ export default async function analyze(
});

await assetEmissionPromises;
return { assets, deps, imports, isESM };
return { assets, deps, imports, isESM, assetsLocation };

async function emitAssetPath(assetPath: string) {
// verify the asset file / directory exists
Expand Down
Loading
Loading