skir 0.0.1
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/README.md +447 -0
- package/dist/casing.d.ts +8 -0
- package/dist/casing.d.ts.map +1 -0
- package/dist/casing.js +49 -0
- package/dist/casing.js.map +1 -0
- package/dist/casing.test.d.ts +2 -0
- package/dist/casing.test.d.ts.map +1 -0
- package/dist/casing.test.js +134 -0
- package/dist/casing.test.js.map +1 -0
- package/dist/command_line_parser.d.ts +33 -0
- package/dist/command_line_parser.d.ts.map +1 -0
- package/dist/command_line_parser.js +171 -0
- package/dist/command_line_parser.js.map +1 -0
- package/dist/command_line_parser.test.d.ts +2 -0
- package/dist/command_line_parser.test.d.ts.map +1 -0
- package/dist/command_line_parser.test.js +302 -0
- package/dist/command_line_parser.test.js.map +1 -0
- package/dist/compatibility_checker.d.ts +68 -0
- package/dist/compatibility_checker.d.ts.map +1 -0
- package/dist/compatibility_checker.js +328 -0
- package/dist/compatibility_checker.js.map +1 -0
- package/dist/compatibility_checker.test.d.ts +2 -0
- package/dist/compatibility_checker.test.d.ts.map +1 -0
- package/dist/compatibility_checker.test.js +528 -0
- package/dist/compatibility_checker.test.js.map +1 -0
- package/dist/compiler.d.ts +3 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +358 -0
- package/dist/compiler.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -0
- package/dist/definition_finder.d.ts +12 -0
- package/dist/definition_finder.d.ts.map +1 -0
- package/dist/definition_finder.js +180 -0
- package/dist/definition_finder.js.map +1 -0
- package/dist/definition_finder.test.d.ts +2 -0
- package/dist/definition_finder.test.d.ts.map +1 -0
- package/dist/definition_finder.test.js +164 -0
- package/dist/definition_finder.test.js.map +1 -0
- package/dist/encoding.d.ts +2 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +38 -0
- package/dist/encoding.js.map +1 -0
- package/dist/encoding.test.d.ts +2 -0
- package/dist/encoding.test.d.ts.map +1 -0
- package/dist/encoding.test.js +23 -0
- package/dist/encoding.test.js.map +1 -0
- package/dist/error_renderer.d.ts +10 -0
- package/dist/error_renderer.d.ts.map +1 -0
- package/dist/error_renderer.js +247 -0
- package/dist/error_renderer.js.map +1 -0
- package/dist/formatter.d.ts +3 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +263 -0
- package/dist/formatter.js.map +1 -0
- package/dist/formatter.test.d.ts +2 -0
- package/dist/formatter.test.d.ts.map +1 -0
- package/dist/formatter.test.js +156 -0
- package/dist/formatter.test.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +14 -0
- package/dist/index.test.js.map +1 -0
- package/dist/io.d.ts +13 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +22 -0
- package/dist/io.js.map +1 -0
- package/dist/language_server.d.ts +15 -0
- package/dist/language_server.d.ts.map +1 -0
- package/dist/language_server.js +248 -0
- package/dist/language_server.js.map +1 -0
- package/dist/literals.d.ts +13 -0
- package/dist/literals.d.ts.map +1 -0
- package/dist/literals.js +100 -0
- package/dist/literals.js.map +1 -0
- package/dist/literals.test.d.ts +2 -0
- package/dist/literals.test.d.ts.map +1 -0
- package/dist/literals.test.js +149 -0
- package/dist/literals.test.js.map +1 -0
- package/dist/module_collector.d.ts +3 -0
- package/dist/module_collector.d.ts.map +1 -0
- package/dist/module_collector.js +22 -0
- package/dist/module_collector.js.map +1 -0
- package/dist/module_set.d.ts +44 -0
- package/dist/module_set.d.ts.map +1 -0
- package/dist/module_set.js +1025 -0
- package/dist/module_set.js.map +1 -0
- package/dist/module_set.test.d.ts +2 -0
- package/dist/module_set.test.d.ts.map +1 -0
- package/dist/module_set.test.js +1330 -0
- package/dist/module_set.test.js.map +1 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +971 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +2 -0
- package/dist/parser.test.d.ts.map +1 -0
- package/dist/parser.test.js +1366 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/snapshotter.d.ts +6 -0
- package/dist/snapshotter.d.ts.map +1 -0
- package/dist/snapshotter.js +107 -0
- package/dist/snapshotter.js.map +1 -0
- package/dist/tokenizer.d.ts +4 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +192 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/tokenizer.test.d.ts +2 -0
- package/dist/tokenizer.test.d.ts.map +1 -0
- package/dist/tokenizer.test.js +425 -0
- package/dist/tokenizer.test.js.map +1 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/casing.ts +64 -0
- package/src/command_line_parser.ts +249 -0
- package/src/compatibility_checker.ts +470 -0
- package/src/compiler.ts +435 -0
- package/src/config.ts +28 -0
- package/src/definition_finder.ts +221 -0
- package/src/encoding.ts +32 -0
- package/src/error_renderer.ts +278 -0
- package/src/formatter.ts +274 -0
- package/src/index.ts +6 -0
- package/src/io.ts +33 -0
- package/src/language_server.ts +301 -0
- package/src/literals.ts +120 -0
- package/src/module_collector.ts +22 -0
- package/src/module_set.ts +1175 -0
- package/src/parser.ts +1122 -0
- package/src/snapshotter.ts +136 -0
- package/src/tokenizer.ts +216 -0
- package/src/types.ts +518 -0
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import * as paths from "path";
|
|
5
|
+
import Watcher from "watcher";
|
|
6
|
+
import * as yaml from "yaml";
|
|
7
|
+
import { fromZodError } from "zod-validation-error";
|
|
8
|
+
import { parseCommandLine } from "./command_line_parser.js";
|
|
9
|
+
import { GeneratorConfig, SkirConfig } from "./config.js";
|
|
10
|
+
import {
|
|
11
|
+
makeGray,
|
|
12
|
+
makeGreen,
|
|
13
|
+
makeRed,
|
|
14
|
+
renderErrors,
|
|
15
|
+
} from "./error_renderer.js";
|
|
16
|
+
import { formatModule } from "./formatter.js";
|
|
17
|
+
import { REAL_FILE_SYSTEM } from "./io.js";
|
|
18
|
+
import { collectModules } from "./module_collector.js";
|
|
19
|
+
import { ModuleSet } from "./module_set.js";
|
|
20
|
+
import { takeSnapshot } from "./snapshotter.js";
|
|
21
|
+
import { tokenizeModule } from "./tokenizer.js";
|
|
22
|
+
import type { CodeGenerator } from "./types.js";
|
|
23
|
+
|
|
24
|
+
interface GeneratorBundle<Config = unknown> {
|
|
25
|
+
generator: CodeGenerator<Config>;
|
|
26
|
+
config: Config;
|
|
27
|
+
/// Absolute paths to the skirout directories.
|
|
28
|
+
skiroutDirs: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function makeGeneratorBundle(
|
|
32
|
+
config: GeneratorConfig,
|
|
33
|
+
root: string,
|
|
34
|
+
): Promise<GeneratorBundle> {
|
|
35
|
+
const mod = await import(config.mod);
|
|
36
|
+
const generator = mod.GENERATOR;
|
|
37
|
+
if (typeof generator !== "object") {
|
|
38
|
+
throw new Error(`Cannot import GENERATOR from module ${config.mod}`);
|
|
39
|
+
}
|
|
40
|
+
// Validate the generator config.
|
|
41
|
+
const parsedConfig = generator.configType.safeParse(config.config);
|
|
42
|
+
if (!parsedConfig.success) {
|
|
43
|
+
const { id } = generator;
|
|
44
|
+
console.error(makeRed(`Invalid config for ${id} generator`));
|
|
45
|
+
const validationError = fromZodError(parsedConfig.error);
|
|
46
|
+
console.error(validationError.toString());
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
let skiroutDirs: string[];
|
|
50
|
+
if (config.skiroutDir === undefined) {
|
|
51
|
+
skiroutDirs = ["skirout"];
|
|
52
|
+
} else if (typeof config.skiroutDir === "string") {
|
|
53
|
+
skiroutDirs = [config.skiroutDir];
|
|
54
|
+
} else {
|
|
55
|
+
skiroutDirs = config.skiroutDir;
|
|
56
|
+
}
|
|
57
|
+
skiroutDirs = skiroutDirs.map((d) => paths.join(root, d));
|
|
58
|
+
return {
|
|
59
|
+
generator,
|
|
60
|
+
config: parsedConfig.data,
|
|
61
|
+
skiroutDirs: skiroutDirs,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface WriteBatch {
|
|
66
|
+
/** Key: path to a generated file relative to the skirout dir. */
|
|
67
|
+
readonly pathToFile: ReadonlyMap<string, CodeGenerator.OutputFile>;
|
|
68
|
+
readonly writeTime: Date;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class WatchModeMainLoop {
|
|
72
|
+
private readonly skiroutDirs = new Set<string>();
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
private readonly srcDir: string,
|
|
76
|
+
private readonly generatorBundles: readonly GeneratorBundle[],
|
|
77
|
+
private readonly watchModeOn: boolean,
|
|
78
|
+
) {
|
|
79
|
+
for (const generatorBundle of generatorBundles) {
|
|
80
|
+
for (const skiroutDir of generatorBundle.skiroutDirs) {
|
|
81
|
+
this.skiroutDirs.add(skiroutDir);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
checkNoOverlappingSkiroutDirs([...this.skiroutDirs]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async start(): Promise<void> {
|
|
88
|
+
await this.generate();
|
|
89
|
+
const watcher = new Watcher(this.srcDir, {
|
|
90
|
+
renameDetection: true,
|
|
91
|
+
recursive: true,
|
|
92
|
+
persistent: true,
|
|
93
|
+
});
|
|
94
|
+
watcher.on("all", (_, targetPath, targetPathNext) => {
|
|
95
|
+
if (
|
|
96
|
+
targetPath.endsWith(".skir") ||
|
|
97
|
+
(targetPathNext && targetPathNext.endsWith(".skir"))
|
|
98
|
+
) {
|
|
99
|
+
this.triggerGeneration();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private triggerGeneration(): void {
|
|
105
|
+
if (this.generating) {
|
|
106
|
+
this.mustRegenerate = true;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (this.timeoutId !== undefined) {
|
|
110
|
+
globalThis.clearTimeout(this.timeoutId);
|
|
111
|
+
}
|
|
112
|
+
const delayMillis = 200;
|
|
113
|
+
const callback = (): void => {
|
|
114
|
+
try {
|
|
115
|
+
this.generate();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
const message =
|
|
118
|
+
e && typeof e === "object" && "message" in e ? e.message : e;
|
|
119
|
+
console.error(message);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
this.timeoutId = globalThis.setTimeout(callback, delayMillis);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async generate(): Promise<boolean> {
|
|
126
|
+
this.generating = true;
|
|
127
|
+
this.timeoutId = undefined;
|
|
128
|
+
this.mustRegenerate = false;
|
|
129
|
+
if (this.watchModeOn) {
|
|
130
|
+
console.clear();
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const moduleSet = await collectModules(this.srcDir);
|
|
134
|
+
const errors = moduleSet.errors.filter((e) => !e.errorIsInOtherModule);
|
|
135
|
+
if (errors.length) {
|
|
136
|
+
renderErrors(errors);
|
|
137
|
+
return false;
|
|
138
|
+
} else {
|
|
139
|
+
await this.doGenerate(moduleSet);
|
|
140
|
+
if (this.watchModeOn) {
|
|
141
|
+
const date = new Date().toLocaleTimeString("en-US");
|
|
142
|
+
const successMessage = `Generation succeeded at ${date}`;
|
|
143
|
+
console.log(makeGreen(successMessage));
|
|
144
|
+
console.log("\nWaiting for changes in files matching:");
|
|
145
|
+
const glob = paths.resolve(paths.join(this.srcDir, "/**/*.skir"));
|
|
146
|
+
console.log(` ${glob}`);
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
} finally {
|
|
151
|
+
this.generating = false;
|
|
152
|
+
if (this.mustRegenerate) {
|
|
153
|
+
this.triggerGeneration();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async doGenerate(moduleSet: ModuleSet): Promise<void> {
|
|
159
|
+
const { skiroutDirs } = this;
|
|
160
|
+
const preExistingAbsolutePaths = new Set<string>();
|
|
161
|
+
for (const skiroutDir of skiroutDirs) {
|
|
162
|
+
await fs.mkdir(skiroutDir, { recursive: true });
|
|
163
|
+
|
|
164
|
+
// Collect all the files in all the skirout dirs.
|
|
165
|
+
(
|
|
166
|
+
await glob(paths.join(skiroutDir, "**/*"), { withFileTypes: true })
|
|
167
|
+
).forEach((p) => preExistingAbsolutePaths.add(p.fullpath()));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const pathToFile = new Map<string, CodeGenerator.OutputFile>();
|
|
171
|
+
const pathToGenerator = new Map<string, GeneratorBundle>();
|
|
172
|
+
for (const generator of this.generatorBundles) {
|
|
173
|
+
const files = generator.generator.generateCode({
|
|
174
|
+
modules: moduleSet.resolvedModules,
|
|
175
|
+
recordMap: moduleSet.recordMap,
|
|
176
|
+
config: generator.config,
|
|
177
|
+
}).files;
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const { path } = file;
|
|
180
|
+
if (pathToFile.has(path)) {
|
|
181
|
+
throw new Error(`Multiple generators produce ${path}`);
|
|
182
|
+
}
|
|
183
|
+
pathToFile.set(path, file);
|
|
184
|
+
pathToGenerator.set(path, generator);
|
|
185
|
+
for (const skiroutDir of generator.skiroutDirs) {
|
|
186
|
+
// Remove this path and all its parents from the set of paths to remove
|
|
187
|
+
// at the end of the generation.
|
|
188
|
+
for (
|
|
189
|
+
let pathToKeep = path;
|
|
190
|
+
pathToKeep !== ".";
|
|
191
|
+
pathToKeep = paths.dirname(pathToKeep)
|
|
192
|
+
) {
|
|
193
|
+
preExistingAbsolutePaths.delete(
|
|
194
|
+
paths.resolve(paths.join(skiroutDir, pathToKeep)),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Write or override all the generated files.
|
|
202
|
+
const { lastWriteBatch } = this;
|
|
203
|
+
await Promise.all(
|
|
204
|
+
Array.from(pathToFile).map(async ([p, newFile]) => {
|
|
205
|
+
const oldFile = lastWriteBatch.pathToFile.get(p);
|
|
206
|
+
const generator = pathToGenerator.get(p)!;
|
|
207
|
+
for (const skiroutDir of generator.skiroutDirs) {
|
|
208
|
+
const fsPath = paths.join(skiroutDir, p);
|
|
209
|
+
if (oldFile?.code === newFile.code) {
|
|
210
|
+
const mtime = (await fs.stat(fsPath)).mtime;
|
|
211
|
+
if (
|
|
212
|
+
mtime !== null &&
|
|
213
|
+
mtime.getDate() <= lastWriteBatch.writeTime.getDate()
|
|
214
|
+
) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
await fs.mkdir(paths.dirname(fsPath), { recursive: true });
|
|
219
|
+
await fs.writeFile(fsPath, newFile.code, "utf-8");
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Remove all the pre-existing paths which haven't been overridden.
|
|
225
|
+
await Promise.all(
|
|
226
|
+
Array.from(preExistingAbsolutePaths)
|
|
227
|
+
.sort((a, b) => b.localeCompare(a, "en-US"))
|
|
228
|
+
.map(async (p) => {
|
|
229
|
+
try {
|
|
230
|
+
await fs.rm(p, { force: true, recursive: true });
|
|
231
|
+
} catch (e) {
|
|
232
|
+
// Ignore error.
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
this.lastWriteBatch = {
|
|
238
|
+
pathToFile: pathToFile,
|
|
239
|
+
writeTime: new Date(),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private timeoutId?: NodeJS.Timeout;
|
|
244
|
+
private generating = false;
|
|
245
|
+
private mustRegenerate = false;
|
|
246
|
+
private lastWriteBatch: WriteBatch = {
|
|
247
|
+
pathToFile: new Map(),
|
|
248
|
+
writeTime: new Date(0),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function isDirectory(path: string): Promise<boolean> {
|
|
253
|
+
try {
|
|
254
|
+
return (await fs.lstat(path)).isDirectory();
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function checkNoOverlappingSkiroutDirs(skiroutDirs: readonly string[]): void {
|
|
261
|
+
for (let i = 0; i < skiroutDirs.length; ++i) {
|
|
262
|
+
for (let j = i + 1; j < skiroutDirs.length; ++j) {
|
|
263
|
+
const dirA = paths.normalize(skiroutDirs[i]!);
|
|
264
|
+
const dirB = paths.normalize(skiroutDirs[j]!);
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
dirA.startsWith(dirB + paths.sep) ||
|
|
268
|
+
dirB.startsWith(dirA + paths.sep)
|
|
269
|
+
) {
|
|
270
|
+
throw new Error(`Overlapping skirout directories: ${dirA} and ${dirB}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface ModuleFormatResult {
|
|
277
|
+
formattedCode: string;
|
|
278
|
+
alreadyFormatted: boolean;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function format(root: string, mode: "fix" | "check"): Promise<void> {
|
|
282
|
+
const skirFiles = await glob(paths.join(root, "**/*.skir"), {
|
|
283
|
+
withFileTypes: true,
|
|
284
|
+
});
|
|
285
|
+
const pathToFormatResult = new Map<string, ModuleFormatResult>();
|
|
286
|
+
for await (const skirFile of skirFiles) {
|
|
287
|
+
if (!skirFile.isFile) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const unformattedCode = REAL_FILE_SYSTEM.readTextFile(skirFile.fullpath());
|
|
291
|
+
if (unformattedCode === undefined) {
|
|
292
|
+
throw new Error(`Cannot read ${skirFile.fullpath()}`);
|
|
293
|
+
}
|
|
294
|
+
const tokens = tokenizeModule(unformattedCode, "", "keep-comments");
|
|
295
|
+
if (tokens.errors.length) {
|
|
296
|
+
renderErrors(tokens.errors);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
const formattedCode = formatModule(tokens.result);
|
|
300
|
+
pathToFormatResult.set(skirFile.fullpath(), {
|
|
301
|
+
formattedCode: formattedCode,
|
|
302
|
+
alreadyFormatted: formattedCode === unformattedCode,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
let numFilesNotFormatted = 0;
|
|
306
|
+
for (const [path, result] of pathToFormatResult) {
|
|
307
|
+
const relativePath = paths.relative(root, path).replace(/\\/g, "/");
|
|
308
|
+
if (mode === "fix") {
|
|
309
|
+
if (result.alreadyFormatted) {
|
|
310
|
+
console.log(`${makeGray(relativePath)} (unchanged)`);
|
|
311
|
+
} else {
|
|
312
|
+
REAL_FILE_SYSTEM.writeTextFile(path, result.formattedCode);
|
|
313
|
+
console.log(makeGray(relativePath));
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
const _: "check" = mode;
|
|
317
|
+
if (result.alreadyFormatted) {
|
|
318
|
+
console.log(`${makeGray(relativePath)} (OK)`);
|
|
319
|
+
} else {
|
|
320
|
+
console.log(makeRed(relativePath));
|
|
321
|
+
++numFilesNotFormatted;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (numFilesNotFormatted) {
|
|
326
|
+
console.log();
|
|
327
|
+
console.log(
|
|
328
|
+
makeRed(
|
|
329
|
+
`${numFilesNotFormatted} file${
|
|
330
|
+
numFilesNotFormatted > 1 ? "s" : ""
|
|
331
|
+
} not formatted; run with 'format fix' to format ${
|
|
332
|
+
numFilesNotFormatted > 1 ? "them" : "it"
|
|
333
|
+
}`,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function main(): Promise<void> {
|
|
341
|
+
const args = parseCommandLine(process.argv.slice(2));
|
|
342
|
+
|
|
343
|
+
const root = args.root || ".";
|
|
344
|
+
|
|
345
|
+
if (!(await isDirectory(root!))) {
|
|
346
|
+
console.error(makeRed(`Not a directory: ${root}`));
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Use an absolute path to make error messages more helpful.
|
|
351
|
+
const skirConfigPath = paths.resolve(paths.join(root!, "skir.yml"));
|
|
352
|
+
const skirConfigContents = REAL_FILE_SYSTEM.readTextFile(skirConfigPath);
|
|
353
|
+
if (skirConfigContents === undefined) {
|
|
354
|
+
console.error(makeRed(`Cannot find ${skirConfigPath}`));
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let skirConfig: SkirConfig;
|
|
359
|
+
{
|
|
360
|
+
// `yaml.parse` fail with a helpful error message, no need to add context.
|
|
361
|
+
const parseResult = SkirConfig.safeParse(yaml.parse(skirConfigContents));
|
|
362
|
+
if (parseResult.success) {
|
|
363
|
+
skirConfig = parseResult.data;
|
|
364
|
+
} else {
|
|
365
|
+
console.error(makeRed("Invalid skir config"));
|
|
366
|
+
console.error(` Path: ${skirConfigPath}`);
|
|
367
|
+
const validationError = fromZodError(parseResult.error);
|
|
368
|
+
console.error(validationError.toString());
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const srcDir = paths.join(root!, skirConfig.srcDir || ".");
|
|
374
|
+
|
|
375
|
+
switch (args.kind) {
|
|
376
|
+
case "format": {
|
|
377
|
+
// Check or fix the formatting to the .skir files in the source directory.
|
|
378
|
+
await format(srcDir, args.check ? "check" : "fix");
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case "gen": {
|
|
382
|
+
// Run the skir code generators in watch mode or once.
|
|
383
|
+
const generatorBundles: GeneratorBundle[] = await Promise.all(
|
|
384
|
+
skirConfig.generators.map((config) =>
|
|
385
|
+
makeGeneratorBundle(config, root!),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
// Sort for consistency.
|
|
389
|
+
generatorBundles.sort((a, b) => {
|
|
390
|
+
const aId = a.generator.id;
|
|
391
|
+
const bId = b.generator.id;
|
|
392
|
+
return aId.localeCompare(bId, "en-US");
|
|
393
|
+
});
|
|
394
|
+
// Look for duplicates.
|
|
395
|
+
for (let i = 0; i < generatorBundles.length - 1; ++i) {
|
|
396
|
+
const { id } = generatorBundles[i]!.generator;
|
|
397
|
+
if (id === generatorBundles[i + 1]!.generator.id) {
|
|
398
|
+
console.error(makeRed(`Duplicate generator: ${id}`));
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const watch = !!args.watch;
|
|
403
|
+
const watchModeMainLoop = new WatchModeMainLoop(
|
|
404
|
+
srcDir,
|
|
405
|
+
generatorBundles,
|
|
406
|
+
watch,
|
|
407
|
+
);
|
|
408
|
+
if (watch) {
|
|
409
|
+
await watchModeMainLoop.start();
|
|
410
|
+
} else {
|
|
411
|
+
const success: boolean = await watchModeMainLoop.generate();
|
|
412
|
+
process.exit(success ? 0 : 1);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
case "init": {
|
|
417
|
+
// TODO
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "snapshot": {
|
|
421
|
+
takeSnapshot({
|
|
422
|
+
rootDir: root!,
|
|
423
|
+
srcDir: srcDir,
|
|
424
|
+
check: !!args.check,
|
|
425
|
+
});
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case "help":
|
|
429
|
+
case "error": {
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
main();
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const GeneratorConfig = z
|
|
4
|
+
.object({
|
|
5
|
+
mod: z.string(),
|
|
6
|
+
config: z.any(),
|
|
7
|
+
skiroutDir: z
|
|
8
|
+
.union([
|
|
9
|
+
z
|
|
10
|
+
.string()
|
|
11
|
+
.regex(/^.*\/skirout$/)
|
|
12
|
+
.optional(),
|
|
13
|
+
z.array(z.string().regex(/^.*\/skirout$/)),
|
|
14
|
+
])
|
|
15
|
+
.optional(),
|
|
16
|
+
})
|
|
17
|
+
.strict();
|
|
18
|
+
|
|
19
|
+
export type GeneratorConfig = z.infer<typeof GeneratorConfig>;
|
|
20
|
+
|
|
21
|
+
export const SkirConfig = z
|
|
22
|
+
.object({
|
|
23
|
+
generators: z.array(GeneratorConfig),
|
|
24
|
+
srcDir: z.string().optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
|
|
28
|
+
export type SkirConfig = z.infer<typeof SkirConfig>;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities to help implement the jump-to-definition functionality for skir
|
|
3
|
+
* files in IDEs.
|
|
4
|
+
*/
|
|
5
|
+
import type { Declaration, Module, ResolvedType, Token } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface DefinitionMatch {
|
|
8
|
+
modulePath: string;
|
|
9
|
+
position: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function findDefinition(
|
|
13
|
+
module: Module,
|
|
14
|
+
position: number,
|
|
15
|
+
): DefinitionMatch | null {
|
|
16
|
+
return findDefinitionInDeclarations(module.declarations, position);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findDefinitionInDeclarations(
|
|
20
|
+
declarations: readonly Declaration[],
|
|
21
|
+
position: number,
|
|
22
|
+
): DefinitionMatch | null {
|
|
23
|
+
for (const declaration of declarations) {
|
|
24
|
+
const maybeMatch = findDefinitionInDeclaration(declaration, position);
|
|
25
|
+
if (maybeMatch) {
|
|
26
|
+
return maybeMatch;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findDefinitionInDeclaration(
|
|
33
|
+
declaration: Declaration,
|
|
34
|
+
position: number,
|
|
35
|
+
): DefinitionMatch | null {
|
|
36
|
+
switch (declaration.kind) {
|
|
37
|
+
case "constant": {
|
|
38
|
+
if (declaration.type) {
|
|
39
|
+
const maybeMatch = findDefinitionInResolvedType(
|
|
40
|
+
declaration.type,
|
|
41
|
+
position,
|
|
42
|
+
);
|
|
43
|
+
if (maybeMatch) {
|
|
44
|
+
return maybeMatch;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
case "field": {
|
|
50
|
+
if (declaration.type) {
|
|
51
|
+
return findDefinitionInResolvedType(declaration.type, position);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
case "import":
|
|
56
|
+
case "import-alias": {
|
|
57
|
+
if (
|
|
58
|
+
tokenContains(declaration.modulePath, position) &&
|
|
59
|
+
declaration.resolvedModulePath
|
|
60
|
+
) {
|
|
61
|
+
return {
|
|
62
|
+
modulePath: declaration.resolvedModulePath,
|
|
63
|
+
position: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
case "method": {
|
|
69
|
+
if (declaration.requestType) {
|
|
70
|
+
const maybeMatch = findDefinitionInResolvedType(
|
|
71
|
+
declaration.requestType,
|
|
72
|
+
position,
|
|
73
|
+
);
|
|
74
|
+
if (maybeMatch) {
|
|
75
|
+
return maybeMatch;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (declaration.responseType) {
|
|
79
|
+
const maybeMatch = findDefinitionInResolvedType(
|
|
80
|
+
declaration.responseType,
|
|
81
|
+
position,
|
|
82
|
+
);
|
|
83
|
+
if (maybeMatch) {
|
|
84
|
+
return maybeMatch;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
case "record": {
|
|
90
|
+
return findDefinitionInDeclarations(declaration.fields, position);
|
|
91
|
+
}
|
|
92
|
+
case "removed": {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findDefinitionInResolvedType(
|
|
99
|
+
type: ResolvedType,
|
|
100
|
+
position: number,
|
|
101
|
+
): DefinitionMatch | null {
|
|
102
|
+
switch (type.kind) {
|
|
103
|
+
case "array": {
|
|
104
|
+
if (type.key) {
|
|
105
|
+
for (const item of type.key.path) {
|
|
106
|
+
if (tokenContains(item.name, position)) {
|
|
107
|
+
const maybeMatch = tokenToMatch(item.name);
|
|
108
|
+
if (maybeMatch) {
|
|
109
|
+
return maybeMatch;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return findDefinitionInResolvedType(type.item, position);
|
|
115
|
+
}
|
|
116
|
+
case "optional": {
|
|
117
|
+
return findDefinitionInResolvedType(type.other, position);
|
|
118
|
+
}
|
|
119
|
+
case "primitive": {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
case "record": {
|
|
123
|
+
for (const namePart of type.nameParts) {
|
|
124
|
+
if (tokenContains(namePart.token, position)) {
|
|
125
|
+
return tokenToMatch(namePart.declaration.name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function tokenContains(token: Token, position: number): boolean {
|
|
134
|
+
const end = token.position + token.text.length;
|
|
135
|
+
return position >= token.position && position < end;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function tokenToMatch(token: Token): DefinitionMatch {
|
|
139
|
+
return {
|
|
140
|
+
modulePath: token.line.modulePath,
|
|
141
|
+
position: token.position,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function findTokensWithDefinition(module: Module): readonly Token[] {
|
|
146
|
+
const finder = new TokensWithDefinitionFinder();
|
|
147
|
+
finder.findInDeclarations(module.declarations);
|
|
148
|
+
return finder.tokens;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class TokensWithDefinitionFinder {
|
|
152
|
+
tokens: Token[] = [];
|
|
153
|
+
|
|
154
|
+
findInDeclarations(declarations: readonly Declaration[]): void {
|
|
155
|
+
for (const declaration of declarations) {
|
|
156
|
+
this.findInDeclaration(declaration);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
findInDeclaration(declaration: Declaration): null {
|
|
161
|
+
switch (declaration.kind) {
|
|
162
|
+
case "constant":
|
|
163
|
+
case "field": {
|
|
164
|
+
if (declaration.type) {
|
|
165
|
+
this.findInResolvedType(declaration.type);
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
case "import":
|
|
170
|
+
case "import-alias": {
|
|
171
|
+
this.tokens.push(declaration.modulePath);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
case "method": {
|
|
175
|
+
if (declaration.requestType) {
|
|
176
|
+
this.findInResolvedType(declaration.requestType);
|
|
177
|
+
}
|
|
178
|
+
if (declaration.responseType) {
|
|
179
|
+
this.findInResolvedType(declaration.responseType);
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
case "record": {
|
|
184
|
+
this.findInDeclarations(declaration.fields);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
case "removed": {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
findInResolvedType(type: ResolvedType): null {
|
|
194
|
+
switch (type.kind) {
|
|
195
|
+
case "array": {
|
|
196
|
+
if (type.key) {
|
|
197
|
+
for (const item of type.key.path) {
|
|
198
|
+
if (item.declaration) {
|
|
199
|
+
this.tokens.push(item.name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return this.findInResolvedType(type.item);
|
|
204
|
+
}
|
|
205
|
+
case "optional": {
|
|
206
|
+
return this.findInResolvedType(type.other);
|
|
207
|
+
}
|
|
208
|
+
case "primitive": {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
case "record": {
|
|
212
|
+
for (const namePart of type.nameParts) {
|
|
213
|
+
if (namePart.declaration) {
|
|
214
|
+
this.tokens.push(namePart.token);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/encoding.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function encodeInt32(i: number): Uint8Array {
|
|
2
|
+
const buffer = new ArrayBuffer(5);
|
|
3
|
+
const dataView = new DataView(buffer);
|
|
4
|
+
let n: number;
|
|
5
|
+
if (i < 0) {
|
|
6
|
+
if (i >= -256) {
|
|
7
|
+
dataView.setUint8(0, 235);
|
|
8
|
+
dataView.setUint8(1, i + 256);
|
|
9
|
+
n = 2;
|
|
10
|
+
} else if (i >= -65536) {
|
|
11
|
+
dataView.setUint8(0, 236);
|
|
12
|
+
dataView.setUint16(1, i + 65536, true);
|
|
13
|
+
n = 3;
|
|
14
|
+
} else {
|
|
15
|
+
dataView.setUint8(0, 237);
|
|
16
|
+
dataView.setInt32(1, i, true);
|
|
17
|
+
n = 5;
|
|
18
|
+
}
|
|
19
|
+
} else if (i < 232) {
|
|
20
|
+
dataView.setUint8(0, i);
|
|
21
|
+
n = 1;
|
|
22
|
+
} else if (i < 65536) {
|
|
23
|
+
dataView.setUint8(0, 232);
|
|
24
|
+
dataView.setUint16(1, i, true);
|
|
25
|
+
n = 3;
|
|
26
|
+
} else {
|
|
27
|
+
dataView.setUint8(0, 233);
|
|
28
|
+
dataView.setInt32(1, i, true);
|
|
29
|
+
n = 5;
|
|
30
|
+
}
|
|
31
|
+
return new Uint8Array(buffer, 0, n);
|
|
32
|
+
}
|