@chilts/weave 0.7.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.
package/dist/cli/index.js CHANGED
@@ -4,6 +4,8 @@ import { compile, compileWithSourceMap, definedMixins } from '../codegen/index.j
4
4
  import { lex, LexerError } from '../lexer/index.js';
5
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;
@@ -183,7 +221,7 @@ export function includeResolver(inputFile) {
183
221
  }
184
222
  catch (err) {
185
223
  if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
186
- throw new Error(`${path.relative(process.cwd(), file)}: no such file`);
224
+ throw new Error(`${path.relative(process.cwd(), file)}: no such file`, { cause: err });
187
225
  }
188
226
  throw err;
189
227
  }
@@ -283,22 +321,49 @@ export function compileDir(inputDir, outputDir, sourcemap = true) {
283
321
  console.error(`weave: no .weave files found in ${inputDir}`);
284
322
  process.exit(1);
285
323
  }
286
- let hasErrors = false;
287
- for (const relPath of files) {
288
- const inFile = path.join(inputDir, relPath);
289
- const outFile = path.join(outputDir, relPath.replace(/\.weave$/, '.ts'));
290
- 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;
291
348
  try {
292
- const elapsed = writeCompiled(inFile, outFile, source, sourcemap);
293
- console.log(`${outFile} (${formatElapsed(elapsed)})`);
349
+ source = readFileSync(inputFile, 'utf-8');
294
350
  }
295
351
  catch (err) {
296
- console.error(formatError(inFile, err, source));
297
- 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);
298
364
  }
299
365
  }
300
- pruneOrphans(inputDir, outputDir);
301
- return hasErrors;
366
+ return { written, failed };
302
367
  }
303
368
  // How often the reconciliation pass runs (see reconcile()).
304
369
  const RECONCILE_INTERVAL_MS = 2000;
@@ -529,6 +594,12 @@ export function main(argv) {
529
594
  console.log(usage());
530
595
  process.exit(0);
531
596
  }
597
+ if (args.mode === 'check') {
598
+ const code = check(args);
599
+ if (code !== 0)
600
+ process.exit(code);
601
+ return;
602
+ }
532
603
  const { inputPath, outputPath, sourcemap } = args;
533
604
  if (args.mode === 'watch') {
534
605
  watchDir(inputPath, outputPath, sourcemap);
@@ -536,9 +607,7 @@ export function main(argv) {
536
607
  }
537
608
  if (inputPath.endsWith('.weave')) {
538
609
  // Single-file mode
539
- const outFile = outputPath.endsWith('.ts')
540
- ? outputPath
541
- : path.join(outputPath, path.basename(inputPath).replace(/\.weave$/, '.ts'));
610
+ const outFile = singleOutputFile(inputPath, outputPath);
542
611
  const source = readFileSync(inputPath, 'utf-8');
543
612
  try {
544
613
  const elapsed = writeCompiled(inputPath, outFile, source, sourcemap);