@vistta/cli 1.0.1 → 1.2.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 +150 -11
- package/bin.js +14 -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 +110 -74
- package/commands/quality.js +52 -0
- package/commands/task.js +23 -0
- package/commands/test.js +39 -171
- package/config.js +128 -0
- package/config.schema.json +150 -0
- package/global.d.ts +68 -0
- package/index.js +4 -0
- package/loaders/bundler.js +3 -1
- package/loaders/index.js +2 -2
- package/main.js +1 -1
- package/package.json +17 -14
- package/register.js +15 -7
- package/tests/bundler.test.js +118 -0
- package/tests/config.test.js +217 -0
- package/tests/index.test.js +140 -0
- package/tests/loader.test.js +23 -0
- package/tests/project.test.js +116 -0
- package/tests/runtime.test.js +76 -0
- package/tests/test-runner.test.js +83 -0
- package/tools.js +210 -0
- package/tsconfig.json +4 -5
- package/utils.js +122 -108
- package/.gitea/workflows/prerelease.yml +0 -62
- package/.gitea/workflows/release.yml +0 -124
package/README.md
CHANGED
|
@@ -30,21 +30,160 @@ npm install @vistta/cli
|
|
|
30
30
|
- `vistta project name`
|
|
31
31
|
Outputs the current project name from `package.json`.
|
|
32
32
|
|
|
33
|
-
- `vistta project version [
|
|
34
|
-
|
|
33
|
+
- `vistta project version [increment|version] [prerelease-id]`
|
|
34
|
+
Prints the current project version, increments it (for example, `patch` or
|
|
35
|
+
`prerelease`), or sets an explicit version such as `1.2.3`.
|
|
36
|
+
|
|
37
|
+
- `vistta project outdated [dependency]`
|
|
38
|
+
Shows installed (`Current`), range-compatible (`Wanted`), and `Latest`
|
|
39
|
+
versions, plus the owning manifest and dependency type, using `npm outdated`,
|
|
40
|
+
respecting npm registry settings and policies such as `min-release-age`.
|
|
41
|
+
Inside a workspace package, only that package's dependencies are checked;
|
|
42
|
+
running from the workspace root checks all workspace consumers.
|
|
43
|
+
Linked workspace packages are excluded. Available compatible updates cause a
|
|
44
|
+
nonzero exit status; newer versions outside the declared range are informational.
|
|
45
|
+
|
|
46
|
+
- `vistta project update [dependency] [--compatible] [--dry-run]`
|
|
47
|
+
Updates declared dependency ranges to latest versions by default, which may
|
|
48
|
+
include major upgrades. `--compatible` selects npm's wanted version within
|
|
49
|
+
each dependency's existing range instead. `--dry-run` previews the exact
|
|
50
|
+
manifest changes without writing files.
|
|
51
|
+
|
|
52
|
+
- `vistta project patch [dependency] [--dry-run]`
|
|
53
|
+
Backward-compatible alias for `project update --compatible`. Despite the
|
|
54
|
+
historical name, this can select a compatible minor update, not just patches.
|
|
55
|
+
|
|
56
|
+
Dependency updates only edit `package.json` ranges; run `npm install` afterward
|
|
57
|
+
to update installed versions and the lockfile. Workspace-root updates preserve
|
|
58
|
+
each consumer's own target version. If no ranges need changing, the command
|
|
59
|
+
says so explicitly. A dependency filter selects matching outdated entries; no
|
|
60
|
+
match means no outdated entry was found, not that the dependency exists.
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
```sh
|
|
63
|
+
vistta project --help
|
|
64
|
+
vistta project update --help
|
|
65
|
+
vistta project outdated react
|
|
66
|
+
vistta project update --compatible --dry-run
|
|
67
|
+
vistta project update react --compatible
|
|
68
|
+
vistta project update react --dry-run
|
|
69
|
+
```
|
|
44
70
|
|
|
45
71
|
- `vistta test [pattern]`
|
|
46
72
|
Run tests matching the specified pattern.
|
|
47
73
|
|
|
74
|
+
For browser-based test harnesses, import `TestRunner` from `@vistta/cli/test-runner` to avoid bundling Node-only CLI modules.
|
|
75
|
+
|
|
76
|
+
### Unified Project Configuration
|
|
77
|
+
|
|
78
|
+
Keep VISTTA settings in the `cli` block of `tsconfig.json` or `jsconfig.json`.
|
|
79
|
+
Comments and trailing commas are supported. If both files exist, `tsconfig.json`
|
|
80
|
+
takes precedence. The existing `cliOptions` block remains supported; `cli` wins
|
|
81
|
+
when both specify the same setting.
|
|
82
|
+
|
|
83
|
+
Install the tools you want to use locally:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
npm install --save-dev eslint prettier typescript
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```jsonc
|
|
90
|
+
{
|
|
91
|
+
"$schema": "./node_modules/@vistta/cli/config.schema.json",
|
|
92
|
+
"compilerOptions": {
|
|
93
|
+
"allowJs": true,
|
|
94
|
+
"checkJs": true,
|
|
95
|
+
"noEmit": true,
|
|
96
|
+
},
|
|
97
|
+
"include": ["src/**/*.js", "src/**/*.ts"],
|
|
98
|
+
"exclude": ["dist"],
|
|
99
|
+
"cli": {
|
|
100
|
+
"check": { "format": true, "lint": true, "types": true },
|
|
101
|
+
"format": {
|
|
102
|
+
"engine": "prettier",
|
|
103
|
+
"options": { "printWidth": 120, "semi": true },
|
|
104
|
+
},
|
|
105
|
+
"lint": {
|
|
106
|
+
"engine": "eslint",
|
|
107
|
+
"rules": { "no-unused-vars": "warn" },
|
|
108
|
+
},
|
|
109
|
+
"tasks": {
|
|
110
|
+
"validate": { "dependsOn": ["check", "test"] },
|
|
111
|
+
"release": { "dependsOn": ["validate"], "script": "publish:package" },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The optional `$schema` enables completion and validation for VISTTA settings.
|
|
118
|
+
The schema leaves other TypeScript and framework settings open; TypeScript
|
|
119
|
+
validates compiler options during `vistta types` or `vistta check`.
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
vistta check
|
|
123
|
+
vistta check --fix
|
|
124
|
+
vistta lint "src/**/*.js" --fix
|
|
125
|
+
vistta fmt --check
|
|
126
|
+
vistta fmt --write
|
|
127
|
+
vistta types
|
|
128
|
+
vistta config
|
|
129
|
+
vistta config --sources
|
|
130
|
+
vistta task validate
|
|
131
|
+
vistta run validate
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- `check` runs formatting, linting, and type checking by default. Disable a stage
|
|
135
|
+
with `cli.check`. All enabled stages run, and any failure produces a nonzero
|
|
136
|
+
exit code. Lint warnings also fail the check.
|
|
137
|
+
- Checks are read-only unless `--fix` or `fmt --write` is supplied. Fix mode
|
|
138
|
+
applies Prettier formatting and ESLint fixes; type checking never emits files.
|
|
139
|
+
- Tools are resolved from the current project, not downloaded automatically.
|
|
140
|
+
Only enabled tools need to be installed. ESLint 9+ and Prettier 3 are supported.
|
|
141
|
+
- `format.include` and `lint.include` override top-level `include`. Explicit
|
|
142
|
+
command-line patterns override both and are relative to the working directory.
|
|
143
|
+
The corresponding `exclude` settings, `compilerOptions.outDir`, dependencies,
|
|
144
|
+
and Git metadata are excluded. Inherited patterns are relative to their owning
|
|
145
|
+
configuration file. Type checking always covers the whole configured project.
|
|
146
|
+
- Existing ESLint and Prettier configs are used when present. Inline settings
|
|
147
|
+
override them. Without an ESLint config, inline rules/options provide the flat
|
|
148
|
+
config. `lint.options` contains flat-config properties, not ESLint constructor
|
|
149
|
+
options. Without explicit includes, lint scans JS/TS and formatting scans files
|
|
150
|
+
supported by Prettier. An empty match set passes without changing files.
|
|
151
|
+
- For plugins, parsers, or other executable settings, set `lint.config` or
|
|
152
|
+
`format.config` to a module path such as `"./tools/eslint.js"`. It must default
|
|
153
|
+
export the relevant tool configuration. Relative paths resolve from the owning
|
|
154
|
+
config file; package references resolve from that same directory. Referenced
|
|
155
|
+
lint configs replace ESLint config discovery; inline settings still win.
|
|
156
|
+
|
|
157
|
+
### Presets And Tasks
|
|
158
|
+
|
|
159
|
+
`extends` accepts a relative JSON/JSONC config, a package config export, or an
|
|
160
|
+
array of either. Parents apply left to right, then local settings override them.
|
|
161
|
+
Objects merge recursively; arrays and scalar values replace inherited values.
|
|
162
|
+
Circular presets, malformed JSONC, and invalid VISTTA tool/task settings fail
|
|
163
|
+
explicitly. `vistta config --sources` shows the merged configuration and the
|
|
164
|
+
file responsible for each leaf setting, including built-in check defaults.
|
|
165
|
+
|
|
166
|
+
Workspace packages can extend a shared root/preset config and override `cli`
|
|
167
|
+
locally. Commands run in the current package directory; they do not automatically
|
|
168
|
+
fan out across workspaces. Existing `compilerOptions`, `bundlerOptions`, and
|
|
169
|
+
custom commands continue to work.
|
|
170
|
+
|
|
171
|
+
Tasks run their `dependsOn` dependencies once, in order, before their optional
|
|
172
|
+
package `script`. Dependencies can name other tasks, current-package scripts, or
|
|
173
|
+
registered commands such as `check`, `lint`, `fmt`, `types`, `test`, and `bundle`
|
|
174
|
+
(or `build` when provided by a framework).
|
|
175
|
+
Missing tasks/scripts and dependency cycles fail before execution. The first
|
|
176
|
+
failed command stops the plan and propagates its exit status. Arguments after
|
|
177
|
+
`--` are forwarded only to the requested task's command or script:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
vistta task release -- --dry-run
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`vistta run` is a task alias in the base CLI. Framework command overrides are
|
|
184
|
+
preserved: `@vistta/js` keeps its application-oriented `run`, so use `vistta task`
|
|
185
|
+
there. Task caching and automatic cross-workspace scheduling are not implemented.
|
|
186
|
+
|
|
48
187
|
## Extending the CLI
|
|
49
188
|
|
|
50
189
|
You can create custom loaders and commands to fit your workflow.
|
|
@@ -108,7 +247,7 @@ vistta my-command [options]
|
|
|
108
247
|
|
|
109
248
|
## **License**
|
|
110
249
|
|
|
111
|
-
|
|
250
|
+
Licensed under the GNU General Public License version 3 only (`GPL-3.0-only`). See [LICENSE](LICENSE) for the full terms.
|
|
112
251
|
|
|
113
252
|
## **Contributing**
|
|
114
253
|
|
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";
|
|
@@ -22,6 +22,10 @@ const argv = [];
|
|
|
22
22
|
const execArgv = ["--import", pathToFileURL(fs.resolve(dirname, "register.js")), "--experimental-sqlite", "--title=VISTTA"];
|
|
23
23
|
for (let i = 2, len = process.argv.length; i < len; i++) {
|
|
24
24
|
const [arg, value] = process.argv[i].toLowerCase().split("=");
|
|
25
|
+
if (process.argv[i] === "--") {
|
|
26
|
+
argv.push(...process.argv.slice(i));
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
25
29
|
const nodeOption = ENABLED_NODE_OPTIONS[arg];
|
|
26
30
|
if (nodeOption) {
|
|
27
31
|
if (typeof nodeOption === "string") env[nodeOption] = value || true;
|
|
@@ -43,9 +47,15 @@ start(fs.resolve(dirname, "main.js"), argv, {
|
|
|
43
47
|
stdio: env.NODE_SILENT ? "ignore" : "inherit",
|
|
44
48
|
});
|
|
45
49
|
|
|
46
|
-
function start(
|
|
47
|
-
fork(
|
|
48
|
-
|
|
50
|
+
function start(modulePath, argv, options) {
|
|
51
|
+
const subprocess = fork(modulePath, argv, options);
|
|
52
|
+
let restart = false;
|
|
53
|
+
subprocess.on("message", (message) => {
|
|
54
|
+
if (typeof message === "object" && message && "restart" in message && message.restart)
|
|
55
|
+
((restart = true), subprocess.kill());
|
|
56
|
+
});
|
|
57
|
+
subprocess.on("exit", (code) => {
|
|
58
|
+
if ((env.NODE_AUTO_RESTART && code != 0) || restart) start(modulePath, argv, options);
|
|
49
59
|
else process.exit(code);
|
|
50
60
|
});
|
|
51
61
|
}
|
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;
|