@chilts/weave 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { SourceLocation } from '../parser/types.js';
1
+ import type { SourceLocation, ValueLoc } from '../parser/types.js';
2
2
  /** A single mapping segment: generated position → source position (all 0-based) */
3
3
  export interface Mapping {
4
4
  genLine: number;
@@ -29,6 +29,34 @@ export declare function encodeVLQ(value: number): string;
29
29
  export declare function encodeMappings(mappings: Mapping[]): string;
30
30
  /** Create a CodeChunk with an optional mapping at position (0,0) tied to a source location. */
31
31
  export declare function chunk(code: string, loc?: SourceLocation): CodeChunk;
32
+ /** A zero-width chunk that maps the code following it to `loc`. */
33
+ export declare function mark(loc: SourceLocation): CodeChunk;
34
+ /**
35
+ * Concatenate code and chunks, placing each chunk's mappings where it lands:
36
+ * its first line shifts by the column it starts at, every line by the line.
37
+ */
38
+ export declare function concat(...parts: (string | CodeChunk)[]): CodeChunk;
39
+ /** Captured TS text and where its lines start in the source, when known. */
40
+ export interface Captured {
41
+ text: string;
42
+ loc?: ValueLoc | undefined;
43
+ }
44
+ /**
45
+ * The part of a captured value from `start` to `end`, with its line starts
46
+ * moved to match. Trims surrounding whitespace, as the slice is re-emitted on
47
+ * its own.
48
+ */
49
+ export declare function sliceCaptured(value: Captured, start: number, end?: number): Captured;
50
+ /**
51
+ * A chunk for captured TS, which weave re-emits character for character. Every
52
+ * token (a run of word characters, or any other non-space character) gets a
53
+ * mapping to its own source position, so a diagnostic anywhere in the value
54
+ * lands on the token it names. Splitting on character classes is not a TS
55
+ * parse; it only needs to put a mapping wherever tsc might start a span.
56
+ * Without a location the text is emitted unmapped, covered by whatever
57
+ * mapping precedes it.
58
+ */
59
+ export declare function verbatim(value: Captured): CodeChunk;
32
60
  /** Offset all mappings in an array by a line delta. */
33
61
  export declare function offsetMappings(mappings: Mapping[], lineOffset: number): Mapping[];
34
62
  /** Count newlines in a string. */
@@ -40,4 +68,24 @@ export declare function lineCount(code: string): number;
40
68
  export declare function joinChunks(chunks: CodeChunk[], separator: string): CodeChunk;
41
69
  /** Build a complete V3 source map object. */
42
70
  export declare function buildSourceMap(mappings: Mapping[], file: string, sourceFile: string): SourceMap;
71
+ /**
72
+ * Decode a V3 `mappings` string into absolute mappings (the inverse of
73
+ * encodeMappings). The source index and names are ignored: weave emits a
74
+ * single source and no names. Segments with no source position are skipped.
75
+ */
76
+ export declare function decodeMappings(mappings: string): Mapping[];
77
+ /** A position in the original source (1-based line and col). */
78
+ export interface OriginalPosition {
79
+ source: string;
80
+ line: number;
81
+ col: number;
82
+ }
83
+ /**
84
+ * Find the source position for a generated position (1-based line and col).
85
+ * Uses the last mapping at or before it on the same line, else the last
86
+ * mapping on the nearest earlier line. Weave maps every token of a captured TS
87
+ * expression to its own source position, and generated wrapper code to the
88
+ * start of the node that produced it.
89
+ */
90
+ export declare function originalPosition(map: SourceMap, line: number, col: number): OriginalPosition | undefined;
43
91
  //# sourceMappingURL=sourcemap.d.ts.map
@@ -24,14 +24,13 @@ export function encodeMappings(mappings) {
24
24
  return '';
25
25
  // Find max generated line
26
26
  const maxLine = sorted[sorted.length - 1].genLine;
27
- let prevGenCol = 0;
28
27
  let prevSrcLine = 0;
29
28
  let prevSrcCol = 0;
30
29
  let idx = 0;
31
30
  const lines = [];
32
31
  for (let line = 0; line <= maxLine; line++) {
33
32
  const segments = [];
34
- prevGenCol = 0; // genCol resets per line
33
+ let prevGenCol = 0; // genCol resets per line
35
34
  while (idx < sorted.length && sorted[idx].genLine === line) {
36
35
  const m = sorted[idx];
37
36
  let seg = encodeVLQ(m.genCol - prevGenCol);
@@ -62,6 +61,88 @@ export function chunk(code, loc) {
62
61
  }
63
62
  return { code, mappings };
64
63
  }
64
+ /** A zero-width chunk that maps the code following it to `loc`. */
65
+ export function mark(loc) {
66
+ return chunk('', loc);
67
+ }
68
+ /**
69
+ * Concatenate code and chunks, placing each chunk's mappings where it lands:
70
+ * its first line shifts by the column it starts at, every line by the line.
71
+ */
72
+ export function concat(...parts) {
73
+ let code = '';
74
+ const mappings = [];
75
+ let line = 0;
76
+ let col = 0;
77
+ for (const part of parts) {
78
+ const c = typeof part === 'string' ? { code: part, mappings: [] } : part;
79
+ for (const m of c.mappings) {
80
+ mappings.push({
81
+ ...m,
82
+ genLine: m.genLine + line,
83
+ genCol: m.genLine === 0 ? m.genCol + col : m.genCol,
84
+ });
85
+ }
86
+ code += c.code;
87
+ const lastNewline = c.code.lastIndexOf('\n');
88
+ if (lastNewline === -1) {
89
+ col += c.code.length;
90
+ }
91
+ else {
92
+ line += lineCount(c.code);
93
+ col = c.code.length - lastNewline - 1;
94
+ }
95
+ }
96
+ return { code, mappings };
97
+ }
98
+ /**
99
+ * The part of a captured value from `start` to `end`, with its line starts
100
+ * moved to match. Trims surrounding whitespace, as the slice is re-emitted on
101
+ * its own.
102
+ */
103
+ export function sliceCaptured(value, start, end = value.text.length) {
104
+ const raw = value.text.slice(start, end);
105
+ const from = start + (raw.length - raw.trimStart().length);
106
+ const text = raw.trim();
107
+ if (!value.loc)
108
+ return { text };
109
+ const before = value.text.slice(0, from);
110
+ const line = lineCount(before);
111
+ const first = value.loc[line];
112
+ if (!first)
113
+ return { text };
114
+ const col = first.col + from - (before.lastIndexOf('\n') + 1);
115
+ return { text, loc: [{ line: first.line, col }, ...value.loc.slice(line + 1, line + 1 + lineCount(text))] };
116
+ }
117
+ /**
118
+ * A chunk for captured TS, which weave re-emits character for character. Every
119
+ * token (a run of word characters, or any other non-space character) gets a
120
+ * mapping to its own source position, so a diagnostic anywhere in the value
121
+ * lands on the token it names. Splitting on character classes is not a TS
122
+ * parse; it only needs to put a mapping wherever tsc might start a span.
123
+ * Without a location the text is emitted unmapped, covered by whatever
124
+ * mapping precedes it.
125
+ */
126
+ export function verbatim(value) {
127
+ const { text, loc } = value;
128
+ const mappings = [];
129
+ if (loc) {
130
+ text.split('\n').forEach((lineText, genLine) => {
131
+ const start = loc[genLine];
132
+ if (!start)
133
+ return;
134
+ for (const m of lineText.matchAll(/[\w$]+|\S/g)) {
135
+ mappings.push({
136
+ genLine,
137
+ genCol: m.index,
138
+ srcLine: start.line - 1,
139
+ srcCol: start.col - 1 + m.index,
140
+ });
141
+ }
142
+ });
143
+ }
144
+ return { code: text, mappings };
145
+ }
65
146
  /** Offset all mappings in an array by a line delta. */
66
147
  export function offsetMappings(mappings, lineOffset) {
67
148
  if (lineOffset === 0)
@@ -110,4 +191,75 @@ export function buildSourceMap(mappings, file, sourceFile) {
110
191
  mappings: encodeMappings(mappings),
111
192
  };
112
193
  }
194
+ // --- Decoding ---
195
+ const BASE64_VALUES = new Map([...BASE64].map((c, i) => [c, i]));
196
+ /** Decode one segment's VLQ fields. */
197
+ function decodeSegment(segment) {
198
+ const values = [];
199
+ let shift = 0;
200
+ let vlq = 0;
201
+ for (const c of segment) {
202
+ const digit = BASE64_VALUES.get(c);
203
+ if (digit === undefined)
204
+ throw new Error(`Invalid base64 character in source map: ${c}`);
205
+ vlq += (digit & 0b11111) << shift;
206
+ if (digit & 0b100000) {
207
+ shift += 5;
208
+ }
209
+ else {
210
+ values.push(vlq & 1 ? -(vlq >>> 1) : vlq >>> 1);
211
+ shift = 0;
212
+ vlq = 0;
213
+ }
214
+ }
215
+ return values;
216
+ }
217
+ /**
218
+ * Decode a V3 `mappings` string into absolute mappings (the inverse of
219
+ * encodeMappings). The source index and names are ignored: weave emits a
220
+ * single source and no names. Segments with no source position are skipped.
221
+ */
222
+ export function decodeMappings(mappings) {
223
+ const result = [];
224
+ let srcLine = 0;
225
+ let srcCol = 0;
226
+ mappings.split(';').forEach((line, genLine) => {
227
+ let genCol = 0; // genCol resets per line
228
+ for (const segment of line.split(',')) {
229
+ if (segment === '')
230
+ continue;
231
+ const fields = decodeSegment(segment);
232
+ genCol += fields[0];
233
+ if (fields.length < 4)
234
+ continue;
235
+ srcLine += fields[2];
236
+ srcCol += fields[3];
237
+ result.push({ genLine, genCol, srcLine, srcCol });
238
+ }
239
+ });
240
+ return result;
241
+ }
242
+ /**
243
+ * Find the source position for a generated position (1-based line and col).
244
+ * Uses the last mapping at or before it on the same line, else the last
245
+ * mapping on the nearest earlier line. Weave maps every token of a captured TS
246
+ * expression to its own source position, and generated wrapper code to the
247
+ * start of the node that produced it.
248
+ */
249
+ export function originalPosition(map, line, col) {
250
+ const source = map.sources[0];
251
+ if (source === undefined)
252
+ return undefined;
253
+ const genLine = line - 1;
254
+ const genCol = col - 1;
255
+ let best;
256
+ for (const m of decodeMappings(map.mappings)) {
257
+ if (m.genLine > genLine || (m.genLine === genLine && m.genCol > genCol))
258
+ break;
259
+ best = m;
260
+ }
261
+ if (!best)
262
+ return undefined;
263
+ return { source, line: best.srcLine + 1, col: best.srcCol + 1 };
264
+ }
113
265
  //# sourceMappingURL=sourcemap.js.map
@@ -0,0 +1,25 @@
1
+ /** A diagnostic at a 1-based line and col, from tsc or anything else. */
2
+ export interface Diagnostic {
3
+ file: string;
4
+ line: number;
5
+ col: number;
6
+ message: string;
7
+ }
8
+ /** Returns a file's contents, or undefined if it cannot be read. */
9
+ export type ReadFile = (file: string) => string | undefined;
10
+ /**
11
+ * Translate a diagnostic in a compiled .ts file to its .weave source, using
12
+ * the source map weave wrote beside it. The returned file is the .weave path,
13
+ * resolved the same way as the diagnostic's file (relative stays relative).
14
+ * Returns undefined when the file has no usable map — it is not a weave
15
+ * output, or was compiled with --no-sourcemap — so the caller can report the
16
+ * diagnostic as-is.
17
+ */
18
+ export declare function mapDiagnostic(diag: Diagnostic, readFile?: ReadFile): Diagnostic | undefined;
19
+ export declare function formatCodeFrame(source: string, line: number, col: number): string;
20
+ /**
21
+ * Format a diagnostic like a LexerError or ParserError: `weave: file:L:C: msg`,
22
+ * followed by a code frame when the source is given.
23
+ */
24
+ export declare function formatDiagnostic(diag: Diagnostic, source?: string): string;
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { originalPosition } from '../codegen/sourcemap.js';
4
+ function readFromDisk(file) {
5
+ try {
6
+ return readFileSync(file, 'utf-8');
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
12
+ /** The map file a compiled .ts points at, falling back to `<file>.map`. */
13
+ function mapFileFor(file, code) {
14
+ const url = code?.match(/^\/\/# sourceMappingURL=(.+)$/m)?.[1]?.trim();
15
+ return url ? path.join(path.dirname(file), url) : file + '.map';
16
+ }
17
+ function parseSourceMap(json) {
18
+ try {
19
+ const map = JSON.parse(json);
20
+ return map.version === 3 && Array.isArray(map.sources) && typeof map.mappings === 'string'
21
+ ? map
22
+ : undefined;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ /**
29
+ * Translate a diagnostic in a compiled .ts file to its .weave source, using
30
+ * the source map weave wrote beside it. The returned file is the .weave path,
31
+ * resolved the same way as the diagnostic's file (relative stays relative).
32
+ * Returns undefined when the file has no usable map — it is not a weave
33
+ * output, or was compiled with --no-sourcemap — so the caller can report the
34
+ * diagnostic as-is.
35
+ */
36
+ export function mapDiagnostic(diag, readFile = readFromDisk) {
37
+ const json = readFile(mapFileFor(diag.file, readFile(diag.file)));
38
+ if (json === undefined)
39
+ return undefined;
40
+ const map = parseSourceMap(json);
41
+ if (!map)
42
+ return undefined;
43
+ const pos = originalPosition(map, diag.line, diag.col);
44
+ if (!pos)
45
+ return undefined;
46
+ const file = path.join(path.dirname(diag.file), map.sourceRoot ?? '', pos.source);
47
+ return { file, line: pos.line, col: pos.col, message: diag.message };
48
+ }
49
+ export function formatCodeFrame(source, line, col) {
50
+ const lines = source.split('\n');
51
+ if (line < 1 || line > lines.length)
52
+ return '';
53
+ const sourceLine = lines[line - 1];
54
+ const lineNum = String(line);
55
+ const gutter = ' '.repeat(lineNum.length);
56
+ return `\n ${lineNum} | ${sourceLine}\n ${gutter} | ${' '.repeat(col - 1)}^`;
57
+ }
58
+ /**
59
+ * Format a diagnostic like a LexerError or ParserError: `weave: file:L:C: msg`,
60
+ * followed by a code frame when the source is given.
61
+ */
62
+ export function formatDiagnostic(diag, source) {
63
+ let result = `weave: ${diag.file}:${diag.line}:${diag.col}: ${diag.message}`;
64
+ if (source)
65
+ result += formatCodeFrame(source, diag.line, diag.col);
66
+ return result;
67
+ }
68
+ //# sourceMappingURL=index.js.map
package/dist/index.d.ts CHANGED
@@ -1,7 +1,11 @@
1
- export { h, doctype, Fragment, raw, escape, SafeHtml } from './runtime/index.js';
2
- export type { Attrs } from './runtime/index.js';
3
- export { compile, compileWithSourceMap, generate, generateExpr, CodegenError } from './codegen/index.js';
4
- export type { GenerateResult, CodeChunk, Mapping, SourceMap } from './codegen/index.js';
1
+ export { h, doctype, Fragment, raw, mergeClass, each, interpolate, escape, SafeHtml } from './runtime/index.js';
2
+ export type { Attrs, AttrValue, ClassValue, StyleValue } from './runtime/index.js';
3
+ export { compile, compileWithSourceMap, generate, generateExpr, definedMixins, CodegenError } from './codegen/index.js';
4
+ export type { GenerateResult, GenerateOptions, IncludeResolver, CodeChunk, Mapping, SourceMap } from './codegen/index.js';
5
+ export { decodeMappings, originalPosition } from './codegen/sourcemap.js';
6
+ export type { OriginalPosition } from './codegen/sourcemap.js';
7
+ export { mapDiagnostic, formatDiagnostic, formatCodeFrame } from './diagnostics/index.js';
8
+ export type { Diagnostic, ReadFile } from './diagnostics/index.js';
5
9
  export { lex, LexerError } from './lexer/index.js';
6
10
  export type { Token, TokenType } from './lexer/index.js';
7
11
  export { parse, ParserError } from './parser/index.js';
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // Runtime
2
- export { h, doctype, Fragment, raw, escape, SafeHtml } from './runtime/index.js';
2
+ export { h, doctype, Fragment, raw, mergeClass, each, interpolate, escape, SafeHtml } from './runtime/index.js';
3
3
  // Compiler
4
- export { compile, compileWithSourceMap, generate, generateExpr, CodegenError } from './codegen/index.js';
4
+ export { compile, compileWithSourceMap, generate, generateExpr, definedMixins, CodegenError } from './codegen/index.js';
5
+ export { decodeMappings, originalPosition } from './codegen/sourcemap.js';
6
+ export { mapDiagnostic, formatDiagnostic, formatCodeFrame } from './diagnostics/index.js';
5
7
  export { lex, LexerError } from './lexer/index.js';
6
8
  export { parse, ParserError } from './parser/index.js';
7
9
  //# sourceMappingURL=index.js.map
@@ -1,9 +1,13 @@
1
- export type TokenType = 'NEWLINE' | 'INDENT' | 'DEDENT' | 'EOF' | 'IMPORT' | 'TEMPLATE' | 'MIXIN' | 'EXTENDS' | 'BLOCK' | 'INCLUDE' | 'DOCTYPE' | 'IF' | 'ELSE_IF' | 'ELSE' | 'FOR' | 'TAG' | 'CLASS' | 'ID' | 'ATTR_START' | 'ATTR_END' | 'ATTR_NAME' | 'ATTR_VALUE' | 'SPREAD' | 'BUFFERED_CODE' | 'TEXT' | 'INTERPOLATION' | 'PIPE' | 'RAW_TEXT' | 'MIXIN_CALL' | 'COMMENT' | 'PASSTHROUGH_COMMENT' | 'PARAMS' | 'IDENTIFIER' | 'PATH';
1
+ export type TokenType = 'NEWLINE' | 'INDENT' | 'DEDENT' | 'EOF' | 'IMPORT' | 'TEMPLATE' | 'MIXIN' | 'EXTENDS' | 'BLOCK' | 'INCLUDE' | 'DOCTYPE' | 'IF' | 'UNLESS' | 'ELSE_IF' | 'ELSE' | 'FOR' | 'TAG' | 'CLASS' | 'ID' | 'ATTR_START' | 'ATTR_END' | 'ATTR_NAME' | 'ATTR_VALUE' | 'SPREAD' | 'BUFFERED_CODE' | 'TEXT' | 'INTERPOLATION' | 'PIPE' | 'RAW_TEXT' | 'MIXIN_CALL' | 'COMMENT' | 'PASSTHROUGH_COMMENT' | 'PARAMS' | 'IDENTIFIER' | 'PATH';
2
2
  export interface Token {
3
3
  type: TokenType;
4
4
  value: string;
5
5
  line: number;
6
6
  col: number;
7
+ starts?: {
8
+ line: number;
9
+ col: number;
10
+ }[];
7
11
  }
8
12
  export declare class LexerError extends Error {
9
13
  readonly line: number;