@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.
@@ -0,0 +1,14 @@
1
+ export interface CheckOptions {
2
+ inputPath: string;
3
+ outputPath: string;
4
+ /** tsconfig.json to check against; defaults to the nearest one above the cwd. */
5
+ project?: string;
6
+ }
7
+ /**
8
+ * `weave check`: compile the templates, type-check the whole tsconfig program
9
+ * with the compiled views added, and print every diagnostic — those in a
10
+ * compiled view at its .weave location. Progress goes to stderr and
11
+ * diagnostics to stdout. Returns the exit code: 1 if anything failed.
12
+ */
13
+ export declare function check(options: CheckOptions): number;
14
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { mapDiagnostic, formatDiagnostic } from '../diagnostics/index.js';
5
+ import { compileFiles, discoverFiles, pruneOrphans, singleOutputFile } from './index.js';
6
+ // tsc's "No inputs were found in config file". The compiled views are added
7
+ // as roots, so a tsconfig whose include matches nothing is still fine.
8
+ const NO_INPUTS = 18003;
9
+ /**
10
+ * The project's own typescript, so `check` agrees with the user's `tsc`.
11
+ * Falls back to one weave itself can resolve. Not a dependency of weave: only
12
+ * `check` needs it, and only when it runs.
13
+ */
14
+ function loadTypeScript() {
15
+ for (const base of [path.join(process.cwd(), 'noop.js'), import.meta.url]) {
16
+ try {
17
+ return createRequire(base)('typescript');
18
+ }
19
+ catch {
20
+ // Try the next location.
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ // Relative to the cwd when it is under it, so the path is short and editors
26
+ // resolve it against the directory `weave check` ran in.
27
+ function displayPath(file) {
28
+ const rel = path.relative(process.cwd(), file);
29
+ return rel.startsWith('..') || path.isAbsolute(rel) ? file : rel;
30
+ }
31
+ function readSource(file) {
32
+ try {
33
+ return readFileSync(file, 'utf-8');
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ function compilePairs(inputPath, outputPath) {
40
+ if (inputPath.endsWith('.weave')) {
41
+ return [{ inputFile: inputPath, outputFile: singleOutputFile(inputPath, outputPath) }];
42
+ }
43
+ let files;
44
+ try {
45
+ files = discoverFiles(inputPath);
46
+ }
47
+ catch (err) {
48
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
49
+ return `${inputPath}: no such file or directory`;
50
+ }
51
+ return `${inputPath}: ${err instanceof Error ? err.message : String(err)}`;
52
+ }
53
+ if (files.length === 0)
54
+ return `no .weave files found in ${inputPath}`;
55
+ return files.map(relPath => ({
56
+ inputFile: path.join(inputPath, relPath),
57
+ outputFile: path.join(outputPath, relPath.replace(/\.weave$/, '.ts')),
58
+ }));
59
+ }
60
+ /**
61
+ * `weave check`: compile the templates, type-check the whole tsconfig program
62
+ * with the compiled views added, and print every diagnostic — those in a
63
+ * compiled view at its .weave location. Progress goes to stderr and
64
+ * diagnostics to stdout. Returns the exit code: 1 if anything failed.
65
+ */
66
+ export function check(options) {
67
+ const ts = loadTypeScript();
68
+ if (!ts) {
69
+ console.error('weave: check needs the "typescript" package — npm i -D typescript');
70
+ return 1;
71
+ }
72
+ const project = options.project ?? ts.findConfigFile(process.cwd(), ts.sys.fileExists);
73
+ if (!project) {
74
+ console.error('weave: no tsconfig.json found; pass one with --project');
75
+ return 1;
76
+ }
77
+ if (!ts.sys.fileExists(project)) {
78
+ console.error(`weave: ${project}: no such file`);
79
+ return 1;
80
+ }
81
+ const pairs = compilePairs(options.inputPath, options.outputPath);
82
+ if (typeof pairs === 'string') {
83
+ console.error(`weave: ${pairs}`);
84
+ return 1;
85
+ }
86
+ const log = (msg) => console.error(msg);
87
+ const { written, failed } = compileFiles(pairs, true, log);
88
+ if (!options.inputPath.endsWith('.weave'))
89
+ pruneOrphans(options.inputPath, options.outputPath, log);
90
+ const config = ts.readConfigFile(project, ts.sys.readFile);
91
+ const parsed = config.error
92
+ ? undefined
93
+ : ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(path.resolve(project)), { noEmit: true }, project);
94
+ const rootNames = new Set((parsed?.fileNames ?? []).map(f => path.resolve(f)));
95
+ for (const file of written)
96
+ rootNames.add(path.resolve(file));
97
+ const diagnostics = config.error
98
+ ? [config.error]
99
+ : parsed.errors.filter(d => d.code !== NO_INPUTS);
100
+ if (!config.error) {
101
+ const program = ts.createProgram([...rootNames], parsed.options);
102
+ diagnostics.push(...ts.getPreEmitDiagnostics(program));
103
+ }
104
+ const errorFiles = new Set(failed.map(f => path.resolve(f)));
105
+ let errors = failed.length;
106
+ for (const d of diagnostics) {
107
+ const isError = d.category === ts.DiagnosticCategory.Error;
108
+ const message = (isError ? '' : 'warning: ')
109
+ + ts.flattenDiagnosticMessageText(d.messageText, '\n') + ` TS${d.code}`;
110
+ if (!d.file || d.start === undefined) {
111
+ console.log(`weave: ${message}`);
112
+ if (isError)
113
+ errors++;
114
+ continue;
115
+ }
116
+ const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
117
+ const diag = { file: d.file.fileName, line: line + 1, col: character + 1, message };
118
+ const mapped = mapDiagnostic(diag) ?? diag;
119
+ const source = mapped === diag ? d.file.text : readSource(mapped.file);
120
+ console.log(formatDiagnostic({ ...mapped, file: displayPath(mapped.file) }, source));
121
+ if (isError) {
122
+ errors++;
123
+ errorFiles.add(path.resolve(mapped.file));
124
+ }
125
+ }
126
+ if (errors === 0)
127
+ return 0;
128
+ const noun = errors === 1 ? 'error' : 'errors';
129
+ const where = errorFiles.size > 1 ? ` in ${errorFiles.size} files` : '';
130
+ console.log(`\nFound ${errors} ${noun}${where}.`);
131
+ return 1;
132
+ }
133
+ //# sourceMappingURL=check.js.map
@@ -1,3 +1,6 @@
1
+ import type { IncludeResolver } from '../codegen/index.js';
2
+ import { formatCodeFrame } from '../diagnostics/index.js';
3
+ import { check } from './check.js';
1
4
  interface CompileArgs {
2
5
  mode: 'compile';
3
6
  inputPath: string;
@@ -10,24 +13,59 @@ interface WatchArgs {
10
13
  outputPath: string;
11
14
  sourcemap: boolean;
12
15
  }
16
+ interface CheckArgs {
17
+ mode: 'check';
18
+ inputPath: string;
19
+ outputPath: string;
20
+ project?: string;
21
+ }
13
22
  interface HelpArgs {
14
23
  mode: 'help';
15
24
  }
16
- type CliArgs = CompileArgs | WatchArgs | HelpArgs;
25
+ type CliArgs = CompileArgs | WatchArgs | CheckArgs | HelpArgs;
17
26
  export declare class UsageError extends Error {
18
27
  constructor(message: string);
19
28
  }
20
29
  export declare function parseArgs(argv: string[]): CliArgs;
21
30
  export declare function usage(): string;
22
31
  export declare function discoverFiles(inputDir: string): string[];
23
- export declare function pruneOrphans(inputDir: string, outputDir: string): void;
32
+ export declare function pruneOrphans(inputDir: string, outputDir: string, log?: (msg: string) => void): void;
24
33
  export declare function formatElapsed(ms: number): string;
25
- export declare function formatCodeFrame(source: string, line: number, col: number): string;
34
+ export { formatCodeFrame, check };
26
35
  export declare function formatError(filePath: string, err: unknown, source?: string): string;
36
+ /** Resolves `include` paths against the including file and reads their mixins. */
37
+ export declare function includeResolver(inputFile: string): IncludeResolver;
38
+ /**
39
+ * Absolute paths of the .weave files a template includes. A template that does
40
+ * not lex or parse includes nothing as far as the watcher is concerned — its
41
+ * own compile reports the error.
42
+ */
43
+ export declare function includedFiles(inputFile: string, source: string): string[];
44
+ /**
45
+ * Templates under inputDir (relative paths) that include relPath. An includer's
46
+ * import line is built from the mixins its includes define, so it must be
47
+ * recompiled when one of them changes. Includes do not re-export, so only
48
+ * direct includers are affected.
49
+ */
50
+ export declare function findIncluders(inputDir: string, relPath: string): string[];
27
51
  export declare function compileFile(inputFile: string, outputFile: string, sourcemap?: boolean): void;
28
52
  export declare function compileDir(inputDir: string, outputDir: string, sourcemap?: boolean): boolean;
53
+ /** Where single-file mode writes: <output> itself if it is a .ts, else a file inside it. */
54
+ export declare function singleOutputFile(inputPath: string, outputPath: string): string;
55
+ export interface CompilePair {
56
+ inputFile: string;
57
+ outputFile: string;
58
+ }
59
+ /**
60
+ * Compile each pair, logging one line per success and printing each error to
61
+ * stderr. Returns the outputs written and the inputs that failed, so a caller
62
+ * can decide what a failure means.
63
+ */
64
+ export declare function compileFiles(pairs: CompilePair[], sourcemap: boolean, log?: (msg: string) => void): {
65
+ written: string[];
66
+ failed: string[];
67
+ };
29
68
  export declare function watchDir(inputDir: string, outputDir: string, sourcemap?: boolean): () => void;
30
69
  export declare function reconcile(inputDir: string, outputDir: string, sourcemap: boolean, pending: Map<string, ReturnType<typeof setTimeout>>): void;
31
70
  export declare function main(argv: string[]): void;
32
- export {};
33
71
  //# sourceMappingURL=index.d.ts.map
package/dist/cli/index.js CHANGED
@@ -1,9 +1,11 @@
1
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync, rmdirSync, watch, } from 'node:fs';
1
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync, rmdirSync, rmSync, watch, } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { compile, compileWithSourceMap } from '../codegen/index.js';
4
- import { LexerError } from '../lexer/index.js';
5
- import { ParserError } from '../parser/index.js';
3
+ import { compile, compileWithSourceMap, definedMixins } from '../codegen/index.js';
4
+ import { lex, LexerError } from '../lexer/index.js';
5
+ import { parse, ParserError } from '../parser/index.js';
6
6
  import { CodegenError } from '../codegen/index.js';
7
+ import { formatCodeFrame } from '../diagnostics/index.js';
8
+ import { check } from './check.js';
7
9
  export class UsageError extends Error {
8
10
  constructor(message) {
9
11
  super(message);
@@ -16,6 +18,9 @@ export function parseArgs(argv) {
16
18
  if (argv.includes('--help') || argv.includes('-h')) {
17
19
  return { mode: 'help' };
18
20
  }
21
+ if (argv[0] === 'check') {
22
+ return parseCheckArgs(argv.slice(1));
23
+ }
19
24
  const isWatch = argv.includes('--watch') || argv.includes('-w');
20
25
  const noSourcemap = argv.includes('--no-sourcemap');
21
26
  const positional = argv.filter(a => !FLAGS.includes(a));
@@ -35,12 +40,51 @@ export function parseArgs(argv) {
35
40
  }
36
41
  return { mode: 'compile', inputPath, outputPath, sourcemap: !noSourcemap };
37
42
  }
43
+ function parseCheckArgs(argv) {
44
+ let project;
45
+ const positional = [];
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const arg = argv[i];
48
+ if (arg === '--project' || arg === '-p') {
49
+ project = argv[++i];
50
+ if (project === undefined)
51
+ throw new UsageError(`${arg} requires a path`);
52
+ }
53
+ else if (arg.startsWith('--project=')) {
54
+ project = arg.slice('--project='.length);
55
+ }
56
+ else if (arg === '--watch' || arg === '-w') {
57
+ throw new UsageError('check does not support --watch yet');
58
+ }
59
+ else if (arg === '--no-sourcemap') {
60
+ throw new UsageError('check always writes source maps; --no-sourcemap is not allowed');
61
+ }
62
+ else {
63
+ positional.push(arg);
64
+ }
65
+ }
66
+ if (positional.length < 2) {
67
+ throw new UsageError('missing required arguments');
68
+ }
69
+ if (positional.length > 2) {
70
+ throw new UsageError('too many arguments');
71
+ }
72
+ const args = { mode: 'check', inputPath: positional[0], outputPath: positional[1] };
73
+ if (project !== undefined)
74
+ args.project = project;
75
+ return args;
76
+ }
38
77
  export function usage() {
39
78
  return [
40
79
  'Usage: weave [--watch] [--no-sourcemap] <input> <output>',
80
+ ' weave check [--project <tsconfig>] <input> <output>',
41
81
  '',
42
82
  ' Compile .weave templates to TypeScript.',
43
83
  '',
84
+ ' check compiles the templates, then type-checks the project with the',
85
+ ' TypeScript compiler and reports each error at its .weave location. It',
86
+ ' exits 1 if there are any errors. It needs the typescript package.',
87
+ '',
44
88
  ' <input> A .weave file or directory containing .weave files',
45
89
  ' <output> Output .ts file or directory for compiled output',
46
90
  '',
@@ -50,11 +94,13 @@ export function usage() {
50
94
  'Options:',
51
95
  ' --watch, -w Watch for changes and recompile (directory mode only)',
52
96
  ' --no-sourcemap Disable source map generation',
97
+ ' --project, -p tsconfig.json for check (default: nearest to the cwd)',
53
98
  '',
54
99
  'Examples:',
55
100
  ' weave views/ lib/views/ Compile all templates',
56
101
  ' weave views/page.weave lib/views/page.ts Compile single file',
57
102
  ' weave --watch views/ lib/views/ Watch and recompile on changes',
103
+ ' weave check views/ lib/views/ Compile and type-check',
58
104
  ].join('\n');
59
105
  }
60
106
  export function discoverFiles(inputDir) {
@@ -98,7 +144,7 @@ function walkAll(dir, base, files, dirs) {
98
144
  // gone is an orphan and is removed, so a deleted template stops being compiled
99
145
  // into the app. Files weave never emits — anything that is not .ts/.ts.map — are
100
146
  // left where they are, as are directories that still hold one of them.
101
- export function pruneOrphans(inputDir, outputDir) {
147
+ export function pruneOrphans(inputDir, outputDir, log = console.log) {
102
148
  const files = [];
103
149
  const dirs = [];
104
150
  try {
@@ -117,7 +163,7 @@ export function pruneOrphans(inputDir, outputDir) {
117
163
  const outFile = path.join(outputDir, relPath);
118
164
  try {
119
165
  unlinkSync(outFile);
120
- console.log(`${outFile} (removed)`);
166
+ log(`${outFile} (removed)`);
121
167
  }
122
168
  catch {
123
169
  // Already gone, or not ours to delete.
@@ -142,15 +188,7 @@ export function formatElapsed(ms) {
142
188
  return `${Math.round(ms)}ms`;
143
189
  return `${(ms / 1000).toFixed(2)}s`;
144
190
  }
145
- export function formatCodeFrame(source, line, col) {
146
- const lines = source.split('\n');
147
- if (line < 1 || line > lines.length)
148
- return '';
149
- const sourceLine = lines[line - 1];
150
- const lineNum = String(line);
151
- const gutter = ' '.repeat(lineNum.length);
152
- return `\n ${lineNum} | ${sourceLine}\n ${gutter} | ${' '.repeat(col - 1)}^`;
153
- }
191
+ export { formatCodeFrame, check };
154
192
  export function formatError(filePath, err, source) {
155
193
  let line;
156
194
  let col;
@@ -173,6 +211,69 @@ export function formatError(filePath, err, source) {
173
211
  }
174
212
  return result;
175
213
  }
214
+ /** Resolves `include` paths against the including file and reads their mixins. */
215
+ export function includeResolver(inputFile) {
216
+ return includePath => {
217
+ const file = path.resolve(path.dirname(inputFile), includePath + '.weave');
218
+ let source;
219
+ try {
220
+ source = readFileSync(file, 'utf-8');
221
+ }
222
+ catch (err) {
223
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
224
+ throw new Error(`${path.relative(process.cwd(), file)}: no such file`, { cause: err });
225
+ }
226
+ throw err;
227
+ }
228
+ return definedMixins(parse(lex(source)));
229
+ };
230
+ }
231
+ /**
232
+ * Absolute paths of the .weave files a template includes. A template that does
233
+ * not lex or parse includes nothing as far as the watcher is concerned — its
234
+ * own compile reports the error.
235
+ */
236
+ export function includedFiles(inputFile, source) {
237
+ if (!/^include\s/m.test(source))
238
+ return [];
239
+ let doc;
240
+ try {
241
+ doc = parse(lex(source));
242
+ }
243
+ catch {
244
+ return [];
245
+ }
246
+ return doc.children.flatMap(c => c.type === 'Include' ? [path.resolve(path.dirname(inputFile), c.path + '.weave')] : []);
247
+ }
248
+ /**
249
+ * Templates under inputDir (relative paths) that include relPath. An includer's
250
+ * import line is built from the mixins its includes define, so it must be
251
+ * recompiled when one of them changes. Includes do not re-export, so only
252
+ * direct includers are affected.
253
+ */
254
+ export function findIncluders(inputDir, relPath) {
255
+ const target = path.resolve(inputDir, relPath);
256
+ let files;
257
+ try {
258
+ files = discoverFiles(inputDir);
259
+ }
260
+ catch {
261
+ return [];
262
+ }
263
+ return files.filter(file => {
264
+ if (file === relPath)
265
+ return false;
266
+ const inFile = path.join(inputDir, file);
267
+ let source;
268
+ try {
269
+ source = readFileSync(inFile, 'utf-8');
270
+ }
271
+ catch {
272
+ return false;
273
+ }
274
+ return includedFiles(inFile, source).includes(target);
275
+ });
276
+ }
176
277
  function writeCompiled(inputFile, outputFile, source, sourcemap) {
177
278
  const start = performance.now();
178
279
  if (sourcemap) {
@@ -181,15 +282,19 @@ function writeCompiled(inputFile, outputFile, source, sourcemap) {
181
282
  const { code, sourceMap } = compileWithSourceMap(source, {
182
283
  file: basename,
183
284
  sourceFile: sourceRelPath,
285
+ resolveInclude: includeResolver(inputFile),
184
286
  });
185
287
  mkdirSync(path.dirname(outputFile), { recursive: true });
186
288
  writeFileSync(outputFile, code + `//# sourceMappingURL=${basename}.map\n`);
187
289
  writeFileSync(outputFile + '.map', JSON.stringify(sourceMap));
188
290
  }
189
291
  else {
190
- const result = compile(source);
292
+ const result = compile(source, { resolveInclude: includeResolver(inputFile) });
191
293
  mkdirSync(path.dirname(outputFile), { recursive: true });
192
294
  writeFileSync(outputFile, result);
295
+ // A map from an earlier sourcemap build no longer matches this .ts,
296
+ // and pruning keeps it because the .weave still exists.
297
+ rmSync(outputFile + '.map', { force: true });
193
298
  }
194
299
  return performance.now() - start;
195
300
  }
@@ -216,22 +321,49 @@ export function compileDir(inputDir, outputDir, sourcemap = true) {
216
321
  console.error(`weave: no .weave files found in ${inputDir}`);
217
322
  process.exit(1);
218
323
  }
219
- let hasErrors = false;
220
- for (const relPath of files) {
221
- const inFile = path.join(inputDir, relPath);
222
- const outFile = path.join(outputDir, relPath.replace(/\.weave$/, '.ts'));
223
- const source = readFileSync(inFile, 'utf-8');
324
+ const pairs = files.map(relPath => ({
325
+ inputFile: path.join(inputDir, relPath),
326
+ outputFile: path.join(outputDir, relPath.replace(/\.weave$/, '.ts')),
327
+ }));
328
+ const { failed } = compileFiles(pairs, sourcemap);
329
+ pruneOrphans(inputDir, outputDir);
330
+ return failed.length > 0;
331
+ }
332
+ /** Where single-file mode writes: <output> itself if it is a .ts, else a file inside it. */
333
+ export function singleOutputFile(inputPath, outputPath) {
334
+ return outputPath.endsWith('.ts')
335
+ ? outputPath
336
+ : path.join(outputPath, path.basename(inputPath).replace(/\.weave$/, '.ts'));
337
+ }
338
+ /**
339
+ * Compile each pair, logging one line per success and printing each error to
340
+ * stderr. Returns the outputs written and the inputs that failed, so a caller
341
+ * can decide what a failure means.
342
+ */
343
+ export function compileFiles(pairs, sourcemap, log = console.log) {
344
+ const written = [];
345
+ const failed = [];
346
+ for (const { inputFile, outputFile } of pairs) {
347
+ let source;
224
348
  try {
225
- const elapsed = writeCompiled(inFile, outFile, source, sourcemap);
226
- console.log(`${outFile} (${formatElapsed(elapsed)})`);
349
+ source = readFileSync(inputFile, 'utf-8');
227
350
  }
228
351
  catch (err) {
229
- console.error(formatError(inFile, err, source));
230
- hasErrors = true;
352
+ console.error(formatError(inputFile, err));
353
+ failed.push(inputFile);
354
+ continue;
355
+ }
356
+ try {
357
+ const elapsed = writeCompiled(inputFile, outputFile, source, sourcemap);
358
+ log(`${outputFile} (${formatElapsed(elapsed)})`);
359
+ written.push(outputFile);
360
+ }
361
+ catch (err) {
362
+ console.error(formatError(inputFile, err, source));
363
+ failed.push(inputFile);
231
364
  }
232
365
  }
233
- pruneOrphans(inputDir, outputDir);
234
- return hasErrors;
366
+ return { written, failed };
235
367
  }
236
368
  // How often the reconciliation pass runs (see reconcile()).
237
369
  const RECONCILE_INTERVAL_MS = 2000;
@@ -259,6 +391,13 @@ export function watchDir(inputDir, outputDir, sourcemap = true) {
259
391
  pending.set(relPath, setTimeout(() => {
260
392
  pending.delete(relPath);
261
393
  compileWatchedFile(inputDir, outputDir, relPath, sourcemap);
394
+ // Compiled directly rather than scheduled, so an include cycle
395
+ // cannot keep re-triggering itself. One with its own pending
396
+ // timer will be compiled by that.
397
+ for (const includer of findIncluders(inputDir, relPath)) {
398
+ if (!pending.has(includer))
399
+ compileWatchedFile(inputDir, outputDir, includer, sourcemap);
400
+ }
262
401
  }, 50));
263
402
  };
264
403
  const isDir = (relPath) => {
@@ -355,9 +494,9 @@ export function watchDir(inputDir, outputDir, sourcemap = true) {
355
494
  };
356
495
  }
357
496
  // Walk the input dir and recompile any .weave whose .ts output is missing or
358
- // older than the source. Files with a pending debounce timer are skipped so we
359
- // never race an edit the watcher is already handling — this only catches
360
- // changes whose fs.watch event was dropped.
497
+ // older than the source or any file it includes. Files with a pending debounce
498
+ // timer are skipped so we never race an edit the watcher is already handling —
499
+ // this only catches changes whose fs.watch event was dropped.
361
500
  export function reconcile(inputDir, outputDir, sourcemap, pending) {
362
501
  let files;
363
502
  try {
@@ -386,9 +525,6 @@ export function reconcile(inputDir, outputDir, sourcemap, pending) {
386
525
  catch {
387
526
  outMtime = undefined;
388
527
  }
389
- // Up to date — the watcher already handled this file.
390
- if (outMtime !== undefined && outMtime >= sourceMtime)
391
- continue;
392
528
  let source;
393
529
  try {
394
530
  source = readFileSync(inFile, 'utf-8');
@@ -396,6 +532,20 @@ export function reconcile(inputDir, outputDir, sourcemap, pending) {
396
532
  catch {
397
533
  continue;
398
534
  }
535
+ // The output also depends on the mixins each include defines, so an
536
+ // include edited since the last compile makes it stale too.
537
+ let newestMtime = sourceMtime;
538
+ for (const included of includedFiles(inFile, source)) {
539
+ try {
540
+ newestMtime = Math.max(newestMtime, statSync(included).mtimeMs);
541
+ }
542
+ catch {
543
+ // A missing include is reported when the template compiles.
544
+ }
545
+ }
546
+ // Up to date — the watcher already handled this file.
547
+ if (outMtime !== undefined && outMtime >= newestMtime)
548
+ continue;
399
549
  try {
400
550
  const elapsed = writeCompiled(inFile, outFile, source, sourcemap);
401
551
  console.log(`${outFile} (reconciled, ${formatElapsed(elapsed)})`);
@@ -444,6 +594,12 @@ export function main(argv) {
444
594
  console.log(usage());
445
595
  process.exit(0);
446
596
  }
597
+ if (args.mode === 'check') {
598
+ const code = check(args);
599
+ if (code !== 0)
600
+ process.exit(code);
601
+ return;
602
+ }
447
603
  const { inputPath, outputPath, sourcemap } = args;
448
604
  if (args.mode === 'watch') {
449
605
  watchDir(inputPath, outputPath, sourcemap);
@@ -451,9 +607,7 @@ export function main(argv) {
451
607
  }
452
608
  if (inputPath.endsWith('.weave')) {
453
609
  // Single-file mode
454
- const outFile = outputPath.endsWith('.ts')
455
- ? outputPath
456
- : path.join(outputPath, path.basename(inputPath).replace(/\.weave$/, '.ts'));
610
+ const outFile = singleOutputFile(inputPath, outputPath);
457
611
  const source = readFileSync(inputPath, 'utf-8');
458
612
  try {
459
613
  const elapsed = writeCompiled(inputPath, outFile, source, sourcemap);
@@ -5,22 +5,36 @@ export interface GenerateResult {
5
5
  code: string;
6
6
  mappings: Mapping[];
7
7
  }
8
+ /**
9
+ * Lists the mixins defined by the `.weave` file an `include` path names.
10
+ * The path is exactly as written after `include`; throw if it cannot be read.
11
+ */
12
+ export type IncludeResolver = (includePath: string) => string[];
13
+ export interface GenerateOptions {
14
+ /**
15
+ * Without a resolver, a lone `include` is assumed to provide every called
16
+ * mixin not defined in this file, and more than one `include` is an error.
17
+ */
18
+ resolveInclude?: IncludeResolver;
19
+ }
8
20
  export declare class CodegenError extends Error {
9
21
  readonly line: number;
10
22
  readonly col: number;
11
23
  constructor(message: string, line: number, col: number);
12
24
  }
25
+ /** Names of the mixins a document defines at top level. */
26
+ export declare function definedMixins(doc: DocumentNode): string[];
13
27
  /** Generate a TypeScript expression from a single AST node. */
14
28
  export declare function generateExpr(node: AstNode): string;
15
29
  /** Generate TypeScript source from a DocumentNode. */
16
- export declare function generate(doc: DocumentNode): GenerateResult;
30
+ export declare function generate(doc: DocumentNode, options?: GenerateOptions): GenerateResult;
17
31
  /** Full pipeline: source string → TypeScript source. */
18
- export declare function compile(source: string): string;
32
+ export declare function compile(source: string, options?: GenerateOptions): string;
19
33
  /** Full pipeline with source map generation. */
20
34
  export declare function compileWithSourceMap(source: string, opts: {
21
35
  file: string;
22
36
  sourceFile: string;
23
- }): {
37
+ } & GenerateOptions): {
24
38
  code: string;
25
39
  sourceMap: SourceMap;
26
40
  };