@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/utils.js CHANGED
@@ -1,9 +1,17 @@
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";
5
- import { inc, satisfies, valid } from "semver";
4
+ import { platform } from "node:os";
5
+ import { relative } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { inc, valid } from "semver";
8
+ import { loadProjectConfig, mergeConfig } from "./config.js";
6
9
 
10
+ export const CWD = process.cwd();
11
+ export const FALLBACK = "*";
12
+ export const BUNDLER_SUFFIX = "?__bundler__";
13
+ export const BUNDLER_SUFFIX_RE = /\?__bundler__$/;
14
+ export const WITH_TYPE_RE = /[?&]__with_type=([^&#]*)/;
7
15
  export const ENABLED_NODE_OPTIONS = {
8
16
  "-w": "NODE_WATCH",
9
17
  "--watch": "NODE_WATCH",
@@ -20,7 +28,7 @@ export const ENABLED_NODE_OPTIONS = {
20
28
 
21
29
  export async function importJSON(filepath) {
22
30
  try {
23
- return JSON.parse(await fs.readFile(filepath));
31
+ return JSON.parse(String(await fs.readFile(filepath)));
24
32
  } catch {
25
33
  return {};
26
34
  }
@@ -29,7 +37,7 @@ export async function importJSON(filepath) {
29
37
  export async function importEnv(filepath) {
30
38
  try {
31
39
  const env = {};
32
- const entries = (await fs.readFile(filepath, "utf8")).split("\n");
40
+ const entries = String(await fs.readFile(filepath, "utf8")).split("\n");
33
41
  for (let i = 0, len = entries.length; i < len; i++) {
34
42
  const [key, value] = entries[i].split("=");
35
43
  env[key] = value;
@@ -41,85 +49,70 @@ export async function importEnv(filepath) {
41
49
  }
42
50
 
43
51
  export async function importConfig(options = {}) {
44
- try {
45
- const helper = async (file) => {
46
- file = fileURLToPath(import.meta.resolve(file));
47
- const {
48
- extends: parent,
49
- cliOptions: cli = {},
50
- compilerOptions: compiler = {},
51
- bundlerOptions: bundler = {},
52
- } = await importJSON(file);
53
- if (cli.commands) {
54
- const commands = Object.keys(cli.commands);
55
- const dirname = fs.dirname(file);
56
- for (let i = 0, len = commands.length; i < len; i++)
57
- cli.commands[commands[i]] = fs.resolve(dirname, cli.commands[commands[i]]);
58
- }
59
- if (parent) return assign(await helper(parent), { cli, compiler, bundler });
60
- return { cli, compiler, bundler };
61
- };
62
- let file = fs.resolve(process.cwd(), "tsconfig.json");
63
- if (!fs.existsSync(file)) file = fs.resolve(process.cwd(), "jsconfig.json");
64
- if (!fs.existsSync(file)) return options;
65
- return assign(options, await helper(file));
66
- } catch (e) {
67
- console.error(e.trace || e.message);
68
- return options;
69
- }
52
+ const { config } = loadProjectConfig();
53
+ return mergeConfig(options, {
54
+ cli: config.cli || {},
55
+ compiler: config.compilerOptions || {},
56
+ bundler: config.bundlerOptions || {},
57
+ });
70
58
  }
71
59
 
72
60
  export async function getOutdatedPackages(dirname) {
73
- const packages = (await getProjectLock(dirname))?.packages || {};
74
- const data = {};
75
- const promises = [];
76
- async function processPackage(filepath, workspace) {
77
- if (!fs.existsSync(filepath)) {
78
- if (workspace) return;
79
- else throw new Error("No Package found");
80
- }
81
- const { dependencies, devDependencies, workspaces } = await importJSON(filepath);
82
- await fetchPackageUpdates(filepath, dependencies);
83
- await fetchPackageUpdates(filepath, devDependencies, true);
84
- if (!(workspaces?.length > 0)) return;
85
- const workspaceDirnames = await resolveWorkspacesDirnames(workspaces);
86
- for (let i = 0, len = workspaceDirnames.length; i < len; i++)
87
- await processPackage(fs.resolve(workspaceDirnames[i], "package.json"), true);
88
- }
89
- async function fetchPackageUpdates(path, deps = {}, dev) {
90
- const keys = Object.keys(deps);
91
- for (let i = 0, len = keys.length; i < len; i++) {
92
- const packageName = keys[i];
93
- const { version, resolved, link } = packages["node_modules/" + packageName] || {};
94
- if (link) continue;
95
- if (!version) throw new Error(`Module ${packageName} not found `);
96
- promises.push(
97
- new Promise((resolve, reject) =>
98
- fetch(resolved ? resolved.split("/-/")[0] + "/latest" : `https://registry.npmjs.org/${packageName}/latest`)
99
- .then((response) => response.json())
100
- .then(resolve)
101
- .catch(reject),
102
- ),
103
- );
104
- data[packageName] = {
105
- package: path,
106
- current: version,
107
- wanted: deps[packageName],
108
- dev,
109
- };
110
- }
111
- }
61
+ const lock = await getProjectLock(dirname);
62
+ const packages = lock.packages || {};
63
+ if (!fs.existsSync(fs.resolve(dirname, "package.json"))) throw new Error("No Package found");
64
+ const output = await new Promise((resolve, reject) => {
65
+ const npmPath = process.env.npm_execpath;
66
+ const args = ["outdated", "--json", "--long", "--include=dev"];
67
+ const windowsShell = !npmPath && platform() === "win32";
68
+ const command = npmPath ? process.execPath : windowsShell ? process.env.ComSpec || "cmd.exe" : "npm";
69
+ const child = spawn(
70
+ command,
71
+ npmPath ? [npmPath, ...args] : windowsShell ? ["/d", "/s", "/c", "npm " + args.join(" ")] : args,
72
+ {
73
+ cwd: dirname,
74
+ windowsHide: true,
75
+ },
76
+ );
77
+ let stdout = "";
78
+ let stderr = "";
79
+ child.stdout.on("data", (value) => {
80
+ stdout += value;
81
+ });
82
+ child.stderr.on("data", (value) => {
83
+ stderr += value;
84
+ });
85
+ child.on("error", reject);
86
+ child.on("close", (code) => {
87
+ try {
88
+ const data = JSON.parse(stdout || "{}");
89
+ if ((code !== 0 && code !== 1) || data.error)
90
+ reject(new Error(data.error?.summary || stderr || `npm outdated exited with code ${code}`));
91
+ else resolve(data);
92
+ } catch (error) {
93
+ reject(new Error(stderr || error.message));
94
+ }
95
+ });
96
+ });
112
97
  const result = [];
113
- await processPackage(fs.resolve(dirname, "package.json"));
114
- const values = await Promise.all(promises);
115
- for (let i = 0, len = values.length; i < len; i++) {
116
- const module = data[values[i]?.name];
117
- module.name = values[i]?.name;
118
- module.latest = values[i]?.version;
119
- if (!module || !module?.latest || !module?.current || !module?.wanted) throw new Error(`Internal Error`);
120
- if (module.current === module.latest) continue;
121
- module.wanted = satisfies(module.latest, module.wanted) ? module.latest : module.current;
122
- result.push(module);
98
+ for (const [name, entries] of Object.entries(output)) {
99
+ for (const entry of Array.isArray(entries) ? entries : [entries]) {
100
+ const location = entry.location
101
+ ? relative(lock.dirname, entry.location).replaceAll("\\", "/")
102
+ : "node_modules/" + name;
103
+ if (packages[location]?.link || packages["node_modules/" + name]?.link) continue;
104
+ if (entry.type !== "dependencies" && entry.type !== "devDependencies") continue;
105
+ const packagePath = fs.resolve(lock.dirname, entry.dependedByLocation || "", "package.json");
106
+ if (relative(lock.dirname, dirname) && relative(dirname, packagePath) !== "package.json") continue;
107
+ result.push({
108
+ name,
109
+ package: packagePath,
110
+ current: entry.current,
111
+ wanted: entry.wanted,
112
+ latest: entry.latest,
113
+ dev: entry.type === "devDependencies",
114
+ });
115
+ }
123
116
  }
124
117
  return result;
125
118
  }
@@ -140,9 +133,8 @@ export function saveCrashReport() {
140
133
  if (!fs.existsSync(crashFolder)) fs.mkdir(crashFolder);
141
134
  const logs = console.logs;
142
135
  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
- }
136
+ for (let i = 0, len = logs.length; i < len; i++)
137
+ output += logs[i].time.toLocalISOString() + ": " + logs[i].toString({ time: false });
146
138
  fs.writeFileSync(fs.resolve(crashFolder, Date.now() + ".log"), output);
147
139
  }
148
140
 
@@ -160,47 +152,69 @@ export function run(script, ...args) {
160
152
  });
161
153
  }
162
154
 
163
- /**
164
- * Function to parse command line arguments.
165
- * @param {string[]} args
166
- * @returns
167
- */
168
155
  export function parseArgs(args) {
169
- const result = [[], {}];
156
+ const result = /** @type {[string[], Record<string, any>]} */ ([[], {}]);
170
157
  for (let i = 0, len = args.length; i < len; i++) {
171
- const [option, value] = args[i].toLowerCase().split("=");
158
+ const lower = args[i].toLowerCase();
159
+ const eq = lower.indexOf("=");
160
+ const option = eq === -1 ? lower : lower.slice(0, eq);
161
+ const value = eq === -1 ? undefined : args[i].slice(eq + 1);
172
162
  if (option.startsWith("--")) result[1][option.slice(2)] = evaluate(value);
173
163
  else result[0].push(args[i]);
174
164
  }
175
165
  return result;
176
166
  }
177
167
 
178
- export { assign, async, extract, fs, remove };
168
+ export function resolve(...values) {
169
+ if (platform() === "win32") return pathToFileURL(fs.resolve(...values)).toString();
170
+ return fs.resolve(...values);
171
+ }
172
+
173
+ /**
174
+ * Append the bundler marker to a URL/path so it occupies a distinct slot in
175
+ * Node's ESM module cache from the same file imported without the marker.
176
+ * @param {string} url
177
+ * @returns {string}
178
+ */
179
+ export function markBundler(url) {
180
+ return BUNDLER_SUFFIX_RE.test(url) ? url : url + BUNDLER_SUFFIX;
181
+ }
182
+
183
+ /**
184
+ * Remove the bundler marker from a URL/path if present.
185
+ * @param {string | undefined | null} url
186
+ * @returns {string}
187
+ */
188
+ export function stripBundler(url) {
189
+ return url ? url.replace(BUNDLER_SUFFIX_RE, "") : "";
190
+ }
191
+
192
+ /**
193
+ * Build a stable, sorted query string encoding the import attributes so that
194
+ * two imports of the same file with different `with: { ... }` attributes land
195
+ * in distinct ESM module cache slots. Mirrors the bundler marker pattern.
196
+ * @param {Record<string, any> | undefined | null} attrs
197
+ * @returns {string}
198
+ */
199
+ export function markAttributes(attrs) {
200
+ if (!attrs) return "";
201
+ const keys = Object.keys(attrs)
202
+ .filter((k) => attrs[k] != null)
203
+ .sort();
204
+ if (keys.length === 0) return "";
205
+ return keys.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(String(attrs[k]))}`).join("&");
206
+ }
207
+
208
+ export { assign, async, extract, remove };
179
209
 
180
210
  async function getProjectLock(path) {
181
- if (fs.existsSync(path + "/package-lock.json")) return await importJSON(fs.resolve(path, "package-lock.json"));
211
+ if (fs.existsSync(path + "/package-lock.json"))
212
+ return { ...(await importJSON(fs.resolve(path, "package-lock.json"))), dirname: path };
182
213
  const newPath = fs.resolve(path, "..");
183
214
  if (newPath === path) throw new Error("No Package Lock found.");
184
215
  return await getProjectLock(newPath);
185
216
  }
186
217
 
187
- async function resolveWorkspacesDirnames(workspaces) {
188
- const paths = [];
189
- for (let i = 0, len = workspaces.length; i < len; i++) {
190
- if (!workspaces[i].includes("*")) {
191
- paths.push(workspaces[i]);
192
- continue;
193
- }
194
- const entries = fs.glob(workspaces[i]);
195
- let entry = (await entries.next())?.value;
196
- while (entry) {
197
- paths.push(entry);
198
- entry = (await entries.next())?.value;
199
- }
200
- }
201
- return paths;
202
- }
203
-
204
218
  function evaluate(value) {
205
219
  if (value == null || value === "true") return true;
206
220
  if (value === "false") return false;
@@ -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);