@epic-web/workshop-utils 6.74.3 → 6.75.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.
@@ -165,6 +165,32 @@ function getArgumentsForLineNumber(editor, fileName, lineNumber, colNumber, work
165
165
  // can result in errors or confusing behavior.
166
166
  return [fileName];
167
167
  }
168
+ function getWindowsProcessPaths() {
169
+ const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
170
+ const wmicPath = systemRoot
171
+ ? path.join(systemRoot, 'System32', 'wbem', 'wmic.exe')
172
+ : null;
173
+ if (wmicPath && fs.existsSync(wmicPath)) {
174
+ try {
175
+ const output = child_process
176
+ .execSync(`"${wmicPath}" process where "executablepath is not null" get executablepath`, { stdio: ['ignore', 'pipe', 'ignore'] })
177
+ .toString();
178
+ return output.split(/\r?\n/);
179
+ }
180
+ catch {
181
+ // Fall through to PowerShell
182
+ }
183
+ }
184
+ try {
185
+ const output = child_process
186
+ .execSync('powershell.exe -NoProfile -Command "Get-Process | Where-Object { $_.Path } | ForEach-Object { $_.Path }"', { stdio: ['ignore', 'pipe', 'ignore'] })
187
+ .toString();
188
+ return output.split(/\r?\n/);
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ }
168
194
  function guessEditor() {
169
195
  // Explicit config always wins
170
196
  if (process.env.EPICSHOP_EDITOR) {
@@ -188,10 +214,7 @@ function guessEditor() {
188
214
  else if (process.platform === 'win32') {
189
215
  // Some processes need elevated rights to get its executable path.
190
216
  // Just filter them out upfront. This also saves 10-20ms on the command.
191
- const output = child_process
192
- .execSync('wmic process where "executablepath is not null" get executablepath')
193
- .toString();
194
- const runningProcesses = output.split('\r\n');
217
+ const runningProcesses = getWindowsProcessPaths();
195
218
  for (let i = 0; i < runningProcesses.length; i++) {
196
219
  const processPath = runningProcesses[i]?.trim();
197
220
  if (!processPath)
@@ -7,7 +7,7 @@ export type RootPackageInstallStatus = {
7
7
  missingDependencies: Array<string>;
8
8
  missingDevDependencies: Array<string>;
9
9
  missingOptionalDependencies: Array<string>;
10
- reason: 'missing-node-modules' | 'missing-dependencies' | 'package-json-unreadable' | 'up-to-date';
10
+ reason: 'missing-dependencies' | 'package-json-unreadable' | 'up-to-date';
11
11
  };
12
12
  export type WorkspaceInstallStatus = {
13
13
  roots: Array<RootPackageInstallStatus>;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { execa } from 'execa';
4
5
  import { globby } from 'globby';
5
6
  import { getErrorMessage } from "../utils.js";
6
7
  const workspaceIgnorePatterns = [
@@ -76,39 +77,53 @@ async function listPackageJsonPaths(cwd) {
76
77
  ignore: workspaceIgnorePatterns,
77
78
  });
78
79
  }
79
- async function listInstalledPackages(nodeModulesPath) {
80
+ function parseNpmLsOutput(raw) {
81
+ const trimmed = raw.trim();
82
+ if (!trimmed)
83
+ return null;
80
84
  try {
81
- const entries = await fs.readdir(nodeModulesPath, { withFileTypes: true });
82
- const packages = new Set();
83
- for (const entry of entries) {
84
- if (entry.name.startsWith('.'))
85
- continue;
86
- if (entry.name.startsWith('@')) {
87
- if (!entry.isDirectory())
88
- continue;
89
- const scopePath = path.join(nodeModulesPath, entry.name);
90
- const scopeEntries = await fs.readdir(scopePath, {
91
- withFileTypes: true,
92
- });
93
- for (const scopedEntry of scopeEntries) {
94
- if (scopedEntry.name.startsWith('.'))
95
- continue;
96
- if (scopedEntry.isDirectory() || scopedEntry.isSymbolicLink()) {
97
- packages.add(`${entry.name}/${scopedEntry.name}`);
98
- }
99
- }
100
- continue;
101
- }
102
- if (entry.isDirectory() || entry.isSymbolicLink()) {
103
- packages.add(entry.name);
104
- }
105
- }
106
- return packages;
85
+ return JSON.parse(trimmed);
107
86
  }
108
87
  catch {
109
88
  return null;
110
89
  }
111
90
  }
91
+ function getFailingDependencies(expectedDependencies, output) {
92
+ if (!output?.dependencies)
93
+ return expectedDependencies;
94
+ return expectedDependencies.filter((dependency) => {
95
+ const entry = output.dependencies?.[dependency];
96
+ if (!entry)
97
+ return true;
98
+ return Boolean(entry.missing || entry.invalid);
99
+ });
100
+ }
101
+ async function checkDependenciesWithNpmLs(rootDir, expectedDependencies, packageManager) {
102
+ if (expectedDependencies.length === 0) {
103
+ return { ok: true, failingDependencies: [] };
104
+ }
105
+ // Use the detected package manager, defaulting to npm
106
+ // pnpm has compatible ls output format
107
+ const command = packageManager === 'pnpm' ? 'pnpm' : 'npm';
108
+ try {
109
+ const result = await execa(command, ['ls', '--depth=0', '--json', ...expectedDependencies], { cwd: rootDir, reject: false });
110
+ const output = parseNpmLsOutput(result.stdout);
111
+ const ok = result.exitCode === 0;
112
+ const failingDependencies = ok
113
+ ? []
114
+ : getFailingDependencies(expectedDependencies, output);
115
+ return {
116
+ ok,
117
+ failingDependencies: ok || failingDependencies.length > 0
118
+ ? failingDependencies
119
+ : expectedDependencies,
120
+ };
121
+ }
122
+ catch (error) {
123
+ console.warn(`⚠️ Failed to run npm ls in ${rootDir}:`, getErrorMessage(error));
124
+ return { ok: false, failingDependencies: expectedDependencies };
125
+ }
126
+ }
112
127
  function getExpectedDependencies(dependencies) {
113
128
  return Object.keys(dependencies ?? {}).sort();
114
129
  }
@@ -148,11 +163,7 @@ export async function getRootPackageInstallStatus(packageJsonPath) {
148
163
  const dependencies = getExpectedDependencies(packageJson.dependencies);
149
164
  const devDependencies = getExpectedDependencies(packageJson.devDependencies);
150
165
  const optionalDependencies = getExpectedDependencies(packageJson.optionalDependencies);
151
- const expectedDependencies = [
152
- ...dependencies,
153
- ...devDependencies,
154
- ...optionalDependencies,
155
- ];
166
+ const expectedDependencies = Array.from(new Set([...dependencies, ...devDependencies, ...optionalDependencies]));
156
167
  if (expectedDependencies.length === 0) {
157
168
  return {
158
169
  rootDir,
@@ -166,23 +177,12 @@ export async function getRootPackageInstallStatus(packageJsonPath) {
166
177
  reason: 'up-to-date',
167
178
  };
168
179
  }
169
- const installedPackages = await listInstalledPackages(path.join(rootDir, 'node_modules'));
170
- if (!installedPackages) {
171
- return {
172
- rootDir,
173
- packageJsonPath,
174
- packageManager,
175
- dependencyHash,
176
- dependenciesNeedInstall: true,
177
- missingDependencies: dependencies,
178
- missingDevDependencies: devDependencies,
179
- missingOptionalDependencies: optionalDependencies,
180
- reason: 'missing-node-modules',
181
- };
182
- }
183
- const missingDependencies = dependencies.filter((dep) => !installedPackages.has(dep));
184
- const missingDevDependencies = devDependencies.filter((dep) => !installedPackages.has(dep));
185
- const missingOptionalDependencies = optionalDependencies.filter((dep) => !installedPackages.has(dep));
180
+ const npmLsResult = await checkDependenciesWithNpmLs(rootDir, expectedDependencies, packageManager);
181
+ const failingDependencies = new Set(npmLsResult.failingDependencies);
182
+ const missingDependencies = dependencies.filter((dep) => failingDependencies.has(dep));
183
+ const missingDevDependencies = devDependencies.filter((dep) => failingDependencies.has(dep));
184
+ const missingOptionalDependencies = optionalDependencies.filter((dep) => failingDependencies.has(dep));
185
+ // Optional dependencies should not trigger install requirement
186
186
  const dependenciesNeedInstall = missingDependencies.length > 0 || missingDevDependencies.length > 0;
187
187
  return {
188
188
  rootDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-utils",
3
- "version": "6.74.3",
3
+ "version": "6.75.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },