skir 0.0.6 → 0.0.7

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 (49) hide show
  1. package/dist/command_line_parser.d.ts.map +1 -1
  2. package/dist/command_line_parser.js +76 -36
  3. package/dist/command_line_parser.js.map +1 -1
  4. package/dist/command_line_parser.test.js +119 -31
  5. package/dist/command_line_parser.test.js.map +1 -1
  6. package/dist/compiler.js +45 -51
  7. package/dist/compiler.js.map +1 -1
  8. package/dist/config.d.ts +2 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +3 -5
  11. package/dist/config.js.map +1 -1
  12. package/dist/config_parser.d.ts +6 -0
  13. package/dist/config_parser.d.ts.map +1 -1
  14. package/dist/config_parser.js +52 -31
  15. package/dist/config_parser.js.map +1 -1
  16. package/dist/config_parser.test.js +53 -20
  17. package/dist/config_parser.test.js.map +1 -1
  18. package/dist/error_renderer.d.ts +1 -0
  19. package/dist/error_renderer.d.ts.map +1 -1
  20. package/dist/error_renderer.js +6 -3
  21. package/dist/error_renderer.js.map +1 -1
  22. package/dist/exit_error.d.ts +8 -0
  23. package/dist/exit_error.d.ts.map +1 -0
  24. package/dist/exit_error.js +8 -0
  25. package/dist/exit_error.js.map +1 -0
  26. package/dist/io.d.ts +2 -0
  27. package/dist/io.d.ts.map +1 -1
  28. package/dist/io.js +22 -3
  29. package/dist/io.js.map +1 -1
  30. package/dist/module_collector.d.ts.map +1 -1
  31. package/dist/module_collector.js +12 -7
  32. package/dist/module_collector.js.map +1 -1
  33. package/dist/module_set.js +4 -4
  34. package/dist/parser.js +6 -6
  35. package/dist/project_initializer.d.ts.map +1 -1
  36. package/dist/project_initializer.js +97 -15
  37. package/dist/project_initializer.js.map +1 -1
  38. package/package.json +8 -6
  39. package/src/command_line_parser.ts +95 -40
  40. package/src/compiler.ts +59 -57
  41. package/src/config.ts +4 -6
  42. package/src/config_parser.ts +61 -32
  43. package/src/error_renderer.ts +11 -3
  44. package/src/exit_error.ts +6 -0
  45. package/src/io.ts +22 -3
  46. package/src/module_collector.ts +21 -7
  47. package/src/module_set.ts +4 -4
  48. package/src/parser.ts +6 -6
  49. package/src/project_initializer.ts +97 -15
@@ -1,31 +1,113 @@
1
- import * as fs from "fs";
2
- import * as paths from "path";
1
+ import * as FileSystem from "fs";
2
+ import * as Paths from "path";
3
+ import { rewritePathForRendering } from "./io.js";
3
4
  export function initializeProject(rootDir) {
4
- const skirYmlPath = paths.join(rootDir, "skir.yml");
5
+ const skirYmlPath = Paths.join(rootDir, "skir.yml");
5
6
  // Check if skir.yml already exists
6
- if (fs.existsSync(skirYmlPath)) {
7
+ if (FileSystem.existsSync(skirYmlPath)) {
7
8
  console.log("A skir.yml file already exists in this directory. Skipping project initialization.");
8
9
  return;
9
10
  }
10
11
  // Create skir.yml file
11
- fs.writeFileSync(skirYmlPath, SKIR_YML_CONTENT, "utf-8");
12
+ FileSystem.writeFileSync(skirYmlPath, SKIR_YML_CONTENT, "utf-8");
12
13
  // Check if skir-src directory exists
13
- const skirSrcDir = paths.join(rootDir, "skir-src");
14
- if (!fs.existsSync(skirSrcDir)) {
14
+ const skirSrcDir = Paths.join(rootDir, "skir-src");
15
+ if (!FileSystem.existsSync(skirSrcDir)) {
15
16
  // Create skir-src directory
16
- fs.mkdirSync(skirSrcDir, { recursive: true });
17
+ FileSystem.mkdirSync(skirSrcDir, { recursive: true });
17
18
  // Create hello_world.skir file
18
- const helloWorldPath = paths.join(skirSrcDir, "hello_world.skir");
19
- fs.writeFileSync(helloWorldPath, HELLO_WORLD_SKIR_CONTENT, "utf-8");
19
+ const helloWorldPath = Paths.join(skirSrcDir, "hello_world.skir");
20
+ FileSystem.writeFileSync(helloWorldPath, HELLO_WORLD_SKIR_CONTENT, "utf-8");
20
21
  }
21
- console.log(`Done. Please edit: ${paths.resolve(skirYmlPath)}`);
22
+ console.log(`Done. Please edit: ${rewritePathForRendering(skirYmlPath)}`);
22
23
  }
23
- const SKIR_YML_CONTENT = `srcDir: skir-src
24
+ const SKIR_YML_CONTENT = `# Configuration file for Skir code generator
25
+ #
26
+ # Documentation: https://skir.build/
27
+ #
28
+ # Cheat sheet:
29
+ # npx skir gen Generate code from .skir files
30
+ # npx skir gen --watch Watch for changes and regenerate automatically
31
+ # npx skir format Format all .skir files
32
+ # npx skir snapshot Take a snapshot of the source directory, verify no
33
+ # breaking changes since last snapshot
24
34
 
35
+ # Directory containing .skir files
36
+ srcDir: skir-src
37
+
38
+ # Uncomment and configure the generators for your target language(s).
25
39
  generators:
26
- - mod: skir-python-gen
27
- skiroutDir: ./skirout
28
- config: {}
40
+ # # --------------------------------------------------------------------------
41
+ # # C++ code generator
42
+ # # Home: https://github.com/gepheum/skir-cc-gen
43
+ # # To install runtime dependencies, follow instructions in repository README
44
+ # # --------------------------------------------------------------------------
45
+ # - mod: skir-cc-gen
46
+ # outDir: ./skirout
47
+ # config:
48
+ # # Set to true if you use GoogleTest
49
+ # writeGoogleTestHeaders: false
50
+
51
+ # # --------------------------------------------------------------------------
52
+ # # Dart code generator
53
+ # # Home: https://github.com/gepheum/skir-dart-gen
54
+ # # To install runtime dependencies: dart pub add skir_client
55
+ # # --------------------------------------------------------------------------
56
+ # - mod: skir-dart-gen
57
+ # outDir: ./skirout
58
+ # config: {}
59
+
60
+ # # --------------------------------------------------------------------------
61
+ # # Java code generator
62
+ # # Home: https://github.com/gepheum/skir-java-gen
63
+ # # Add the following line to your build.gradle dependencies:
64
+ # # implementation("build.skir:skir-client:latest.release")
65
+ # # --------------------------------------------------------------------------
66
+ # - mod: skir-java-gen
67
+ # outDir: ./src/main/java/skirout
68
+ # config: {}
69
+ # # Alternatively:
70
+ # # outDir: ./src/main/java/skirout/my/project
71
+ # # config:
72
+ # # packagePrefix: "my.project."
73
+
74
+ # # --------------------------------------------------------------------------
75
+ # # Kotlin code generator
76
+ # # Home: https://github.com/gepheum/skir-kotlin-gen
77
+ # # Add the following line to your build.gradle dependencies:
78
+ # # implementation("build.skir:skir-client:latest.release")
79
+ # # --------------------------------------------------------------------------
80
+ # - mod: skir-kotlin-gen
81
+ # outDir: ./src/main/kotlin/skirout
82
+ # config: {}
83
+ # # Alternatively:
84
+ # # outDir: ./src/main/kotlin/skirout/my/project
85
+ # # config:
86
+ # # packagePrefix: "my.project."
87
+
88
+ # # --------------------------------------------------------------------------
89
+ # # Python code generator
90
+ # # Home: https://github.com/gepheum/skir-python-gen
91
+ # # To install runtime dependencies: pip install skir-client
92
+ # # --------------------------------------------------------------------------
93
+ # - mod: skir-python-gen
94
+ # outDir: ./skirout
95
+ # config: {}
96
+ # # Alternatively:
97
+ # # outDir: ./my/project/skirout
98
+ # # config:
99
+ # # packagePrefix: "my.project."
100
+
101
+ # # --------------------------------------------------------------------------
102
+ # # TypeScript/JavaScript code generator
103
+ # # Home: https://github.com/gepheum/skir-typescript-gen
104
+ # # To install runtime dependencies: npm i skir-client
105
+ # # --------------------------------------------------------------------------
106
+ # - mod: skir-typescript-gen
107
+ # outDir: ./src/skirout
108
+ # config:
109
+ # # Use ".js" for ES modules, "" for CommonJS
110
+ # importPathExtension: ".js"
29
111
  `;
30
112
  const HELLO_WORLD_SKIR_CONTENT = `/// A point in 2D space.
31
113
  struct Point {
@@ -1 +1 @@
1
- {"version":3,"file":"project_initializer.js","sourceRoot":"","sources":["../src/project_initializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,KAAK,MAAM,MAAM,CAAC;AAE9B,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAEpD,mCAAmC;IACnC,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CACT,oFAAoF,CACrF,CAAC;QACF,OAAO;IACT,CAAC;IAED,uBAAuB;IACvB,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;IAEzD,qCAAqC;IACrC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,4BAA4B;QAC5B,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE9C,+BAA+B;QAC/B,MAAM,cAAc,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;QAClE,EAAE,CAAC,aAAa,CAAC,cAAc,EAAE,wBAAwB,EAAE,OAAO,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,gBAAgB,GAAG;;;;;;CAMxB,CAAC;AAEF,MAAM,wBAAwB,GAAG;;;;;;;CAOhC,CAAC"}
1
+ {"version":3,"file":"project_initializer.js","sourceRoot":"","sources":["../src/project_initializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,IAAI,CAAC;AACjC,OAAO,KAAK,KAAK,MAAM,MAAM,CAAC;AAC9B,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAElD,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAEpD,mCAAmC;IACnC,IAAI,UAAU,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QACvC,OAAO,CAAC,GAAG,CACT,oFAAoF,CACrF,CAAC;QACF,OAAO;IACT,CAAC;IAED,uBAAuB;IACvB,UAAU,CAAC,aAAa,CAAC,WAAW,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;IAEjE,qCAAqC;IACrC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACnD,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACvC,4BAA4B;QAC5B,UAAU,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEtD,+BAA+B;QAC/B,MAAM,cAAc,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;QAClE,UAAU,CAAC,aAAa,CAAC,cAAc,EAAE,wBAAwB,EAAE,OAAO,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,uBAAuB,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuFxB,CAAC;AAEF,MAAM,wBAAwB,GAAG;;;;;;;CAOhC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skir",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "",
5
5
  "homepage": "https://github.com/gepheum/skir#readme",
6
6
  "bugs": {
@@ -54,17 +54,19 @@
54
54
  "zod": "^4.2.1"
55
55
  },
56
56
  "devDependencies": {
57
+ "@eslint/js": "^9.39.2",
57
58
  "@types/mocha": "^10.0.1",
58
59
  "@types/node": "^20.6.0",
59
- "@typescript-eslint/eslint-plugin": "^6.13.0",
60
- "@typescript-eslint/parser": "^6.13.0",
60
+ "@typescript-eslint/eslint-plugin": "^8.50.1",
61
+ "@typescript-eslint/parser": "^8.50.1",
61
62
  "buckwheat": "^1.1.2",
62
- "eslint": "^8.54.0",
63
- "mocha": "^10.8.2",
63
+ "eslint": "^9.39.2",
64
+ "mocha": "^11.7.5",
64
65
  "prettier": "^3.2.4",
65
66
  "prettier-plugin-organize-imports": "^4.2.0",
66
67
  "ts-node": "^10.9.2",
67
68
  "tsx": "^4.21.0",
68
- "typescript": "^5.2.2"
69
+ "typescript": "^5.2.2",
70
+ "typescript-eslint": "^8.50.1"
69
71
  }
70
72
  }
@@ -80,30 +80,33 @@ export function parseCommandLine(args: string[]): ParsedArgs {
80
80
  const COMMAND_BASE = "npx skir";
81
81
 
82
82
  const HELP_TEXT = `
83
- Usage: ${COMMAND_BASE} <command> [subcommand] [options]
83
+ Usage: ${COMMAND_BASE} <command> [options]
84
84
 
85
85
  Commands:
86
- gen [watch] Generate code from Skir source files to target languages
87
- watch: Automatically regenerate when .skir files change
88
- format [check] Format all .skir files in the specified directory
89
- check: Fail if code is not properly formatted
90
- snapshot [check|view] Manage .skir file snapshots for compatibility checking
91
- check: Fail if there are breaking changes since last snapshot
92
- view: Display the last snapshot
93
- init Initialize a new Skir project with a minimal skir.yml file
94
- help Display this help message
86
+ init Initialize a new project with a minimal skir.yml file
87
+ gen Generate code from .skir files to target languages
88
+ format Format all .skir files in the source directory
89
+ snapshot Take a snapshot of the source directory, look for
90
+ breaking changes since the last snapshot
91
+ help Display this help message
95
92
 
96
93
  Options:
97
- --root, -r <path> Path to the directory containing the skir.yml configuration file
94
+ --root, -r <path> Path to the directory containing the skir.yml file
95
+ --watch, -w Automatically run code generation when .skir files change
96
+ (gen only)
97
+ --check Fail if code is not properly formatted (format) or if there
98
+ are breaking changes (snapshot)
99
+ --view Display the last snapshot (snapshot only)
98
100
 
99
101
  Examples:
102
+ ${COMMAND_BASE} init
100
103
  ${COMMAND_BASE} gen
101
- ${COMMAND_BASE} gen watch
104
+ ${COMMAND_BASE} gen --watch
102
105
  ${COMMAND_BASE} format --root path/to/root/dir
103
- ${COMMAND_BASE} format check -r path/to/root/dir
106
+ ${COMMAND_BASE} format --check -r path/to/root/dir
104
107
  ${COMMAND_BASE} snapshot
105
- ${COMMAND_BASE} snapshot check
106
- ${COMMAND_BASE} snapshot view --root path/to/root/dir
108
+ ${COMMAND_BASE} snapshot --check
109
+ ${COMMAND_BASE} snapshot --view --root path/to/root/dir
107
110
  `;
108
111
 
109
112
  export class CommandLineParseError extends Error {
@@ -115,7 +118,9 @@ export class CommandLineParseError extends Error {
115
118
 
116
119
  type ParsedOptions = {
117
120
  root?: string;
118
- subcommand?: string;
121
+ watch?: boolean;
122
+ check?: boolean;
123
+ view?: boolean;
119
124
  unknown: string[];
120
125
  };
121
126
 
@@ -128,7 +133,21 @@ function parseOptions(args: string[]): ParsedOptions {
128
133
  const arg = args[i];
129
134
  if (!arg) continue;
130
135
 
131
- if (arg === "--root" || arg === "-r") {
136
+ // Check for --option=value syntax
137
+ if (arg.startsWith("--root=") || arg.startsWith("-r=")) {
138
+ const value = arg.substring(arg.indexOf("=") + 1);
139
+ if (!value) {
140
+ throw new CommandLineParseError(
141
+ `Option ${arg.split("=")[0]} requires a value`,
142
+ );
143
+ }
144
+ if (options.root !== undefined) {
145
+ throw new CommandLineParseError(
146
+ `Option ${arg.split("=")[0]} specified multiple times`,
147
+ );
148
+ }
149
+ options.root = value;
150
+ } else if (arg === "--root" || arg === "-r") {
132
151
  if (i + 1 >= args.length) {
133
152
  throw new CommandLineParseError(`Option ${arg} requires a value`);
134
153
  }
@@ -139,14 +158,32 @@ function parseOptions(args: string[]): ParsedOptions {
139
158
  }
140
159
  options.root = args[i + 1];
141
160
  i++; // Skip the next argument as it's the value
161
+ } else if (arg === "--watch" || arg === "-w") {
162
+ if (options.watch) {
163
+ throw new CommandLineParseError(
164
+ `Option ${arg} specified multiple times`,
165
+ );
166
+ }
167
+ options.watch = true;
168
+ } else if (arg === "--check") {
169
+ if (options.check) {
170
+ throw new CommandLineParseError(
171
+ `Option --check specified multiple times`,
172
+ );
173
+ }
174
+ options.check = true;
175
+ } else if (arg === "--view") {
176
+ if (options.view) {
177
+ throw new CommandLineParseError(
178
+ `Option --view specified multiple times`,
179
+ );
180
+ }
181
+ options.view = true;
142
182
  } else if (arg.startsWith("-")) {
143
183
  options.unknown.push(arg);
144
184
  } else {
145
- // Positional argument - treat as subcommand
146
- if (options.subcommand !== undefined) {
147
- throw new CommandLineParseError(`Unexpected argument: ${arg}`);
148
- }
149
- options.subcommand = arg;
185
+ // Positional argument - not allowed anymore
186
+ throw new CommandLineParseError(`Unexpected argument: ${arg}`);
150
187
  }
151
188
  }
152
189
 
@@ -156,64 +193,82 @@ function parseOptions(args: string[]): ParsedOptions {
156
193
  function buildGenCommand(options: ParsedOptions): ParsedArgs {
157
194
  validateNoUnknownOptions(options, "gen");
158
195
 
159
- if (options.subcommand !== undefined && options.subcommand !== "watch") {
196
+ if (options.check) {
197
+ throw new CommandLineParseError(
198
+ `Option --check is not valid for 'gen' command`,
199
+ );
200
+ }
201
+ if (options.view) {
160
202
  throw new CommandLineParseError(
161
- `Unknown subcommand for 'gen': ${options.subcommand}`,
203
+ `Option --view is not valid for 'gen' command`,
162
204
  );
163
205
  }
164
206
 
165
207
  return {
166
208
  kind: "gen",
167
209
  root: options.root,
168
- subcommand: options.subcommand === "watch" ? "watch" : undefined,
210
+ subcommand: options.watch ? "watch" : undefined,
169
211
  };
170
212
  }
171
213
 
172
214
  function buildFormatCommand(options: ParsedOptions): ParsedArgs {
173
215
  validateNoUnknownOptions(options, "format");
174
216
 
175
- if (options.subcommand !== undefined && options.subcommand !== "check") {
217
+ if (options.watch) {
176
218
  throw new CommandLineParseError(
177
- `Unknown subcommand for 'format': ${options.subcommand}`,
219
+ `Option --watch is not valid for 'format' command`,
220
+ );
221
+ }
222
+ if (options.view) {
223
+ throw new CommandLineParseError(
224
+ `Option --view is not valid for 'format' command`,
178
225
  );
179
226
  }
180
227
 
181
228
  return {
182
229
  kind: "format",
183
230
  root: options.root,
184
- subcommand: options.subcommand === "check" ? "check" : undefined,
231
+ subcommand: options.check ? "check" : undefined,
185
232
  };
186
233
  }
187
234
 
188
235
  function buildSnapshotCommand(options: ParsedOptions): ParsedArgs {
189
236
  validateNoUnknownOptions(options, "snapshot");
190
237
 
191
- if (
192
- options.subcommand !== undefined &&
193
- options.subcommand !== "check" &&
194
- options.subcommand !== "view"
195
- ) {
238
+ if (options.watch) {
239
+ throw new CommandLineParseError(
240
+ `Option --watch is not valid for 'snapshot' command`,
241
+ );
242
+ }
243
+ if (options.check && options.view) {
196
244
  throw new CommandLineParseError(
197
- `Unknown subcommand for 'snapshot': ${options.subcommand}`,
245
+ `Options --check and --view cannot be used together`,
198
246
  );
199
247
  }
200
248
 
201
249
  return {
202
250
  kind: "snapshot",
203
251
  root: options.root,
204
- subcommand:
205
- options.subcommand === "check" || options.subcommand === "view"
206
- ? options.subcommand
207
- : undefined,
252
+ subcommand: options.check ? "check" : options.view ? "view" : undefined,
208
253
  };
209
254
  }
210
255
 
211
256
  function buildInitCommand(options: ParsedOptions): ParsedArgs {
212
257
  validateNoUnknownOptions(options, "init");
213
258
 
214
- if (options.subcommand !== undefined) {
259
+ if (options.watch) {
260
+ throw new CommandLineParseError(
261
+ `Option --watch is not valid for 'init' command`,
262
+ );
263
+ }
264
+ if (options.check) {
265
+ throw new CommandLineParseError(
266
+ `Option --check is not valid for 'init' command`,
267
+ );
268
+ }
269
+ if (options.view) {
215
270
  throw new CommandLineParseError(
216
- `Unknown subcommand for 'init': ${options.subcommand}`,
271
+ `Option --view is not valid for 'init' command`,
217
272
  );
218
273
  }
219
274
 
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,12 +360,16 @@ 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": {
@@ -383,15 +381,9 @@ async function main(): Promise<void> {
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,11 +409,11 @@ 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
415
  takeSnapshot({
424
- rootDir: root!,
416
+ rootDir: root,
425
417
  srcDir: srcDir,
426
418
  check: args.subcommand === "check",
427
419
  });
@@ -435,4 +427,14 @@ async function main(): Promise<void> {
435
427
  }
436
428
  }
437
429
 
438
- main();
430
+ try {
431
+ await main();
432
+ } catch (e) {
433
+ if (e instanceof Error) {
434
+ console.error(makeRed(e.message));
435
+ if (e instanceof ExitError) {
436
+ process.exit(1);
437
+ }
438
+ }
439
+ throw e;
440
+ }