nomen-lang 0.0.5 → 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.
package/src/index.ts CHANGED
@@ -8,14 +8,64 @@ import { hideBin } from "yargs/helpers";
8
8
  import yargs from "yargs/yargs";
9
9
 
10
10
  import build, { default_platform } from "../../src/build.ts";
11
+ import { format_source, type FormatOptions } from "../../src/format.ts";
11
12
  import join from "../../src/join.ts";
12
13
  import { get_library } from "../../src/lib.ts";
13
14
  import parse from "../../src/parse.ts";
14
- import render_errors from "./format_errors.ts";
15
+ import { run_docs } from "./docs.ts";
16
+ import render_errors, { render_warnings } from "./format_errors.ts";
17
+ import { runTests } from "./test.ts";
15
18
  import type Config from "./types/Config.ts";
16
19
 
17
20
  const SUPPORTED_EXTENSION = ".nm";
18
21
 
22
+ type Mode = "check" | "build" | "run";
23
+
24
+ // Strip `//` line and `/* */` block comments so a .jsonc file parses as JSON.
25
+ function parse_jsonc(text: string): any {
26
+ return JSON.parse(text.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""));
27
+ }
28
+
29
+ /** Read the `format` options from the nearest package.jsonc above `start`. */
30
+ function load_format_options(start: string): Partial<FormatOptions> {
31
+ let dir = fs.lstatSync(start).isDirectory() ? start : path.dirname(start);
32
+ for (let i = 0; i < 20; i++) {
33
+ const config_path = path.join(dir, "package.jsonc");
34
+ if (fs.existsSync(config_path)) {
35
+ try {
36
+ const parsed = parse_jsonc(fs.readFileSync(config_path, "utf8"));
37
+ if (parsed.format) return parsed.format as Partial<FormatOptions>;
38
+ } catch {
39
+ // ignore malformed package.jsonc and keep searching
40
+ }
41
+ }
42
+ const lib_config = path.join(dir, "core", "package.jsonc");
43
+ if (fs.existsSync(lib_config)) {
44
+ try {
45
+ const parsed = parse_jsonc(fs.readFileSync(lib_config, "utf8"));
46
+ if (parsed.format) return parsed.format as Partial<FormatOptions>;
47
+ } catch {
48
+ // ignore
49
+ }
50
+ }
51
+ const parent = path.dirname(dir);
52
+ if (parent === dir) break;
53
+ dir = parent;
54
+ }
55
+ return {};
56
+ }
57
+
58
+ /** Recursively collect every `.nm` file under `folder`. */
59
+ function collect_nm_files(folder: string): string[] {
60
+ const out: string[] = [];
61
+ for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
62
+ const full = path.join(folder, entry.name);
63
+ if (entry.isDirectory()) out.push(...collect_nm_files(full));
64
+ else if (shouldProcessFile(entry.name)) out.push(full);
65
+ }
66
+ return out;
67
+ }
68
+
19
69
  // The folder whose `build/` subdirectory receives compiler output. Set during
20
70
  // input resolution: the --in folder, the .nm file's folder, or — for
21
71
  // package.jsonc discovery — the package folder (cwd), not the entry's folder.
@@ -23,8 +73,21 @@ let build_root: string | undefined;
23
73
 
24
74
  console.log("\n~ NOMEN ~\n");
25
75
 
26
- const options = yargs(hideBin(process.argv))
27
- .usage("Usage: nomen --in [file/folder]")
76
+ const parser = yargs(hideBin(process.argv))
77
+ .usage(
78
+ "Usage:\n" +
79
+ " nomen run --in [file/folder] Parse, check, build and run a program\n" +
80
+ " nomen build --in [file/folder] Parse, check and build (no run)\n" +
81
+ " nomen check --in [file/folder] Parse and check only\n" +
82
+ " nomen format [--in folder] Reformat every .nm file\n" +
83
+ " nomen docs [--in file] Generate markdown documentation",
84
+ )
85
+ .command("run", "Parse, check, build and run a program")
86
+ .command("build", "Parse, check and build (compile and link, but do not run)")
87
+ .command("check", "Parse and check only")
88
+ .command("format", "Reformat every .nm file")
89
+ .command("docs", "Generate markdown documentation")
90
+ .command("test", "Discover and run *.test.nm files with the Tester harness")
28
91
  .option("in", {
29
92
  alias: "i",
30
93
  describe: "Input file or folder",
@@ -45,6 +108,11 @@ const options = yargs(hideBin(process.argv))
45
108
  describe: "Whether to watch for file changes",
46
109
  type: "boolean",
47
110
  })
111
+ .option("filter", {
112
+ alias: "f",
113
+ describe: "Only run test files whose path matches this regex",
114
+ type: "string",
115
+ })
48
116
  .option("arch", {
49
117
  alias: "a",
50
118
  describe: "Target architecture (aarch64 or c)",
@@ -69,10 +137,69 @@ const options = yargs(hideBin(process.argv))
69
137
  describe: "Path to audit_runtime.c, linked in when --audit is set",
70
138
  type: "string",
71
139
  })
72
- .help(true)
73
- .parseSync();
140
+ .option("check", {
141
+ describe: "For `nomen format`: report files that would change without writing them",
142
+ type: "boolean",
143
+ })
144
+ .help(true);
145
+
146
+ const options = parser.parseSync();
147
+
148
+ const command = options._[0];
74
149
 
75
150
  try {
151
+ // `nomen docs` generates markdown documentation instead of compiling.
152
+ if (command === "docs") {
153
+ run_docs(typeof options.in === "string" ? options.in : undefined);
154
+ process.exit(0);
155
+ }
156
+
157
+ // `nomen test` discovers and runs every `*.test.nm` file under --in (or
158
+ // the cwd), compiling each into a Tester harness and reporting results.
159
+ if (command === "test") {
160
+ const root = options.in ?? process.cwd();
161
+ const filter = typeof options.filter === "string" ? new RegExp(options.filter) : undefined;
162
+ const arch = (options.arch as string | undefined) ?? "aarch64";
163
+ const ok = runTests(root, { arch, filter });
164
+ process.exit(ok ? 0 : 1);
165
+ }
166
+
167
+ // `nomen format` re-indents and tidies every .nm file under a folder.
168
+ if (command === "format") {
169
+ const root = options.in ?? process.cwd();
170
+ const format_options = load_format_options(root);
171
+ const files = collect_nm_files(root);
172
+ let changed = 0;
173
+ for (const file of files) {
174
+ const source = fs.readFileSync(file, "utf8");
175
+ const result = format_source(source, format_options);
176
+ if (result.unsafe) {
177
+ console.log(`Skipped ${file}: ${result.unsafe}`);
178
+ continue;
179
+ }
180
+ if (result.changed) {
181
+ if (!options.check) fs.writeFileSync(file, result.code);
182
+ changed += 1;
183
+ console.log(`Formatted ${file}`);
184
+ }
185
+ }
186
+ console.log(`\nFormatted ${changed} of ${files.length} file(s).`);
187
+ process.exit(0);
188
+ }
189
+
190
+ // `run`, `build` and `check` all start from parsed + checked source; `run`
191
+ // also links and executes, `build` stops after linking, `check` stops after
192
+ // checking. An unknown (or missing) command prints the help instead.
193
+ let mode: Mode | undefined;
194
+ if (command === "run") mode = "run";
195
+ else if (command === "build") mode = "build";
196
+ else if (command === "check") mode = "check";
197
+
198
+ if (!mode) {
199
+ parser.showHelp("log");
200
+ process.exit(1);
201
+ }
202
+
76
203
  // An explicit --in wins; otherwise discover what to compile from the
77
204
  // working folder — a package.jsonc `entry`, or a lone .nm file.
78
205
  options.in = options.in ?? resolve_input();
@@ -100,24 +227,21 @@ try {
100
227
 
101
228
  // Is the --in path a folder
102
229
  if (fs.lstatSync(options.in).isDirectory()) {
103
- // Loop through files in the folder
104
- //processFolder(options.in);
105
230
  if (options.watch) {
106
- watchPath(options.in, config);
231
+ watchPath(options.in, config, mode);
107
232
  } else {
108
- processFolder(options.in, config);
233
+ processFolder(options.in, config, mode);
109
234
  }
110
235
  } else {
111
236
  // Process the supplied file
112
237
  const extname = path.extname(options.in);
113
238
  if (shouldProcessFile(options.in)) {
114
- //processFile(options.in);
115
239
  // NOTE: We get add notifications for all watched files immediately
116
240
  // TODO: Is this the case on Windows etc too?
117
241
  if (options.watch) {
118
- watchPath(options.in, config);
242
+ watchPath(options.in, config, mode);
119
243
  } else {
120
- processFile(options.in, config);
244
+ processFile(options.in, config, mode);
121
245
  }
122
246
  } else {
123
247
  console.log("Unsupported file type: " + extname);
@@ -227,20 +351,20 @@ function compile_audit_runtime(config: Config, input_path: string, buildDir: str
227
351
  return audit_obj;
228
352
  }
229
353
 
230
- function watchPath(p: string, config: Config) {
354
+ function watchPath(p: string, config: Config, mode: Mode) {
231
355
  chokidar.watch(p).on("all", (event, filePath) => {
232
356
  if (shouldProcessFile(filePath)) {
233
- processFile(filePath, config);
357
+ processFile(filePath, config, mode);
234
358
  }
235
359
  });
236
360
  }
237
361
 
238
- function processFolder(folder: string, config: Config) {
362
+ function processFolder(folder: string, config: Config, mode: Mode) {
239
363
  const dir = fs.opendirSync(folder);
240
364
  let dirent;
241
365
  while ((dirent = dir.readSync()) !== null) {
242
366
  if (shouldProcessFile(dirent.name)) {
243
- processFile(path.join(folder, dirent.name), config);
367
+ processFile(path.join(folder, dirent.name), config, mode);
244
368
  // @ts-ignore
245
369
  let _ = fs.watch;
246
370
  }
@@ -252,7 +376,7 @@ function shouldProcessFile(filename: string) {
252
376
  return path.extname(filename) === SUPPORTED_EXTENSION;
253
377
  }
254
378
 
255
- function processFile(filename: string, config: Config) {
379
+ function processFile(filename: string, config: Config, mode: Mode) {
256
380
  console.log("Processing", filename);
257
381
 
258
382
  const arch = config.arch || "aarch64";
@@ -265,22 +389,28 @@ function processFile(filename: string, config: Config) {
265
389
 
266
390
  let startTime = performance.now();
267
391
 
268
- let input = join(path.resolve(filename), config.lib);
392
+ const resolved_path = path.resolve(filename);
393
+ const input = join(resolved_path, config.lib);
269
394
  const library = config.lib ? get_library(config.lib) : undefined;
270
- const parsed = parse(input, library);
271
- // TODO: If verbose flag
272
- // console.log("Parsed");
395
+ const parsed = parse(input, library, resolved_path);
273
396
 
274
397
  let errors = parsed.errors;
275
- const ok = !errors.length;
276
398
 
277
- if (!ok) {
399
+ if (errors.length) {
278
400
  console.log(render_errors(input, errors));
279
401
  return;
280
402
  }
281
403
 
282
- // TODO: If verbose flag
283
- // console.log("Built");
404
+ // Warnings come out of the parse/check phase, so every mode reports them.
405
+ if (parsed.warnings.length) console.log(render_warnings(input, parsed.warnings));
406
+
407
+ // `check` stops after parsing and checking — no building, linking or running.
408
+ if (mode === "check") {
409
+ const checkTime = performance.now();
410
+ console.log(`Checked in ${(checkTime - startTime).toFixed(2)}ms`);
411
+ return;
412
+ }
413
+
284
414
  const result = build(parsed.root, { arch, platform, audit: config.audit });
285
415
 
286
416
  if (result.errors && result.errors.length > 0) {
@@ -311,6 +441,7 @@ function processFile(filename: string, config: Config) {
311
441
  console.log(`Created ${codefile} in ${(compileTime - startTime).toFixed(2)}ms`);
312
442
  console.log("");
313
443
 
444
+ // `build` links the executable but does not run it; `run` links and runs.
314
445
  startTime = performance.now();
315
446
 
316
447
  const audit_obj = config.audit ? compile_audit_runtime(config, resolved, buildDir) : undefined;
@@ -322,6 +453,13 @@ function processFile(filename: string, config: Config) {
322
453
  ? " -framework CoreGraphics -framework Foundation -framework AppKit -lobjc"
323
454
  : "";
324
455
  execSync(`clang -o ${outfile} ${link_inputs}${framework_flags}`);
456
+
457
+ if (mode === "build") {
458
+ const buildTime = performance.now();
459
+ console.log(`Built ${outfile} in ${(buildTime - startTime).toFixed(2)}ms`);
460
+ return;
461
+ }
462
+
325
463
  execSync(outfile, { stdio: "inherit" });
326
464
 
327
465
  const runTime = performance.now();