@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.
- package/CHANGELOG.md +303 -0
- package/ReadMe.md +261 -8
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.js +133 -0
- package/dist/cli/index.d.ts +42 -4
- package/dist/cli/index.js +190 -36
- package/dist/codegen/index.d.ts +17 -3
- package/dist/codegen/index.js +436 -238
- package/dist/codegen/sourcemap.d.ts +49 -1
- package/dist/codegen/sourcemap.js +154 -2
- package/dist/diagnostics/index.d.ts +25 -0
- package/dist/diagnostics/index.js +68 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -2
- package/dist/lexer/index.d.ts +5 -1
- package/dist/lexer/index.js +340 -92
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.js +74 -9
- package/dist/parser/types.d.ts +13 -0
- package/dist/runtime/index.d.ts +45 -1
- package/dist/runtime/index.js +67 -2
- package/package.json +23 -5
|
@@ -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
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
console.log(`${outFile} (${formatElapsed(elapsed)})`);
|
|
349
|
+
source = readFileSync(inputFile, 'utf-8');
|
|
227
350
|
}
|
|
228
351
|
catch (err) {
|
|
229
|
-
console.error(formatError(
|
|
230
|
-
|
|
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
|
-
|
|
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
|
|
359
|
-
// never race an edit the watcher is already handling —
|
|
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
|
|
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);
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -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
|
};
|