Generates and consumes source maps for debugging tools that map minified code back to original source code
—
The SourceNode class provides a high-level API for building source maps through code concatenation and manipulation. It offers fluent method chaining and automatic source map generation, making it ideal for template engines, code generators, and build tools that construct JavaScript programmatically.
Build source maps through hierarchical code construction with automatic position tracking.
/**
* SourceNodes provide a way to abstract over interpolating/concatenating
* snippets of generated JavaScript source code while maintaining the line and
* column information associated with the original source code
* @param line - The original line number (1-based, nullable)
* @param column - The original column number (0-based, nullable)
* @param source - The original source's filename (nullable)
* @param chunks - Optional array of strings or SourceNodes to add as children
* @param name - The original identifier name (nullable)
*/
class SourceNode {
constructor(
line?: number | null,
column?: number | null,
source?: string | null,
chunks?: Array<string | SourceNode> | SourceNode | string,
name?: string
);
/** Child source nodes and strings */
children: SourceNode[];
/** Source content storage for associated files */
sourceContents: { [sourceFile: string]: string };
/** Original line number (1-based, can be null) */
line: number | null;
/** Original column number (0-based, can be null) */
column: number | null;
/** Original source file name (can be null) */
source: string | null;
/** Original identifier name (can be null) */
name: string | null;
}Usage Examples:
import { SourceNode } from "source-map";
// Create a simple source node
const node = new SourceNode(1, 0, "math.js", "function add(a, b) {");
// Create with multiple chunks
const complexNode = new SourceNode(10, 4, "utils.js", [
"if (",
new SourceNode(10, 8, "utils.js", "condition"),
") {\n return true;\n}"
]);
// Create without position info (for generated code)
const generatedNode = new SourceNode(null, null, null, "/* Generated code */");
// Access properties directly
console.log(complexNode.children.length); // Number of child nodes
console.log(complexNode.sourceContents); // Object containing source file contents
console.log(complexNode.line, complexNode.column); // Original position infoBuild SourceNodes from code with an existing source map.
/**
* Creates a SourceNode from generated code and a SourceMapConsumer
* @param code - The generated code string
* @param sourceMapConsumer - The source map for the generated code
* @param relativePath - Optional relative path for the generated code file
* @returns SourceNode representing the code with source map information
*/
static fromStringWithSourceMap(
code: string,
sourceMapConsumer: SourceMapConsumer,
relativePath?: string
): SourceNode;Usage:
import { SourceNode, SourceMapConsumer } from "source-map";
// Load existing source map
const consumer = await new SourceMapConsumer(existingSourceMap);
// Create SourceNode from code + source map
const node = SourceNode.fromStringWithSourceMap(
generatedCode,
consumer,
"output.js"
);
// Now you can manipulate the node further
node.add("\n// Additional code");
// Don't forget to clean up
consumer.destroy();Add, prepend, and modify source node content with fluent chaining.
/**
* Add a chunk to the end of this source node
* @param chunk - String, SourceNode, or array of strings/SourceNodes to add
* @returns This SourceNode for chaining
*/
add(chunk: Array<string | SourceNode> | SourceNode | string): SourceNode;
/**
* Add a chunk to the beginning of this source node
* @param chunk - String, SourceNode, or array of strings/SourceNodes to prepend
* @returns This SourceNode for chaining
*/
prepend(chunk: Array<string | SourceNode> | SourceNode | string): SourceNode;
/**
* Set the source content for a source file
* @param sourceFile - The filename of the original source
* @param sourceContent - The content of the original source file
*/
setSourceContent(sourceFile: string, sourceContent: string): void;Usage Examples:
// Build complex structure with chaining
const functionNode = new SourceNode(1, 0, "math.js")
.add("function multiply(")
.add(new SourceNode(1, 17, "math.js", "a", "paramA"))
.add(", ")
.add(new SourceNode(1, 20, "math.js", "b", "paramB"))
.add(") {\n")
.add(new SourceNode(2, 2, "math.js", "return a * b;"))
.add("\n}");
// Prepend header comment
functionNode.prepend("/* Math utilities */\n");
// Add multiple chunks at once
functionNode.add([
"\n\n",
new SourceNode(5, 0, "math.js", "function divide(a, b) {"),
new SourceNode(6, 2, "math.js", "return a / b;"),
"\n}"
]);
// Set source content for debugging
functionNode.setSourceContent("math.js", `
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
return a / b;
}
`);Modify source node content with string operations while preserving source map information.
/**
* Join all children with a separator string
* @param sep - Separator string to insert between children
* @returns New SourceNode with joined children
*/
join(sep: string): SourceNode;
/**
* Call String.prototype.replace on the rightmost child (if it's a string)
* @param pattern - Pattern to search for (string or RegExp)
* @param replacement - Replacement string
* @returns This SourceNode for chaining
*/
replaceRight(pattern: string, replacement: string): SourceNode;Usage:
// Create array of elements
const elements = [
new SourceNode(1, 0, "data.js", "first"),
new SourceNode(2, 0, "data.js", "second"),
new SourceNode(3, 0, "data.js", "third")
];
// Join with commas
const arrayNode = new SourceNode(null, null, null, elements).join(", ");
console.log(arrayNode.toString()); // "first, second, third"
// Replace trailing content
const codeNode = new SourceNode(10, 0, "test.js", "console.log('debug');");
codeNode.replaceRight("'debug'", "'production'");
console.log(codeNode.toString()); // "console.log('production');"Walk through source node hierarchies for analysis and transformation.
/**
* Walk over the tree of JS snippets in this node and its children
* @param fn - Function called for each chunk with its associated mapping
*/
walk(fn: (chunk: string, mapping: MappedPosition) => void): void;
/**
* Walk over the tree of SourceNodes and gather their source content
* @param fn - Function called for each source file and its content
*/
walkSourceContents(fn: (file: string, content: string) => void): void;
interface MappedPosition {
source: string;
line: number;
column: number;
name?: string;
}Usage Examples:
// Analyze all code chunks
node.walk((chunk, mapping) => {
console.log(`Chunk: "${chunk}"`);
if (mapping.source) {
console.log(` From: ${mapping.source}:${mapping.line}:${mapping.column}`);
if (mapping.name) {
console.log(` Name: ${mapping.name}`);
}
}
});
// Gather all source contents
const sourceFiles = new Map();
node.walkSourceContents((file, content) => {
sourceFiles.set(file, content);
console.log(`Source file: ${file} (${content.length} characters)`);
});
// Find all references to a specific identifier
const references = [];
node.walk((chunk, mapping) => {
if (mapping.name === "myFunction") {
references.push({
chunk,
source: mapping.source,
line: mapping.line,
column: mapping.column
});
}
});Generate final code and source maps from the source node hierarchy.
/**
* Return the string representation of this source node
* @returns Concatenated string of all children
*/
toString(): string;
/**
* Returns the string representation of this source node along with a SourceMapGenerator
* @param startOfSourceMap - Optional configuration for the generated source map
* @returns Object containing the generated code and source map
*/
toStringWithSourceMap(startOfSourceMap?: StartOfSourceMap): CodeWithSourceMap;
interface StartOfSourceMap {
file?: string;
sourceRoot?: string;
skipValidation?: boolean;
}
interface CodeWithSourceMap {
code: string;
map: SourceMapGenerator;
}Usage Examples:
// Get generated code only
const generatedCode = node.toString();
console.log(generatedCode);
// Get code with source map
const result = node.toStringWithSourceMap({
file: "bundle.js",
sourceRoot: "/project/src/"
});
console.log("Generated code:", result.code);
console.log("Source map:", result.map.toString());
// Write to files
import fs from 'fs';
fs.writeFileSync('bundle.js', result.code);
fs.writeFileSync('bundle.js.map', result.map.toString());
// Add source map reference to code
const codeWithReference = result.code + '\n//# sourceMappingURL=bundle.js.map';
fs.writeFileSync('bundle-with-map.js', codeWithReference);// Template compilation with source mapping
function compileTemplate(template: string, filename: string): CodeWithSourceMap {
const root = new SourceNode(null, null, null);
// Parse template and build nodes
const lines = template.split('\n');
lines.forEach((line, index) => {
if (line.startsWith('<%= ')) {
// Expression: map to original template line
const expr = line.slice(4, -3);
root.add(new SourceNode(index + 1, 4, filename, `output += ${expr};\n`));
} else {
// Literal: add as generated code
root.add(`output += ${JSON.stringify(line + '\n')};\n`);
}
});
// Set original template content
root.setSourceContent(filename, template);
return root.toStringWithSourceMap({
file: filename.replace('.template', '.js')
});
}// Bundle multiple files with source maps
function bundleFiles(files: Array<{name: string, content: string}>): CodeWithSourceMap {
const bundle = new SourceNode(null, null, null);
files.forEach(file => {
// Add file separator comment
bundle.add(`\n/* === ${file.name} === */\n`);
// Add each line with position mapping
const lines = file.content.split('\n');
lines.forEach((line, index) => {
bundle.add(new SourceNode(index + 1, 0, file.name, line + '\n'));
});
// Embed source content
bundle.setSourceContent(file.name, file.content);
});
return bundle.toStringWithSourceMap({
file: 'bundle.js',
sourceRoot: '/src/'
});
}// Multi-stage transformation with source map preservation
async function transformCode(code: string, filename: string): Promise<CodeWithSourceMap> {
// Stage 1: Parse and create initial source node
let node = new SourceNode(1, 0, filename, code);
node.setSourceContent(filename, code);
// Stage 2: Apply transformations
node = node
.add('\n// Transformed code')
.prepend('(function() {\n')
.add('\n})();');
// Stage 3: Generate final result
return node.toStringWithSourceMap({
file: filename.replace('.js', '.transformed.js')
});
}// Combine SourceNode with manual SourceMapGenerator
const generator = new SourceMapGenerator({ file: 'output.js' });
const node = new SourceNode(null, null, null);
// Add manual mappings to generator
generator.addMapping({
generated: { line: 1, column: 0 },
original: { line: 1, column: 0 },
source: 'input.js'
});
// Convert generator to SourceNode for further manipulation
const consumer = SourceMapConsumer.fromSourceMap(generator);
const nodeFromGenerator = SourceNode.fromStringWithSourceMap(
'var x = 1;',
await consumer
);
consumer.destroy();// Load existing source map and extend it
const consumer = await new SourceMapConsumer(existingSourceMap);
const existingNode = SourceNode.fromStringWithSourceMap(existingCode, consumer);
// Extend with additional code
const extendedNode = new SourceNode(null, null, null, [
existingNode,
'\n// Additional functionality\n',
new SourceNode(100, 0, 'extensions.js', 'function extend() {}')
]);
consumer.destroy();
const final = extendedNode.toStringWithSourceMap({
file: 'extended.js'
});Install with Tessl CLI
npx tessl i tessl/npm-source-map