@lango-project/lango-js 0.0.0-stage → 0.1.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/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@lango-project/lango-js",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "license": "Apache-2.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "bin": {
10
+ "lango": "src/cli.mjs"
11
+ },
12
+ "exports": {
13
+ ".": "./src/compiler.mjs",
14
+ "./runtime": "./src/runtime.mjs",
15
+ "./database": "./src/database.mjs"
16
+ },
17
+ "files": [
18
+ "src/*.mjs",
19
+ "src/vendor/**",
20
+ "src/template/**",
21
+ "README.md",
22
+ "LICENSE",
23
+ "NOTICE"
24
+ ],
25
+ "scripts": {
26
+ "build": "node src/cli.mjs build",
27
+ "run": "node src/cli.mjs run",
28
+ "test:lango": "node src/cli.mjs test",
29
+ "test": "node --test test/**/*.test.mjs"
30
+ },
31
+ "engines": {
32
+ "node": ">=24"
33
+ }
34
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { mkdir, mkdtemp, readFile, readdir, writeFile } from "node:fs/promises";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import { buildFile, formatDiagnostics, formatSource, LangoError } from "./compiler.mjs";
8
+ import { createSqliteAdapter } from "./database.mjs";
9
+ import { Decimal } from "./runtime.mjs";
10
+
11
+ const [command, inputPath, ...rest] = process.argv.slice(2);
12
+ const displayError = (error) => error instanceof LangoError
13
+ ? formatDiagnostics(error)
14
+ : error instanceof Error ? error.message : error;
15
+ const buildOptions = (arguments_) => {
16
+ if (arguments_[0] !== "--out" || !arguments_[1]) return null;
17
+ const profile = {};
18
+ for (let index = 2; index < arguments_.length; index += 2) {
19
+ const option = arguments_[index];
20
+ const value = arguments_[index + 1];
21
+ if (!value) return null;
22
+ if (option === "--decimal-division-scale")
23
+ profile.decimal_division_scale = Number(value);
24
+ else if (option === "--decimal-division-rounding")
25
+ profile.decimal_division_rounding = Decimal.Rounding[value] ?? value;
26
+ else return null;
27
+ }
28
+ return { outputPath: arguments_[1], profile };
29
+ };
30
+ const build = command === "build" && inputPath ? buildOptions(rest) : null;
31
+ const runOptions = (arguments_) => {
32
+ const options = { database: null, catalog: null };
33
+ let index = 0;
34
+ while (arguments_[index] === "--database" || arguments_[index] === "--catalog") {
35
+ const option = arguments_[index];
36
+ const value = arguments_[index + 1];
37
+ if (!value) return null;
38
+ if (option === "--database") options.database = value;
39
+ else options.catalog = value;
40
+ index += 2;
41
+ }
42
+ if (arguments_[index] === "--") index += 1;
43
+ return { ...options, args: arguments_.slice(index) };
44
+ };
45
+ const testOptions = command === "test" && inputPath ? runOptions(rest) : null;
46
+
47
+ async function initProject(directory) {
48
+ const target = path.resolve(directory);
49
+ await mkdir(target, { recursive: true });
50
+ if ((await readdir(target)).length)
51
+ throw new Error(`Lango init requires an empty directory: ${target}`);
52
+ const packageJson = JSON.parse(
53
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
54
+ );
55
+ const files = [
56
+ ["gitignore", ".gitignore"],
57
+ "README.md",
58
+ "package.json",
59
+ "src/main.lango",
60
+ "test/main.lango",
61
+ ".vscode/extensions.json",
62
+ ".vscode/settings.json",
63
+ ];
64
+ for (const entry of files) {
65
+ const [templateFile, outputFile] = Array.isArray(entry) ? entry : [entry, entry];
66
+ const template = await readFile(
67
+ new URL(`./template/${templateFile}`, import.meta.url),
68
+ "utf8",
69
+ );
70
+ const output = path.join(target, outputFile);
71
+ await mkdir(path.dirname(output), { recursive: true });
72
+ await writeFile(
73
+ output,
74
+ template.replaceAll("{{LANGO_VERSION}}", `^${packageJson.version}`),
75
+ "utf8",
76
+ );
77
+ }
78
+ }
79
+
80
+ if (command === "init" && inputPath && rest.length === 0) {
81
+ try {
82
+ await initProject(inputPath);
83
+ console.log(`Created Lango project in ${path.resolve(inputPath)}`);
84
+ } catch (error) {
85
+ console.error(displayError(error));
86
+ process.exitCode = 2;
87
+ }
88
+ } else if (build) {
89
+ try {
90
+ await buildFile(path.resolve(inputPath), path.resolve(build.outputPath), new Set(), false, build.profile);
91
+ } catch (error) {
92
+ console.error(displayError(error));
93
+ process.exitCode = error instanceof LangoError ? 1 : 2;
94
+ }
95
+ } else if (command === "run" && inputPath) {
96
+ let database = null;
97
+ try {
98
+ const options = runOptions(rest);
99
+ if (!options) throw new Error("Lango run requires a database filename after --database");
100
+ const directory = await mkdtemp(path.join(os.tmpdir(), "lango-run-"));
101
+ const outputPath = path.join(directory, "main.mjs");
102
+ await buildFile(path.resolve(inputPath), outputPath);
103
+ const runtime = await import(pathToFileURL(path.join(directory, ".lango/runtime.mjs")).href);
104
+ const program = await import(pathToFileURL(outputPath).href);
105
+ if (typeof program.main !== "function")
106
+ throw new Error("Lango run requires an exported main(args)");
107
+ if (options.database) database = createSqliteAdapter(path.resolve(options.database));
108
+ const catalog = options.catalog
109
+ ? JSON.parse(await readFile(path.resolve(options.catalog), "utf8"))
110
+ : {};
111
+ if (!catalog || Array.isArray(catalog) || typeof catalog !== "object")
112
+ throw new Error("Lango run catalog must be a JSON object");
113
+ runtime.configure({ output: (value) => console.log(value), catalog, database_adapter: database });
114
+ await program.main(options.args);
115
+ } catch (error) {
116
+ console.error(displayError(error));
117
+ process.exitCode = error instanceof LangoError ? 1 : 2;
118
+ } finally {
119
+ database?.close();
120
+ }
121
+ } else if (testOptions) {
122
+ let database = null;
123
+ try {
124
+ if (testOptions.args.length)
125
+ throw new Error("Lango test does not accept program arguments");
126
+ const directory = await mkdtemp(path.join(os.tmpdir(), "lango-test-"));
127
+ const outputPath = path.join(directory, "main.mjs");
128
+ await buildFile(path.resolve(inputPath), outputPath, new Set(), true);
129
+ const runtime = await import(pathToFileURL(path.join(directory, ".lango/runtime.mjs")).href);
130
+ const program = await import(pathToFileURL(outputPath).href);
131
+ if (testOptions.database) database = createSqliteAdapter(path.resolve(testOptions.database));
132
+ const catalog = testOptions.catalog
133
+ ? JSON.parse(await readFile(path.resolve(testOptions.catalog), "utf8"))
134
+ : {};
135
+ if (!catalog || Array.isArray(catalog) || typeof catalog !== "object")
136
+ throw new Error("Lango test catalog must be a JSON object");
137
+ runtime.configure({ output: (value) => console.log(value), catalog, database_adapter: database });
138
+ let failures = 0;
139
+ const seenTests = new Set();
140
+ for (const test of program.__langoTests ?? []) {
141
+ if (seenTests.has(test.run)) continue;
142
+ seenTests.add(test.run);
143
+ try {
144
+ program.__langoResetTests?.();
145
+ runtime.configure({ output: (value) => console.log(value), catalog, database_adapter: database });
146
+ test.run();
147
+ console.log(`PASS ${test.name}`);
148
+ } catch (error) {
149
+ failures += 1;
150
+ console.error(`FAIL ${test.name}: ${displayError(error)}`);
151
+ }
152
+ }
153
+ if (failures) process.exitCode = 1;
154
+ } catch (error) {
155
+ console.error(displayError(error));
156
+ process.exitCode = error instanceof LangoError ? 1 : 2;
157
+ } finally {
158
+ database?.close();
159
+ }
160
+ } else if (command === "format" && inputPath && rest.length === 0) {
161
+ try {
162
+ const input = path.resolve(inputPath);
163
+ const source = await readFile(input, "utf8");
164
+ await writeFile(input, formatSource(source, input), "utf8");
165
+ } catch (error) {
166
+ console.error(displayError(error));
167
+ process.exitCode = error instanceof LangoError ? 1 : 2;
168
+ }
169
+ } else {
170
+ console.error("Usage: node src/cli.mjs init <directory>\n node src/cli.mjs build <input.lango> --out <output.mjs> [--decimal-division-scale <n>] [--decimal-division-rounding <EnumName>]\n node src/cli.mjs run <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>] [-- args...]\n node src/cli.mjs test <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>]\n node src/cli.mjs format <input.lango>");
171
+ process.exitCode = 2;
172
+ }