-
Notifications
You must be signed in to change notification settings - Fork 167
Description
Summary
Add location tracking to the analyze function to capture where assets, dependencies, and imports are referenced in source code. This enables downstream tools (like bundlers and transformers) to rewrite asset references while preserving source maps and code structure.
Motivation
Currently, @vercel/nft identifies which assets/dependencies a file uses but doesn't track where in the source code those references occur. This limits the ability to:
- Transform asset references: Build tools that need to rewrite asset paths (e.g., Vite plugins, custom bundlers) must re-parse and duplicate nft's sophisticated detection heuristics
- Generate accurate source maps: Without position information, transformations can't maintain proper source map mappings
Use Case
Vite/Rolldown/Esbuild Plugin for Asset Transformation
// Input code
const fontPath = path.join(__dirname, 'fonts', 'somefile.ttf');
const data = fs.readFileSync(fontPath);
// With location tracking, a Vite plugin can:
// 1. Detect the asset reference at the exact location
// 2. Emit the asset to the build output
// 3. Replace the expression with the new asset URL
// 4. Maintain accurate source maps
// Output code
const fontPath = new URL('./assets/somefile-abc123.ttf', import.meta.url).href;
const data = fs.readFileSync(fontPath);Proposed API
Enhanced AnalyzeResult Interface
export interface AssetLocation {
start: number; // Character offset where reference starts
end: number; // Character offset where reference ends
source: string; // Original source code text (maybe not necessary)
type: // Type of asset reference
| 'fs.readFileSync'
| 'fs.readFile'
| 'fs.createReadStream'
| 'path.join'
| 'path.resolve'
| '__dirname'
| '__filename'
| 'import.meta.url'
| 'bindings'
| 'node-gyp-build'
// | ...
| 'other';
}
export interface AnalyzeResult {
assets: Set<string>;
deps: Set<string>;
imports: Set<string>;
isESM: boolean;
// New: Location tracking
assetsLocation: Map<string, AssetLocation[]>;
}Example Usage
const result = await analyze('app.js', code, job);
// Access location information
for (const [assetPath, locations] of Object.entries(result.assetsLocation)) {
console.log(`Asset: ${assetPath}`);
for (const loc of locations) {
console.log(` Found at ${loc.start}-${loc.end}`);
console.log(` Type: ${loc.type}`);
console.log(` Code: ${loc.source}`);
}
}
// Use in transformations
const s = new MagicString(code);
for (const [assetPath, locations] of Object.entries(result.assetsLocation)) {
const newPath = transformAsset(assetPath);
// Replace in reverse order to preserve positions
for (const loc of locations.sort((a, b) => b.start - a.start)) {
s.overwrite(loc.start, loc.end, newPath);
}
}Coverage
I think that the following detection heuristics can be enhanced:
Fully Trackable (13 cases):
- ✅
require(),module.require(),require.resolve() - ✅ ESM
import/exportdeclarations - ✅ Dynamic
import()expressions - ✅
fs.*function calls (readFileSync, readFile, etc.) - ✅
path.*functions (join, resolve) - ✅
__dirname,__filenamereferences - ✅
import.meta.url - ✅ Special bindings (BINDINGS, NODE_GYP_BUILD, NBIND_INIT, etc.)
Partially Trackable (3 cases):
⚠️ Glob patterns (emitAssetDirectory,emitWildcardRequire)- Can track the expression that triggered the glob
- Cannot track individual files discovered from filesystem
Open Questions
-
Opt-in flag: Should location tracking be behind a feature flag to avoid performance overhead for users who don't need it?
const result = await analyze(id, code, { ...job, trackLocations: true });
-
Glob patterns: How should we represent assets discovered through glob patterns? Current proposal is to track the pattern expression but not individual files.