skir 0.0.6 → 0.0.8

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.
Files changed (55) hide show
  1. package/dist/command_line_parser.d.ts +2 -2
  2. package/dist/command_line_parser.d.ts.map +1 -1
  3. package/dist/command_line_parser.js +105 -36
  4. package/dist/command_line_parser.js.map +1 -1
  5. package/dist/command_line_parser.test.js +166 -39
  6. package/dist/command_line_parser.test.js.map +1 -1
  7. package/dist/compiler.js +51 -54
  8. package/dist/compiler.js.map +1 -1
  9. package/dist/config.d.ts +2 -2
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +3 -5
  12. package/dist/config.js.map +1 -1
  13. package/dist/config_parser.d.ts +6 -0
  14. package/dist/config_parser.d.ts.map +1 -1
  15. package/dist/config_parser.js +55 -31
  16. package/dist/config_parser.js.map +1 -1
  17. package/dist/config_parser.test.js +53 -20
  18. package/dist/config_parser.test.js.map +1 -1
  19. package/dist/error_renderer.d.ts +1 -0
  20. package/dist/error_renderer.d.ts.map +1 -1
  21. package/dist/error_renderer.js +6 -3
  22. package/dist/error_renderer.js.map +1 -1
  23. package/dist/exit_error.d.ts +8 -0
  24. package/dist/exit_error.d.ts.map +1 -0
  25. package/dist/exit_error.js +8 -0
  26. package/dist/exit_error.js.map +1 -0
  27. package/dist/io.d.ts +2 -0
  28. package/dist/io.d.ts.map +1 -1
  29. package/dist/io.js +22 -3
  30. package/dist/io.js.map +1 -1
  31. package/dist/module_collector.d.ts.map +1 -1
  32. package/dist/module_collector.js +12 -7
  33. package/dist/module_collector.js.map +1 -1
  34. package/dist/module_set.js +4 -4
  35. package/dist/parser.js +6 -6
  36. package/dist/project_initializer.d.ts.map +1 -1
  37. package/dist/project_initializer.js +97 -15
  38. package/dist/project_initializer.js.map +1 -1
  39. package/dist/snapshotter.d.ts +9 -2
  40. package/dist/snapshotter.d.ts.map +1 -1
  41. package/dist/snapshotter.js +35 -17
  42. package/dist/snapshotter.js.map +1 -1
  43. package/package.json +8 -6
  44. package/src/command_line_parser.ts +134 -42
  45. package/src/compiler.ts +65 -60
  46. package/src/config.ts +4 -6
  47. package/src/config_parser.ts +66 -32
  48. package/src/error_renderer.ts +11 -3
  49. package/src/exit_error.ts +6 -0
  50. package/src/io.ts +22 -3
  51. package/src/module_collector.ts +21 -7
  52. package/src/module_set.ts +4 -4
  53. package/src/parser.ts +6 -6
  54. package/src/project_initializer.ts +97 -15
  55. package/src/snapshotter.ts +49 -30
package/src/compiler.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import * as fs from "fs/promises";
2
+ import * as FileSystem from "fs/promises";
3
3
  import { glob } from "glob";
4
- import * as paths from "path";
4
+ import * as Paths from "path";
5
5
  import type { CodeGenerator } from "skir-internal";
6
6
  import Watcher from "watcher";
7
7
  import { parseCommandLine } from "./command_line_parser.js";
@@ -14,8 +14,13 @@ import {
14
14
  renderErrors,
15
15
  renderSkirConfigErrors,
16
16
  } from "./error_renderer.js";
17
+ import { ExitError } from "./exit_error.js";
17
18
  import { formatModule } from "./formatter.js";
18
- import { REAL_FILE_SYSTEM } from "./io.js";
19
+ import {
20
+ isDirectory,
21
+ REAL_FILE_SYSTEM,
22
+ rewritePathForRendering,
23
+ } from "./io.js";
19
24
  import { collectModules } from "./module_collector.js";
20
25
  import { ModuleSet } from "./module_set.js";
21
26
  import { initializeProject } from "./project_initializer.js";
@@ -35,14 +40,12 @@ async function makeGeneratorBundle(
35
40
  ): Promise<GeneratorBundle> {
36
41
  const generator = await importCodeGenerator(config.mod);
37
42
  let skiroutDirs: string[];
38
- if (config.outDir === undefined) {
39
- skiroutDirs = ["skirout"];
40
- } else if (typeof config.outDir === "string") {
43
+ if (typeof config.outDir === "string") {
41
44
  skiroutDirs = [config.outDir];
42
45
  } else {
43
46
  skiroutDirs = config.outDir;
44
47
  }
45
- skiroutDirs = skiroutDirs.map((d) => paths.join(root, d));
48
+ skiroutDirs = skiroutDirs.map((d) => Paths.join(root, d));
46
49
  return {
47
50
  generator: generator,
48
51
  config: config.config,
@@ -133,7 +136,7 @@ class WatchModeMainLoop {
133
136
  const successMessage = `Generation succeeded at ${date}`;
134
137
  console.log(makeGreen(successMessage));
135
138
  console.log("\nWaiting for changes in files matching:");
136
- const glob = paths.resolve(paths.join(this.srcDir, "/**/*.skir"));
139
+ const glob = Paths.resolve(Paths.join(this.srcDir, "/**/*.skir"));
137
140
  console.log(` ${glob}`);
138
141
  }
139
142
  return true;
@@ -150,11 +153,11 @@ class WatchModeMainLoop {
150
153
  const { skiroutDirs } = this;
151
154
  const preExistingAbsolutePaths = new Set<string>();
152
155
  for (const skiroutDir of skiroutDirs) {
153
- await fs.mkdir(skiroutDir, { recursive: true });
156
+ await FileSystem.mkdir(skiroutDir, { recursive: true });
154
157
 
155
158
  // Collect all the files in all the skirout dirs.
156
159
  (
157
- await glob(paths.join(skiroutDir, "**/*"), { withFileTypes: true })
160
+ await glob(Paths.join(skiroutDir, "**/*"), { withFileTypes: true })
158
161
  ).forEach((p) => preExistingAbsolutePaths.add(p.fullpath()));
159
162
  }
160
163
 
@@ -169,7 +172,9 @@ class WatchModeMainLoop {
169
172
  for (const file of files) {
170
173
  const { path } = file;
171
174
  if (pathToFile.has(path)) {
172
- throw new Error(`Multiple generators produce ${path}`);
175
+ throw new ExitError(
176
+ "Multiple generators produce " + rewritePathForRendering(path),
177
+ );
173
178
  }
174
179
  pathToFile.set(path, file);
175
180
  pathToGenerator.set(path, generator);
@@ -179,10 +184,10 @@ class WatchModeMainLoop {
179
184
  for (
180
185
  let pathToKeep = path;
181
186
  pathToKeep !== ".";
182
- pathToKeep = paths.dirname(pathToKeep)
187
+ pathToKeep = Paths.dirname(pathToKeep)
183
188
  ) {
184
189
  preExistingAbsolutePaths.delete(
185
- paths.resolve(paths.join(skiroutDir, pathToKeep)),
190
+ Paths.resolve(Paths.join(skiroutDir, pathToKeep)),
186
191
  );
187
192
  }
188
193
  }
@@ -196,9 +201,9 @@ class WatchModeMainLoop {
196
201
  const oldFile = lastWriteBatch.pathToFile.get(p);
197
202
  const generator = pathToGenerator.get(p)!;
198
203
  for (const skiroutDir of generator.skiroutDirs) {
199
- const fsPath = paths.join(skiroutDir, p);
204
+ const fsPath = Paths.join(skiroutDir, p);
200
205
  if (oldFile?.code === newFile.code) {
201
- const mtime = (await fs.stat(fsPath)).mtime;
206
+ const mtime = (await FileSystem.stat(fsPath)).mtime;
202
207
  if (
203
208
  mtime !== null &&
204
209
  mtime.getDate() <= lastWriteBatch.writeTime.getDate()
@@ -206,8 +211,8 @@ class WatchModeMainLoop {
206
211
  return;
207
212
  }
208
213
  }
209
- await fs.mkdir(paths.dirname(fsPath), { recursive: true });
210
- await fs.writeFile(fsPath, newFile.code, "utf-8");
214
+ await FileSystem.mkdir(Paths.dirname(fsPath), { recursive: true });
215
+ await FileSystem.writeFile(fsPath, newFile.code, "utf-8");
211
216
  }
212
217
  }),
213
218
  );
@@ -218,8 +223,8 @@ class WatchModeMainLoop {
218
223
  .sort((a, b) => b.localeCompare(a, "en-US"))
219
224
  .map(async (p) => {
220
225
  try {
221
- await fs.rm(p, { force: true, recursive: true });
222
- } catch (e) {
226
+ await FileSystem.rm(p, { force: true, recursive: true });
227
+ } catch (_e) {
223
228
  // Ignore error.
224
229
  }
225
230
  }),
@@ -240,25 +245,19 @@ class WatchModeMainLoop {
240
245
  };
241
246
  }
242
247
 
243
- async function isDirectory(path: string): Promise<boolean> {
244
- try {
245
- return (await fs.lstat(path)).isDirectory();
246
- } catch (e) {
247
- return false;
248
- }
249
- }
250
-
251
248
  function checkNoOverlappingSkiroutDirs(skiroutDirs: readonly string[]): void {
252
249
  for (let i = 0; i < skiroutDirs.length; ++i) {
253
250
  for (let j = i + 1; j < skiroutDirs.length; ++j) {
254
- const dirA = paths.normalize(skiroutDirs[i]!);
255
- const dirB = paths.normalize(skiroutDirs[j]!);
251
+ const dirA = Paths.normalize(skiroutDirs[i]!);
252
+ const dirB = Paths.normalize(skiroutDirs[j]!);
256
253
 
257
254
  if (
258
- dirA.startsWith(dirB + paths.sep) ||
259
- dirB.startsWith(dirA + paths.sep)
255
+ dirA.startsWith(dirB + Paths.sep) ||
256
+ dirB.startsWith(dirA + Paths.sep)
260
257
  ) {
261
- throw new Error(`Overlapping skirout directories: ${dirA} and ${dirB}`);
258
+ throw new ExitError(
259
+ `Overlapping skirout directories: ${dirA} and ${dirB}`,
260
+ );
262
261
  }
263
262
  }
264
263
  }
@@ -270,7 +269,7 @@ interface ModuleFormatResult {
270
269
  }
271
270
 
272
271
  async function format(root: string, mode: "fix" | "check"): Promise<void> {
273
- const skirFiles = await glob(paths.join(root, "**/*.skir"), {
272
+ const skirFiles = await glob(Paths.join(root, "**/*.skir"), {
274
273
  withFileTypes: true,
275
274
  });
276
275
  const pathToFormatResult = new Map<string, ModuleFormatResult>();
@@ -280,7 +279,9 @@ async function format(root: string, mode: "fix" | "check"): Promise<void> {
280
279
  }
281
280
  const unformattedCode = REAL_FILE_SYSTEM.readTextFile(skirFile.fullpath());
282
281
  if (unformattedCode === undefined) {
283
- throw new Error(`Cannot read ${skirFile.fullpath()}`);
282
+ throw new ExitError(
283
+ "Cannot read " + rewritePathForRendering(skirFile.fullpath()),
284
+ );
284
285
  }
285
286
  const tokens = tokenizeModule(unformattedCode, "");
286
287
  if (tokens.errors.length) {
@@ -295,7 +296,7 @@ async function format(root: string, mode: "fix" | "check"): Promise<void> {
295
296
  }
296
297
  let numFilesNotFormatted = 0;
297
298
  for (const [path, result] of pathToFormatResult) {
298
- const relativePath = paths.relative(root, path).replace(/\\/g, "/");
299
+ const relativePath = Paths.relative(root, path).replace(/\\/g, "/");
299
300
  if (mode === "fix") {
300
301
  if (result.alreadyFormatted) {
301
302
  console.log(`${makeGray(relativePath)} (unchanged)`);
@@ -333,14 +334,14 @@ async function main(): Promise<void> {
333
334
 
334
335
  const root = args.root || ".";
335
336
 
336
- if (!(await isDirectory(root!))) {
337
+ if (!(await isDirectory(root))) {
337
338
  console.error(makeRed(`Not a directory: ${root}`));
338
339
  process.exit(1);
339
340
  }
340
341
 
341
342
  switch (args.kind) {
342
343
  case "init": {
343
- initializeProject(root!);
344
+ initializeProject(root);
344
345
  return;
345
346
  }
346
347
  case "help":
@@ -349,14 +350,7 @@ async function main(): Promise<void> {
349
350
  }
350
351
  }
351
352
 
352
- let skirConfigPath = paths.join(root!, "skir.yml");
353
- if (
354
- !paths.isAbsolute(skirConfigPath) &&
355
- !/^\.{1,2}[/\\]$/.test(skirConfigPath)
356
- ) {
357
- // To make it clear that it's a path, prepend "./"
358
- skirConfigPath = `.${paths.sep}${skirConfigPath}`;
359
- }
353
+ const skirConfigPath = rewritePathForRendering(Paths.join(root, "skir.yml"));
360
354
  const skirConfigCode = REAL_FILE_SYSTEM.readTextFile(skirConfigPath);
361
355
  if (skirConfigCode === undefined) {
362
356
  console.error(makeRed(`Cannot find ${skirConfigPath}`));
@@ -366,32 +360,30 @@ async function main(): Promise<void> {
366
360
  const skirConfigResult = await parseSkirConfig(skirConfigCode, "import-mods");
367
361
  if (skirConfigResult.errors.length > 0) {
368
362
  console.error(makeRed("Invalid skir config"));
369
- renderSkirConfigErrors(skirConfigResult.errors, { skirConfigPath });
363
+ const { maybeForgotToEditAfterInit } = skirConfigResult;
364
+ renderSkirConfigErrors(skirConfigResult.errors, {
365
+ skirConfigPath,
366
+ maybeForgotToEditAfterInit,
367
+ });
370
368
  process.exit(1);
371
369
  }
372
370
  const skirConfig = skirConfigResult.skirConfig!;
373
371
 
374
- const srcDir = paths.join(root!, skirConfig.srcDir || ".");
372
+ const srcDir = Paths.join(root, skirConfig.srcDir || ".");
375
373
 
376
374
  switch (args.kind) {
377
375
  case "format": {
378
376
  // Check or fix the formatting to the .skir files in the source directory.
379
- await format(srcDir, args.subcommand === "check" ? "check" : "fix");
377
+ await format(srcDir, args.subcommand === "ci" ? "check" : "fix");
380
378
  break;
381
379
  }
382
380
  case "gen": {
383
381
  // Run the skir code generators in watch mode or once.
384
382
  const generatorBundles: GeneratorBundle[] = await Promise.all(
385
383
  skirConfig.generators.map((config) =>
386
- makeGeneratorBundle(config, root!),
384
+ makeGeneratorBundle(config, root),
387
385
  ),
388
386
  );
389
- // Sort for consistency.
390
- generatorBundles.sort((a, b) => {
391
- const aId = a.generator.id;
392
- const bId = b.generator.id;
393
- return aId.localeCompare(bId, "en-US");
394
- });
395
387
  // Look for duplicates.
396
388
  for (let i = 0; i < generatorBundles.length - 1; ++i) {
397
389
  const { id } = generatorBundles[i]!.generator;
@@ -417,14 +409,17 @@ async function main(): Promise<void> {
417
409
  case "snapshot": {
418
410
  if (args.subcommand === "view") {
419
411
  viewSnapshot({
420
- rootDir: root!,
412
+ rootDir: root,
421
413
  });
422
414
  } else {
423
- takeSnapshot({
424
- rootDir: root!,
415
+ const success = takeSnapshot({
416
+ rootDir: root,
425
417
  srcDir: srcDir,
426
- check: args.subcommand === "check",
418
+ subcommand: args.subcommand,
427
419
  });
420
+ if (!success) {
421
+ process.exit(1);
422
+ }
428
423
  }
429
424
  break;
430
425
  }
@@ -435,4 +430,14 @@ async function main(): Promise<void> {
435
430
  }
436
431
  }
437
432
 
438
- main();
433
+ try {
434
+ await main();
435
+ } catch (e) {
436
+ if (e instanceof Error) {
437
+ console.error(makeRed(e.message));
438
+ if (e instanceof ExitError) {
439
+ process.exit(1);
440
+ }
441
+ }
442
+ throw e;
443
+ }
package/src/config.ts CHANGED
@@ -2,13 +2,11 @@ import { z } from "zod";
2
2
 
3
3
  export const GeneratorConfig = z.strictObject({
4
4
  mod: z.string(),
5
+ outDir: z.union([
6
+ z.string().endsWith("/skirout"),
7
+ z.array(z.string().endsWith("/skirout")),
8
+ ]),
5
9
  config: z.any(),
6
- outDir: z
7
- .union([
8
- z.string().endsWith("/skirout"),
9
- z.array(z.string().endsWith("/skirout")),
10
- ])
11
- .optional(),
12
10
  });
13
11
 
14
12
  export type GeneratorConfig = z.infer<typeof GeneratorConfig>;
@@ -1,16 +1,22 @@
1
- import * as ccGen from "skir-cc-gen";
2
- import * as dartGen from "skir-dart-gen";
1
+ import * as CcGen from "skir-cc-gen";
2
+ import * as DartGen from "skir-dart-gen";
3
3
  import { CodeGenerator } from "skir-internal";
4
- import * as javaGen from "skir-java-gen";
5
- import * as kotlinGen from "skir-kotlin-gen";
6
- import * as pythonGen from "skir-python-gen";
7
- import * as typescriptGen from "skir-typescript-gen";
4
+ import * as JavaGen from "skir-java-gen";
5
+ import * as KotlinGen from "skir-kotlin-gen";
6
+ import * as PythonGen from "skir-python-gen";
7
+ import * as TypescriptGen from "skir-typescript-gen";
8
8
  import { LineCounter, parseDocument, Scalar, YAMLMap } from "yaml";
9
9
  import { SkirConfig } from "./config.js";
10
10
 
11
11
  export interface SkirConfigResult {
12
+ /** Defined if and only if `errors` is empty. */
12
13
  skirConfig: SkirConfig | undefined;
13
14
  errors: readonly SkirConfigError[];
15
+ /**
16
+ * If true, the user may have forgotten to edit skir.yml after running
17
+ * `npx skir init`.
18
+ */
19
+ maybeForgotToEditAfterInit?: boolean;
14
20
  }
15
21
 
16
22
  export interface SkirConfigError {
@@ -66,6 +72,39 @@ export async function parseSkirConfig(
66
72
  }
67
73
  return offsetRangeToRange(node.range[0], node.range[1]);
68
74
  };
75
+ const pushErrorAtPath = (
76
+ path: readonly PropertyKey[],
77
+ message: string,
78
+ ): void => {
79
+ const pathRemainder: PropertyKey[] = [];
80
+ while (path.length !== 0) {
81
+ const range = pathToRange(path);
82
+ if (range) {
83
+ break;
84
+ } else {
85
+ // It's possible that 'path' does not map to a node if 'path' refers to
86
+ // a property which is missing. In that case, we pop the last element
87
+ // of 'path' and try again, until we find a node that exists. The
88
+ // elements which were popped will be included in the error message.
89
+ pathRemainder.push(path.at(-1)!);
90
+ path = path.slice(0, -1);
91
+ }
92
+ }
93
+ pathRemainder.reverse();
94
+ const pathRemainderStr = pathRemainder
95
+ .map((p, i) =>
96
+ typeof p === "number" ? `[${p}]` : i === 0 ? p : `.${String(p)}`,
97
+ )
98
+ .join("");
99
+ const messagePrefix = pathRemainder.length
100
+ ? `Missing property '${pathRemainderStr}': `
101
+ : "";
102
+ const range = pathToRange(path);
103
+ errors.push({
104
+ message: messagePrefix + message,
105
+ range: range,
106
+ });
107
+ };
69
108
 
70
109
  // Check for YAML parsing errors
71
110
  if (doc.errors.length > 0) {
@@ -76,24 +115,27 @@ export async function parseSkirConfig(
76
115
  range: range,
77
116
  });
78
117
  }
79
- return { skirConfig: undefined, errors: errors };
118
+ return {
119
+ skirConfig: undefined,
120
+ errors: errors,
121
+ };
80
122
  }
81
123
 
82
- const jsData = doc.toJS();
83
-
84
124
  // 2. Validate with Zod schema
125
+ const jsData = doc.toJS();
85
126
  const result = SkirConfig.safeParse(jsData);
86
127
 
87
128
  if (!result.success) {
88
129
  for (const issue of result.error.issues) {
89
- // Map the Zod path to the YAML node
90
- const range = pathToRange(issue.path);
91
- errors.push({
92
- message: issue.message,
93
- range: range,
94
- });
130
+ pushErrorAtPath(issue.path, issue.message);
95
131
  }
96
- return { skirConfig: undefined, errors: errors };
132
+ const maybeForgotToEditAfterInit =
133
+ jsData && typeof jsData === "object" && jsData.generators === null;
134
+ return {
135
+ skirConfig: undefined,
136
+ errors: errors,
137
+ maybeForgotToEditAfterInit,
138
+ };
97
139
  }
98
140
 
99
141
  // 3. Validate each generator's config with Zod schema
@@ -106,11 +148,7 @@ export async function parseSkirConfig(
106
148
  generator = await importCodeGenerator(mod);
107
149
  } catch (e) {
108
150
  if (e instanceof Error) {
109
- const range = pathToRange(["generators", i, "mod"]);
110
- errors.push({
111
- message: e.message,
112
- range: range,
113
- });
151
+ pushErrorAtPath(["generators", i, "mod"], e.message);
114
152
  continue;
115
153
  } else {
116
154
  throw e;
@@ -119,13 +157,13 @@ export async function parseSkirConfig(
119
157
  } else {
120
158
  // TODO: rm the casts
121
159
  const modToGenerator: Record<string, CodeGenerator<unknown>> = {
122
- "skir-cc-gen": ccGen.GENERATOR as any as CodeGenerator<unknown>,
123
- "skir-dart-gen": dartGen.GENERATOR as any as CodeGenerator<unknown>,
124
- "skir-java-gen": javaGen.GENERATOR as any as CodeGenerator<unknown>,
125
- "skir-kotlin-gen": kotlinGen.GENERATOR as any as CodeGenerator<unknown>,
126
- "skir-python-gen": pythonGen.GENERATOR as any as CodeGenerator<unknown>,
160
+ "skir-cc-gen": CcGen.GENERATOR as any as CodeGenerator<unknown>,
161
+ "skir-dart-gen": DartGen.GENERATOR as any as CodeGenerator<unknown>,
162
+ "skir-java-gen": JavaGen.GENERATOR as any as CodeGenerator<unknown>,
163
+ "skir-kotlin-gen": KotlinGen.GENERATOR as any as CodeGenerator<unknown>,
164
+ "skir-python-gen": PythonGen.GENERATOR as any as CodeGenerator<unknown>,
127
165
  "skir-typescript-gen":
128
- typescriptGen.GENERATOR as any as CodeGenerator<unknown>,
166
+ TypescriptGen.GENERATOR as any as CodeGenerator<unknown>,
129
167
  };
130
168
  generator = modToGenerator[mod];
131
169
  }
@@ -141,11 +179,7 @@ export async function parseSkirConfig(
141
179
  "config",
142
180
  ...issue.path,
143
181
  ];
144
- const range = pathToRange(path);
145
- errors.push({
146
- message: issue.message ?? "Error",
147
- range: range,
148
- });
182
+ pushErrorAtPath(path, issue.message ?? "Error");
149
183
  }
150
184
  }
151
185
  }
@@ -61,6 +61,7 @@ export function renderSkirConfigErrors(
61
61
  errors: readonly SkirConfigError[],
62
62
  context: {
63
63
  skirConfigPath: string;
64
+ maybeForgotToEditAfterInit: boolean | undefined;
64
65
  },
65
66
  ): void {
66
67
  for (const error of errors) {
@@ -68,6 +69,13 @@ export function renderSkirConfigErrors(
68
69
  console.error(formatSkirConfigError(error, context));
69
70
  }
70
71
  console.error();
72
+ if (context.maybeForgotToEditAfterInit) {
73
+ const { skirConfigPath } = context;
74
+ console.warn(
75
+ `Did you forget to edit ${skirConfigPath} after running 'npx skir init'?`,
76
+ );
77
+ console.warn();
78
+ }
71
79
  }
72
80
 
73
81
  function formatSkirConfigError(
@@ -179,9 +187,9 @@ function formatBreakingChange(
179
187
  const { enumEpression, number, record, variantName } = breakingChange;
180
188
  const errorHeader = makeRed("Illegal variant kind change");
181
189
  const enumName = map(record, getQualifiedName);
182
- const variantKind = map(variantName, (vn) => {
183
- caseMatches(vn.text, "lower_underscore") ? "wrapper" : "constant";
184
- });
190
+ const variantKind = map(variantName, (vn) =>
191
+ caseMatches(vn.text, "lower_underscore") ? "wrapper" : "constant",
192
+ );
185
193
  return [
186
194
  `${locationPrefix}${errorHeader}\n`,
187
195
  " [Last snapshot]\n",
@@ -0,0 +1,6 @@
1
+ /**
2
+ * If this error is thrown during the execution of the compiler, the message
3
+ * will be printed to stderr before exiting. The stack trace will not be
4
+ * printed.
5
+ */
6
+ export class ExitError extends Error {}
package/src/io.ts CHANGED
@@ -1,4 +1,6 @@
1
- import * as fs from "fs";
1
+ import * as FileSystem from "fs";
2
+ import * as FileSystemPromises from "fs/promises";
3
+ import * as Paths from "path";
2
4
 
3
5
  export interface FileReader {
4
6
  readTextFile(path: string): string | undefined;
@@ -11,7 +13,7 @@ export interface FileWriter {
11
13
  class RealFileSystem implements FileReader, FileWriter {
12
14
  readTextFile(path: string): string | undefined {
13
15
  try {
14
- return fs.readFileSync(path, "utf-8");
16
+ return FileSystem.readFileSync(path, "utf-8");
15
17
  } catch (error) {
16
18
  if (
17
19
  error &&
@@ -26,8 +28,25 @@ class RealFileSystem implements FileReader, FileWriter {
26
28
  }
27
29
 
28
30
  writeTextFile(path: string, contents: string): void {
29
- fs.writeFileSync(path, contents, "utf-8");
31
+ FileSystem.writeFileSync(path, contents, "utf-8");
30
32
  }
31
33
  }
32
34
 
33
35
  export const REAL_FILE_SYSTEM = new RealFileSystem();
36
+
37
+ export async function isDirectory(path: string): Promise<boolean> {
38
+ try {
39
+ return (await FileSystemPromises.lstat(path)).isDirectory();
40
+ } catch (_e) {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ export function rewritePathForRendering(path: string): string {
46
+ if (Paths.isAbsolute(path) || /^\.{1,2}[/\\]$/.test(path)) {
47
+ return path;
48
+ } else {
49
+ // To make it clear that it's a path, prepend "./"
50
+ return `.${Paths.sep}${path}`;
51
+ }
52
+ }
@@ -1,21 +1,35 @@
1
1
  import { glob } from "glob";
2
- import * as paths from "path";
3
- import { REAL_FILE_SYSTEM } from "./io.js";
2
+ import * as Paths from "path";
3
+ import { ExitError } from "./exit_error.js";
4
+ import {
5
+ isDirectory,
6
+ REAL_FILE_SYSTEM,
7
+ rewritePathForRendering,
8
+ } from "./io.js";
4
9
  import { ModuleSet } from "./module_set.js";
5
10
 
6
11
  export async function collectModules(srcDir: string): Promise<ModuleSet> {
7
12
  const modules = ModuleSet.create(REAL_FILE_SYSTEM, srcDir);
8
- const skirFiles = await glob(paths.join(srcDir, "**/*.skir"), {
13
+ const skirFiles = await glob(Paths.join(srcDir, "**/*.skir"), {
9
14
  stat: true,
10
15
  withFileTypes: true,
11
16
  });
12
- for await (const skirFile of skirFiles) {
17
+ if (skirFiles.length === 0) {
18
+ const isDir = await isDirectory(srcDir);
19
+ if (!isDir) {
20
+ throw new ExitError(
21
+ "Source directory does not exist: " + rewritePathForRendering(srcDir),
22
+ );
23
+ }
24
+ }
25
+ for (const skirFile of skirFiles) {
13
26
  if (!skirFile.isFile) {
14
27
  continue;
15
28
  }
16
- const relativePath = paths
17
- .relative(srcDir, skirFile.fullpath())
18
- .replace(/\\/g, "/");
29
+ const relativePath = Paths.relative(srcDir, skirFile.fullpath()).replace(
30
+ /\\/g,
31
+ "/",
32
+ );
19
33
  modules.parseAndResolve(relativePath);
20
34
  }
21
35
  return modules;
package/src/module_set.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as paths from "path";
1
+ import * as Paths from "path";
2
2
  import {
3
3
  MutableDocReferenceName,
4
4
  unquoteAndUnescape,
@@ -1311,7 +1311,7 @@ class DefaultModuleParser extends ModuleParserBase {
1311
1311
  }
1312
1312
 
1313
1313
  readSourceCode(modulePath: string): string | undefined {
1314
- return this.fileReader.readTextFile(paths.join(this.rootPath, modulePath));
1314
+ return this.fileReader.readTextFile(Paths.join(this.rootPath, modulePath));
1315
1315
  }
1316
1316
  }
1317
1317
 
@@ -1341,12 +1341,12 @@ function resolveModulePath(
1341
1341
  if (modulePath.startsWith("./") || modulePath.startsWith("../")) {
1342
1342
  // This is a relative path from the module. Let's transform it into a
1343
1343
  // relative path from root.
1344
- modulePath = paths.join(originModulePath, "..", modulePath);
1344
+ modulePath = Paths.join(originModulePath, "..", modulePath);
1345
1345
  }
1346
1346
  // "a/./b/../c" => "a/c"
1347
1347
  // Note that `paths.normalize` will use backslashes on Windows.
1348
1348
  // We don't want that.
1349
- modulePath = paths.normalize(modulePath).replace(/\\/g, "/");
1349
+ modulePath = Paths.normalize(modulePath).replace(/\\/g, "/");
1350
1350
  if (modulePath.startsWith(`../`)) {
1351
1351
  errors.push({
1352
1352
  token: pathToken,
package/src/parser.ts CHANGED
@@ -28,7 +28,7 @@ import type {
28
28
  UnresolvedType,
29
29
  } from "skir-internal";
30
30
  import { convertCase, simpleHash } from "skir-internal";
31
- import * as casing from "./casing.js";
31
+ import * as Casing from "./casing.js";
32
32
  import { mergeDocs } from "./doc_comment_parser.js";
33
33
  import { ModuleTokens } from "./tokenizer.js";
34
34
 
@@ -398,7 +398,7 @@ function parseRecord(
398
398
  if (nameMatch.case < 0) {
399
399
  return null;
400
400
  }
401
- casing.validate(nameMatch.token, "UpperCamel", it.errors);
401
+ Casing.validate(nameMatch.token, "UpperCamel", it.errors);
402
402
  nameToken = nameMatch.token;
403
403
  }
404
404
  let stableId: number | null = null;
@@ -476,7 +476,7 @@ function parseField(
476
476
  }
477
477
  case 2: {
478
478
  const expectedCasing = type ? "lower_underscore" : "UPPER_UNDERSCORE";
479
- casing.validate(name, expectedCasing, it.errors);
479
+ Casing.validate(name, expectedCasing, it.errors);
480
480
  if (recordType === "enum" && name.text === "UNKNOWN") {
481
481
  it.errors.push({
482
482
  token: name,
@@ -790,7 +790,7 @@ function parseImportAs(it: TokenIterator): ImportAlias | null {
790
790
  if (aliasMatch.case < 0) {
791
791
  return null;
792
792
  }
793
- casing.validate(aliasMatch.token, "lower_underscore", it.errors);
793
+ Casing.validate(aliasMatch.token, "lower_underscore", it.errors);
794
794
  if (it.expectThenNext(["from"]).case < 0) return null;
795
795
  const modulePathMatch = it.expectThenNext([TOKEN_IS_STRING_LITERAL]);
796
796
  if (modulePathMatch.case < 0) {
@@ -838,7 +838,7 @@ function parseMethod(it: TokenIterator, doc: Doc): MutableMethod | null {
838
838
  return null;
839
839
  }
840
840
  const name = nameMatch.token;
841
- casing.validate(name, "UpperCamel", it.errors);
841
+ Casing.validate(name, "UpperCamel", it.errors);
842
842
  if (it.expectThenNext(["("]).case < 0) {
843
843
  return null;
844
844
  }
@@ -898,7 +898,7 @@ function parseConstant(it: TokenIterator, doc: Doc): MutableConstant | null {
898
898
  if (nameMatch.case < 0) {
899
899
  return null;
900
900
  }
901
- casing.validate(nameMatch.token, "UPPER_UNDERSCORE", it.errors);
901
+ Casing.validate(nameMatch.token, "UPPER_UNDERSCORE", it.errors);
902
902
  if (it.expectThenNext([":"]).case < 0) {
903
903
  return null;
904
904
  }