@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.
- package/LICENSE +674 -188
- package/README.md +3 -1
- package/bin.js +10 -4
- package/classes/bundler.js +172 -36
- package/classes/loader.js +1 -1
- package/classes/runtime.js +25 -15
- package/classes/test-runner.js +210 -0
- package/commands/bundle.js +4 -3
- package/commands/default.js +3 -1
- package/commands/project.js +1 -5
- package/commands/test.js +38 -170
- package/global.d.ts +68 -0
- package/index.js +1 -0
- package/loaders/bundler.js +3 -1
- package/loaders/index.js +2 -2
- package/main.js +1 -1
- package/package.json +10 -11
- package/register.js +8 -7
- package/tests/bundler.test.js +118 -0
- package/tests/loader.test.js +23 -0
- package/tests/runtime.test.js +76 -0
- package/tests/test-runner.test.js +83 -0
- package/tsconfig.json +4 -5
- package/utils.js +85 -20
- package/.gitea/workflows/prerelease.yml +0 -62
- package/.gitea/workflows/release.yml +0 -124
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ npm install @vistta/cli
|
|
|
45
45
|
- `vistta test [pattern]`
|
|
46
46
|
Run tests matching the specified pattern.
|
|
47
47
|
|
|
48
|
+
For browser-based test harnesses, import `TestRunner` from `@vistta/cli/test-runner` to avoid bundling Node-only CLI modules.
|
|
49
|
+
|
|
48
50
|
## Extending the CLI
|
|
49
51
|
|
|
50
52
|
You can create custom loaders and commands to fit your workflow.
|
|
@@ -108,7 +110,7 @@ vistta my-command [options]
|
|
|
108
110
|
|
|
109
111
|
## **License**
|
|
110
112
|
|
|
111
|
-
|
|
113
|
+
Licensed under the GNU General Public License version 3 only (`GPL-3.0-only`). See [LICENSE](LICENSE) for the full terms.
|
|
112
114
|
|
|
113
115
|
## **Contributing**
|
|
114
116
|
|
package/bin.js
CHANGED
|
@@ -9,7 +9,7 @@ const dirname = import.meta.dirname;
|
|
|
9
9
|
const cwd = process.cwd();
|
|
10
10
|
const rootPackage = await importJSON(fs.resolve(dirname, "package.json"));
|
|
11
11
|
const projectPackage = await importJSON(fs.resolve(cwd, "package.json"));
|
|
12
|
-
const env = await importEnv(fs.resolve(cwd, ".env"));
|
|
12
|
+
const env = { ...process.env, ...(await importEnv(fs.resolve(cwd, ".env"))) };
|
|
13
13
|
const projectEnvKeys = Object.keys(projectPackage?.env || {});
|
|
14
14
|
for (let i = 0, len = projectEnvKeys.length; i < len; i++) env[projectEnvKeys[i]] = projectPackage.env[projectEnvKeys[i]];
|
|
15
15
|
env.NODE_ENV = "production";
|
|
@@ -43,9 +43,15 @@ start(fs.resolve(dirname, "main.js"), argv, {
|
|
|
43
43
|
stdio: env.NODE_SILENT ? "ignore" : "inherit",
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
function start(
|
|
47
|
-
fork(
|
|
48
|
-
|
|
46
|
+
function start(modulePath, argv, options) {
|
|
47
|
+
const subprocess = fork(modulePath, argv, options);
|
|
48
|
+
let restart = false;
|
|
49
|
+
subprocess.on("message", (message) => {
|
|
50
|
+
if (typeof message === "object" && message && "restart" in message && message.restart)
|
|
51
|
+
((restart = true), subprocess.kill());
|
|
52
|
+
});
|
|
53
|
+
subprocess.on("exit", (code) => {
|
|
54
|
+
if ((env.NODE_AUTO_RESTART && code != 0) || restart) start(modulePath, argv, options);
|
|
49
55
|
else process.exit(code);
|
|
50
56
|
});
|
|
51
57
|
}
|
package/classes/bundler.js
CHANGED
|
@@ -1,56 +1,124 @@
|
|
|
1
1
|
import fs from "@vistta/fs";
|
|
2
2
|
import { build } from "esbuild";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { assign, extract } from "../utils.js";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { assign, extract, stripBundler } from "../utils.js";
|
|
5
5
|
import { Command } from "./command.js";
|
|
6
6
|
import { Runtime } from "./runtime.js";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{
|
|
10
|
+
* paths?: Record<string, any>;
|
|
11
|
+
* exports?: string;
|
|
12
|
+
* sideEffects?: "none" | string | boolean;
|
|
13
|
+
* importAttributes?: Record<string, any>;
|
|
14
|
+
* [key: string]: any;
|
|
15
|
+
* }} BundlerRunOptions
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* code: string;
|
|
21
|
+
* files: string[];
|
|
22
|
+
* resources: Record<string, any>;
|
|
23
|
+
* errors: any[];
|
|
24
|
+
* warnings: any[];
|
|
25
|
+
* warning?: any;
|
|
26
|
+
* }} BundlerRunResult
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{
|
|
31
|
+
* entries: string[];
|
|
32
|
+
* files: string[];
|
|
33
|
+
* resources: Record<string, any>;
|
|
34
|
+
* loader: Runtime;
|
|
35
|
+
* paths?: Record<string, any>;
|
|
36
|
+
* exports?: string;
|
|
37
|
+
* sideEffects?: boolean;
|
|
38
|
+
* platform?: string;
|
|
39
|
+
* importAttributes?: Record<string, any>;
|
|
40
|
+
* }} SetupOptions
|
|
41
|
+
*/
|
|
42
|
+
|
|
8
43
|
export class Bundler {
|
|
44
|
+
/** @type {Runtime} */
|
|
9
45
|
#loader;
|
|
46
|
+
/** @type {BundlerRunOptions} */
|
|
10
47
|
#options;
|
|
11
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @param {Runtime | Command | undefined} [arg1]
|
|
51
|
+
*/
|
|
12
52
|
constructor(arg1) {
|
|
13
53
|
if (arg1 instanceof Runtime) this.#loader = arg1;
|
|
14
|
-
else if (arg1 instanceof Command)
|
|
54
|
+
else if (arg1 instanceof Command)
|
|
55
|
+
this.#loader = new Runtime({
|
|
56
|
+
loaders: arg1.loaders,
|
|
57
|
+
resolvers: arg1.resolvers,
|
|
58
|
+
options: process.vistta?.options || {},
|
|
59
|
+
});
|
|
15
60
|
else if (!arg1) this.#loader = new Runtime(process.vistta);
|
|
16
61
|
else throw new Error("Invalid first argument. Expected Loader or CLI instance.");
|
|
17
62
|
this.#options = this.#loader?.options?.bundler ?? {};
|
|
18
63
|
}
|
|
19
64
|
|
|
20
|
-
|
|
65
|
+
/**
|
|
66
|
+
* @param {string | string[]} target
|
|
67
|
+
* @param {BundlerRunOptions} [options]
|
|
68
|
+
* @returns {Promise<BundlerRunResult>}
|
|
69
|
+
*/
|
|
70
|
+
async run(target, options = {}) {
|
|
21
71
|
const files = [];
|
|
22
72
|
const resources = {};
|
|
23
|
-
assign(options, this.#options);
|
|
24
|
-
const [paths, exports] = extract(
|
|
25
|
-
|
|
73
|
+
options = assign({ ...options }, this.#options);
|
|
74
|
+
const [paths, exports, sideEffects, importAttributes] = extract(
|
|
75
|
+
options,
|
|
76
|
+
"paths",
|
|
77
|
+
"exports",
|
|
78
|
+
"sideEffects",
|
|
79
|
+
"importAttributes",
|
|
80
|
+
);
|
|
81
|
+
const multiple = Array.isArray(target);
|
|
82
|
+
if (exports && multiple) throw new Error("Cannot use 'exports' option with multiple entry points");
|
|
83
|
+
const entries = multiple ? target : [target];
|
|
84
|
+
options.entryPoints = exports ? ["@exports"] : multiple ? ["@bundle"] : entries;
|
|
26
85
|
options.bundle = true;
|
|
27
86
|
options.outdir ??= "dist";
|
|
28
87
|
options.legalComments ??= "none";
|
|
29
88
|
options.keepNames ??= true;
|
|
30
89
|
options.treeShaking ??= true;
|
|
31
|
-
options.define ??= {};
|
|
32
90
|
options.plugins = [];
|
|
33
91
|
options.minify ??= process.env.NODE_ENV === "production";
|
|
34
92
|
options.format ??= options.globalName ? "iife" : "esm";
|
|
35
93
|
options.target ??= "esnext";
|
|
36
|
-
options.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
94
|
+
options.logLevel ??= "error";
|
|
95
|
+
options.define ??= {};
|
|
96
|
+
if (options.platform !== "node") {
|
|
97
|
+
options.define[`process`] = "undefined";
|
|
98
|
+
const keys = Object.keys(process.env);
|
|
99
|
+
for (let i = 0, len = keys.length; i < len; i++) {
|
|
100
|
+
if (/[():;,.\s]/.test(keys[i])) continue;
|
|
101
|
+
options.define[`process.env.${keys[i]}`] = JSON.stringify(process.env[keys[i]]);
|
|
102
|
+
}
|
|
41
103
|
}
|
|
42
104
|
options.plugins.unshift(
|
|
43
105
|
setup({
|
|
106
|
+
entries,
|
|
44
107
|
loader: this.#loader,
|
|
45
108
|
paths,
|
|
46
|
-
filename,
|
|
47
109
|
files,
|
|
48
110
|
resources,
|
|
49
111
|
exports,
|
|
112
|
+
sideEffects: sideEffects !== "none",
|
|
50
113
|
platform: options.platform,
|
|
114
|
+
importAttributes,
|
|
51
115
|
}),
|
|
52
116
|
);
|
|
53
117
|
const { outputFiles, errors, warnings } = await build(options);
|
|
118
|
+
if ((outputFiles?.length ?? 0) > 1)
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Unexpected Bundler Error: Expected 1 output file, but got ${outputFiles.length}. Please report this issue.`,
|
|
121
|
+
);
|
|
54
122
|
return {
|
|
55
123
|
code: outputFiles?.[0]?.text ?? "",
|
|
56
124
|
files: Array.from(new Set(files)),
|
|
@@ -60,49 +128,92 @@ export class Bundler {
|
|
|
60
128
|
};
|
|
61
129
|
}
|
|
62
130
|
|
|
63
|
-
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} target
|
|
133
|
+
* @param {BundlerRunOptions} [options]
|
|
134
|
+
* @returns {Promise<any>}
|
|
135
|
+
*/
|
|
136
|
+
async import(target, options = {}) {
|
|
64
137
|
options.write = false;
|
|
65
|
-
const { code, errors, warning } = await this.run(
|
|
138
|
+
const { code, errors, warning } = await this.run(target, options);
|
|
66
139
|
if (code === "") return { errors, warning };
|
|
67
140
|
return await import(`data:text/javascript;base64,${Buffer.from(code).toString(`base64`)}`);
|
|
68
141
|
}
|
|
69
142
|
}
|
|
70
143
|
|
|
71
|
-
|
|
144
|
+
/**
|
|
145
|
+
* @param {SetupOptions} options
|
|
146
|
+
*/
|
|
147
|
+
function setup({
|
|
148
|
+
entries,
|
|
149
|
+
files: bundlerFiles,
|
|
150
|
+
resources: bundlerResources,
|
|
151
|
+
loader,
|
|
152
|
+
paths = {},
|
|
153
|
+
exports,
|
|
154
|
+
sideEffects,
|
|
155
|
+
platform,
|
|
156
|
+
importAttributes: bundlerImportAttributes = {},
|
|
157
|
+
}) {
|
|
72
158
|
const filter = /.*/;
|
|
73
|
-
const basename = fs.basename(filename);
|
|
74
|
-
const dirname = fs.dirname(filename);
|
|
75
159
|
return {
|
|
76
160
|
name: "vistta",
|
|
77
161
|
setup: (build) => (
|
|
78
162
|
build.onResolve({ filter }, async ({ path, importer, resolveDir }) => {
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
if (
|
|
163
|
+
const cleanPath = stripBundler(path);
|
|
164
|
+
if (cleanPath === "@exports") return { path: "@exports", namespace: "exports", sideEffects };
|
|
165
|
+
if (cleanPath === "@bundle") return { path: "@bundle", namespace: "bundle", sideEffects };
|
|
166
|
+
if (paths[cleanPath]) {
|
|
167
|
+
const mapping = paths[cleanPath];
|
|
168
|
+
if (mapping.namespace !== "window") return Object.assign({ sideEffects }, mapping);
|
|
169
|
+
const names = await getImportNames(importer, cleanPath);
|
|
170
|
+
return {
|
|
171
|
+
path: `${cleanPath}?window-import=${encodeURIComponent(importer)}`,
|
|
172
|
+
namespace: "window",
|
|
173
|
+
pluginData: { name: mapping.path, names },
|
|
174
|
+
sideEffects,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const isVirtualPath = importer === "@exports" || importer === "@bundle";
|
|
178
|
+
if (!isVirtualPath && !fs.isAbsolute(importer)) importer = fs.resolve(resolveDir, importer);
|
|
82
179
|
const { final, builtin, file } = await loader.resolve(
|
|
83
|
-
|
|
180
|
+
cleanPath,
|
|
84
181
|
{
|
|
85
182
|
conditions: ["bundler"],
|
|
86
|
-
parentURL: pathToFileURL(importer).href,
|
|
183
|
+
parentURL: !isVirtualPath ? pathToFileURL(importer).href : undefined,
|
|
87
184
|
},
|
|
88
185
|
(final, { builtin, file }) => ({ final, builtin, file }),
|
|
89
186
|
);
|
|
90
|
-
|
|
187
|
+
const cleanFinal = stripBundler(final);
|
|
188
|
+
const finalPath = cleanFinal.startsWith("file://") ? fileURLToPath(cleanFinal) : cleanFinal;
|
|
189
|
+
if (!builtin && file) return { path: finalPath, sideEffects };
|
|
190
|
+
if (!builtin) return { sideEffects };
|
|
91
191
|
if (platform === "node") return { external: true };
|
|
92
|
-
|
|
93
|
-
return { path: final, namespace: "ignore" };
|
|
192
|
+
return { path: finalPath, namespace: "ignore" };
|
|
94
193
|
}),
|
|
95
|
-
build.onLoad({ filter }, async ({ path, namespace, with: importAttributes
|
|
194
|
+
build.onLoad({ filter }, async ({ path, namespace, pluginData, with: importAttributes }) => {
|
|
195
|
+
const cleanPath = stripBundler(path);
|
|
96
196
|
if (namespace === "ignore") return { contents: "" };
|
|
97
|
-
if (namespace === "window") return { contents:
|
|
98
|
-
if (namespace === "global") return { contents: `module.exports = global.${
|
|
99
|
-
if (namespace === "exports")
|
|
100
|
-
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
197
|
+
if (namespace === "window") return { contents: createWindowModule(pluginData.name, pluginData.names) };
|
|
198
|
+
if (namespace === "global") return { contents: `module.exports = global.${cleanPath};` };
|
|
199
|
+
if (namespace === "exports") {
|
|
200
|
+
if (exports === "*") return { contents: `export * from ${JSON.stringify(entries[0])};` };
|
|
201
|
+
return { contents: `export { ${exports} } from ${JSON.stringify(entries[0])};` };
|
|
202
|
+
}
|
|
203
|
+
if (namespace === "bundle") {
|
|
204
|
+
let contents = "";
|
|
205
|
+
for (let i = 0, len = entries.length; i < len; i++) contents += `export * from ${JSON.stringify(entries[i])};\n`;
|
|
206
|
+
return { contents };
|
|
207
|
+
}
|
|
208
|
+
if (importAttributes.type === "bundler") delete importAttributes.type;
|
|
104
209
|
importAttributes.bundler = true;
|
|
105
|
-
const
|
|
210
|
+
const mergedImportAttributes = { ...bundlerImportAttributes, ...importAttributes };
|
|
211
|
+
if (cleanPath.match(/\.(jsx|tsx)$/)) delete mergedImportAttributes.type;
|
|
212
|
+
const {
|
|
213
|
+
source,
|
|
214
|
+
resources,
|
|
215
|
+
files = [],
|
|
216
|
+
} = await loader.load(cleanPath, { importAttributes: mergedImportAttributes }, () => ({}));
|
|
106
217
|
bundlerFiles.push(...files);
|
|
107
218
|
if (!source) return { loader: "js" };
|
|
108
219
|
for (let i = 0, len = resources?.length || 0; i < len; i++) {
|
|
@@ -114,3 +225,28 @@ function setup({ files: bundlerFiles, resources: bundlerResources, filename, loa
|
|
|
114
225
|
),
|
|
115
226
|
};
|
|
116
227
|
}
|
|
228
|
+
|
|
229
|
+
async function getImportNames(importer, path) {
|
|
230
|
+
if (!importer || !fs.existsSync(importer)) return [];
|
|
231
|
+
const source = String(await fs.readFile(importer, "utf8"));
|
|
232
|
+
const escapedPath = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
233
|
+
const pattern = new RegExp(
|
|
234
|
+
`(?:import\\s+(?:[\\w$]+\\s*,\\s*)?|export\\s*){([^}]+)}\\s*from\\s*["']${escapedPath}["']`,
|
|
235
|
+
"g",
|
|
236
|
+
);
|
|
237
|
+
const names = new Set();
|
|
238
|
+
for (const match of source.matchAll(pattern)) {
|
|
239
|
+
for (const specifier of match[1].split(",")) names.add(specifier.trim().split(/\s+as\s+/i)[0]);
|
|
240
|
+
}
|
|
241
|
+
return [...names].filter(Boolean);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createWindowModule(name, names) {
|
|
245
|
+
const getNamespace = `() => window[${JSON.stringify(name)}]`;
|
|
246
|
+
let contents = `const getNamespace = ${getNamespace};\nconst exports = {};\nObject.defineProperty(exports, "__esModule", { value: true });\n`;
|
|
247
|
+
contents += `Object.defineProperty(exports, "default", { enumerable: true, get: () => { const namespace = getNamespace(); return namespace?.default ?? namespace; } });\n`;
|
|
248
|
+
for (const exportName of names)
|
|
249
|
+
contents += `Object.defineProperty(exports, ${JSON.stringify(exportName)}, { enumerable: true, get: () => getNamespace()?.[${JSON.stringify(exportName)}] });\n`;
|
|
250
|
+
contents += `module.exports = new Proxy(exports, { get: (exports, name) => Reflect.has(exports, name) ? Reflect.get(exports, name) : (getNamespace()?.default ?? getNamespace())?.[name] });\n`;
|
|
251
|
+
return contents;
|
|
252
|
+
}
|
package/classes/loader.js
CHANGED
package/classes/runtime.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import fs from "@vistta/fs";
|
|
2
2
|
import { isBuiltin } from "node:module";
|
|
3
3
|
import { fileURLToPath, pathToFileURL, URLSearchParams } from "node:url";
|
|
4
|
+
import { CWD, FALLBACK, markAttributes, markBundler, stripBundler, WITH_TYPE_RE } from "../utils.js";
|
|
4
5
|
import { Loader } from "./loader.js";
|
|
5
6
|
|
|
6
|
-
const CWD = process.cwd();
|
|
7
|
-
const FALLBACK = "*";
|
|
8
|
-
|
|
9
7
|
export class Runtime {
|
|
10
8
|
static Error = class extends Error {
|
|
11
9
|
constructor(name, message) {
|
|
@@ -17,22 +15,27 @@ export class Runtime {
|
|
|
17
15
|
#init;
|
|
18
16
|
#loaders;
|
|
19
17
|
#resolvers;
|
|
18
|
+
/** @type {Record<string, any>} */
|
|
20
19
|
#options;
|
|
21
20
|
|
|
22
21
|
get loaders() {
|
|
23
22
|
return this.#loaders;
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
/** @returns {Record<string, any>} */
|
|
26
26
|
get options() {
|
|
27
27
|
return this.#options;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* @param {{ loaders?: any[]; resolvers?: string[]; options?: Record<string, any> }} [config]
|
|
32
|
+
*/
|
|
33
|
+
constructor({ loaders = [], resolvers = [], options = {} } = {}) {
|
|
31
34
|
this.#init = async () => {
|
|
32
35
|
this.#loaders = {};
|
|
33
|
-
for (let i = 0, len = loaders
|
|
36
|
+
for (let i = 0, len = loaders.length; i < len; i++) {
|
|
34
37
|
let { main, type, filter } = loaders[i];
|
|
35
|
-
const { default: LoaderClass } = await import(main);
|
|
38
|
+
const { default: LoaderClass } = await import(pathToFileURL(main).href);
|
|
36
39
|
if (!(LoaderClass?.prototype instanceof Loader)) throw new Runtime.Error(`Invalid loader "${loaders[i].main}"`);
|
|
37
40
|
if (!type) type = FALLBACK;
|
|
38
41
|
if (!this.#loaders[type]) this.#loaders[type] = {};
|
|
@@ -48,8 +51,11 @@ export class Runtime {
|
|
|
48
51
|
[specifier, options] = specifier.split(/\?(.*)/);
|
|
49
52
|
options = new URLSearchParams(options);
|
|
50
53
|
const { parentURL = CWD, conditions: [type] = [] } = context;
|
|
54
|
+
const importType = context?.importAttributes?.type;
|
|
55
|
+
const isBundler = type === "bundler" || importType === "bundler";
|
|
56
|
+
const cleanParentURL = stripBundler(parentURL);
|
|
51
57
|
if (isBuiltin(specifier)) {
|
|
52
|
-
if (
|
|
58
|
+
if (isBundler) context.builtin = true;
|
|
53
59
|
return nextResolve(specifier, context);
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -74,7 +80,7 @@ export class Runtime {
|
|
|
74
80
|
if (specifier?.startsWith("file://")) specifier = fileURLToPath(specifier);
|
|
75
81
|
|
|
76
82
|
if (!fs.isAbsolute(specifier))
|
|
77
|
-
specifier = fs.resolve(
|
|
83
|
+
specifier = fs.resolve(cleanParentURL?.startsWith("file://") ? fs.dirname(cleanParentURL) : cleanParentURL, specifier);
|
|
78
84
|
const len = this.#resolvers.length;
|
|
79
85
|
if (fs.existsSync(specifier)) {
|
|
80
86
|
if (fs.isDirectory(specifier)) {
|
|
@@ -95,22 +101,26 @@ export class Runtime {
|
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
if (
|
|
104
|
+
if (isBundler) {
|
|
99
105
|
if (fs.isAbsolute(specifier)) context.file = true;
|
|
100
|
-
return nextResolve(specifier, context);
|
|
106
|
+
return nextResolve(markBundler(pathToFileURL(specifier).href), context);
|
|
101
107
|
}
|
|
102
108
|
|
|
109
|
+
let query = "";
|
|
103
110
|
const params = options.toString();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
if (params) query = params;
|
|
112
|
+
const attrSuffix = markAttributes(context?.importAttributes);
|
|
113
|
+
if (attrSuffix) query += (query ? "&" : "") + attrSuffix;
|
|
114
|
+
let url = pathToFileURL(specifier).href;
|
|
115
|
+
if (query) url += (url.includes("?") ? "&" : "?") + query;
|
|
116
|
+
return nextResolve(url, context);
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
async load(url, context, nextLoad) {
|
|
110
120
|
if (isBuiltin(url)) return nextLoad(url, context);
|
|
111
121
|
const { ...options } = context?.importAttributes || {};
|
|
112
|
-
|
|
113
|
-
const path = fileURLToPath(
|
|
122
|
+
const cleanUrl = url.replace(WITH_TYPE_RE, "").split("?")[0];
|
|
123
|
+
const path = cleanUrl.startsWith("file://") ? fileURLToPath(cleanUrl) : cleanUrl;
|
|
114
124
|
const loader = match(this.#loaders, path, options.type);
|
|
115
125
|
if (!loader) return nextLoad(url, context);
|
|
116
126
|
options.path = path;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{ only?: boolean; filter?: string }} TestRunnerOptions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{
|
|
7
|
+
* name: string;
|
|
8
|
+
* start?: number;
|
|
9
|
+
* end?: number;
|
|
10
|
+
* status?: "pass" | "fail";
|
|
11
|
+
* result?: any;
|
|
12
|
+
* error?: any;
|
|
13
|
+
* }} TestResult
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{ only: boolean; skip: boolean; name: string; callback: () => any }} SuiteDefinition
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{ only?: boolean; skip?: boolean; name?: string; tests: TestResult[]; time?: number }} SuiteResult
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{
|
|
26
|
+
* toEqual: (value: any, unit?: string) => void;
|
|
27
|
+
* toBeLessThan: (value: any, unit?: string) => void;
|
|
28
|
+
* toBeGreaterThan: (value: any, unit?: string) => void;
|
|
29
|
+
* toInclude: (value: any) => void;
|
|
30
|
+
* toMatch: (regex: string | RegExp) => void;
|
|
31
|
+
* toThrow: (message?: string) => void;
|
|
32
|
+
* }} ExpectMatcherSet
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {ExpectMatcherSet & { not: ExpectMatcherSet }} ExpectMatchers
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export class TestRunner {
|
|
40
|
+
/** @type {TestRunnerOptions} */
|
|
41
|
+
#options = {};
|
|
42
|
+
/** @type {SuiteDefinition[]} */
|
|
43
|
+
#suites = [];
|
|
44
|
+
/** @type {SuiteResult[]} */
|
|
45
|
+
#results;
|
|
46
|
+
/** @type {SuiteResult | null} */
|
|
47
|
+
#active;
|
|
48
|
+
/** @type {number} */
|
|
49
|
+
#running;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {TestRunnerOptions} [options]
|
|
53
|
+
*/
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.#options = options;
|
|
56
|
+
this.suite = this.#suite.bind(this);
|
|
57
|
+
this.suite.only = (name, callback) => this.#suite(name, callback, { only: true });
|
|
58
|
+
this.suite.skip = (name, callback) => this.#suite(name, callback, { skip: true });
|
|
59
|
+
this.test = this.#test.bind(this);
|
|
60
|
+
this.test.only = (name, callback) => this.#test(name, callback, { only: true });
|
|
61
|
+
this.test.skip = (name, callback) => this.#test(name, callback, { skip: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @returns {Promise<SuiteResult[]>}
|
|
66
|
+
*/
|
|
67
|
+
async run() {
|
|
68
|
+
this.#running = 0;
|
|
69
|
+
this.#results = [];
|
|
70
|
+
for (const suite of this.#suites) {
|
|
71
|
+
this.#active = { only: suite.only, name: suite.name, skip: suite.skip, tests: [] };
|
|
72
|
+
try {
|
|
73
|
+
await suite.callback();
|
|
74
|
+
} catch {
|
|
75
|
+
/* Do Nothing */
|
|
76
|
+
}
|
|
77
|
+
this.#results.push(this.#active);
|
|
78
|
+
this.#active = null;
|
|
79
|
+
}
|
|
80
|
+
while (this.#running > 0) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
81
|
+
return this.#results;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} name
|
|
86
|
+
* @param {() => any} callback
|
|
87
|
+
* @param {{ only?: boolean, skip?: boolean }} options
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
async #suite(name, callback, { only, skip } = {}) {
|
|
91
|
+
if (this.#active) throw new Error("Suites cannot be stacked");
|
|
92
|
+
this.#suites.push({ only, skip, name, callback });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} name
|
|
97
|
+
* @param {() => any | Promise<any>} callback
|
|
98
|
+
* @param {{ only?: boolean, skip?: boolean }} options
|
|
99
|
+
* @returns {Promise<void>}
|
|
100
|
+
*/
|
|
101
|
+
async #test(name, callback, { only, skip } = {}) {
|
|
102
|
+
if (
|
|
103
|
+
(this.#options.filter && !name.match(new RegExp(this.#options.filter, "i"))) ||
|
|
104
|
+
(this.#options.only && !(only || this.#active?.only))
|
|
105
|
+
)
|
|
106
|
+
return;
|
|
107
|
+
const test = { name };
|
|
108
|
+
if (skip || this.#active?.skip) {
|
|
109
|
+
test.status = "skip";
|
|
110
|
+
this.#active?.tests.push(test);
|
|
111
|
+
return;
|
|
112
|
+
} else this.#running++;
|
|
113
|
+
test.start = performance();
|
|
114
|
+
if (this.#active) this.#active.tests.push(test);
|
|
115
|
+
else this.#results.push({ tests: [test], time: test.time });
|
|
116
|
+
try {
|
|
117
|
+
test.result = await callback();
|
|
118
|
+
test.status = "pass";
|
|
119
|
+
test.end = performance();
|
|
120
|
+
} catch (error) {
|
|
121
|
+
test.status = "fail";
|
|
122
|
+
test.error = error;
|
|
123
|
+
test.end = performance();
|
|
124
|
+
}
|
|
125
|
+
this.#running--;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {any} target
|
|
130
|
+
* @param {string} [label]
|
|
131
|
+
* @returns {ExpectMatchers}
|
|
132
|
+
*/
|
|
133
|
+
expect(target, label) {
|
|
134
|
+
return {
|
|
135
|
+
not: {
|
|
136
|
+
toEqual: (value, unit = "") => {
|
|
137
|
+
if (target !== value) return;
|
|
138
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not equal ${value}${unit}`);
|
|
139
|
+
},
|
|
140
|
+
toBeLessThan: (value, unit = "") => {
|
|
141
|
+
if (target >= value) return;
|
|
142
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not be less than ${value}${unit}`);
|
|
143
|
+
},
|
|
144
|
+
toBeGreaterThan: (value, unit = "") => {
|
|
145
|
+
if (target <= value) return;
|
|
146
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not be greater than ${value}${unit}`);
|
|
147
|
+
},
|
|
148
|
+
toInclude: (value) => {
|
|
149
|
+
if (target.indexOf(value) === -1) return;
|
|
150
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not include ${value}`);
|
|
151
|
+
},
|
|
152
|
+
toMatch: (regex) => {
|
|
153
|
+
if (!target.match(new RegExp(regex))) return;
|
|
154
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to not match ${regex}`);
|
|
155
|
+
},
|
|
156
|
+
toThrow: (message) => {
|
|
157
|
+
try {
|
|
158
|
+
target();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!message)
|
|
161
|
+
throw new Error(`expected ${label ? label + "()" : "function()"} to not throw, but got ${error.message}`);
|
|
162
|
+
if (error.message === message)
|
|
163
|
+
throw new Error(
|
|
164
|
+
`expected ${label ? label + "()" : "function()"} to not throw ${message}, but got ${error.message}`,
|
|
165
|
+
);
|
|
166
|
+
if (error.message.match(new RegExp(message)))
|
|
167
|
+
throw new Error(
|
|
168
|
+
`expected ${label ? label + "()" : "function()"} to not throw ${message}, but got ${error.message}`,
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
toEqual: (value, unit = "") => {
|
|
175
|
+
if (target === value) return;
|
|
176
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to equal ${value}${unit}`);
|
|
177
|
+
},
|
|
178
|
+
toBeLessThan: (value, unit = "") => {
|
|
179
|
+
if (target < value) return;
|
|
180
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to be less than ${value}${unit}`);
|
|
181
|
+
},
|
|
182
|
+
toBeGreaterThan: (value, unit = "") => {
|
|
183
|
+
if (target > value) return;
|
|
184
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to be greater than ${value}${unit}`);
|
|
185
|
+
},
|
|
186
|
+
toInclude: (value) => {
|
|
187
|
+
if (target.indexOf(value) > -1) return;
|
|
188
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to include ${value}`);
|
|
189
|
+
},
|
|
190
|
+
toMatch: (regex) => {
|
|
191
|
+
if (target.match(new RegExp(regex))) return;
|
|
192
|
+
throw new Error(`expected ${label ? label + "(" + target + ")" : target} to match ${regex}`);
|
|
193
|
+
},
|
|
194
|
+
toThrow: (message) => {
|
|
195
|
+
try {
|
|
196
|
+
target();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!message) return;
|
|
199
|
+
if (error.message === message) return;
|
|
200
|
+
if (error.message.match(new RegExp(message))) return;
|
|
201
|
+
throw new Error(`expected ${label ? label + "()" : "function()"} to throw ${message}, but got ${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function performance() {
|
|
209
|
+
return globalThis.performance.now();
|
|
210
|
+
}
|
package/commands/bundle.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { Bundler } from "../classes/bundler.js";
|
|
3
3
|
import { Runtime } from "../classes/runtime.js";
|
|
4
|
+
import { parseArgs } from "../utils.js";
|
|
4
5
|
import DefaultCommand from "./default.js";
|
|
5
6
|
|
|
6
7
|
export default class BundlerCommand extends DefaultCommand {
|
|
7
|
-
help() {
|
|
8
|
+
async help() {
|
|
8
9
|
console.print("Bundles the specified entry file");
|
|
9
10
|
console.print("\nUsage:");
|
|
10
11
|
console.print("vistta bundle [filename] [outdir]\tBundles the specified entry file");
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
async main(_, ...argv) {
|
|
14
|
-
let [args, options] =
|
|
15
|
+
let [args, options] = parseArgs(argv);
|
|
15
16
|
if (args.length === 0) return this.help();
|
|
16
|
-
options = Object.assign({},
|
|
17
|
+
options = Object.assign({}, process.vistta?.options?.bundler || {}, options, { outdir: args[1] });
|
|
17
18
|
new Bundler(new Runtime(global.vistta)).run(resolve(process.cwd(), args[0]), options);
|
|
18
19
|
}
|
|
19
20
|
}
|