mirror of
https://github.com/glitternetwork/pinme.git
synced 2026-08-28 17:42:38 +08:00
Merge pull request #42 from glitternetwork/isolate-npm-install-cache
This commit is contained in:
+241
-13
@@ -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
@@ -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.',
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pinme",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user