@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.
@@ -39,7 +39,9 @@ export default class DefaultCommand extends Command {
39
39
  const modules = await (await import("../utils.js"))?.getOutdatedPackages(process.cwd());
40
40
  for (let i = 0, len = modules.length; i < len; i++) {
41
41
  const { name, current, wanted, latest } = modules[i];
42
- if (current === wanted) console.print(`${COLORS.CYAN}Module "${name}" has a new version (${latest})${COLORS.RESET}`);
42
+ if (!current) console.print(`${COLORS.YELLOW}Module "${name}" is missing${COLORS.RESET}`);
43
+ else if (current === wanted)
44
+ console.print(`${COLORS.CYAN}Module "${name}" has a new version (${latest})${COLORS.RESET}`);
43
45
  else console.print(`${COLORS.YELLOW}Module "${name}" is outdated (${latest})${COLORS.RESET}`);
44
46
  }
45
47
  } catch {
@@ -16,11 +16,7 @@ const HELP = {
16
16
  };
17
17
 
18
18
  export default class extends Command {
19
- constructor(options) {
20
- super(options);
21
- }
22
-
23
- help(_, command) {
19
+ async help(_, command) {
24
20
  switch (command) {
25
21
  case "name":
26
22
  console.print(capitalize(HELP["name"]));
package/commands/test.js CHANGED
@@ -1,30 +1,20 @@
1
1
  import { COLORS } from "@vistta/console";
2
2
  import fs from "@vistta/fs";
3
+ import { TestRunner } from "../classes/test-runner.js";
3
4
  import { parseArgs } from "../utils.js";
4
5
  import DefaultCommand from "./default.js";
5
6
 
6
7
  const cwd = process.cwd();
7
8
 
8
9
  export default class extends DefaultCommand {
9
- #options = {};
10
- #results = [];
11
- #failed;
12
- #suite;
13
- #runningCounter = 0;
10
+ #testRunner;
14
11
 
15
12
  constructor() {
16
13
  super();
17
- const suite = this.suite.bind(this, false);
18
- suite.only = this.suite.bind(this, true);
19
14
  this.define("env", "NODE_ENV", "testing");
20
- this.define("global", "suite", suite);
21
- const test = this.test.bind(this, false);
22
- test.only = this.test.bind(this, true);
23
- this.define("global", "test", test);
24
- this.define("global", "expect", this.expect.bind(this));
25
15
  }
26
16
 
27
- help() {
17
+ async help() {
28
18
  console.print("Runs all the tests files that match the pattern/s in the current project");
29
19
  console.print("\nUsage:");
30
20
  console.print("vistta test [...patterns]\tRuns all the tests files that match the pattern/s in the current project");
@@ -36,179 +26,57 @@ export default class extends DefaultCommand {
36
26
 
37
27
  async main(_, ...argv) {
38
28
  let [args, options] = parseArgs(argv);
39
- this.#options = options;
40
- if (args.length === 0) args = ["**/*.test.js", "**/*.test.ts"];
29
+ this.#testRunner = new TestRunner(options);
30
+ this.define("global", "suite", this.#testRunner.suite);
31
+ this.define("global", "test", this.#testRunner.test);
32
+ this.define("global", "expect", this.#testRunner.expect);
33
+ if (args.length === 0) args = this.resolvers.map((ext) => `**/*.test.${ext}`);
41
34
  for (let i = 0, len = args.length; i < len; i++) args[i] = fs.resolve(cwd, args[i]);
42
35
  const entries = fs.glob(args);
43
36
  let entry = (await entries.next())?.value;
44
37
  if (!process.env.NODE_DEBUG) console.disable();
38
+ let failed = false;
45
39
  while (entry) {
46
40
  try {
47
41
  process.chdir(fs.dirname(entry));
48
42
  if (!entry.includes("\\node_modules\\")) await import(fs.resolve(entry));
49
- await this.#await();
50
43
  } catch {
51
- this.#failed = true;
44
+ failed = true;
52
45
  }
53
46
  entry = (await entries.next())?.value;
54
47
  }
55
48
  if (!process.env.NODE_DEBUG) console.enable();
56
49
  let output = "";
57
- for (let i = 0, len = this.#results.length; i < len; i++) {
58
- const { name, tests } = this.#results[i];
50
+ const results = await this.#testRunner.run();
51
+ for (let i = 0, len = results.length; i < len; i++) {
52
+ const { name, tests } = results[i];
53
+ let passing = 0;
54
+ let total = 0;
55
+ let first, last;
59
56
  if (name) output += `\n${name}\n`;
60
- const [results, passing, total, time] = processTests(tests);
61
- output += `${results}${passing === total ? COLORS.GREEN : COLORS.RED}${passing}/${total} passing ${COLORS.RESET + COLORS.DIM}(${time}ms)${COLORS.RESET}\n`;
57
+ for (let n = 0, nlen = tests?.length || 0; n < nlen; n++) {
58
+ const { name, status, start, end, result, error } = tests[n];
59
+ if (!first || start < first) first = start;
60
+ if (!last || end > last) last = end;
61
+ switch (status) {
62
+ case "pass":
63
+ output += ` ${COLORS.GREEN}PASSED ${name} ${COLORS.RESET + COLORS.DIM}(${Math.round(end - start)}ms)${COLORS.RESET}\n${typeof result === "string" ? "\t" + COLORS.GREEN + result + COLORS.RESET + "\n" : ""}`;
64
+ passing++;
65
+ total++;
66
+ break;
67
+ case "fail":
68
+ output += ` ${COLORS.RED}FAILED ${name} ${COLORS.RESET + COLORS.DIM}(${Math.round(end - start)}ms)${COLORS.RESET}\n\t${COLORS.RED + error + COLORS.RESET}\n`;
69
+ total++;
70
+ break;
71
+ case "skip":
72
+ output += ` ${COLORS.YELLOW}SKIPPED ${name} ${COLORS.RESET + COLORS.DIM}(${Math.round(end - start)}ms)${COLORS.RESET}\n`;
73
+ break;
74
+ }
75
+ }
76
+ if (total !== passing) failed = true;
77
+ output += `${passing === total ? COLORS.GREEN : COLORS.RED}${passing}/${total} passing ${COLORS.RESET + COLORS.DIM}(${Math.round(last - first)}ms)${COLORS.RESET}\n`;
62
78
  }
63
79
  console.print(output);
64
- process.exit(this.#failed ? -1 : 0);
65
- }
66
-
67
- async suite(only, name, callback) {
68
- if (this.#suite) throw new Error("Suites/Describes cannot be stacked");
69
- this.#runningCounter++;
70
- this.#suite = { name, only, tests: [] };
71
- try {
72
- const value = callback();
73
- if (value instanceof Promise) await value;
74
- } catch {
75
- /* Do Nothing */
76
- }
77
- this.#results.push(this.#suite);
78
- this.#suite = null;
79
- this.#runningCounter--;
80
+ process.exit(failed ? -1 : 0);
80
81
  }
81
-
82
- async test(only, name, callback) {
83
- if (
84
- (this.#options.filter && !name.match(new RegExp(this.#options.filter, "i"))) ||
85
- (this.#options.only && !(only || this.#suite?.only))
86
- )
87
- return;
88
- this.#runningCounter++;
89
- const test = { name };
90
- test.start = performance();
91
- if (this.#suite) this.#suite.tests.push(test);
92
- else this.#results.push({ tests: [test], time: test.time });
93
-
94
- try {
95
- test.result = callback();
96
- if (test.result instanceof Promise) test.result = await test.result;
97
- test.status = "pass";
98
- test.end = performance();
99
- } catch (error) {
100
- test.status = "fail";
101
- test.error = error;
102
- test.end = performance();
103
- this.#failed = true;
104
- }
105
- this.#runningCounter--;
106
- }
107
-
108
- expect(target, label) {
109
- return {
110
- not: {
111
- toEqual: (value, unit = "") => {
112
- if (target !== value) return;
113
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not equal ${value}${unit}`);
114
- },
115
- toBeLessThan: (value, unit = "") => {
116
- if (target >= value) return;
117
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not be less than ${value}${unit}`);
118
- },
119
- toBeGreaterThan: (value, unit = "") => {
120
- if (target <= value) return;
121
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not be greater than ${value}${unit}`);
122
- },
123
- toInclude: (value) => {
124
- if (target.indexOf(value) === -1) return;
125
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not include ${value}`);
126
- },
127
- toMatch: (regex) => {
128
- if (!target.match(new RegExp(regex))) return;
129
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not match ${regex}`);
130
- },
131
- toThrow: (message) => {
132
- try {
133
- target();
134
- } catch (error) {
135
- if (!message)
136
- throw new Error(`expected ${label ? label + "()" : "function()"} to not throw, but got ${error.message}`);
137
- if (error.message === message)
138
- throw new Error(
139
- `expected ${label ? label + "()" : "function()"} to not throw ${message}, but got ${error.message}`,
140
- );
141
- if (error.message.match(new RegExp(message)))
142
- throw new Error(
143
- `expected ${label ? label + "()" : "function()"} to not throw ${message}, but got ${error.message}`,
144
- );
145
- return;
146
- }
147
- },
148
- },
149
- toEqual: (value, unit = "") => {
150
- if (target === value) return;
151
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to equal ${value}${unit}`);
152
- },
153
- toBeLessThan: (value, unit = "") => {
154
- if (target < value) return;
155
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to be less than ${value}${unit}`);
156
- },
157
- toBeGreaterThan: (value, unit = "") => {
158
- if (target > value) return;
159
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to be greater than ${value}${unit}`);
160
- },
161
- toInclude: (value) => {
162
- if (target.indexOf(value) > -1) return;
163
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to include ${value}`);
164
- },
165
- toMatch: (regex) => {
166
- if (target.match(new RegExp(regex))) return;
167
- throw new Error(`expected ${label ? label + "(" + target + ")" : target} to match ${regex}`);
168
- },
169
- toThrow: (message) => {
170
- try {
171
- target();
172
- } catch (error) {
173
- if (!message) return;
174
- if (error.message === message) return;
175
- if (error.message.match(new RegExp(message))) return;
176
- throw new Error(`expected ${label ? label + "()" : "function()"} to throw ${message}, but got ${error.message}`);
177
- }
178
- },
179
- };
180
- }
181
-
182
- #await() {
183
- return new Promise((resolve) => {
184
- const timer = () => {
185
- if (this.#runningCounter === 0) resolve();
186
- else setTimeout(timer, 500);
187
- };
188
- timer();
189
- });
190
- }
191
- }
192
-
193
- function processTests(tests) {
194
- let passing = 0;
195
- let total = 0;
196
- let acc = "";
197
- let first, last;
198
- for (let i = 0, len = tests?.length || 0; i < len; i++) {
199
- const { name, status, start, end, result, error } = tests[i];
200
- if (!first || start < first) first = start;
201
- if (!last || end > last) last = end;
202
- if (status === "pass") {
203
- acc += ` ${COLORS.GREEN}✔ ${name} ${COLORS.RESET + COLORS.DIM}(${Math.round(end - start)}ms)${COLORS.RESET}\n${typeof result === "string" ? "\t" + COLORS.GREEN + result + COLORS.RESET + "\n" : ""}`;
204
- passing++;
205
- } else
206
- acc += ` ${COLORS.RED}✖ ${name} ${COLORS.RESET + COLORS.DIM}(${Math.round(end - start)}ms)${COLORS.RESET}\n\t${COLORS.RED + error + COLORS.RESET}\n`;
207
- total++;
208
- }
209
- return [acc, passing, total, Math.round(last - first)];
210
- }
211
-
212
- function performance(time = process.hrtime()) {
213
- return time[0] * 1000 + time[1] / 1e6;
214
82
  }
package/global.d.ts CHANGED
@@ -1 +1,69 @@
1
1
  /// <reference path="@vistta/console" />
2
+
3
+ interface CliRuntimeData {
4
+ loaders: any[];
5
+ resolvers: string[];
6
+ options: any;
7
+ port?: any;
8
+ main?: (...args: any[]) => any;
9
+ }
10
+
11
+ declare namespace NodeJS {
12
+ interface Process {
13
+ argv: string[];
14
+ env: Record<string, string | undefined>;
15
+ chdir(directory: string): void;
16
+ cwd(): string;
17
+ exit(code?: number): never;
18
+ on(event: string, listener: (...args: any[]) => void): void;
19
+ send?(message: any): void;
20
+ restart?: () => void;
21
+ vistta: CliRuntimeData;
22
+ }
23
+ }
24
+
25
+ declare var vistta: CliRuntimeData;
26
+ declare var loaders: {
27
+ send: (event: string, ...args: any[]) => void;
28
+ on: (event: string, callback: (...args: any[]) => void) => void;
29
+ shared: Record<string, any>;
30
+ };
31
+ declare var main: {
32
+ send: (event: string, ...args: any[]) => void;
33
+ on: (event: string, callback: (...args: any[]) => void) => void;
34
+ shared: Record<string, any>;
35
+ };
36
+
37
+ declare var suite: {
38
+ (name: string, callback: () => void, options?: { only?: boolean; skip?: boolean }): void;
39
+ only(name: string, callback: () => void): void;
40
+ skip(name: string, callback: () => void): void;
41
+ };
42
+
43
+ declare var test: {
44
+ (name: string, callback: () => void, options?: { only?: boolean; skip?: boolean }): void;
45
+ only(name: string, callback: () => void): void;
46
+ skip(name: string, callback: () => void): void;
47
+ };
48
+
49
+ interface ExpectNotMatchers {
50
+ toEqual(expected: any, unit?: string): void;
51
+ toBeLessThan(expected: any, unit?: string): void;
52
+ toBeGreaterThan(expected: any, unit?: string): void;
53
+ toInclude(expected: any): void;
54
+ toMatch(expected: string | RegExp): void;
55
+ toThrow(expected?: any): void;
56
+ }
57
+
58
+ interface ExpectMatchers {
59
+ toEqual(expected: any): void;
60
+ toBeLessThan(expected: any, unit?: string): void;
61
+ toBeGreaterThan(expected: any, unit?: string): void;
62
+ toInclude(expected: any): void;
63
+ toMatch(expected: string | RegExp): void;
64
+ toThrow(expected?: any): void;
65
+ not: ExpectNotMatchers;
66
+ }
67
+
68
+ declare function expect(value: any, label?: string): ExpectMatchers;
69
+ declare function setImmediate(callback: (...args: any[]) => void): any;
package/index.js CHANGED
@@ -2,6 +2,7 @@ export { Bundler } from "./classes/bundler.js";
2
2
  export { Command } from "./classes/command.js";
3
3
  export { Loader } from "./classes/loader.js";
4
4
  export { Runtime } from "./classes/runtime.js";
5
+ export { TestRunner } from "./classes/test-runner.js";
5
6
  export { default as BundleCommand } from "./commands/bundle.js";
6
7
  export { default as DefaultCommand } from "./commands/default.js";
7
8
  export { default as ProjectCommand } from "./commands/project.js";
@@ -6,7 +6,9 @@ export default class BundlerLoader extends Loader {
6
6
  async load(_, { path, ...options }) {
7
7
  options.write = false;
8
8
  remove(options, "type", "extension");
9
- const { code, files, resources, errors, warnings } = await new Bundler(this).run(path, options);
9
+ if (options.importAttributes?.type === "bundler") delete options.importAttributes.type;
10
+ const runtime = /** @type {import("../classes/runtime").Runtime} */ (/** @type {unknown} */ (this));
11
+ const { code, files, resources, errors, warnings } = await new Bundler(runtime).run(path, options);
10
12
  return {
11
13
  code:
12
14
  `export const code = ${JSON.stringify(code)}; ` +
package/loaders/index.js CHANGED
@@ -8,12 +8,12 @@ export async function initialize({ loaders, resolvers, options, port }) {
8
8
  const shared = {};
9
9
  const events = {};
10
10
  global.main = {
11
- send: (event, ...args) => args.port.postMessage(JSON.stringify({ event, args })),
11
+ send: (event, ...args) => port.postMessage(JSON.stringify({ event, args })),
12
12
  on: (event, callback) => (events[event] ? events[event].push(callback) : (events[event] = [callback])),
13
13
  shared: new Proxy(shared, {
14
14
  set(_, prop, value) {
15
15
  port.postMessage(JSON.stringify({ sync: true, prop, value }));
16
- return Reflect.set(...arguments);
16
+ return Reflect.set(shared, prop, value);
17
17
  },
18
18
  }),
19
19
  };
package/main.js CHANGED
@@ -1 +1 @@
1
- process.vistta.main();
1
+ process.vistta.main?.();
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@vistta/cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": "./index.js",
8
+ "./global": "./global.d.ts",
8
9
  "./config": "./tsconfig.json",
9
- "./commands/default": "./commands/default.js",
10
- "./commands/bundle": "./commands/bundle.js",
11
- "./commands/project": "./commands/project.js",
12
- "./commands/test": "./commands/test.js",
10
+ "./default": "./commands/default.js",
11
+ "./bundle": "./commands/bundle.js",
12
+ "./project": "./commands/project.js",
13
+ "./test": "./commands/test.js",
14
+ "./test-runner": "./classes/test-runner.js",
13
15
  "./loaders/bundler": "./loaders/bundler.js",
14
16
  "./loaders/file": "./loaders/file.js",
15
17
  "./loaders/json": "./loaders/json.js",
@@ -18,9 +20,6 @@
18
20
  "bin": {
19
21
  "vistta": "bin.js"
20
22
  },
21
- "scripts": {
22
- "postinstall": ""
23
- },
24
23
  "repository": {
25
24
  "type": "git",
26
25
  "url": "https://github.com/vistta-org/cli.git"
@@ -32,10 +31,10 @@
32
31
  "@vistta/console": "latest",
33
32
  "@vistta/fs": "latest",
34
33
  "@vistta/utils": "latest",
35
- "esbuild": "^0.25.5",
36
- "semver": "^7.7.2"
34
+ "esbuild": "^0.28.2",
35
+ "semver": "^7.8.5"
37
36
  },
38
37
  "author": "Tiago Terenas Almeida",
39
- "license": "Apache 2.0 with Commons Clause",
38
+ "license": "GPL-3.0-only",
40
39
  "description": "Vistta CLI is a framework for extending the Node.js loader API and building reusable, customizable commands tools"
41
40
  }
package/register.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import "@vistta/console/global";
2
2
  import { register } from "node:module";
3
3
  import { MessageChannel } from "node:worker_threads";
4
- import { fs, importConfig, saveCrashReport } from "./utils.js";
4
+ import { importConfig, resolve, saveCrashReport } from "./utils.js";
5
5
 
6
6
  console.clear();
7
7
  console.print(`Vistta CLI v${process.env.CLI_VERSION}\n`);
@@ -15,7 +15,7 @@ global.loaders = {
15
15
  shared: new Proxy(shared, {
16
16
  set(_, prop, value) {
17
17
  port1.postMessage(JSON.stringify({ sync: true, prop, value }));
18
- return Reflect.set(...arguments);
18
+ return Reflect.set(shared, prop, value);
19
19
  },
20
20
  }),
21
21
  };
@@ -24,19 +24,20 @@ port1.on("message", (msg) => {
24
24
  if (event) events[event]?.forEach((callback) => callback(...args));
25
25
  else if (sync) shared[prop] = value;
26
26
  });
27
- port1.unref();
27
+ /** @type {any} */ (port1).unref?.();
28
28
 
29
29
  const config = await importConfig({
30
30
  cli: {
31
31
  commands: {
32
- default: fs.resolve(import.meta.dirname, "./commands/default.js"),
33
- bundle: fs.resolve(import.meta.dirname, "./commands/bundle.js"),
34
- project: fs.resolve(import.meta.dirname, "./commands/project.js"),
35
- test: fs.resolve(import.meta.dirname, "./commands/test.js"),
32
+ default: resolve(import.meta.dirname, "./commands/default.js"),
33
+ bundle: resolve(import.meta.dirname, "./commands/bundle.js"),
34
+ project: resolve(import.meta.dirname, "./commands/project.js"),
35
+ test: resolve(import.meta.dirname, "./commands/test.js"),
36
36
  },
37
37
  },
38
38
  });
39
39
  const command = new (await import(config.cli.commands[process.argv[2]] || config.cli.commands["default"])).default();
40
+ process.restart = () => process.send({ restart: true });
40
41
  process.vistta = {
41
42
  loaders: command.loaders,
42
43
  resolvers: command.resolvers,
@@ -0,0 +1,118 @@
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { Bundler } from "../classes/bundler.js";
5
+ import { Command } from "../classes/command.js";
6
+ import { Runtime } from "../classes/runtime.js";
7
+
8
+ suite("Bundler", () => {
9
+ test("constructor with Runtime stores it as the loader", () => {
10
+ const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
11
+ const bundler = new Bundler(runtime);
12
+ expect(typeof bundler.run).toEqual("function");
13
+ expect(typeof bundler.import).toEqual("function");
14
+ });
15
+
16
+ test("constructor with Command builds a Runtime from command options", () => {
17
+ const cmd = new Command();
18
+ const bundler = new Bundler(cmd);
19
+ expect(typeof bundler.run).toEqual("function");
20
+ });
21
+
22
+ test("constructor with no argument falls back to process.vistta", () => {
23
+ process.vistta = { loaders: [], resolvers: [], options: {} };
24
+ const bundler = new Bundler();
25
+ expect(typeof bundler.run).toEqual("function");
26
+ });
27
+
28
+ test("constructor throws on invalid argument", () => {
29
+ let caught;
30
+ try {
31
+ new Bundler(123);
32
+ } catch (e) {
33
+ caught = e;
34
+ }
35
+ expect(caught.message).toMatch("Invalid first argument");
36
+ });
37
+
38
+ test("run() applies defaults from runtime.options.bundler", async () => {
39
+ const dir = mkdtempSync(join(tmpdir(), "vistta-bundler-"));
40
+ const entry = join(dir, "entry.js");
41
+ writeFileSync(entry, "export const greeting = 'hi';\n");
42
+
43
+ const runtime = new Runtime({
44
+ loaders: [],
45
+ resolvers: ["js"],
46
+ options: { bundler: { target: "es2020" } },
47
+ });
48
+ const bundler = new Bundler(runtime);
49
+ const { code, errors } = await bundler.run(entry, { write: false, logLevel: "silent" });
50
+ expect((errors || []).length).toEqual(0);
51
+ expect(code).toInclude("greeting");
52
+ expect(code).toInclude("hi");
53
+ });
54
+
55
+ test("run() with exports option rewrites entry to a re-export via @exports namespace", async () => {
56
+ const dir = mkdtempSync(join(tmpdir(), "vistta-bundler-"));
57
+ const entry = join(dir, "page.jsx");
58
+ writeFileSync(entry, "export default function Page() { return null; }\n");
59
+
60
+ const runtime = new Runtime({ loaders: [], resolvers: ["jsx"], options: {} });
61
+ const bundler = new Bundler(runtime);
62
+ const { code, errors } = await bundler.run(entry, {
63
+ write: false,
64
+ logLevel: "silent",
65
+ exports: "default",
66
+ });
67
+ expect((errors || []).length).toEqual(0);
68
+ expect(code).toInclude("Page");
69
+ });
70
+
71
+ test("run() exposes window namespace defaults and named exports", async () => {
72
+ const dir = mkdtempSync(join(tmpdir(), "vistta-bundler-"));
73
+ const entry = join(dir, "entry.js");
74
+ writeFileSync(
75
+ entry,
76
+ 'import Files, { Files as NamedFiles } from "@vistta/connect/files";\nimport { equals } from "@vistta/connect/utils";\nexport const getFile = () => Files.get();\nexport const getNamedFile = () => NamedFiles.get();\nexport const compare = (left, right) => equals(left, right);\n',
77
+ );
78
+
79
+ const runtime = new Runtime({ loaders: [], resolvers: ["js"], options: {} });
80
+ const bundler = new Bundler(runtime);
81
+ const { code, errors } = await bundler.run(entry, {
82
+ write: false,
83
+ logLevel: "silent",
84
+ format: "cjs",
85
+ paths: {
86
+ "@vistta/connect/files": { path: "files", namespace: "window" },
87
+ "@vistta/connect/utils": { path: "utils", namespace: "window" },
88
+ },
89
+ });
90
+ expect((errors || []).length).toEqual(0);
91
+
92
+ const browser = /** @type {any} */ (globalThis);
93
+ browser.window = {};
94
+ const module = { exports: {} };
95
+ new Function("window", "module", "exports", code)(browser.window, module, module.exports);
96
+ browser.window.files = { default: { get: () => "file" }, Files: { get: () => "named" } };
97
+ browser.window.utils = { equals: (left, right) => left === right };
98
+ expect(module.exports.getFile()).toEqual("file");
99
+ expect(module.exports.getNamedFile()).toEqual("named");
100
+ expect(module.exports.compare("utils", "utils")).toEqual(true);
101
+ });
102
+
103
+ test("run() throws when source has syntax errors", async () => {
104
+ const dir = mkdtempSync(join(tmpdir(), "vistta-bundler-"));
105
+ const entry = join(dir, "entry.js");
106
+ writeFileSync(entry, "this is not valid javascript !@#$\n");
107
+
108
+ const runtime = new Runtime({ loaders: [], resolvers: ["js"], options: {} });
109
+ const bundler = new Bundler(runtime);
110
+ let caught;
111
+ try {
112
+ await bundler.run(entry, { write: false, logLevel: "silent" });
113
+ } catch (e) {
114
+ caught = e;
115
+ }
116
+ expect(caught.message).toMatch("Build failed");
117
+ });
118
+ });
@@ -0,0 +1,23 @@
1
+ import { Loader } from "../classes/loader.js";
2
+
3
+ suite("Loader", () => {
4
+ test("base load() throws 'must be implemented'", () => {
5
+ const loader = new Loader({});
6
+ expect(() => loader.load("source", {})).toThrow("Method 'load' must be implemented.");
7
+ });
8
+
9
+ test("subclass can override load()", () => {
10
+ let captured;
11
+ class Custom extends Loader {
12
+ load(source, properties) {
13
+ captured = { source, properties };
14
+ return { code: "ok" };
15
+ }
16
+ }
17
+ const loader = new Custom({ x: 1 });
18
+ const result = loader.load("input", { type: "json" });
19
+ expect(result.code).toEqual("ok");
20
+ expect(captured.source).toEqual("input");
21
+ expect(captured.properties.type).toEqual("json");
22
+ });
23
+ });