Skip to content

Commit 2eaf2cc

Browse files
committed
fix: rework config validator to use pnpm
1 parent a86db8f commit 2eaf2cc

File tree

8 files changed

+6760
-5238
lines changed

8 files changed

+6760
-5238
lines changed

adminforth/modules/codeInjector.ts

Lines changed: 60 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import fsExtra from 'fs-extra';
55
import os from 'os';
66
import path from 'path';
77
import { promisify } from 'util';
8+
import yaml from 'yaml';
89
import AdminForth, { AdminForthConfigMenuItem } from '../index.js';
910
import { ADMIN_FORTH_ABSOLUTE_PATH, getComponentNameFromPath, transformObject, deepMerge, md5hash, slugifyString } from './utils.js';
1011
import { ICodeInjector } from '../types/Back.js';
@@ -142,54 +143,37 @@ class CodeInjector implements ICodeInjector {
142143

143144
}
144145

145-
async runNpmShell({command, cwd, envOverrides = {}}: {
146+
async runPnpmShell({command, cwd, envOverrides = {}}: {
146147
command: string,
147148
cwd: string,
148149
envOverrides?: { [key: string]: string }
149150
}) {
150-
const nodeBinary = process.execPath; // Path to the Node.js binary running this script
151-
// On Windows, npm is npm.cmd, on Unix systems it's npm
152-
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
153-
const npmPath = path.join(path.dirname(nodeBinary), npmExecutable); // Path to the npm executable
151+
const pnpmExecutable = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
154152
const env = {
155153
VITE_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl,
156154
FORCE_COLOR: '1',
157155
...process.env,
158156
...envOverrides,
159157
};
160158

161-
afLogger.trace(`⚙️ exec: npm ${command}`);
162-
afLogger.trace(`🪲 npm ${command} cwd: ${cwd}`);
163-
afLogger.trace(`npm ${command} done in`);
164-
165-
// On Windows, execute npm.cmd directly; on Unix, use node + npm
166-
let execCommand: string;
167-
if (process.platform === 'win32') {
168-
// Quote path if it contains spaces
169-
const quotedNpmPath = npmPath.includes(' ') ? `"${npmPath}"` : npmPath;
170-
execCommand = `${quotedNpmPath} ${command}`;
171-
} else {
172-
// Quote paths that contain spaces (for Unix systems)
173-
const quotedNodeBinary = nodeBinary.includes(' ') ? `"${nodeBinary}"` : nodeBinary;
174-
const quotedNpmPath = npmPath.includes(' ') ? `"${npmPath}"` : npmPath;
175-
execCommand = `${quotedNodeBinary} ${quotedNpmPath} ${command}`;
176-
}
177-
159+
afLogger.trace(`⚙️ exec: pnpm ${command}`);
160+
afLogger.trace(`🪲 pnpm ${command} cwd: ${cwd}`);
161+
162+
const execCommand = `${pnpmExecutable} ${command}`;
178163
const execOptions: any = {
179164
cwd,
180165
env,
181166
};
182-
183-
// On Windows, use shell to execute .cmd files
167+
184168
if (process.platform === 'win32') {
185169
execOptions.shell = true;
186170
}
187-
188-
const { stdout: out, stderr: err } = await execAsync(execCommand, execOptions);
189-
afLogger.trace(`npm ${command} done in`);
171+
172+
const { stderr: err } = await execAsync(execCommand, execOptions);
173+
afLogger.trace(`pnpm ${command} done in`);
190174

191175
if (err) {
192-
afLogger.trace(`🪲npm ${command} errors/warnings: ${err}`);
176+
afLogger.trace(`🪲pnpm ${command} errors/warnings: ${err}`);
193177
}
194178
}
195179

@@ -207,7 +191,7 @@ class CodeInjector implements ICodeInjector {
207191
return path.join(this.getSpaDir(), 'dist');
208192
}
209193

210-
async packagesFromNpm(dir: string): Promise<[string, string[]]> {
194+
async packagesFromPnpm(dir: string): Promise<[string, string[]]> {
211195
const usersPackagePath = path.join(dir, 'package.json');
212196
let packageContent: { dependencies: any, devDependencies: any } = null;
213197
let lockHash: string = '';
@@ -218,28 +202,43 @@ class CodeInjector implements ICodeInjector {
218202
// user package.json does not exist, user does not have custom components
219203
}
220204
if (packageContent) {
221-
const lockPath = path.join(dir, 'package-lock.json');
222-
let lock = null;
205+
const lockPath = path.join(dir, 'pnpm-lock.yaml');
206+
let lock: any = null;
223207
try {
224-
lock = JSON.parse(await fs.promises.readFile(lockPath, 'utf-8'));
208+
lock = yaml.parse(await fs.promises.readFile(lockPath, 'utf-8'));
225209
} catch (e) {
226-
throw new Error(`Custom package-lock.json does not exist in ${dir}, but package.json does.
227-
We can't determine version of packages without package-lock.json. Please run npm install in ${dir}`);
210+
throw new Error(`Custom pnpm-lock.yaml does not exist in ${dir}, but package.json does.
211+
We can't determine version of packages without pnpm-lock.yaml. Please run pnpm install in ${dir}`);
228212
}
229213
lockHash = hashify(lock);
214+
const importer = lock?.importers?.['.'];
215+
if (!importer) {
216+
throw new Error(`pnpm-lock.yaml in ${dir} does not contain importer ".". Please run pnpm install in ${dir}`);
217+
}
218+
219+
const importerDeps = {
220+
...(importer.dependencies || {}),
221+
...(importer.devDependencies || {}),
222+
...(importer.optionalDependencies || {}),
223+
};
230224

231225
packages = [
232-
...Object.keys(packageContent.dependencies || []),
233-
...Object.keys(packageContent.devDependencies || [])
226+
...Object.keys(packageContent.dependencies || {}),
227+
...Object.keys(packageContent.devDependencies || {})
234228
].reduce(
235229
(acc, packageName) => {
236-
const pack = lock.packages[`node_modules/${packageName}`];
237-
if (!pack) {
238-
throw new Error(`Package ${packageName} is not in package-lock.json but is in package.json. Please run 'npm install' in ${dir}`);
230+
const depInfo = importerDeps[packageName];
231+
const raw = typeof depInfo === 'string'
232+
? depInfo
233+
: (depInfo?.specifier || depInfo?.version);
234+
235+
if (!raw) {
236+
throw new Error(`Package ${packageName} is not in pnpm-lock.yaml but is in package.json. Please run 'pnpm install' in ${dir}`);
239237
}
240-
const version = pack.version;
241238

242-
acc.push(`${packageName}@${version}`);
239+
const cleaned = raw.includes('(') ? raw.split('(')[0] : raw;
240+
241+
acc.push(`${packageName}@${cleaned}`);
243242
return acc;
244243
}, []
245244
);
@@ -653,17 +652,17 @@ class CodeInjector implements ICodeInjector {
653652

654653

655654
/* hash checking */
656-
const spaPackageLockPath = path.join(this.spaTmpPath(), 'package-lock.json');
657-
const spaPackageLock = JSON.parse(await fs.promises.readFile(spaPackageLockPath, 'utf-8'));
658-
const spaLockHash = hashify(spaPackageLock);
655+
const spaPnpmLockPath = path.join(this.spaTmpPath(), 'pnpm-lock.yaml');
656+
const spaPnpmLock = yaml.parse(await fs.promises.readFile(spaPnpmLockPath, 'utf-8'));
657+
const spaLockHash = hashify(spaPnpmLock);
659658

660659
/* customPackageLock */
661660
let usersLockHash: string = '';
662661
let usersPackages: string[] = [];
663662

664663

665664
if (this.adminforth.config.customization?.customComponentsDir) {
666-
[usersLockHash, usersPackages] = await this.packagesFromNpm(this.adminforth.config.customization.customComponentsDir);
665+
[usersLockHash, usersPackages] = await this.packagesFromPnpm(this.adminforth.config.customization.customComponentsDir);
667666
}
668667

669668
const pluginPackages: {
@@ -675,7 +674,7 @@ class CodeInjector implements ICodeInjector {
675674
// for every installed plugin generate packages
676675
for (const plugin of this.adminforth.activatedPlugins) {
677676
afLogger.trace(`🔧 Checking packages for plugin, ${plugin.constructor.name}, ${plugin.customFolderPath}`);
678-
const [lockHash, packages] = await this.packagesFromNpm(plugin.customFolderPath);
677+
const [lockHash, packages] = await this.packagesFromPnpm(plugin.customFolderPath);
679678
if (packages.length) {
680679
pluginPackages.push({
681680
pluginName: plugin.constructor.name,
@@ -696,18 +695,18 @@ class CodeInjector implements ICodeInjector {
696695
const existingHash = await fs.promises.readFile(hashPath, 'utf-8');
697696
await this.checkIconNames(icons);
698697
if (existingHash === fullHash) {
699-
afLogger.trace(`🪲Hashes match, skipping npm ci/install, from file: ${existingHash}, actual: ${fullHash}`);
698+
afLogger.trace(`🪲Hashes match, skipping pnpm install, from file: ${existingHash}, actual: ${fullHash}`);
700699
return;
701700
} else {
702-
afLogger.trace(`🪲 Hashes do not match: from file: ${existingHash} actual: ${fullHash}, proceeding with npm ci/install`);
701+
afLogger.trace(`🪲 Hashes do not match: from file: ${existingHash} actual: ${fullHash}, proceeding with pnpm install`);
703702
}
704703
} catch (e) {
705704
// ignore
706-
afLogger.trace(`🪲Hash file does not exist, proceeding with npm ci/install, ${e}`);
705+
afLogger.trace(`🪲Hash file does not exist, proceeding with pnpm install, ${e}`);
707706
}
708707

709-
await this.runNpmShell({command: 'ci', cwd: this.spaTmpPath(), envOverrides: {
710-
NODE_ENV: 'development' // othewrwise it will not install devDependencies which we still need, e.g for extract
708+
await this.runPnpmShell({command: 'install --frozen-lockfile', cwd: this.spaTmpPath(), envOverrides: {
709+
NODE_ENV: 'development' // otherwise it will not install devDependencies which we still need, e.g for extract
711710
}});
712711

713712
const allPacks = [
@@ -726,11 +725,11 @@ class CodeInjector implements ICodeInjector {
726725
const allPacksUnique = Array.from(new Set(allPacksFiltered));
727726

728727
if (allPacks.length) {
729-
const npmInstallCommand = `install ${allPacksUnique.join(' ')}`;
730-
await this.runNpmShell({
731-
command: npmInstallCommand, cwd: this.spaTmpPath(),
728+
const pnpmInstallCommand = `install ${allPacksUnique.join(' ')}`;
729+
await this.runPnpmShell({
730+
command: pnpmInstallCommand, cwd: this.spaTmpPath(),
732731
envOverrides: {
733-
NODE_ENV: 'development' // othewrwise it will not install devDependencies which we still need, e.g for extract
732+
NODE_ENV: 'development' // otherwise it will not install devDependencies which we still need, e.g for extract
734733
}
735734
});
736735
}
@@ -938,7 +937,7 @@ class CodeInjector implements ICodeInjector {
938937
}
939938

940939
if (!skipExtract) {
941-
await this.runNpmShell({command: 'run i18n:extract', cwd});
940+
await this.runPnpmShell({command: 'run i18n:extract', cwd});
942941

943942
// create serveDir if not exists
944943
await fs.promises.mkdir(serveDir, { recursive: true });
@@ -956,7 +955,7 @@ class CodeInjector implements ICodeInjector {
956955
if (!skipBuild) {
957956

958957
// TODO probably add option to build with tsh check (plain 'build')
959-
await this.runNpmShell({command: 'run build-only', cwd});
958+
await this.runPnpmShell({command: 'run build-only', cwd});
960959

961960
// coy dist to serveDir
962961
await fsExtra.copy(path.join(cwd, 'dist'), serveDir, { recursive: true });
@@ -969,7 +968,7 @@ class CodeInjector implements ICodeInjector {
969968
} else {
970969

971970
const command = 'run dev';
972-
afLogger.info(`⚙️ spawn: npm ${command}...`);
971+
afLogger.info(`⚙️ spawn: pnpm ${command}...`);
973972
if (process.env.VITE_ADMINFORTH_PUBLIC_PATH) {
974973
afLogger.info(`⚠️ Your VITE_ADMINFORTH_PUBLIC_PATH: ${process.env.VITE_ADMINFORTH_PUBLIC_PATH} has no effect`);
975974
}
@@ -979,14 +978,12 @@ class CodeInjector implements ICodeInjector {
979978
...process.env,
980979
};
981980

982-
const nodeBinary = process.execPath;
983-
const npmPath = path.join(path.dirname(nodeBinary), 'npm');
984-
981+
const pnpmExecutable = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
985982
let devServer;
986983
if (process.platform === 'win32') {
987-
devServer = spawn('npm', command.split(' '), { cwd, env, shell: true });
984+
devServer = spawn(pnpmExecutable, command.split(' '), { cwd, env, shell: true });
988985
} else {
989-
devServer = spawn(`${nodeBinary}`, [`${npmPath}`, ...command.split(' ')], { cwd, env });
986+
devServer = spawn(pnpmExecutable, command.split(' '), { cwd, env });
990987
}
991988
devServer.stdout.on('data', (data) => {
992989
if (data.includes('➜')) {

adminforth/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
"rollout-doc": "cd documentation && pnpm build && pnpm deploy",
2121
"docs": "typedoc",
2222
"--comment_postinstall": "postinstall executed after package installed in other project package and when we do pnpm i in the package",
23-
"postinstall": "node -e \"const fs=require('fs');const path=require('path');const spaPath=path.join(__dirname,'dist','spa');if(fs.existsSync(spaPath)){process.chdir(spaPath);require('child_process').execSync('npm ci',{stdio:'inherit'});console.log('installed spa dependencies');}\""
24-
},
23+
"postinstall": "node -e \"const fs=require('fs');const path=require('path');const spaPath=path.join(__dirname,'dist','spa');if(fs.existsSync(spaPath) && !process.env.PNPM_INSTALL_SPA){process.env.PNPM_INSTALL_SPA=1;require('child_process').execSync('pnpm install --frozen-lockfile',{cwd:spaPath,stdio:'inherit'});console.log('installed spa dependencies');}\""
24+
},
2525
"release": {
2626
"plugins": [
2727
"@semantic-release/commit-analyzer",
@@ -88,7 +88,8 @@
8888
"private-ip": "^3.0.2",
8989
"rate-limiter-flexible": "^8.1.0",
9090
"recast": "^0.23.11",
91-
"ws": "^8.18.0"
91+
"ws": "^8.18.0",
92+
"yaml": "^2.8.2"
9293
},
9394
"devDependencies": {
9495
"@semantic-release/exec": "^7.1.0",

0 commit comments

Comments
 (0)