CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-source-map

Generates and consumes source maps for debugging tools that map minified code back to original source code

Pending
Overview
Eval results
Files

source-node-building.mddocs/

Source Node Building

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.

Capabilities

SourceNode Class

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 info

Creating from Existing Source Maps

Build 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();

Building and Manipulating Nodes

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;
}
`);

String Manipulation

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');"

Traversal and Inspection

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
    });
  }
});

Code Generation

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);

Common Patterns

Template Engine Integration

// 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')
  });
}

Code Concatenation

// 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/'
  });
}

Transformation Pipeline

// 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')
  });
}

Integration with Other APIs

With SourceMapGenerator

// 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();

With SourceMapConsumer

// 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

docs

index.md

source-map-consumption.md

source-map-generation.md

source-node-building.md

tile.json