Merge pull request #42 from glitternetwork/isolate-npm-install-cache

This commit is contained in:
Rr
2026-04-05 19:42:36 +08:00
committed by GitHub
4 changed files with 359 additions and 21 deletions
+241 -13
View File
@@ -5,6 +5,7 @@ import inquirer from 'inquirer';
import axios from 'axios';
import { execSync } from 'child_process';
import { getAuthHeaders } from './utils/webLogin';
import { installProjectDependencies } from './utils/installProjectDependencies';
import {
createApiError,
createCommandError,
@@ -125,7 +126,6 @@ export default async function createCmd(options: CreateOptions): Promise<void> {
console.log(chalk.gray(` API Response: ${JSON.stringify(workerData)}`));
console.log(chalk.green(` API Domain: ${workerData.api_domain}`));
console.log(chalk.green(` Project Name: ${workerData.project_name}`));
console.log(chalk.green(` D1 UUID: ${workerData.uuid}`));
} catch (error: any) {
throw createApiError('project creation', error, [
`Project name: ${normalizedProjectName}`,
@@ -181,6 +181,28 @@ export default async function createCmd(options: CreateOptions): Promise<void> {
// Clean up zip file
fs.removeSync(zipPath);
// Remove any existing node_modules and package-lock.json to ensure clean install
// This fixes issues with rollup platform-specific dependencies
const nodeModulesPath = path.join(targetDir, 'node_modules');
const packageLockPath = path.join(targetDir, 'package-lock.json');
if (fs.existsSync(nodeModulesPath)) {
console.log(chalk.gray(' Removing existing node_modules...'));
fs.removeSync(nodeModulesPath);
}
if (fs.existsSync(packageLockPath)) {
console.log(chalk.gray(' Removing existing package-lock.json...'));
fs.removeSync(packageLockPath);
}
// Also clean frontend and backend subdirectories
const frontendNodeModules = path.join(targetDir, 'frontend', 'node_modules');
const backendNodeModules = path.join(targetDir, 'backend', 'node_modules');
const frontendPackageLock = path.join(targetDir, 'frontend', 'package-lock.json');
const backendPackageLock = path.join(targetDir, 'backend', 'package-lock.json');
if (fs.existsSync(frontendNodeModules)) fs.removeSync(frontendNodeModules);
if (fs.existsSync(backendNodeModules)) fs.removeSync(backendNodeModules);
if (fs.existsSync(frontendPackageLock)) fs.removeSync(frontendPackageLock);
if (fs.existsSync(backendPackageLock)) fs.removeSync(backendPackageLock);
console.log(chalk.green(` Template downloaded to: ${targetDir}`));
} catch (error: any) {
throw createCommandError('template extraction', `unzip -o "${zipPath}" -d "${PROJECT_DIR}"`, error, [
@@ -188,7 +210,7 @@ export default async function createCmd(options: CreateOptions): Promise<void> {
]);
}
// 3. Update pinme.toml with worker URL
// 3. Update pinme.toml with project_name
console.log(chalk.blue('\n3. Updating configuration...'));
const configPath = path.join(targetDir, 'pinme.toml');
const config = fs.readFileSync(configPath, 'utf-8');
@@ -202,7 +224,6 @@ export default async function createCmd(options: CreateOptions): Promise<void> {
fs.writeFileSync(configPath, updatedConfig);
console.log(chalk.green(` Updated pinme.toml`));
console.log(chalk.gray(` metadata: ${workerData.metadata}`));
console.log(chalk.gray(` VITE_API_URL: ${workerData.api_domain}`));
// 4. Save metadata to backend directory
const backendDir = path.join(targetDir, 'backend');
if (fs.existsSync(backendDir) && workerData.metadata) {
@@ -244,26 +265,203 @@ export default async function createCmd(options: CreateOptions): Promise<void> {
);
fs.writeFileSync(envPath, envContent);
console.log(chalk.green(` Created frontend/.env file`));
console.log(chalk.gray(` VITE_API_URL: ${workerData.api_domain}`));
}
// Update pinme.toml with api_url (append after project_name)
let pinmeConfig = fs.readFileSync(configPath, 'utf-8');
if (pinmeConfig.includes('api_url')) {
pinmeConfig = pinmeConfig.replace(
/api_url\s*=\s*"[^"]*"/,
`api_url = "${workerData.api_domain}"`
);
} else {
// Append api_url line after project_name
const lines = pinmeConfig.split('\n');
const newLines: string[] = [];
for (const line of lines) {
newLines.push(line);
if (line.trim().startsWith('project_name')) {
newLines.push(`api_url = "${workerData.api_domain}"`);
}
}
pinmeConfig = newLines.join('\n');
}
fs.writeFileSync(configPath, pinmeConfig);
console.log(chalk.green(` Updated pinme.toml with api_url`));
// 6. Install dependencies and build frontend
console.log(chalk.blue('\n4. Building frontend...'));
// 6. Install dependencies
console.log(chalk.blue('\n4. Installing dependencies...'));
// Install workspace dependencies from the project root.
// The template uses npm workspaces, so a single root install is enough.
try {
execSync('npm install', {
cwd: targetDir,
stdio: 'inherit',
});
installProjectDependencies(targetDir);
console.log(chalk.green(' Project dependencies installed'));
} catch (error: any) {
const errorMsg = error.message || '';
// Check for common permission errors
if (errorMsg.includes('EACCES') || errorMsg.includes('EPERM') || errorMsg.includes('permission denied')) {
throw createCommandError('project dependency install', 'npm install', error, [
'Permission error detected. Here are some solutions:',
'',
'Option 1: Fix npm permissions (Recommended)',
' mkdir -p ~/.npm-global',
' npm config set prefix ~/.npm-global',
' Then add ~/.npm-global/bin to your PATH in ~/.bashrc or ~/.zshrc',
'',
'Option 2: Use npx to avoid global installs',
' npx npm install',
'',
'Option 3: Check if you have write permissions',
' ls -la ' + targetDir,
'',
'For more help: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally',
]);
}
// Check for network errors
if (errorMsg.includes('ENOTFOUND') || errorMsg.includes('ETIMEDOUT') || errorMsg.includes('network')) {
throw createCommandError('project dependency install', 'npm install', error, [
'Network error detected. Please check:',
' 1. Internet connection is available',
' 2. npm registry is accessible (https://registry.npmjs.org)',
' 3. Try using a mirror: npm config set registry https://registry.npmmirror.com',
]);
}
// Generic error
throw createCommandError('project dependency install', 'npm install', error, [
'Dependency installation failed.',
'Check network connectivity and npm registry availability.',
'Inspect the generated workspace `package.json` files for dependency conflicts.',
'',
'If this is a permission issue, try:',
' sudo chown -R $(whoami) ~/.npm',
' sudo chown -R $(whoami) ' + targetDir + '/node_modules',
]);
}
// 7. Build and deploy backend worker
console.log(chalk.blue('\n5. Building backend worker...'));
try {
execSync('npm run build:worker', {
cwd: targetDir,
stdio: 'inherit',
});
console.log(chalk.green(' Worker built'));
} catch (error: any) {
throw createCommandError('worker build', 'npm run build:worker', error, [
'Fix the build error shown above, then rerun `pinme create`.',
]);
}
// 8. Get built worker files and SQL files
const distWorkerDir = path.join(targetDir, 'dist-worker');
const workerJsPath = path.join(distWorkerDir, 'worker.js');
if (!fs.existsSync(distWorkerDir) || !fs.existsSync(workerJsPath)) {
throw createConfigError('Built worker output not found: `dist-worker/worker.js`.', [
'Make sure `npm run build:worker` completed successfully.',
]);
}
// Get module files
const modulePaths: string[] = [];
const files = fs.readdirSync(distWorkerDir);
for (const file of files) {
if (file.endsWith('.js') && file !== 'worker.js') {
modulePaths.push(path.join(distWorkerDir, file));
}
}
// Get SQL files
const sqlDir = path.join(targetDir, 'db');
const sqlFiles: string[] = [];
if (fs.existsSync(sqlDir)) {
const sqlFileNames = fs.readdirSync(sqlDir)
.filter(f => f.endsWith('.sql'))
.sort();
for (const filename of sqlFileNames) {
sqlFiles.push(path.join(sqlDir, filename));
console.log(chalk.gray(` Including SQL: ${filename}`));
}
}
// 9. Save worker to platform
console.log(chalk.blue('\n6. Deploying backend worker...'));
const saveApiUrl = `${API_BASE}/save_worker?project_name=${encodeURIComponent(workerData.project_name)}`;
console.log(chalk.gray(` API URL: ${saveApiUrl}`));
try {
const FormData = (await import('formdata-node')).FormData;
const Blob = (await import('formdata-node')).Blob;
const formData = new FormData() as any;
// Add metadata
const metadataContent = typeof workerData.metadata === 'string'
? workerData.metadata
: JSON.stringify(workerData.metadata, null, 2);
formData.append('metadata', new Blob([metadataContent], {
type: 'application/json',
}), 'metadata.json');
// Add worker.js
const workerCode = fs.readFileSync(workerJsPath, 'utf-8');
formData.append('worker.js', new Blob([workerCode], {
type: 'application/javascript+module',
}), 'worker.js');
// Add other modules
for (const modulePath of modulePaths) {
const filename = path.basename(modulePath);
const content = fs.readFileSync(modulePath, 'utf-8');
formData.append(filename, new Blob([content], {
type: 'application/javascript+module',
}), filename);
}
// Add SQL files
for (const sqlFile of sqlFiles) {
const filename = path.basename(sqlFile);
const content = fs.readFileSync(sqlFile, 'utf-8');
formData.append('sql_file', new Blob([content], {
type: 'application/sql',
}), filename);
}
const response = await axios.put(saveApiUrl, formData, {
headers: { ...headers },
timeout: 120000,
});
if (response.data) {
console.log(chalk.green(' Worker deployed'));
if (response.data?.data?.sql_results) {
for (const result of response.data.data.sql_results) {
console.log(chalk.gray(` SQL ${result.filename}: ${result.status}`));
}
}
} else {
throw createApiError('worker save', { response: { data: response.data } }, [
`Project: ${workerData.project_name}`,
`Endpoint: ${saveApiUrl}`,
], [
'Verify the project exists and your account has permission to update it.',
]);
}
} catch (error: any) {
throw createApiError('worker save', error, [
`Project: ${workerData.project_name}`,
`Endpoint: ${saveApiUrl}`,
], [
'Check whether backend metadata, SQL files, or worker bundle contains invalid content.',
]);
}
// 10. Build and deploy frontend
console.log(chalk.blue('\n7. Building frontend...'));
const frontendDir = path.join(targetDir, 'frontend');
if (fs.existsSync(frontendDir)) {
// Build frontend
@@ -279,18 +477,48 @@ export default async function createCmd(options: CreateOptions): Promise<void> {
]);
}
// Upload to IPFS
// Upload to IPFS and capture the URL
console.log(chalk.blue(' Uploading to IPFS...'));
let frontendUrl = '';
try {
execSync('pinme upload ./dist', {
const uploadOutput = execSync('pinme upload ./dist', {
cwd: frontendDir,
stdio: 'inherit',
encoding: 'utf-8',
env: {
...process.env,
PINME_PROJECT_NAME: workerData.project_name,
},
});
console.log(chalk.green(' Frontend uploaded to IPFS'));
console.log(uploadOutput);
// Extract URL from output (format: https://xxx.pinme.dev)
const urlMatch = uploadOutput.match(/https:\/\/[\w-]+\.pinme\.dev/);
if (urlMatch) {
frontendUrl = urlMatch[0];
console.log(chalk.green(` Frontend uploaded to IPFS: ${frontendUrl}`));
// Update pinme.toml with frontend URL
const configPath = path.join(targetDir, 'pinme.toml');
let config = fs.readFileSync(configPath, 'utf-8');
// Add or update frontend_url
if (config.includes('frontend_url')) {
config = config.replace(
/frontend_url\s*=\s*"[^"]*"/,
`frontend_url = "${frontendUrl}"`
);
} else {
// Add frontend_url after project_name
config = config.replace(
/(project_name\s*=\s*"[^"]*"\n)/,
`$1frontend_url = "${frontendUrl}"\n`
);
}
fs.writeFileSync(configPath, config);
console.log(chalk.green(' Updated pinme.toml with frontend URL'));
} else {
console.log(chalk.green(' Frontend uploaded to IPFS'));
}
} catch (error: any) {
console.log(chalk.yellow(' Warning: IPFS upload failed, you can upload manually later'));
}
+72 -7
View File
@@ -4,6 +4,7 @@ import path from 'path';
import axios from 'axios';
import { execSync } from 'child_process';
import { getAuthHeaders } from './utils/webLogin';
import { installProjectDependencies } from './utils/installProjectDependencies';
import {
createApiError,
createCommandError,
@@ -73,15 +74,50 @@ function installDependencies() {
// The project template uses npm workspaces. Installing from the root
// keeps frontend/backend versions in sync and avoids redundant installs.
try {
execSync('npm install', {
cwd: PROJECT_DIR,
stdio: 'inherit',
});
installProjectDependencies(PROJECT_DIR);
console.log(chalk.green('Project dependencies installed'));
} catch (error: any) {
const errorMsg = error.message || '';
// Check for common permission errors
if (errorMsg.includes('EACCES') || errorMsg.includes('EPERM') || errorMsg.includes('permission denied')) {
throw createCommandError('project dependency install', 'npm install', error, [
'Permission error detected. Here are some solutions:',
'',
'Option 1: Fix npm permissions (Recommended)',
' mkdir -p ~/.npm-global',
' npm config set prefix ~/.npm-global',
' Then add ~/.npm-global/bin to your PATH in ~/.bashrc or ~/.zshrc',
'',
'Option 2: Use npx to avoid global installs',
' npx npm install',
'',
'Option 3: Check if you have write permissions',
' ls -la ' + PROJECT_DIR,
'',
'For more help: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally',
]);
}
// Check for network errors
if (errorMsg.includes('ENOTFOUND') || errorMsg.includes('ETIMEDOUT') || errorMsg.includes('network')) {
throw createCommandError('project dependency install', 'npm install', error, [
'Network error detected. Please check:',
' 1. Internet connection is available',
' 2. npm registry is accessible (https://registry.npmjs.org)',
' 3. Try using a mirror: npm config set registry https://registry.npmmirror.com',
]);
}
// Generic error
throw createCommandError('project dependency install', 'npm install', error, [
'Dependency installation failed.',
'Check network connectivity and npm registry availability.',
'If `package-lock.json` is stale or conflicted, resolve that before retrying.',
'',
'If this is a permission issue, try:',
' sudo chown -R $(whoami) ~/.npm',
' sudo chown -R $(whoami) ' + PROJECT_DIR + '/node_modules',
]);
}
}
@@ -219,15 +255,44 @@ function buildFrontend() {
function deployFrontend(projectName: string) {
console.log(chalk.blue('Deploying frontend to IPFS...'));
try {
execSync('pinme upload ./frontend/dist', {
const uploadOutput = execSync('pinme upload ./frontend/dist', {
cwd: PROJECT_DIR,
stdio: 'inherit',
encoding: 'utf-8',
env: {
...process.env,
PINME_PROJECT_NAME: projectName,
},
});
console.log(chalk.green('Frontend deployed to IPFS'));
console.log(uploadOutput);
// Extract URL from output (format: https://xxx.pinme.dev)
const urlMatch = uploadOutput.match(/https:\/\/[\w-]+\.pinme\.dev/);
if (urlMatch) {
const frontendUrl = urlMatch[0];
console.log(chalk.green(`Frontend deployed to IPFS: ${frontendUrl}`));
// Update pinme.toml with frontend URL
const configPath = path.join(PROJECT_DIR, 'pinme.toml');
let config = fs.readFileSync(configPath, 'utf-8');
// Add or update frontend_url
if (config.includes('frontend_url')) {
config = config.replace(
/frontend_url\s*=\s*"[^"]*"/,
`frontend_url = "${frontendUrl}"`
);
} else {
// Add frontend_url after project_name
config = config.replace(
/(project_name\s*=\s*"[^"]*"\n)/,
`$1frontend_url = "${frontendUrl}"\n`
);
}
fs.writeFileSync(configPath, config);
console.log(chalk.green('Updated pinme.toml with frontend URL'));
} else {
console.log(chalk.green('Frontend deployed to IPFS'));
}
} catch (error: any) {
throw createCommandError('frontend deploy', 'pinme upload ./frontend/dist', error, [
'Make sure `frontend/dist` exists and `pinme upload` works in this environment.',
+45
View File
@@ -0,0 +1,45 @@
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
import chalk from 'chalk';
import { execFileSync } from 'child_process';
function makeTempCacheDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'pinme-npm-cache-'));
}
function runInstall(cwd: string, cacheDir: string): void {
execFileSync('npm', ['install', '--cache', cacheDir, '--no-audit', '--no-fund'], {
cwd,
stdio: 'inherit',
env: {
...process.env,
npm_config_cache: cacheDir,
npm_config_audit: 'false',
npm_config_fund: 'false',
},
});
}
export function installProjectDependencies(cwd: string): void {
let lastError: unknown;
for (let attempt = 1; attempt <= 2; attempt += 1) {
const cacheDir = makeTempCacheDir();
try {
if (attempt > 1) {
console.log(chalk.yellow(' Retrying dependency install with a fresh npm cache...'));
}
runInstall(cwd, cacheDir);
return;
} catch (error) {
lastError = error;
} finally {
fs.removeSync(cacheDir);
}
}
throw lastError;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pinme",
"version": "2.0.0",
"version": "2.0.1",
"publishConfig": {
"access": "public"
},