@swell/cli 2.3.4 → 2.4.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/dist/commands/app/dev.js +6 -2
- package/dist/commands/app/frontend/dev.js +6 -3
- package/dist/commands/create/app.js +2 -2
- package/dist/commands/create/frontend.js +2 -2
- package/dist/commands/create/tests.js +5 -2
- package/dist/commands/theme/dev.js +10 -7
- package/dist/create-app-command.d.ts +8 -2
- package/dist/create-app-command.js +137 -79
- package/dist/lib/apps/index.d.ts +9 -0
- package/dist/lib/apps/index.js +30 -46
- package/dist/lib/create/tests.js +1 -1
- package/dist/lib/package-manager.d.ts +86 -0
- package/dist/lib/package-manager.js +186 -0
- package/dist/push-app-command.js +13 -6
- package/oclif.manifest.json +13 -5
- package/package.json +1 -1
package/dist/commands/app/dev.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as path from 'node:path';
|
|
|
8
8
|
import ora from 'ora';
|
|
9
9
|
import { ConfigType, allConfigFilesInDir, appConfigFromFile, } from '../../lib/apps/index.js';
|
|
10
10
|
import { bundleFunction } from '../../lib/bundle.js';
|
|
11
|
+
import { detectPackageManager, getExecSpawnArgs, } from '../../lib/package-manager.js';
|
|
11
12
|
import style from '../../lib/style.js';
|
|
12
13
|
import { PushAppCommand } from '../../push-app-command.js';
|
|
13
14
|
export default class AppDev extends PushAppCommand {
|
|
@@ -345,8 +346,11 @@ ENVIRONMENT = "development"
|
|
|
345
346
|
const configPath = path.join(this.tmpDir, `${functionName}.toml`);
|
|
346
347
|
await fs.promises.writeFile(configPath, wranglerConfig);
|
|
347
348
|
functionStatus.set(functionName, 'starting');
|
|
348
|
-
// Start wrangler process in background
|
|
349
|
-
const
|
|
349
|
+
// Start wrangler process in background using detected package manager
|
|
350
|
+
const pm = detectPackageManager(this.appPath);
|
|
351
|
+
const { program, prefixArgs } = getExecSpawnArgs(pm);
|
|
352
|
+
const wranglerProcess = spawn(program, [
|
|
353
|
+
...prefixArgs,
|
|
350
354
|
'wrangler',
|
|
351
355
|
'dev',
|
|
352
356
|
`--config=${configPath}`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { getProjectCommands, } from '../../../lib/apps/index.js';
|
|
2
3
|
import { default as localConfig } from '../../../lib/config.js';
|
|
3
4
|
import style from '../../../lib/style.js';
|
|
4
5
|
import { PushAppCommand } from '../../../push-app-command.js';
|
|
@@ -69,9 +70,11 @@ export default class AppFrontendDev extends PushAppCommand {
|
|
|
69
70
|
if (!projectType) {
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
73
|
+
// Get commands transformed for the detected package manager
|
|
74
|
+
const { devCommand } = getProjectCommands(this.appPath, projectType);
|
|
72
75
|
const currentStore = localConfig.getDefaultStore();
|
|
73
76
|
let serverReadyDetected = false;
|
|
74
|
-
await this.execFrontend(
|
|
77
|
+
await this.execFrontend(devCommand.replace('${PORT}', String(serverPort)), (string) => {
|
|
75
78
|
// Framework-specific output handling and ready detection
|
|
76
79
|
switch (projectType.slug) {
|
|
77
80
|
case 'nextjs': {
|
|
@@ -127,8 +130,8 @@ export default class AppFrontendDev extends PushAppCommand {
|
|
|
127
130
|
}
|
|
128
131
|
// 3. READY DETECTION: Detect server ready, show our message
|
|
129
132
|
if (!serverReadyDetected &&
|
|
130
|
-
(/application bundle generation complete
|
|
131
|
-
/watch mode enabled
|
|
133
|
+
(/application bundle generation complete/i.test(string) ||
|
|
134
|
+
/watch mode enabled/i.test(string))) {
|
|
132
135
|
serverReadyDetected = true;
|
|
133
136
|
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
|
|
134
137
|
const sessionId = localConfig.getSessionId(currentStore);
|
|
@@ -70,8 +70,8 @@ export default class CreateApp extends CreateAppCommand {
|
|
|
70
70
|
pkg: Flags.string({
|
|
71
71
|
char: 'p',
|
|
72
72
|
default: 'npm',
|
|
73
|
-
description: 'Package manager: npm | yarn | none',
|
|
74
|
-
options: ['npm', 'yarn', 'none'],
|
|
73
|
+
description: 'Package manager: npm | yarn | pnpm | bun | none',
|
|
74
|
+
options: ['npm', 'yarn', 'pnpm', 'bun', 'none'],
|
|
75
75
|
}),
|
|
76
76
|
type: Flags.string({
|
|
77
77
|
char: 't',
|
|
@@ -18,8 +18,8 @@ export default class CreateFrontend extends CreateAppCommand {
|
|
|
18
18
|
pkg: Flags.string({
|
|
19
19
|
char: 'p',
|
|
20
20
|
default: 'npm',
|
|
21
|
-
description: `
|
|
22
|
-
options: ['npm', 'yarn', 'none'],
|
|
21
|
+
description: `Package manager: npm | yarn | pnpm | bun | none`,
|
|
22
|
+
options: ['npm', 'yarn', 'pnpm', 'bun', 'none'],
|
|
23
23
|
}),
|
|
24
24
|
};
|
|
25
25
|
async run() {
|
|
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { AppCommand } from '../../app-command.js';
|
|
3
3
|
import { toAppId } from '../../lib/create/index.js';
|
|
4
4
|
import { createTestsScaffold, } from '../../lib/create/tests.js';
|
|
5
|
+
import { detectPackageManager, getPackageManagerCommands, } from '../../lib/package-manager.js';
|
|
5
6
|
export default class CreateTests extends AppCommand {
|
|
6
7
|
static description = 'Initialize a vitest + Cloudflare Workers test setup that reuses swell-cli authentication.';
|
|
7
8
|
static examples = [
|
|
@@ -66,10 +67,12 @@ export default class CreateTests extends AppCommand {
|
|
|
66
67
|
this.warn(warning);
|
|
67
68
|
}
|
|
68
69
|
if (hasChanges) {
|
|
70
|
+
const pm = detectPackageManager(this.appPath);
|
|
71
|
+
const commands = getPackageManagerCommands(pm);
|
|
69
72
|
this.log('');
|
|
70
73
|
this.log('Next steps:');
|
|
71
|
-
this.log(
|
|
72
|
-
this.log(
|
|
74
|
+
this.log(` 1. ${commands.install}`);
|
|
75
|
+
this.log(` 2. ${commands.run('test')}`);
|
|
73
76
|
}
|
|
74
77
|
this.log('');
|
|
75
78
|
}
|
|
@@ -4,6 +4,7 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { default as localConfig } from '../../lib/config.js';
|
|
7
|
+
import { findLockFile, getPackageManager } from '../../lib/package-manager.js';
|
|
7
8
|
import style from '../../lib/style.js';
|
|
8
9
|
import { ThemeSync } from '../../lib/theme-sync.js';
|
|
9
10
|
import { PushAppCommand } from '../../push-app-command.js';
|
|
@@ -74,11 +75,10 @@ export default class AppThemeDev extends PushAppCommand {
|
|
|
74
75
|
async findAndInstallDependencies() {
|
|
75
76
|
const spinner = ora('Locating package.json...').start();
|
|
76
77
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
spinner.succeed('package-lock.json found. Skipping npm install.');
|
|
78
|
+
// Check for any package manager lock file
|
|
79
|
+
const lockFile = findLockFile(process.cwd());
|
|
80
|
+
if (lockFile) {
|
|
81
|
+
spinner.succeed(`${lockFile.name} found. Skipping dependency install.`);
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
84
|
const packageJsonPath = await findUp('package.json', { type: 'file' });
|
|
@@ -88,7 +88,8 @@ export default class AppThemeDev extends PushAppCommand {
|
|
|
88
88
|
}
|
|
89
89
|
spinner.succeed(`Found package.json at ${packageJsonPath}`);
|
|
90
90
|
spinner.start('Installing dependencies...');
|
|
91
|
-
|
|
91
|
+
const { commands } = getPackageManager(process.cwd());
|
|
92
|
+
execSync(commands.install, { stdio: 'inherit' });
|
|
92
93
|
spinner.succeed('Dependencies installed');
|
|
93
94
|
return packageJsonPath;
|
|
94
95
|
}
|
|
@@ -109,7 +110,9 @@ export default class AppThemeDev extends PushAppCommand {
|
|
|
109
110
|
console.warn('"bundle" script not found in package.json');
|
|
110
111
|
return;
|
|
111
112
|
}
|
|
112
|
-
const
|
|
113
|
+
const { commands } = getPackageManager(process.cwd());
|
|
114
|
+
const runCommand = commands.run('bundle').split(' ');
|
|
115
|
+
const childProcess = spawn(runCommand[0], runCommand.slice(1), {
|
|
113
116
|
env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: 'true' },
|
|
114
117
|
shell: true,
|
|
115
118
|
stdio: 'pipe',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PackageManager } from './lib/package-manager.js';
|
|
1
2
|
import { SwellCommand } from './swell-command.js';
|
|
2
3
|
export declare abstract class CreateAppCommand extends SwellCommand {
|
|
3
4
|
protected commandExample: string;
|
|
@@ -13,6 +14,11 @@ export declare abstract class CreateAppCommand extends SwellCommand {
|
|
|
13
14
|
addAllowedHostsToAngular(configPath: string): Promise<void>;
|
|
14
15
|
addAllowedHostsToAstro(configPath: string): Promise<void>;
|
|
15
16
|
addAllowedHostsToNuxt(configPath: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Add vite allowedHosts configuration to a framework config file.
|
|
19
|
+
* Supports config files that use a defineX({}) pattern (Astro, Nuxt, etc.)
|
|
20
|
+
*/
|
|
21
|
+
private addViteAllowedHosts;
|
|
16
22
|
createAppConfigFolders(swellConfig: any): Promise<void>;
|
|
17
23
|
createFrontendApp(swellConfig: any, flags: any, directCreate?: boolean, kind?: string): Promise<boolean>;
|
|
18
24
|
createStorefrontApp(swellConfig: any, flags: any, directCreate?: boolean): Promise<boolean>;
|
|
@@ -29,7 +35,7 @@ export declare abstract class CreateAppCommand extends SwellCommand {
|
|
|
29
35
|
name: string;
|
|
30
36
|
slug: string;
|
|
31
37
|
};
|
|
32
|
-
setupPackage(name: string, config: any, pkg:
|
|
33
|
-
tryPackageSetup(name: string, config: any, pkg:
|
|
38
|
+
setupPackage(name: string, config: any, pkg: PackageManager): Promise<void>;
|
|
39
|
+
tryPackageSetup(name: string, config: any, pkg: PackageManager): Promise<void>;
|
|
34
40
|
private addFrontendAllowedHosts;
|
|
35
41
|
}
|
|
@@ -9,9 +9,39 @@ import { promisify } from 'node:util';
|
|
|
9
9
|
import ora from 'ora';
|
|
10
10
|
import Api from './lib/api.js';
|
|
11
11
|
import { FrontendProjectTypes, getFrontendProjectValidValues, getAllConfigPaths, getFrontendProjectSlugs, writeFile, writeJsonFile, } from './lib/apps/index.js';
|
|
12
|
+
import { getPackageManagerCommands, transformCreateCommand, } from './lib/package-manager.js';
|
|
12
13
|
import style from './lib/style.js';
|
|
13
14
|
import { SwellCommand } from './swell-command.js';
|
|
14
15
|
const execAsync = promisify(exec);
|
|
16
|
+
/** Allowed hosts for tunnel providers used in local development */
|
|
17
|
+
const TUNNEL_ALLOWED_HOSTS = ['.ngrok.app', '.loca.lt', '.trycloudflare.com'];
|
|
18
|
+
/**
|
|
19
|
+
* Find the closing brace position for a config function call (e.g., defineConfig, defineNuxtConfig).
|
|
20
|
+
* Uses brace counting to find the matching `}` for the opening `{`.
|
|
21
|
+
* @returns Position of the closing `}`, or -1 if not found
|
|
22
|
+
*/
|
|
23
|
+
function findConfigClosingBrace(content, functionName) {
|
|
24
|
+
const pattern = new RegExp(`${functionName}\\s*\\(\\s*\\{`);
|
|
25
|
+
const match = content.match(pattern);
|
|
26
|
+
if (!match || match.index === undefined) {
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
const startPos = match.index + match[0].length;
|
|
30
|
+
let depth = 1;
|
|
31
|
+
for (let i = startPos; i < content.length && depth > 0; i++) {
|
|
32
|
+
const char = content[i];
|
|
33
|
+
if (char === '{') {
|
|
34
|
+
depth++;
|
|
35
|
+
}
|
|
36
|
+
else if (char === '}') {
|
|
37
|
+
depth--;
|
|
38
|
+
if (depth === 0) {
|
|
39
|
+
return i;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
15
45
|
export class CreateAppCommand extends SwellCommand {
|
|
16
46
|
// Command name used in error message examples; override in subclasses
|
|
17
47
|
commandExample = 'swell create app';
|
|
@@ -76,51 +106,24 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
76
106
|
if (project.architect.serve.options.allowedHosts) {
|
|
77
107
|
return;
|
|
78
108
|
}
|
|
79
|
-
project.architect.serve.options.allowedHosts =
|
|
109
|
+
project.architect.serve.options.allowedHosts = TUNNEL_ALLOWED_HOSTS;
|
|
80
110
|
await fs.writeFile(angularJsonPath, JSON.stringify(config, null, 2), 'utf8');
|
|
81
111
|
}
|
|
82
112
|
async addAllowedHostsToAstro(configPath) {
|
|
83
|
-
|
|
84
|
-
let content;
|
|
85
|
-
try {
|
|
86
|
-
content = await fs.readFile(astroConfigPath, 'utf8');
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (content.includes('allowedHosts')) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const lines = content.split('\n');
|
|
95
|
-
let insertIndex = -1;
|
|
96
|
-
for (const [i, line] of lines.entries()) {
|
|
97
|
-
if (line.includes('defineConfig({')) {
|
|
98
|
-
insertIndex = i + 1;
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (insertIndex === -1) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// Detect indentation from next line or use default
|
|
106
|
-
const nextLine = lines[insertIndex];
|
|
107
|
-
const indentMatch = nextLine?.match(/^(\s+)/);
|
|
108
|
-
const baseIndent = indentMatch ? indentMatch[1] : ' ';
|
|
109
|
-
const viteConfig = [
|
|
110
|
-
`${baseIndent}vite: {`,
|
|
111
|
-
`${baseIndent} server: {`,
|
|
112
|
-
`${baseIndent} allowedHosts: ['.ngrok.app', '.loca.lt'],`,
|
|
113
|
-
`${baseIndent} },`,
|
|
114
|
-
`${baseIndent}},`,
|
|
115
|
-
];
|
|
116
|
-
lines.splice(insertIndex, 0, ...viteConfig);
|
|
117
|
-
await fs.writeFile(astroConfigPath, lines.join('\n'), 'utf8');
|
|
113
|
+
return this.addViteAllowedHosts(configPath, 'astro.config.mjs', 'defineConfig');
|
|
118
114
|
}
|
|
119
115
|
async addAllowedHostsToNuxt(configPath) {
|
|
120
|
-
|
|
116
|
+
return this.addViteAllowedHosts(configPath, 'nuxt.config.ts', 'defineNuxtConfig');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Add vite allowedHosts configuration to a framework config file.
|
|
120
|
+
* Supports config files that use a defineX({}) pattern (Astro, Nuxt, etc.)
|
|
121
|
+
*/
|
|
122
|
+
async addViteAllowedHosts(configPath, configFile, functionName) {
|
|
123
|
+
const configFilePath = path.join(configPath, 'frontend', configFile);
|
|
121
124
|
let content;
|
|
122
125
|
try {
|
|
123
|
-
content = await fs.readFile(
|
|
126
|
+
content = await fs.readFile(configFilePath, 'utf8');
|
|
124
127
|
}
|
|
125
128
|
catch {
|
|
126
129
|
return;
|
|
@@ -128,29 +131,30 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
128
131
|
if (content.includes('allowedHosts')) {
|
|
129
132
|
return;
|
|
130
133
|
}
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
const viteConfig = ` vite: {
|
|
135
|
+
server: {
|
|
136
|
+
allowedHosts: ${JSON.stringify(TUNNEL_ALLOWED_HOSTS)},
|
|
137
|
+
},
|
|
138
|
+
},`;
|
|
139
|
+
// Handle empty config: functionName({})
|
|
140
|
+
const emptyConfig = `${functionName}({})`;
|
|
141
|
+
if (content.includes(emptyConfig)) {
|
|
142
|
+
content = content.replace(emptyConfig, `${functionName}({\n${viteConfig}\n})`);
|
|
143
|
+
await fs.writeFile(configFilePath, content, 'utf8');
|
|
144
|
+
return;
|
|
138
145
|
}
|
|
139
|
-
|
|
146
|
+
// Find the closing brace of the config function using brace counting
|
|
147
|
+
const closingPos = findConfigClosingBrace(content, functionName);
|
|
148
|
+
if (closingPos === -1) {
|
|
140
149
|
return;
|
|
141
150
|
}
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
`${baseIndent} },`,
|
|
150
|
-
`${baseIndent}},`,
|
|
151
|
-
];
|
|
152
|
-
lines.splice(insertIndex, 0, ...viteConfig);
|
|
153
|
-
await fs.writeFile(nuxtConfigPath, lines.join('\n'), 'utf8');
|
|
151
|
+
const before = content.slice(0, closingPos);
|
|
152
|
+
const after = content.slice(closingPos);
|
|
153
|
+
// Ensure proper comma before vite config
|
|
154
|
+
const needsComma = before.trimEnd().slice(-1) !== ',';
|
|
155
|
+
const separator = needsComma ? ',\n' : '\n';
|
|
156
|
+
content = before.trimEnd() + separator + viteConfig + '\n' + after;
|
|
157
|
+
await fs.writeFile(configFilePath, content, 'utf8');
|
|
154
158
|
}
|
|
155
159
|
async createAppConfigFolders(swellConfig) {
|
|
156
160
|
for (const type of getAllConfigPaths(swellConfig.get('type'))) {
|
|
@@ -188,19 +192,63 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
188
192
|
}));
|
|
189
193
|
if (frameworkType && frameworkType !== 'none') {
|
|
190
194
|
const projectType = this.getProjectType(frameworkType);
|
|
195
|
+
// Determine package manager - 'none' is not valid for frontend scaffolding
|
|
196
|
+
let pkg = (flags.pkg || 'npm');
|
|
197
|
+
if (flags.pkg === 'none') {
|
|
198
|
+
this.log(`\n${style.dim('Note: --pkg none is not available for frontend scaffolding, using npm.')}`);
|
|
199
|
+
pkg = 'npm';
|
|
200
|
+
// Ensure root package.json exists (skipped when --pkg none)
|
|
201
|
+
const rootPkgPath = path.join(configPath, 'package.json');
|
|
202
|
+
try {
|
|
203
|
+
await fs.access(rootPkgPath);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Root package.json doesn't exist, create it
|
|
207
|
+
await this.setupPackage(swellConfig.get('id'), swellConfig, pkg);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
191
210
|
this.log();
|
|
211
|
+
// Check for placeholder frontend/package.json and remove it before C3 scaffolding
|
|
212
|
+
const frontendPath = path.join(configPath, 'frontend');
|
|
213
|
+
const frontendPkgPath = path.join(frontendPath, 'package.json');
|
|
214
|
+
try {
|
|
215
|
+
const content = await fs.readFile(frontendPkgPath, 'utf8');
|
|
216
|
+
const frontendPkg = JSON.parse(content);
|
|
217
|
+
// Only remove if it matches our placeholder signature
|
|
218
|
+
if (frontendPkg.version === '0.0.0' &&
|
|
219
|
+
frontendPkg.name === 'frontend' &&
|
|
220
|
+
frontendPkg.private === true) {
|
|
221
|
+
await fs.unlink(frontendPkgPath);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// No package.json or can't read, that's fine
|
|
226
|
+
}
|
|
227
|
+
// Check if frontend folder exists and has files (user-created content)
|
|
228
|
+
try {
|
|
229
|
+
const files = await fs.readdir(frontendPath);
|
|
230
|
+
if (files.length > 0) {
|
|
231
|
+
spinner.fail('frontend/ folder is not empty. Please remove existing files before scaffolding.');
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Folder doesn't exist, that's fine - C3 will create it
|
|
237
|
+
}
|
|
192
238
|
spinner.start(`Creating ${projectType?.name} frontend app (this may take a while)...`);
|
|
193
239
|
try {
|
|
194
240
|
await execAsync(`mkdir -p frontend`, {
|
|
195
241
|
cwd: configPath,
|
|
196
242
|
});
|
|
197
|
-
|
|
243
|
+
// Transform install command for the selected package manager
|
|
244
|
+
const installCommand = transformCreateCommand(projectType.installCommand, pkg);
|
|
245
|
+
await execAsync(installCommand, {
|
|
198
246
|
cwd: configPath,
|
|
199
247
|
});
|
|
200
248
|
// Use this command to debug output, i.e.e when command becomes non-responsive
|
|
201
249
|
/* await this.execWithStdio(
|
|
202
250
|
configPath,
|
|
203
|
-
|
|
251
|
+
installCommand,
|
|
204
252
|
); */
|
|
205
253
|
}
|
|
206
254
|
catch (error) {
|
|
@@ -210,25 +258,23 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
210
258
|
return false;
|
|
211
259
|
}
|
|
212
260
|
// Ensure frontend package.json has correct name for workspace
|
|
213
|
-
const frontendPkgPath = path.join(configPath, 'frontend', 'package.json');
|
|
214
261
|
try {
|
|
215
262
|
const pkgContent = await fs.readFile(frontendPkgPath, 'utf8');
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
await fs.writeFile(frontendPkgPath, JSON.stringify(
|
|
263
|
+
const frontendPkgJson = JSON.parse(pkgContent);
|
|
264
|
+
if (frontendPkgJson.name !== 'frontend') {
|
|
265
|
+
frontendPkgJson.name = 'frontend';
|
|
266
|
+
await fs.writeFile(frontendPkgPath, JSON.stringify(frontendPkgJson, null, 2), 'utf8');
|
|
220
267
|
}
|
|
221
268
|
}
|
|
222
269
|
catch {
|
|
223
270
|
// Ignore if package.json doesn't exist or can't be read
|
|
224
271
|
}
|
|
225
272
|
await this.addFrontendAllowedHosts(projectType, configPath);
|
|
226
|
-
//
|
|
227
|
-
// (removes frontend/package-lock.json, hoists dependencies, creates root lock file)
|
|
273
|
+
// Run install at root to hoist dependencies to workspace
|
|
228
274
|
spinner.start('Initializing workspace...');
|
|
229
275
|
try {
|
|
230
|
-
const
|
|
231
|
-
await execAsync(
|
|
276
|
+
const { install } = getPackageManagerCommands(pkg);
|
|
277
|
+
await execAsync(install, { cwd: configPath });
|
|
232
278
|
spinner.succeed('Workspace initialized');
|
|
233
279
|
}
|
|
234
280
|
catch {
|
|
@@ -375,19 +421,22 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
375
421
|
const configPath = path.dirname(config.path);
|
|
376
422
|
const packageJson = {
|
|
377
423
|
description: config.get('description'),
|
|
378
|
-
// Include workspace even if frontend doesn't exist yet
|
|
379
|
-
// npm tolerates missing workspace directories without errors
|
|
380
|
-
workspaces: ['frontend'],
|
|
381
424
|
devDependencies: {
|
|
382
425
|
'@swell/app-types': '^1.0.5',
|
|
383
426
|
typescript: '^5.9.3',
|
|
384
427
|
},
|
|
385
428
|
name,
|
|
429
|
+
// Required for yarn workspaces, good practice for all package managers
|
|
430
|
+
private: true,
|
|
386
431
|
scripts: {
|
|
387
432
|
typecheck: '([ -z "$(find functions -name \'*.ts\' 2>/dev/null | head -1)" ] || tsc --noEmit) && ([ ! -f test/tsconfig.json ] || tsc --noEmit -p test) && ([ ! -f frontend/tsconfig.json ] || tsc --noEmit -p frontend)',
|
|
388
433
|
},
|
|
389
434
|
version: config.get('version'),
|
|
390
435
|
};
|
|
436
|
+
// pnpm uses pnpm-workspace.yaml instead of workspaces field in package.json
|
|
437
|
+
if (pkg !== 'pnpm') {
|
|
438
|
+
packageJson.workspaces = ['frontend'];
|
|
439
|
+
}
|
|
391
440
|
const tsConfig = {
|
|
392
441
|
compilerOptions: {
|
|
393
442
|
lib: ['esnext', 'webworker'],
|
|
@@ -401,15 +450,24 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
401
450
|
await writeJsonFile(path.join(configPath, 'package.json'), packageJson);
|
|
402
451
|
await writeJsonFile(path.join(configPath, 'tsconfig.json'), tsConfig);
|
|
403
452
|
await writeFile(path.join(configPath, '.gitignore'), `node_modules`);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
await
|
|
407
|
-
|
|
453
|
+
// Create pnpm-workspace.yaml for pnpm (required for workspace support)
|
|
454
|
+
if (pkg === 'pnpm') {
|
|
455
|
+
await writeFile(path.join(configPath, 'pnpm-workspace.yaml'), 'packages:\n - frontend\n');
|
|
456
|
+
}
|
|
457
|
+
// Create placeholder frontend/package.json for bun and yarn
|
|
458
|
+
// These package managers require workspace directories to exist with a package.json
|
|
459
|
+
if (pkg === 'bun' || pkg === 'yarn') {
|
|
460
|
+
const frontendPath = path.join(configPath, 'frontend');
|
|
461
|
+
await fs.mkdir(frontendPath, { recursive: true });
|
|
462
|
+
await writeJsonFile(path.join(frontendPath, 'package.json'), {
|
|
463
|
+
name: 'frontend',
|
|
464
|
+
private: true,
|
|
465
|
+
version: '0.0.0',
|
|
408
466
|
});
|
|
409
467
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
468
|
+
// Install root dependencies using selected package manager
|
|
469
|
+
const { install } = getPackageManagerCommands(pkg);
|
|
470
|
+
await execAsync(install, { cwd: configPath });
|
|
413
471
|
}
|
|
414
472
|
async tryPackageSetup(name, config, pkg) {
|
|
415
473
|
try {
|
package/dist/lib/apps/index.d.ts
CHANGED
|
@@ -131,6 +131,15 @@ export declare function getFrontendProjectSlugs(withNone?: boolean, withLegacy?:
|
|
|
131
131
|
export declare function getFrontendProjectValidValues(withNone?: boolean, withLegacy?: boolean): string;
|
|
132
132
|
export declare function getAppSlugId(app: App): string | undefined;
|
|
133
133
|
export declare function getFrontendProjectType(appPath: string): FrontendProjectType | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* Get project commands transformed for the detected package manager.
|
|
136
|
+
* Detects the package manager from lock files in appPath and transforms
|
|
137
|
+
* npm-style commands to the equivalent for that package manager.
|
|
138
|
+
*/
|
|
139
|
+
export declare function getProjectCommands(appPath: string, projectType: FrontendProjectType): {
|
|
140
|
+
buildCommand?: string;
|
|
141
|
+
devCommand: string;
|
|
142
|
+
};
|
|
134
143
|
export declare function getConfigTypeFromPath(path: string): ConfigType | undefined;
|
|
135
144
|
export declare function getConfigTypeKeyFromValue(value: string): string | undefined;
|
|
136
145
|
export declare function filePathExists(filePath: string): boolean;
|
package/dist/lib/apps/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { pluralize, titleize } from 'inflection';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { toAppId } from '../create/index.js';
|
|
6
|
+
import { detectPackageManager, transformCommand } from '../package-manager.js';
|
|
6
7
|
import { AppConfig } from './app-config.js';
|
|
7
8
|
export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js';
|
|
8
9
|
export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, } from './paths.js';
|
|
@@ -113,54 +114,42 @@ export var ConfigInputFields;
|
|
|
113
114
|
ConfigInputFields["VALUES"] = "values";
|
|
114
115
|
ConfigInputFields["VERSION"] = "version";
|
|
115
116
|
})(ConfigInputFields || (ConfigInputFields = {}));
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
const LegacyFrontendProjectTypes = [
|
|
117
|
+
// Frontend project types - all use direct commands run from frontend/ directory
|
|
118
|
+
export const FrontendProjectTypes = [
|
|
119
119
|
{
|
|
120
120
|
buildCommand: 'npx astro build',
|
|
121
121
|
devCommand: 'npx astro dev --port ${PORT}',
|
|
122
|
-
mainPackage: 'astro',
|
|
123
|
-
name: 'Astro',
|
|
124
|
-
slug: 'astro',
|
|
125
|
-
},
|
|
126
|
-
];
|
|
127
|
-
// Modern apps (workspace-based) - All frameworks
|
|
128
|
-
export const FrontendProjectTypes = [
|
|
129
|
-
{
|
|
130
|
-
buildCommand: 'npm exec --workspace=frontend -- astro build',
|
|
131
|
-
devCommand: 'npm exec --workspace=frontend -- astro dev --port ${PORT}',
|
|
132
122
|
installCommand: 'npm create cloudflare@latest -- frontend --framework=astro --deploy=false --git=false -- --no-git --yes --skip-houston --typescript strict',
|
|
133
123
|
mainPackage: 'astro',
|
|
134
124
|
name: 'Astro',
|
|
135
125
|
slug: 'astro',
|
|
136
126
|
},
|
|
137
127
|
{
|
|
138
|
-
buildCommand: '
|
|
139
|
-
devCommand: '
|
|
128
|
+
buildCommand: 'npx ng build',
|
|
129
|
+
devCommand: 'npx ng serve --port ${PORT}',
|
|
140
130
|
installCommand: 'npm create cloudflare@latest -- frontend --framework=angular --deploy=false --git=false -- --style=sass --zoneless --ai-config=none',
|
|
141
131
|
mainPackage: '@angular/core',
|
|
142
132
|
name: 'Angular',
|
|
143
133
|
slug: 'angular',
|
|
144
134
|
},
|
|
145
135
|
{
|
|
146
|
-
devCommand: '
|
|
136
|
+
devCommand: 'npx wrangler dev --port ${PORT}',
|
|
147
137
|
installCommand: 'npm create cloudflare@latest -- frontend --framework=hono --deploy=false --git=false',
|
|
148
138
|
mainPackage: 'hono',
|
|
149
139
|
name: 'Hono',
|
|
150
140
|
slug: 'hono',
|
|
151
141
|
},
|
|
152
142
|
{
|
|
153
|
-
buildCommand: '
|
|
154
|
-
devCommand: '
|
|
143
|
+
buildCommand: 'npx nuxt build',
|
|
144
|
+
devCommand: 'npx nuxt dev --port ${PORT}',
|
|
155
145
|
installCommand: 'npm create cloudflare@latest -- frontend --framework=nuxt --deploy=false --git=false -- --no-modules -f',
|
|
156
146
|
mainPackage: 'nuxt',
|
|
157
147
|
name: 'Nuxt',
|
|
158
148
|
slug: 'nuxt',
|
|
159
149
|
},
|
|
160
150
|
{
|
|
161
|
-
buildCommand: '
|
|
162
|
-
|
|
163
|
-
devCommand: 'npm exec --workspace=frontend -- next dev --turbopack --port ${PORT}',
|
|
151
|
+
buildCommand: 'npx opennextjs-cloudflare build',
|
|
152
|
+
devCommand: 'npx next dev --turbopack --port ${PORT}',
|
|
164
153
|
installCommand: 'npm create cloudflare@latest -- frontend --framework=next --deploy=false --git=false -- --typescript --use-npm --src-dir --app --eslint --import-alias "@/*" --tailwind --turbopack',
|
|
165
154
|
mainPackage: 'next',
|
|
166
155
|
name: 'Next.js',
|
|
@@ -183,28 +172,8 @@ export function getFrontendProjectValidValues(withNone = true, withLegacy = true
|
|
|
183
172
|
export function getAppSlugId(app) {
|
|
184
173
|
return toAppId(app.private_id) || app.public_id || app.id;
|
|
185
174
|
}
|
|
186
|
-
function hasWorkspaceStructure(appPath) {
|
|
187
|
-
// Check if root package.json has workspaces field including 'frontend'
|
|
188
|
-
const rootPkgPath = path.join(appPath, 'package.json');
|
|
189
|
-
if (!filePathExists(rootPkgPath)) {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
try {
|
|
193
|
-
const content = fs.readFileSync(rootPkgPath, 'utf8');
|
|
194
|
-
const pkg = JSON.parse(content);
|
|
195
|
-
return Array.isArray(pkg.workspaces) && pkg.workspaces.includes('frontend');
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
175
|
export function getFrontendProjectType(appPath) {
|
|
202
|
-
//
|
|
203
|
-
const hasWorkspace = hasWorkspaceStructure(appPath);
|
|
204
|
-
const projectTypes = hasWorkspace
|
|
205
|
-
? FrontendProjectTypes
|
|
206
|
-
: LegacyFrontendProjectTypes;
|
|
207
|
-
// Try frontend/package.json first (new workspace structure), then root package.json (old structure)
|
|
176
|
+
// Try frontend/package.json first (workspace structure), then root package.json (legacy)
|
|
208
177
|
const pkgPaths = [
|
|
209
178
|
path.join(appPath, 'frontend', 'package.json'),
|
|
210
179
|
path.join(appPath, 'package.json'),
|
|
@@ -216,15 +185,16 @@ export function getFrontendProjectType(appPath) {
|
|
|
216
185
|
try {
|
|
217
186
|
const content = fs.readFileSync(pkgPath, 'utf8');
|
|
218
187
|
const pkg = JSON.parse(content);
|
|
219
|
-
for (const projectType of
|
|
188
|
+
for (const projectType of FrontendProjectTypes) {
|
|
220
189
|
if (pkg.dependencies?.[projectType.mainPackage] ||
|
|
221
190
|
pkg.devDependencies?.[projectType.mainPackage]) {
|
|
222
191
|
// Create a copy to avoid mutating the original
|
|
223
192
|
const detectedType = { ...projectType };
|
|
224
193
|
// If buildCommand not explicitly set in framework definition, detect it
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
194
|
+
// Check if package.json has a "build" script, if not set to empty string (no build needed)
|
|
195
|
+
detectedType.buildCommand ||= pkg.scripts?.build
|
|
196
|
+
? 'npm run build'
|
|
197
|
+
: '';
|
|
228
198
|
return detectedType;
|
|
229
199
|
}
|
|
230
200
|
}
|
|
@@ -236,6 +206,20 @@ export function getFrontendProjectType(appPath) {
|
|
|
236
206
|
}
|
|
237
207
|
return undefined;
|
|
238
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Get project commands transformed for the detected package manager.
|
|
211
|
+
* Detects the package manager from lock files in appPath and transforms
|
|
212
|
+
* npm-style commands to the equivalent for that package manager.
|
|
213
|
+
*/
|
|
214
|
+
export function getProjectCommands(appPath, projectType) {
|
|
215
|
+
const pm = detectPackageManager(appPath);
|
|
216
|
+
return {
|
|
217
|
+
buildCommand: projectType.buildCommand
|
|
218
|
+
? transformCommand(projectType.buildCommand, pm)
|
|
219
|
+
: undefined,
|
|
220
|
+
devCommand: transformCommand(projectType.devCommand, pm),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
239
223
|
export function getConfigTypeFromPath(path) {
|
|
240
224
|
for (const type in AllConfigPaths) {
|
|
241
225
|
if (AllConfigPaths[type] === path) {
|
package/dist/lib/create/tests.js
CHANGED
|
@@ -4,7 +4,7 @@ import { filePathExists, writeFile } from '../apps/index.js';
|
|
|
4
4
|
import { envDtsTemplate, integrationTestTemplate, mockRequestTemplate, setupGlobalsTemplate, swellClientTemplate, tsconfigTemplate, unitTestTemplate, vitestConfigTemplate, } from './tests/templates/index.js';
|
|
5
5
|
const TEST_DEV_DEPENDENCIES = {
|
|
6
6
|
'@cloudflare/vitest-pool-workers': 'latest',
|
|
7
|
-
vitest: '
|
|
7
|
+
vitest: '3.2.x',
|
|
8
8
|
'@swell/app-types': 'latest',
|
|
9
9
|
};
|
|
10
10
|
async function writeTextFile(filePath, contents, overwrite) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
|
|
2
|
+
export interface PackageManagerCommands {
|
|
3
|
+
/** Install command (e.g., 'npm install') */
|
|
4
|
+
install: string;
|
|
5
|
+
/** Run a script (e.g., 'npm run build' for script 'build') */
|
|
6
|
+
run: (script: string) => string;
|
|
7
|
+
/** Execute command in workspace (e.g., 'npm exec --workspace=frontend -- astro build') */
|
|
8
|
+
workspaceExec: (workspace: string, command: string) => string;
|
|
9
|
+
/** Run a script in a workspace (e.g., 'npm run build --workspace=frontend') */
|
|
10
|
+
workspaceRun: (workspace: string, script: string) => string;
|
|
11
|
+
/** Execute a binary (e.g., 'npx wrangler') */
|
|
12
|
+
exec: (command: string) => string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Find lock file in directory or parent directories
|
|
16
|
+
* @returns Lock file name and path, or null if not found
|
|
17
|
+
*/
|
|
18
|
+
export declare function findLockFile(directory: string): {
|
|
19
|
+
name: string;
|
|
20
|
+
packageManager: PackageManager;
|
|
21
|
+
path: string;
|
|
22
|
+
} | null;
|
|
23
|
+
/**
|
|
24
|
+
* Detect package manager from lock file in directory tree
|
|
25
|
+
* @returns Detected package manager, defaults to 'npm' if no lock file found
|
|
26
|
+
*/
|
|
27
|
+
export declare function detectPackageManager(directory: string): PackageManager;
|
|
28
|
+
/**
|
|
29
|
+
* Get command templates for a package manager
|
|
30
|
+
*/
|
|
31
|
+
export declare function getPackageManagerCommands(pm: PackageManager): PackageManagerCommands;
|
|
32
|
+
/**
|
|
33
|
+
* Detect package manager and get its commands
|
|
34
|
+
*/
|
|
35
|
+
export declare function getPackageManager(directory: string): {
|
|
36
|
+
commands: PackageManagerCommands;
|
|
37
|
+
lockFile: {
|
|
38
|
+
name: string;
|
|
39
|
+
path: string;
|
|
40
|
+
} | null;
|
|
41
|
+
name: PackageManager;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Transform an npm command to the equivalent for the detected package manager
|
|
45
|
+
* Handles common patterns:
|
|
46
|
+
* - 'npm run X' -> 'yarn run X' / 'pnpm run X' / 'bun run X'
|
|
47
|
+
* - 'npm run X --workspace=Y' -> 'yarn workspace Y run X' / 'pnpm --filter Y run X' / 'bun run --filter Y X'
|
|
48
|
+
* - 'npm exec --workspace=X -- Y' -> equivalent workspace command
|
|
49
|
+
* - 'npx X' -> 'yarn dlx X' / 'pnpm dlx X' / 'bunx X'
|
|
50
|
+
*/
|
|
51
|
+
export declare function transformCommand(command: string, pm: PackageManager): string;
|
|
52
|
+
/**
|
|
53
|
+
* Transform npm create commands to the equivalent for another package manager.
|
|
54
|
+
* Handles 'npm create X@version -- args' -> 'pm create X@version args'
|
|
55
|
+
* Also transforms --use-npm to --use-{pm} for CLIs that support it (e.g., Next.js)
|
|
56
|
+
* For yarn, removes @latest suffix since yarn classic (v1) may not handle it properly.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* transformCreateCommand('npm create cloudflare@latest -- frontend --framework=hono', 'bun')
|
|
60
|
+
* // Returns: 'bun create cloudflare@latest frontend --framework=hono'
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* transformCreateCommand('npm create cloudflare@latest -- frontend --framework=next --use-npm', 'pnpm')
|
|
64
|
+
* // Returns: 'pnpm create cloudflare@latest frontend --framework=next --use-pnpm'
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* transformCreateCommand('npm create cloudflare@latest -- frontend --framework=astro', 'yarn')
|
|
68
|
+
* // Returns: 'yarn create cloudflare -- frontend --framework=astro'
|
|
69
|
+
*/
|
|
70
|
+
export declare function transformCreateCommand(command: string, pm: PackageManager): string;
|
|
71
|
+
/**
|
|
72
|
+
* Get spawn arguments for executing a package binary.
|
|
73
|
+
* Returns the program and any prefix args needed before the command args.
|
|
74
|
+
* Useful for node's spawn() which needs program and args separately.
|
|
75
|
+
*
|
|
76
|
+
* Uses npx for most package managers (universally available via Node.js).
|
|
77
|
+
* Uses bunx for bun projects (performance benefit from Bun runtime).
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* const { program, prefixArgs } = getExecSpawnArgs('npm');
|
|
81
|
+
* spawn(program, [...prefixArgs, 'wrangler', 'dev', '--port=3000']);
|
|
82
|
+
*/
|
|
83
|
+
export declare function getExecSpawnArgs(pm: PackageManager): {
|
|
84
|
+
prefixArgs: string[];
|
|
85
|
+
program: string;
|
|
86
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/** Lock file to package manager mapping (priority order: bun > pnpm > yarn > npm) */
|
|
4
|
+
const LOCK_FILES = [
|
|
5
|
+
['bun.lockb', 'bun'],
|
|
6
|
+
['bun.lock', 'bun'],
|
|
7
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
8
|
+
['yarn.lock', 'yarn'],
|
|
9
|
+
['package-lock.json', 'npm'],
|
|
10
|
+
];
|
|
11
|
+
/** Command templates for each package manager */
|
|
12
|
+
const COMMANDS = {
|
|
13
|
+
npm: {
|
|
14
|
+
install: 'npm install',
|
|
15
|
+
run: (script) => `npm run ${script}`,
|
|
16
|
+
workspaceExec: (workspace, command) => `npm exec --workspace=${workspace} -- ${command}`,
|
|
17
|
+
workspaceRun: (workspace, script) => `npm run ${script} --workspace=${workspace}`,
|
|
18
|
+
exec: (command) => `npx ${command}`,
|
|
19
|
+
},
|
|
20
|
+
yarn: {
|
|
21
|
+
install: 'yarn install',
|
|
22
|
+
run: (script) => `yarn run ${script}`,
|
|
23
|
+
workspaceExec: (workspace, command) => `yarn workspace ${workspace} ${command}`,
|
|
24
|
+
workspaceRun: (workspace, script) => `yarn workspace ${workspace} run ${script}`,
|
|
25
|
+
exec: (command) => `yarn dlx ${command}`,
|
|
26
|
+
},
|
|
27
|
+
pnpm: {
|
|
28
|
+
install: 'pnpm install',
|
|
29
|
+
run: (script) => `pnpm run ${script}`,
|
|
30
|
+
workspaceExec: (workspace, command) => `pnpm --filter ${workspace} exec ${command}`,
|
|
31
|
+
workspaceRun: (workspace, script) => `pnpm --filter ${workspace} run ${script}`,
|
|
32
|
+
exec: (command) => `pnpm dlx ${command}`,
|
|
33
|
+
},
|
|
34
|
+
bun: {
|
|
35
|
+
install: 'bun install',
|
|
36
|
+
run: (script) => `bun run ${script}`,
|
|
37
|
+
// Use --cwd for executing binaries in a workspace (--filter is for scripts only)
|
|
38
|
+
workspaceExec: (workspace, command) => `bun --cwd ${workspace} x ${command}`,
|
|
39
|
+
workspaceRun: (workspace, script) => `bun run --filter ${workspace} ${script}`,
|
|
40
|
+
exec: (command) => `bunx ${command}`,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Find lock file in directory or parent directories
|
|
45
|
+
* @returns Lock file name and path, or null if not found
|
|
46
|
+
*/
|
|
47
|
+
export function findLockFile(directory) {
|
|
48
|
+
let currentDir = path.resolve(directory);
|
|
49
|
+
const { root } = path.parse(currentDir);
|
|
50
|
+
while (currentDir !== root) {
|
|
51
|
+
for (const [lockFile, pm] of LOCK_FILES) {
|
|
52
|
+
const lockPath = path.join(currentDir, lockFile);
|
|
53
|
+
if (fs.existsSync(lockPath)) {
|
|
54
|
+
return { name: lockFile, packageManager: pm, path: lockPath };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
currentDir = path.dirname(currentDir);
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Detect package manager from lock file in directory tree
|
|
63
|
+
* @returns Detected package manager, defaults to 'npm' if no lock file found
|
|
64
|
+
*/
|
|
65
|
+
export function detectPackageManager(directory) {
|
|
66
|
+
const lockFile = findLockFile(directory);
|
|
67
|
+
return lockFile?.packageManager ?? 'npm';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get command templates for a package manager
|
|
71
|
+
*/
|
|
72
|
+
export function getPackageManagerCommands(pm) {
|
|
73
|
+
return COMMANDS[pm];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Detect package manager and get its commands
|
|
77
|
+
*/
|
|
78
|
+
export function getPackageManager(directory) {
|
|
79
|
+
const lockFile = findLockFile(directory);
|
|
80
|
+
const name = lockFile?.packageManager ?? 'npm';
|
|
81
|
+
return {
|
|
82
|
+
commands: COMMANDS[name],
|
|
83
|
+
lockFile: lockFile ? { name: lockFile.name, path: lockFile.path } : null,
|
|
84
|
+
name,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Transform an npm command to the equivalent for the detected package manager
|
|
89
|
+
* Handles common patterns:
|
|
90
|
+
* - 'npm run X' -> 'yarn run X' / 'pnpm run X' / 'bun run X'
|
|
91
|
+
* - 'npm run X --workspace=Y' -> 'yarn workspace Y run X' / 'pnpm --filter Y run X' / 'bun run --filter Y X'
|
|
92
|
+
* - 'npm exec --workspace=X -- Y' -> equivalent workspace command
|
|
93
|
+
* - 'npx X' -> 'yarn dlx X' / 'pnpm dlx X' / 'bunx X'
|
|
94
|
+
*/
|
|
95
|
+
export function transformCommand(command, pm) {
|
|
96
|
+
if (pm === 'npm') {
|
|
97
|
+
return command;
|
|
98
|
+
}
|
|
99
|
+
const commands = COMMANDS[pm];
|
|
100
|
+
// Transform 'npm exec --workspace=X -- Y'
|
|
101
|
+
const workspaceMatch = command.match(/^npm exec --workspace=(\S+) -- (.+)$/);
|
|
102
|
+
if (workspaceMatch) {
|
|
103
|
+
return commands.workspaceExec(workspaceMatch[1], workspaceMatch[2]);
|
|
104
|
+
}
|
|
105
|
+
// Transform 'npm run X --workspace=Y'
|
|
106
|
+
const workspaceRunMatch = command.match(/^npm run (\S+) --workspace=(\S+)$/);
|
|
107
|
+
if (workspaceRunMatch) {
|
|
108
|
+
return commands.workspaceRun(workspaceRunMatch[2], workspaceRunMatch[1]);
|
|
109
|
+
}
|
|
110
|
+
// Transform 'npm run X' (with optional flags)
|
|
111
|
+
const runMatch = command.match(/^npm run (\S.*)$/);
|
|
112
|
+
if (runMatch) {
|
|
113
|
+
return commands.run(runMatch[1]);
|
|
114
|
+
}
|
|
115
|
+
// Transform 'npx X' to 'bunx X' for bun (performance benefit from Bun runtime)
|
|
116
|
+
// For other package managers, keep npx as-is (universally available via Node.js)
|
|
117
|
+
if (pm === 'bun') {
|
|
118
|
+
const npxMatch = command.match(/^npx (.+)$/);
|
|
119
|
+
if (npxMatch) {
|
|
120
|
+
return `bunx ${npxMatch[1]}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Warn if command looks like npm but wasn't transformed
|
|
124
|
+
if (command.startsWith('npm ')) {
|
|
125
|
+
console.warn(`Warning: Command "${command}" was not transformed for ${pm}.`);
|
|
126
|
+
}
|
|
127
|
+
return command;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Transform npm create commands to the equivalent for another package manager.
|
|
131
|
+
* Handles 'npm create X@version -- args' -> 'pm create X@version args'
|
|
132
|
+
* Also transforms --use-npm to --use-{pm} for CLIs that support it (e.g., Next.js)
|
|
133
|
+
* For yarn, removes @latest suffix since yarn classic (v1) may not handle it properly.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* transformCreateCommand('npm create cloudflare@latest -- frontend --framework=hono', 'bun')
|
|
137
|
+
* // Returns: 'bun create cloudflare@latest frontend --framework=hono'
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* transformCreateCommand('npm create cloudflare@latest -- frontend --framework=next --use-npm', 'pnpm')
|
|
141
|
+
* // Returns: 'pnpm create cloudflare@latest frontend --framework=next --use-pnpm'
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* transformCreateCommand('npm create cloudflare@latest -- frontend --framework=astro', 'yarn')
|
|
145
|
+
* // Returns: 'yarn create cloudflare -- frontend --framework=astro'
|
|
146
|
+
*/
|
|
147
|
+
export function transformCreateCommand(command, pm) {
|
|
148
|
+
if (pm === 'npm') {
|
|
149
|
+
return command;
|
|
150
|
+
}
|
|
151
|
+
// Match 'npm create X@version -- args' or 'npm create X -- args'
|
|
152
|
+
const createMatch = command.match(/^npm create (\S+) -- (.+)$/);
|
|
153
|
+
if (createMatch) {
|
|
154
|
+
let [, packageWithVersion, args] = createMatch;
|
|
155
|
+
// Replace --use-npm with --use-{pm} for Next.js and similar CLIs
|
|
156
|
+
const transformedArgs = args.replace(/--use-npm\b/, `--use-${pm}`);
|
|
157
|
+
// For yarn classic (v1), remove @latest since yarn create fetches latest by default
|
|
158
|
+
// and may not properly handle version specifiers. Also add -- separator for args.
|
|
159
|
+
if (pm === 'yarn') {
|
|
160
|
+
packageWithVersion = packageWithVersion.replace(/@latest$/, '');
|
|
161
|
+
return `yarn create ${packageWithVersion} -- ${transformedArgs}`;
|
|
162
|
+
}
|
|
163
|
+
return `${pm} create ${packageWithVersion} ${transformedArgs}`;
|
|
164
|
+
}
|
|
165
|
+
return command;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get spawn arguments for executing a package binary.
|
|
169
|
+
* Returns the program and any prefix args needed before the command args.
|
|
170
|
+
* Useful for node's spawn() which needs program and args separately.
|
|
171
|
+
*
|
|
172
|
+
* Uses npx for most package managers (universally available via Node.js).
|
|
173
|
+
* Uses bunx for bun projects (performance benefit from Bun runtime).
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* const { program, prefixArgs } = getExecSpawnArgs('npm');
|
|
177
|
+
* spawn(program, [...prefixArgs, 'wrangler', 'dev', '--port=3000']);
|
|
178
|
+
*/
|
|
179
|
+
export function getExecSpawnArgs(pm) {
|
|
180
|
+
// Use bunx for bun (performance benefit), npx for everything else
|
|
181
|
+
// npx is universally available via Node.js and avoids yarn dlx v1/v2 issues
|
|
182
|
+
if (pm === 'bun') {
|
|
183
|
+
return { prefixArgs: [], program: 'bunx' };
|
|
184
|
+
}
|
|
185
|
+
return { prefixArgs: [], program: 'npx' };
|
|
186
|
+
}
|
package/dist/push-app-command.js
CHANGED
|
@@ -6,10 +6,11 @@ import * as path from 'node:path';
|
|
|
6
6
|
import Stream from 'node:stream';
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import { default as swellConfig } from './lib/app-config.js';
|
|
9
|
-
import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
|
|
9
|
+
import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
|
|
10
10
|
import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
|
|
11
11
|
import { default as localConfig } from './lib/config.js';
|
|
12
12
|
import { toAppId } from './lib/create/index.js';
|
|
13
|
+
import { detectPackageManager, transformCommand, } from './lib/package-manager.js';
|
|
13
14
|
import { getProxyUrl } from './lib/proxy.js';
|
|
14
15
|
import { selectEnvironmentId } from './lib/stores.js';
|
|
15
16
|
import style from './lib/style.js';
|
|
@@ -109,12 +110,14 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
109
110
|
this.showFrontendMigrationError();
|
|
110
111
|
return; // unreachable but helps TypeScript narrow the type
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
+
// Get commands transformed for the detected package manager
|
|
114
|
+
const { buildCommand } = getProjectCommands(this.appPath, projectType);
|
|
115
|
+
if (!buildCommand) {
|
|
113
116
|
// No build command needed for this framework
|
|
114
117
|
return;
|
|
115
118
|
}
|
|
116
119
|
this.log(`Building ${projectType.name} frontend...\n`);
|
|
117
|
-
await this.execFrontend(
|
|
120
|
+
await this.execFrontend(buildCommand);
|
|
118
121
|
}
|
|
119
122
|
async chooseAppToPull(query) {
|
|
120
123
|
const typeLabelPlural = this.appType === 'theme' ? 'themes' : 'apps';
|
|
@@ -332,7 +335,8 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
332
335
|
}
|
|
333
336
|
}
|
|
334
337
|
async execFrontend(command, onOutput) {
|
|
335
|
-
|
|
338
|
+
// Run from frontend directory where wrangler.toml and framework configs live
|
|
339
|
+
return this.exec(command, this.frontendPath || path.join(this.appPath, 'frontend'), onOutput);
|
|
336
340
|
}
|
|
337
341
|
async getAllAppStorefronts(params) {
|
|
338
342
|
const query = Object.entries(params || {})
|
|
@@ -794,12 +798,15 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
794
798
|
let pagesProjectError = false;
|
|
795
799
|
let outputBuffer = '';
|
|
796
800
|
this.log(`\nDeploying to Cloudflare...\n`);
|
|
801
|
+
// Get the wrangler deploy command for the detected package manager
|
|
802
|
+
const pm = detectPackageManager(this.appPath);
|
|
803
|
+
const deployCommand = transformCommand('npx wrangler deploy', pm);
|
|
797
804
|
try {
|
|
798
|
-
await this.execFrontend(
|
|
805
|
+
await this.execFrontend(deployCommand, (string) => {
|
|
799
806
|
// Accumulate output for URL parsing after command completes
|
|
800
807
|
outputBuffer += string;
|
|
801
808
|
// Suppress workers.dev URL from output (users should use swell domain)
|
|
802
|
-
if (
|
|
809
|
+
if (/^\s*https:\/\/\S+\.workers\.dev\s*$/m.test(string)) {
|
|
803
810
|
return false;
|
|
804
811
|
}
|
|
805
812
|
// Check for Pages project error
|
package/oclif.manifest.json
CHANGED
|
@@ -812,7 +812,7 @@
|
|
|
812
812
|
},
|
|
813
813
|
"pkg": {
|
|
814
814
|
"char": "p",
|
|
815
|
-
"description": "Package manager: npm | yarn | none",
|
|
815
|
+
"description": "Package manager: npm | yarn | pnpm | bun | none",
|
|
816
816
|
"name": "pkg",
|
|
817
817
|
"default": "npm",
|
|
818
818
|
"hasDynamicHelp": false,
|
|
@@ -820,6 +820,8 @@
|
|
|
820
820
|
"options": [
|
|
821
821
|
"npm",
|
|
822
822
|
"yarn",
|
|
823
|
+
"pnpm",
|
|
824
|
+
"bun",
|
|
823
825
|
"none"
|
|
824
826
|
],
|
|
825
827
|
"type": "option"
|
|
@@ -1368,7 +1370,7 @@
|
|
|
1368
1370
|
},
|
|
1369
1371
|
"pkg": {
|
|
1370
1372
|
"char": "p",
|
|
1371
|
-
"description": "Package manager: npm | yarn | none",
|
|
1373
|
+
"description": "Package manager: npm | yarn | pnpm | bun | none",
|
|
1372
1374
|
"name": "pkg",
|
|
1373
1375
|
"default": "npm",
|
|
1374
1376
|
"hasDynamicHelp": false,
|
|
@@ -1376,6 +1378,8 @@
|
|
|
1376
1378
|
"options": [
|
|
1377
1379
|
"npm",
|
|
1378
1380
|
"yarn",
|
|
1381
|
+
"pnpm",
|
|
1382
|
+
"bun",
|
|
1379
1383
|
"none"
|
|
1380
1384
|
],
|
|
1381
1385
|
"type": "option"
|
|
@@ -1635,7 +1639,7 @@
|
|
|
1635
1639
|
},
|
|
1636
1640
|
"pkg": {
|
|
1637
1641
|
"char": "p",
|
|
1638
|
-
"description": "
|
|
1642
|
+
"description": "Package manager: npm | yarn | pnpm | bun | none",
|
|
1639
1643
|
"name": "pkg",
|
|
1640
1644
|
"default": "npm",
|
|
1641
1645
|
"hasDynamicHelp": false,
|
|
@@ -1643,6 +1647,8 @@
|
|
|
1643
1647
|
"options": [
|
|
1644
1648
|
"npm",
|
|
1645
1649
|
"yarn",
|
|
1650
|
+
"pnpm",
|
|
1651
|
+
"bun",
|
|
1646
1652
|
"none"
|
|
1647
1653
|
],
|
|
1648
1654
|
"type": "option"
|
|
@@ -2511,7 +2517,7 @@
|
|
|
2511
2517
|
},
|
|
2512
2518
|
"pkg": {
|
|
2513
2519
|
"char": "p",
|
|
2514
|
-
"description": "Package manager: npm | yarn | none",
|
|
2520
|
+
"description": "Package manager: npm | yarn | pnpm | bun | none",
|
|
2515
2521
|
"name": "pkg",
|
|
2516
2522
|
"default": "npm",
|
|
2517
2523
|
"hasDynamicHelp": false,
|
|
@@ -2519,6 +2525,8 @@
|
|
|
2519
2525
|
"options": [
|
|
2520
2526
|
"npm",
|
|
2521
2527
|
"yarn",
|
|
2528
|
+
"pnpm",
|
|
2529
|
+
"bun",
|
|
2522
2530
|
"none"
|
|
2523
2531
|
],
|
|
2524
2532
|
"type": "option"
|
|
@@ -2876,5 +2884,5 @@
|
|
|
2876
2884
|
]
|
|
2877
2885
|
}
|
|
2878
2886
|
},
|
|
2879
|
-
"version": "2.
|
|
2887
|
+
"version": "2.4.0"
|
|
2880
2888
|
}
|