@vistta/cli 1.0.1 → 1.1.0

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.
@@ -0,0 +1,76 @@
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { Runtime } from "../classes/runtime.js";
5
+
6
+ suite("Runtime", () => {
7
+ test("resolve yields distinct URLs for plain vs bundler imports of the same file", async () => {
8
+ const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
9
+ const file = import.meta.filename || new URL(import.meta.url).pathname.replace(/^\//, "");
10
+ const captured = [];
11
+ const next = (url) => {
12
+ captured.push(url);
13
+ return url;
14
+ };
15
+
16
+ await runtime.resolve(file, {}, next);
17
+ await runtime.resolve(file, { importAttributes: { type: "bundler" } }, next);
18
+ await runtime.resolve(file, { conditions: ["bundler"] }, next);
19
+
20
+ expect(captured[0]).not.toEqual(captured[1]);
21
+ expect(captured[0]).not.toEqual(captured[2]);
22
+ expect(captured[1]).toEqual(captured[2]);
23
+ expect(/\?__bundler__$/.test(captured[1])).toEqual(true);
24
+ expect(/\?__bundler__$/.test(captured[0])).toEqual(false);
25
+ });
26
+
27
+ test("resolve strips bundler suffix from parentURL when resolving relatives", async () => {
28
+ const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
29
+ const here = new URL(".", import.meta.url).href;
30
+ const parentURL = here + "runtime.test.js?__bundler__";
31
+ let captured;
32
+ await runtime.resolve("./runtime.test.js", { parentURL, importAttributes: { type: "bundler" } }, (url) => {
33
+ captured = url;
34
+ return url;
35
+ });
36
+ expect(captured.includes("?__bundler__?")).toEqual(false);
37
+ expect(/\?__bundler__$/.test(captured)).toEqual(true);
38
+ });
39
+
40
+ test("resolve detects bundler mode from importAttributes.type even without conditions", async () => {
41
+ const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
42
+ const file = import.meta.filename || new URL(import.meta.url).pathname.replace(/^\//, "");
43
+ const captured = [];
44
+ await runtime.resolve(file, { importAttributes: { type: "bundler" } }, (url) => {
45
+ captured.push(url);
46
+ return url;
47
+ });
48
+ expect(/\?__bundler__$/.test(captured[0])).toEqual(true);
49
+ });
50
+
51
+ test("resolve marks context.file for absolute bundler imports", async () => {
52
+ const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
53
+ const dir = mkdtempSync(join(tmpdir(), "vistta-runtime-"));
54
+ const file = join(dir, "page.jsx");
55
+ writeFileSync(file, "export default 1;");
56
+ const ctx = { importAttributes: { type: "bundler" } };
57
+ await runtime.resolve(file, ctx, (url) => url);
58
+ expect(ctx.file).toEqual(true);
59
+ });
60
+
61
+ test("resolve preserves non-bundler query string params", async () => {
62
+ const runtime = new Runtime({ loaders: [], resolvers: ["js"], options: {} });
63
+ const dir = mkdtempSync(join(tmpdir(), "vistta-runtime-"));
64
+ const file = join(dir, "data.js");
65
+ writeFileSync(file, "export default 1;");
66
+ const here = new URL(import.meta.url);
67
+ const hereURL = new URL(".", here).href;
68
+ const captured = [];
69
+ await runtime.resolve("./data.js?type=json", { parentURL: hereURL }, (url) => {
70
+ captured.push(url);
71
+ return url;
72
+ });
73
+ expect(captured[0].includes("type=json")).toEqual(true);
74
+ expect(/\?__bundler__$/.test(captured[0])).toEqual(false);
75
+ });
76
+ });
@@ -0,0 +1,83 @@
1
+ import { TestRunner } from "../classes/test-runner.js";
2
+
3
+ suite("TestRunner", () => {
4
+ test("suite().test() registers a passing test", async () => {
5
+ const runner = new TestRunner();
6
+ runner.suite("s", () => {
7
+ runner.test("t", () => 1);
8
+ });
9
+ const results = await runner.run();
10
+ expect(results[0].name).toEqual("s");
11
+ expect(results[0].tests[0].name).toEqual("t");
12
+ expect(results[0].tests[0].status).toEqual("pass");
13
+ expect(results[0].tests[0].result).toEqual(1);
14
+ });
15
+
16
+ test("failing test reports status=fail with the error", async () => {
17
+ const runner = new TestRunner();
18
+ runner.suite("s", () => {
19
+ runner.test("boom", () => {
20
+ throw new Error("kaboom");
21
+ });
22
+ });
23
+ const results = await runner.run();
24
+ expect(results[0].tests[0].status).toEqual("fail");
25
+ expect(results[0].tests[0].error.message).toEqual("kaboom");
26
+ });
27
+
28
+ test("async tests are awaited before status is set", async () => {
29
+ const runner = new TestRunner();
30
+ runner.suite("s", () => {
31
+ runner.test("async", async () => {
32
+ await new Promise((r) => setTimeout(r, 10));
33
+ return "done";
34
+ });
35
+ });
36
+ const results = await runner.run();
37
+ expect(results[0].tests[0].status).toEqual("pass");
38
+ expect(results[0].tests[0].result).toEqual("done");
39
+ });
40
+
41
+ test("test.skip is recorded as 'skip'", async () => {
42
+ const runner = new TestRunner();
43
+ runner.suite("s", () => {
44
+ runner.test.skip("skipped", () => {});
45
+ });
46
+ const results = await runner.run();
47
+ expect(results[0].tests[0].status).toEqual("skip");
48
+ });
49
+
50
+ test("only: option restricts execution to marked tests", async () => {
51
+ const runner = new TestRunner({ only: true });
52
+ runner.suite("s", () => {
53
+ runner.test("not run", () => "x");
54
+ runner.test.only("run", () => "y");
55
+ });
56
+ const results = await runner.run();
57
+ const names = results[0].tests.map((t) => t.name);
58
+ expect(names.length).toEqual(1);
59
+ expect(names[0]).toEqual("run");
60
+ });
61
+
62
+ test("filter option restricts by test name (case-insensitive)", async () => {
63
+ const runner = new TestRunner({ filter: "MATCH" });
64
+ runner.suite("s", () => {
65
+ runner.test("will match", () => {});
66
+ runner.test("will skip", () => {});
67
+ });
68
+ const results = await runner.run();
69
+ const names = results[0].tests.map((t) => t.name);
70
+ expect(names.length).toEqual(1);
71
+ expect(names[0]).toEqual("will match");
72
+ });
73
+
74
+ test("suite errors are swallowed and surfaced via empty tests", async () => {
75
+ const runner = new TestRunner();
76
+ runner.suite("s", () => {
77
+ throw new Error("setup failed");
78
+ });
79
+ const results = await runner.run();
80
+ expect(results.length).toEqual(1);
81
+ expect(results[0].tests.length).toEqual(0);
82
+ });
83
+ });
package/tsconfig.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "strict": false,
4
4
  "noEmit": true,
5
5
  "allowJs": true,
6
+ "checkJs": true,
6
7
  "resolveJsonModule": true,
7
8
  "module": "esnext",
8
9
  "target": "ESNext",
@@ -10,9 +11,7 @@
10
11
  "moduleResolution": "bundler",
11
12
  "skipLibCheck": true,
12
13
  "jsx": "react",
13
- "typeRoots": ["./global.d.ts"],
14
- "paths": {
15
- "@/*": ["./*"]
16
- }
17
- }
14
+ "paths": {}
15
+ },
16
+ "include": ["./**/*.js", "./global.d.ts"]
18
17
  }
package/utils.js CHANGED
@@ -1,9 +1,16 @@
1
1
  import fs from "@vistta/fs";
2
2
  import { assign, async, extract, remove } from "@vistta/utils";
3
3
  import { spawn } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
4
+ import { platform } from "node:os";
5
+ import { isAbsolute } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
7
  import { inc, satisfies, valid } from "semver";
6
8
 
9
+ export const CWD = process.cwd();
10
+ export const FALLBACK = "*";
11
+ export const BUNDLER_SUFFIX = "?__bundler__";
12
+ export const BUNDLER_SUFFIX_RE = /\?__bundler__$/;
13
+ export const WITH_TYPE_RE = /[?&]__with_type=([^&#]*)/;
7
14
  export const ENABLED_NODE_OPTIONS = {
8
15
  "-w": "NODE_WATCH",
9
16
  "--watch": "NODE_WATCH",
@@ -20,7 +27,7 @@ export const ENABLED_NODE_OPTIONS = {
20
27
 
21
28
  export async function importJSON(filepath) {
22
29
  try {
23
- return JSON.parse(await fs.readFile(filepath));
30
+ return JSON.parse(String(await fs.readFile(filepath)));
24
31
  } catch {
25
32
  return {};
26
33
  }
@@ -29,7 +36,7 @@ export async function importJSON(filepath) {
29
36
  export async function importEnv(filepath) {
30
37
  try {
31
38
  const env = {};
32
- const entries = (await fs.readFile(filepath, "utf8")).split("\n");
39
+ const entries = String(await fs.readFile(filepath, "utf8")).split("\n");
33
40
  for (let i = 0, len = entries.length; i < len; i++) {
34
41
  const [key, value] = entries[i].split("=");
35
42
  env[key] = value;
@@ -43,7 +50,6 @@ export async function importEnv(filepath) {
43
50
  export async function importConfig(options = {}) {
44
51
  try {
45
52
  const helper = async (file) => {
46
- file = fileURLToPath(import.meta.resolve(file));
47
53
  const {
48
54
  extends: parent,
49
55
  cliOptions: cli = {},
@@ -54,15 +60,15 @@ export async function importConfig(options = {}) {
54
60
  const commands = Object.keys(cli.commands);
55
61
  const dirname = fs.dirname(file);
56
62
  for (let i = 0, len = commands.length; i < len; i++)
57
- cli.commands[commands[i]] = fs.resolve(dirname, cli.commands[commands[i]]);
63
+ cli.commands[commands[i]] = resolve(dirname, cli.commands[commands[i]]);
58
64
  }
59
- if (parent) return assign(await helper(parent), { cli, compiler, bundler });
65
+ if (parent) return assign(await helper(ensurePath(parent)), { cli, compiler, bundler });
60
66
  return { cli, compiler, bundler };
61
67
  };
62
68
  let file = fs.resolve(process.cwd(), "tsconfig.json");
63
69
  if (!fs.existsSync(file)) file = fs.resolve(process.cwd(), "jsconfig.json");
64
70
  if (!fs.existsSync(file)) return options;
65
- return assign(options, await helper(file));
71
+ return assign(options, await helper(ensurePath(file)));
66
72
  } catch (e) {
67
73
  console.error(e.trace || e.message);
68
74
  return options;
@@ -92,7 +98,14 @@ export async function getOutdatedPackages(dirname) {
92
98
  const packageName = keys[i];
93
99
  const { version, resolved, link } = packages["node_modules/" + packageName] || {};
94
100
  if (link) continue;
95
- if (!version) throw new Error(`Module ${packageName} not found `);
101
+ if (!version) {
102
+ data[packageName] = {
103
+ package: path,
104
+ dev,
105
+ };
106
+ promises.push({ name: packageName });
107
+ continue;
108
+ }
96
109
  promises.push(
97
110
  new Promise((resolve, reject) =>
98
111
  fetch(resolved ? resolved.split("/-/")[0] + "/latest" : `https://registry.npmjs.org/${packageName}/latest`)
@@ -116,7 +129,11 @@ export async function getOutdatedPackages(dirname) {
116
129
  const module = data[values[i]?.name];
117
130
  module.name = values[i]?.name;
118
131
  module.latest = values[i]?.version;
119
- if (!module || !module?.latest || !module?.current || !module?.wanted) throw new Error(`Internal Error`);
132
+ if (!module.current) {
133
+ result.push(module);
134
+ continue;
135
+ }
136
+ if (!module || !module?.latest || !module?.wanted) throw new Error(`Internal Error`);
120
137
  if (module.current === module.latest) continue;
121
138
  module.wanted = satisfies(module.latest, module.wanted) ? module.latest : module.current;
122
139
  result.push(module);
@@ -140,9 +157,8 @@ export function saveCrashReport() {
140
157
  if (!fs.existsSync(crashFolder)) fs.mkdir(crashFolder);
141
158
  const logs = console.logs;
142
159
  let output = "";
143
- for (let i = 0, len = logs.length; i < len; i++) {
144
- output += logs[i].time.toISOString() + " - " + logs[i].toString().replace(/\n/gm, " ") + "\n";
145
- }
160
+ for (let i = 0, len = logs.length; i < len; i++)
161
+ output += logs[i].time.toLocalISOString() + ": " + logs[i].toString({ time: false });
146
162
  fs.writeFileSync(fs.resolve(crashFolder, Date.now() + ".log"), output);
147
163
  }
148
164
 
@@ -160,22 +176,60 @@ export function run(script, ...args) {
160
176
  });
161
177
  }
162
178
 
163
- /**
164
- * Function to parse command line arguments.
165
- * @param {string[]} args
166
- * @returns
167
- */
168
179
  export function parseArgs(args) {
169
- const result = [[], {}];
180
+ const result = /** @type {[string[], Record<string, any>]} */ ([[], {}]);
170
181
  for (let i = 0, len = args.length; i < len; i++) {
171
- const [option, value] = args[i].toLowerCase().split("=");
182
+ const lower = args[i].toLowerCase();
183
+ const eq = lower.indexOf("=");
184
+ const option = eq === -1 ? lower : lower.slice(0, eq);
185
+ const value = eq === -1 ? undefined : args[i].slice(eq + 1);
172
186
  if (option.startsWith("--")) result[1][option.slice(2)] = evaluate(value);
173
187
  else result[0].push(args[i]);
174
188
  }
175
189
  return result;
176
190
  }
177
191
 
178
- export { assign, async, extract, fs, remove };
192
+ export function resolve(...values) {
193
+ if (platform() === "win32") return pathToFileURL(fs.resolve(...values)).toString();
194
+ return fs.resolve(...values);
195
+ }
196
+
197
+ /**
198
+ * Append the bundler marker to a URL/path so it occupies a distinct slot in
199
+ * Node's ESM module cache from the same file imported without the marker.
200
+ * @param {string} url
201
+ * @returns {string}
202
+ */
203
+ export function markBundler(url) {
204
+ return BUNDLER_SUFFIX_RE.test(url) ? url : url + BUNDLER_SUFFIX;
205
+ }
206
+
207
+ /**
208
+ * Remove the bundler marker from a URL/path if present.
209
+ * @param {string | undefined | null} url
210
+ * @returns {string}
211
+ */
212
+ export function stripBundler(url) {
213
+ return url ? url.replace(BUNDLER_SUFFIX_RE, "") : "";
214
+ }
215
+
216
+ /**
217
+ * Build a stable, sorted query string encoding the import attributes so that
218
+ * two imports of the same file with different `with: { ... }` attributes land
219
+ * in distinct ESM module cache slots. Mirrors the bundler marker pattern.
220
+ * @param {Record<string, any> | undefined | null} attrs
221
+ * @returns {string}
222
+ */
223
+ export function markAttributes(attrs) {
224
+ if (!attrs) return "";
225
+ const keys = Object.keys(attrs)
226
+ .filter((k) => attrs[k] != null)
227
+ .sort();
228
+ if (keys.length === 0) return "";
229
+ return keys.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(String(attrs[k]))}`).join("&");
230
+ }
231
+
232
+ export { assign, async, extract, remove };
179
233
 
180
234
  async function getProjectLock(path) {
181
235
  if (fs.existsSync(path + "/package-lock.json")) return await importJSON(fs.resolve(path, "package-lock.json"));
@@ -206,3 +260,14 @@ function evaluate(value) {
206
260
  if (value === "false") return false;
207
261
  return value;
208
262
  }
263
+
264
+ function ensurePath(value) {
265
+ try {
266
+ if (isAbsolute(value)) return value;
267
+ value = import.meta.resolve(value);
268
+ if (isAbsolute(value)) return value;
269
+ return fileURLToPath(value);
270
+ } catch {
271
+ return value;
272
+ }
273
+ }
@@ -1,62 +0,0 @@
1
- name: Development Workflow
2
- on:
3
- push:
4
- branches:
5
- - development
6
- env:
7
- module: cli
8
- jobs:
9
- Validation:
10
- runs-on: ubuntu-latest
11
- container:
12
- image: node:latest
13
- steps:
14
- - name: Checkout
15
- uses: tiagomta/checkout@v1
16
- with:
17
- repository: vistta/workspace
18
- ref: main
19
- token: ${{ secrets.TOKEN }}
20
- fetch-depth: 0
21
- submodules: true
22
- - name: Close Issues
23
- uses: tiagomta/gitea@v1
24
- with:
25
- token: ${{ secrets.TOKEN }}
26
- working-directory: ./packages/${{ env.module }}
27
- command: issues close --fromCommit
28
- - name: Setup Node
29
- run: npm ci
30
- - name: Module Validation
31
- working-directory: ./packages/${{ env.module }}
32
- run: npx vistta project outdated
33
- continue-on-error: true
34
- - name: Eslint Validation
35
- working-directory: ./packages/${{ env.module }}
36
- run: npx eslint .
37
- continue-on-error: true
38
- - name: Typescript Validation
39
- working-directory: ./packages/${{ env.module }}
40
- run: npx tsc
41
- continue-on-error: true
42
- - name: Unit Tests
43
- working-directory: ./packages/${{ env.module }}
44
- run: npx vistta test
45
-
46
- PreRelease:
47
- needs: Validation
48
- runs-on: ubuntu-latest
49
- container:
50
- image: node:latest
51
- steps:
52
- - name: Checkout
53
- uses: tiagomta/checkout@v1
54
- - name: PreRelease
55
- uses: tiagomta/npm@v1
56
- with:
57
- command: version prerelease SNAPSHOT-${{ gitea.run_id }} --email devops@vistta.org
58
- - name: Publish PreRelease (Gitea)
59
- uses: tiagomta/npm@v1
60
- with:
61
- command: publish --scope vistta --registry https://git.vistta.org/api/packages/vistta/npm/ --tag snapshot
62
- token: ${{ secrets.TOKEN }}
@@ -1,124 +0,0 @@
1
- name: Release Workflow
2
- on:
3
- workflow_dispatch:
4
- inputs:
5
- type:
6
- description: "Release Type"
7
- required: true
8
- type: choice
9
- options:
10
- - Patch
11
- - Minor
12
- - Major
13
- npm_publish:
14
- description: "Publish to NPM Registry"
15
- required: false
16
- default: true
17
- type: boolean
18
- gitea_publish:
19
- description: "Publish to Gitea Registry"
20
- required: false
21
- default: true
22
- type: boolean
23
-
24
- env:
25
- module: cli
26
- ref: refs/heads/development
27
- jobs:
28
- Validation:
29
- runs-on: ubuntu-latest
30
- container:
31
- image: node:latest
32
- steps:
33
- - name: Checkout
34
- uses: tiagomta/checkout@v1
35
- with:
36
- repository: vistta/workspace
37
- ref: main
38
- token: ${{ secrets.TOKEN }}
39
- fetch-depth: 0
40
- submodules: true
41
- - name: Setup Node
42
- run: npm ci
43
- - name: Module Validation
44
- working-directory: ./packages/${{ env.module }}
45
- run: npx vistta project outdated
46
- - name: Eslint Validation
47
- working-directory: ./packages/${{ env.module }}
48
- run: npx eslint .
49
- - name: Typescript Validation
50
- working-directory: ./packages/${{ env.module }}
51
- run: npx tsc
52
- - name: Unit Tests
53
- working-directory: ./packages/${{ env.module }}
54
- run: npx vistta test
55
-
56
- Release:
57
- needs: Validation
58
- runs-on: ubuntu-latest
59
- container:
60
- image: node:latest
61
- if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/development'
62
- steps:
63
- - name: Checkout
64
- uses: tiagomta/checkout@v1
65
- with:
66
- ref: ${{ env.ref }}
67
- token: ${{ secrets.TOKEN }}
68
- - name: NPM Release
69
- uses: tiagomta/nodejs-run@v1
70
- id: npm
71
- with:
72
- target: inline
73
- run: |
74
- await git.config("user.email", "devops@vistta.org");
75
- const version = await npm.version("${{ inputs.type }}".toLowerCase());
76
- // Release Gitea
77
- if (`${{ inputs.gitea_publish }}`.toLowerCase() === "true") {
78
- await npm.config("set", "@vistta:registry", "https://git.vistta.org/api/packages/vistta/npm/");
79
- await npm.config("set", "--", "//git.vistta.org/api/packages/vistta/npm/:_authToken", "${{ secrets.TOKEN }}");
80
- await npm.publish();
81
- }
82
- // Release NPM
83
- if (`${{ inputs.npm_publish }}`.toLowerCase() === "true") {
84
- await npm.config("set", "@vistta:registry", "https://registry.npmjs.org/");
85
- await npm.config("set", "--", "//registry.npmjs.org/:_authToken", "${{ secrets.NPM_TOKEN }}");
86
- await npm.publish("--access", "public");
87
- }
88
- // Update Branches
89
- await git.push("origin", "${{ env.ref }}", "--tags");
90
- await git.fetch("origin", "master");
91
- await git.checkout("master");
92
- await git.reset("--hard", "${{ env.ref }}");
93
- await git.push("origin", "master", "--force");
94
- result = version;
95
- - name: Gitea Release
96
- uses: tiagomta/nodejs-run@v1
97
- with:
98
- target: inline
99
- run: |
100
- const version = (`${{ steps.npm.outputs.result }}`).trim();
101
- const milestone = await gitea.milestones.create("${{ inputs.type }} (" + version + ")");
102
- const issues = await gitea.issues.list({ state: "closed" });
103
- const types = {};
104
- for (let issue of issues) {
105
- const type =
106
- issue.labels.find((l) => l.name.startsWith("Project/"))?.name.slice(8) +
107
- "s";
108
- if (type && issue.milestone == milestone.id) {
109
- if (!types[type]) types[type] = `## ${type}\n`;
110
- types[type] += `- ${issue.title} (#${issue.number})\n`;
111
- } else if (issue.milestone == null) {
112
- await gitea.issues.update(issue.number, { milestone: milestone.id });
113
- if (type) {
114
- if (!types[type]) types[type] = `## ${type}\n`;
115
- types[type] += `- ${issue.title} (#${issue.number})\n`;
116
- }
117
- }
118
- }
119
- await gitea.releases.create({
120
- tag_name: version,
121
- name: "${{ inputs.type }} (" + version + ")",
122
- body: Object.values(types).join("\n")
123
- });
124
- await gitea.milestones.close(milestone.id);