@swell/cli 2.9.18 → 2.9.20
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/README.md +90 -0
- package/dist/commands/app/dev.js +21 -34
- package/dist/commands/app/frontend/deploy.js +3 -3
- package/dist/commands/app/frontend/dev.js +5 -2
- package/dist/commands/app/init.js +12 -2
- package/dist/commands/create/app.d.ts +10 -1
- package/dist/commands/create/app.js +48 -25
- package/dist/commands/create/frontend.d.ts +3 -1
- package/dist/commands/create/frontend.js +21 -13
- package/dist/create-app-command.d.ts +10 -51
- package/dist/create-app-command.js +99 -526
- package/dist/help/custom-help.js +28 -7
- package/dist/help/types.d.ts +2 -0
- package/dist/lib/app-config.js +6 -0
- package/dist/lib/apps/frontend-package.d.ts +36 -0
- package/dist/lib/apps/frontend-package.js +375 -0
- package/dist/lib/apps/frontend-routes.d.ts +1 -0
- package/dist/lib/apps/frontend-routes.js +40 -0
- package/dist/lib/apps/frontend-scaffold.d.ts +35 -0
- package/dist/lib/apps/frontend-scaffold.js +502 -0
- package/dist/lib/apps/frontend-templates.d.ts +32 -0
- package/dist/lib/apps/frontend-templates.js +137 -0
- package/dist/lib/apps/index.d.ts +4 -6
- package/dist/lib/apps/index.js +10 -33
- package/dist/lib/apps/storefront-resources.d.ts +3 -2
- package/dist/lib/apps/storefront-resources.js +3 -2
- package/dist/lib/create/tests/templates/mock-request.js +1 -1
- package/dist/lib/create/tests.js +2 -1
- package/dist/push-app-command.js +22 -0
- package/dist/remote-app-command.js +6 -0
- package/dist/types/index.d.ts +5 -0
- package/oclif.manifest.json +63 -94
- package/package.json +5 -3
|
@@ -1,69 +1,27 @@
|
|
|
1
1
|
import { confirm, select } from '@inquirer/prompts';
|
|
2
2
|
import { Flags } from '@oclif/core';
|
|
3
|
-
import { $ } from 'execa';
|
|
4
|
-
import { exec } from 'node:child_process';
|
|
5
3
|
import * as fs from 'node:fs/promises';
|
|
6
4
|
import * as path from 'node:path';
|
|
7
|
-
import Stream from 'node:stream';
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
5
|
import ora from 'ora';
|
|
10
6
|
import Api from './lib/api.js';
|
|
11
|
-
import {
|
|
7
|
+
import { addFrontendAllowedHosts, ensurePnpmBuildApprovals, ensureRootWorkspace, finalizeManagedFrontend, installFrontendWorkspace, normalizeFrontendManifest, prepareFrontendDirectory, scaffoldFrontendTemplate, } from './lib/apps/frontend-scaffold.js';
|
|
8
|
+
import { FRONTEND_HOSTING_LABELS, FRONTEND_TEMPLATE_ALIASES, FRONTEND_TEMPLATE_VALUES, getFrontendTemplate, getFrontendTemplateChoices, } from './lib/apps/frontend-templates.js';
|
|
9
|
+
import { getAllConfigPaths, writeFile, writeJsonFile, } from './lib/apps/index.js';
|
|
12
10
|
import { ensureStorefrontResources, } from './lib/apps/storefront-resources.js';
|
|
13
|
-
import { getPackageManagerCommands, transformCreateCommand, } from './lib/package-manager.js';
|
|
14
11
|
import style from './lib/style.js';
|
|
15
12
|
import { SwellCommand } from './swell-command.js';
|
|
16
|
-
const execAsync = promisify(exec);
|
|
17
|
-
/** Allowed hosts for tunnel providers used in local development */
|
|
18
|
-
const TUNNEL_ALLOWED_HOSTS = ['.trycloudflare.com', '.swell.store'];
|
|
19
|
-
/**
|
|
20
|
-
* Find the closing brace position for a config function call (e.g., defineConfig, defineNuxtConfig).
|
|
21
|
-
* Uses brace counting to find the matching `}` for the opening `{`.
|
|
22
|
-
* @returns Position of the closing `}`, or -1 if not found
|
|
23
|
-
*/
|
|
24
|
-
function findConfigClosingBrace(content, functionName) {
|
|
25
|
-
const pattern = new RegExp(`${functionName}\\s*\\(\\s*\\{`);
|
|
26
|
-
const match = content.match(pattern);
|
|
27
|
-
if (!match || match.index === undefined) {
|
|
28
|
-
return -1;
|
|
29
|
-
}
|
|
30
|
-
const startPos = match.index + match[0].length;
|
|
31
|
-
let depth = 1;
|
|
32
|
-
for (let i = startPos; i < content.length && depth > 0; i++) {
|
|
33
|
-
const char = content[i];
|
|
34
|
-
if (char === '{') {
|
|
35
|
-
depth++;
|
|
36
|
-
}
|
|
37
|
-
else if (char === '}') {
|
|
38
|
-
depth--;
|
|
39
|
-
if (depth === 0) {
|
|
40
|
-
return i;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return -1;
|
|
45
|
-
}
|
|
46
13
|
export class CreateAppCommand extends SwellCommand {
|
|
47
14
|
// Command name used in error message examples; override in subclasses
|
|
48
15
|
commandExample = 'swell create app';
|
|
49
16
|
static baseFlags = {
|
|
50
17
|
frontend: Flags.string({
|
|
51
|
-
description:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'storefront-app': Flags.string({
|
|
55
|
-
description: 'Target storefront app ID',
|
|
56
|
-
}),
|
|
57
|
-
'integration-type': Flags.string({
|
|
58
|
-
description: 'Integration: generic | payment | shipping | tax',
|
|
59
|
-
options: ['generic', 'payment', 'shipping', 'tax'],
|
|
60
|
-
}),
|
|
61
|
-
'integration-id': Flags.string({
|
|
62
|
-
description: 'Service ID (e.g., card, fedex)',
|
|
18
|
+
description: 'Frontend template; see FRONTENDS',
|
|
19
|
+
helpValue: 'template',
|
|
20
|
+
options: [...FRONTEND_TEMPLATE_VALUES, ...FRONTEND_TEMPLATE_ALIASES],
|
|
63
21
|
}),
|
|
64
22
|
yes: Flags.boolean({
|
|
65
23
|
char: 'y',
|
|
66
|
-
description: 'Skip prompts
|
|
24
|
+
description: 'Skip prompts; use defaults for optional values',
|
|
67
25
|
}),
|
|
68
26
|
};
|
|
69
27
|
devApi;
|
|
@@ -71,143 +29,6 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
71
29
|
super(argv, config);
|
|
72
30
|
this.devApi = new Api(undefined, 'test');
|
|
73
31
|
}
|
|
74
|
-
async addAllowedHostsToAngular(configPath) {
|
|
75
|
-
const angularJsonPath = path.join(configPath, 'frontend', 'angular.json');
|
|
76
|
-
let content;
|
|
77
|
-
try {
|
|
78
|
-
content = await fs.readFile(angularJsonPath, 'utf8');
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
let config;
|
|
84
|
-
try {
|
|
85
|
-
config = JSON.parse(content);
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const { projects } = config;
|
|
91
|
-
if (!projects || typeof projects !== 'object') {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const projectNames = Object.keys(projects);
|
|
95
|
-
if (projectNames.length === 0) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
// Use first project (typically only one exists in new apps)
|
|
99
|
-
const projectName = projectNames[0];
|
|
100
|
-
const project = projects[projectName];
|
|
101
|
-
if (!project?.architect?.serve) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (!project.architect.serve.options) {
|
|
105
|
-
project.architect.serve.options = {};
|
|
106
|
-
}
|
|
107
|
-
if (project.architect.serve.options.allowedHosts) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
project.architect.serve.options.allowedHosts = TUNNEL_ALLOWED_HOSTS;
|
|
111
|
-
await fs.writeFile(angularJsonPath, JSON.stringify(config, null, 2), 'utf8');
|
|
112
|
-
}
|
|
113
|
-
async addAllowedHostsToAstro(configPath) {
|
|
114
|
-
return this.addViteAllowedHosts(configPath, 'astro.config.mjs', 'defineConfig');
|
|
115
|
-
}
|
|
116
|
-
async addAllowedHostsToNuxt(configPath) {
|
|
117
|
-
return this.addViteAllowedHosts(configPath, 'nuxt.config.ts', 'defineNuxtConfig');
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* For frameworks whose dev server is Vite directly. Astro/Nuxt nest the
|
|
121
|
-
* vite block inside their own defineConfig — see addViteAllowedHosts.
|
|
122
|
-
*/
|
|
123
|
-
async addAllowedHostsToVite(configPath) {
|
|
124
|
-
const candidates = [
|
|
125
|
-
'vite.config.ts',
|
|
126
|
-
'vite.config.js',
|
|
127
|
-
'vite.config.mts',
|
|
128
|
-
'vite.config.mjs',
|
|
129
|
-
];
|
|
130
|
-
let configFilePath;
|
|
131
|
-
let content;
|
|
132
|
-
for (const fileName of candidates) {
|
|
133
|
-
const candidatePath = path.join(configPath, 'frontend', fileName);
|
|
134
|
-
try {
|
|
135
|
-
// eslint-disable-next-line no-await-in-loop
|
|
136
|
-
content = await fs.readFile(candidatePath, 'utf8');
|
|
137
|
-
configFilePath = candidatePath;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
/* try next */
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (!configFilePath || content === undefined) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (content.includes('allowedHosts')) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const serverBlock = ` server: {
|
|
151
|
-
allowedHosts: ${JSON.stringify(TUNNEL_ALLOWED_HOSTS)},
|
|
152
|
-
},`;
|
|
153
|
-
const emptyConfig = 'defineConfig({})';
|
|
154
|
-
if (content.includes(emptyConfig)) {
|
|
155
|
-
content = content.replace(emptyConfig, `defineConfig({\n${serverBlock}\n})`);
|
|
156
|
-
await fs.writeFile(configFilePath, content, 'utf8');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const closingPos = findConfigClosingBrace(content, 'defineConfig');
|
|
160
|
-
if (closingPos === -1) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const before = content.slice(0, closingPos);
|
|
164
|
-
const after = content.slice(closingPos);
|
|
165
|
-
const needsComma = before.trimEnd().slice(-1) !== ',';
|
|
166
|
-
const separator = needsComma ? ',\n' : '\n';
|
|
167
|
-
content = before.trimEnd() + separator + serverBlock + '\n' + after;
|
|
168
|
-
await fs.writeFile(configFilePath, content, 'utf8');
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Add vite allowedHosts configuration to a framework config file.
|
|
172
|
-
* Supports config files that use a defineX({}) pattern (Astro, Nuxt, etc.)
|
|
173
|
-
*/
|
|
174
|
-
async addViteAllowedHosts(configPath, configFile, functionName) {
|
|
175
|
-
const configFilePath = path.join(configPath, 'frontend', configFile);
|
|
176
|
-
let content;
|
|
177
|
-
try {
|
|
178
|
-
content = await fs.readFile(configFilePath, 'utf8');
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (content.includes('allowedHosts')) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const viteConfig = ` vite: {
|
|
187
|
-
server: {
|
|
188
|
-
allowedHosts: ${JSON.stringify(TUNNEL_ALLOWED_HOSTS)},
|
|
189
|
-
},
|
|
190
|
-
},`;
|
|
191
|
-
// Handle empty config: functionName({})
|
|
192
|
-
const emptyConfig = `${functionName}({})`;
|
|
193
|
-
if (content.includes(emptyConfig)) {
|
|
194
|
-
content = content.replace(emptyConfig, `${functionName}({\n${viteConfig}\n})`);
|
|
195
|
-
await fs.writeFile(configFilePath, content, 'utf8');
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
// Find the closing brace of the config function using brace counting
|
|
199
|
-
const closingPos = findConfigClosingBrace(content, functionName);
|
|
200
|
-
if (closingPos === -1) {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const before = content.slice(0, closingPos);
|
|
204
|
-
const after = content.slice(closingPos);
|
|
205
|
-
// Ensure proper comma before vite config
|
|
206
|
-
const needsComma = before.trimEnd().slice(-1) !== ',';
|
|
207
|
-
const separator = needsComma ? ',\n' : '\n';
|
|
208
|
-
content = before.trimEnd() + separator + viteConfig + '\n' + after;
|
|
209
|
-
await fs.writeFile(configFilePath, content, 'utf8');
|
|
210
|
-
}
|
|
211
32
|
async createAppConfigFolders(swellConfig) {
|
|
212
33
|
const configPath = path.dirname(swellConfig.path);
|
|
213
34
|
for (const type of getAllConfigPaths(swellConfig.get('type'))) {
|
|
@@ -216,179 +37,99 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
216
37
|
await fs.mkdir(path.join(configPath, type), { recursive: true });
|
|
217
38
|
}
|
|
218
39
|
}
|
|
219
|
-
async
|
|
40
|
+
async selectFrontend(appType, flags, direct = false) {
|
|
41
|
+
if (direct && flags.yes && (!flags.frontend || flags.frontend === 'none')) {
|
|
42
|
+
this.error('Choose a frontend template with --frontend. Example: swell create frontend --frontend swell-react -y');
|
|
43
|
+
}
|
|
44
|
+
const choices = getFrontendTemplateChoices(appType, direct);
|
|
45
|
+
const interactive = !flags.frontend && !flags.yes;
|
|
46
|
+
const value = flags.frontend ||
|
|
47
|
+
(flags.yes
|
|
48
|
+
? 'none'
|
|
49
|
+
: await select({
|
|
50
|
+
message: 'Frontend template',
|
|
51
|
+
choices,
|
|
52
|
+
pageSize: choices.length,
|
|
53
|
+
loop: false,
|
|
54
|
+
}, { clearPromptOnDone: true }));
|
|
55
|
+
if (value === 'none') {
|
|
56
|
+
if (interactive)
|
|
57
|
+
this.log(`${style.success('✔')} Frontend template: Skip for now`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const template = this.resolveFrontendTemplate(value);
|
|
61
|
+
if (appType === 'theme' ||
|
|
62
|
+
(template.appTypes && !template.appTypes.includes(appType))) {
|
|
63
|
+
this.error(`${template.name} is not available for '${appType}' apps.`);
|
|
64
|
+
}
|
|
65
|
+
if (interactive) {
|
|
66
|
+
this.log(`${style.success('✔')} Frontend template: ${template.name} · ${FRONTEND_HOSTING_LABELS[template.hosting]}`);
|
|
67
|
+
}
|
|
68
|
+
return template;
|
|
69
|
+
}
|
|
70
|
+
/** Scaffold the local frontend workspace without touching remote resources. */
|
|
71
|
+
async scaffoldFrontend(swellConfig, template, packageManager, directCreate) {
|
|
220
72
|
const spinner = ora();
|
|
221
73
|
const configPath = path.dirname(swellConfig.path);
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
]),
|
|
236
|
-
...FrontendProjectTypes.filter((pt) => !pt.appTypes || pt.appTypes.includes(appType))
|
|
237
|
-
.sort((a, b) => (a.displayOrder ?? Number.POSITIVE_INFINITY) -
|
|
238
|
-
(b.displayOrder ?? Number.POSITIVE_INFINITY))
|
|
239
|
-
.map(({ name, slug }) => ({
|
|
240
|
-
name,
|
|
241
|
-
value: slug,
|
|
242
|
-
})),
|
|
243
|
-
],
|
|
244
|
-
message: directCreate
|
|
245
|
-
? `Choose framework for your hosted${kind ? ` ${kind} ` : ' '}app frontend`
|
|
246
|
-
: `Install framework for a hosted${kind ? ` ${kind} ` : ' '}app frontend?`,
|
|
247
|
-
}));
|
|
248
|
-
if (frameworkType && frameworkType !== 'none') {
|
|
249
|
-
const projectType = this.getProjectType(frameworkType);
|
|
250
|
-
if (projectType.appTypes && !projectType.appTypes.includes(appType)) {
|
|
251
|
-
this.error(`${projectType.name} is not available for '${appType}' apps. Allowed app types: ${projectType.appTypes.join(', ')}.`, { exit: 1 });
|
|
74
|
+
// Determine package manager - 'none' is not valid for frontend scaffolding
|
|
75
|
+
let pkg = (packageManager || 'npm');
|
|
76
|
+
if (packageManager === 'none') {
|
|
77
|
+
this.log(`\n${style.dim('Note: --pkg none is not available for frontend scaffolding, using npm.')}`);
|
|
78
|
+
pkg = 'npm';
|
|
79
|
+
}
|
|
80
|
+
this.log();
|
|
81
|
+
const frontendPath = path.join(configPath, 'frontend');
|
|
82
|
+
try {
|
|
83
|
+
await prepareFrontendDirectory(configPath);
|
|
84
|
+
await ensureRootWorkspace(swellConfig, pkg);
|
|
85
|
+
if (pkg === 'pnpm' && template.hosting === 'managed') {
|
|
86
|
+
await ensurePnpmBuildApprovals(configPath);
|
|
252
87
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
catch {
|
|
264
|
-
// Root package.json doesn't exist, create it
|
|
265
|
-
await this.setupPackage(swellConfig.get('id'), swellConfig, pkg);
|
|
266
|
-
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
this.error(error.message);
|
|
91
|
+
}
|
|
92
|
+
spinner.start(`Creating ${template.name} frontend app (this may take a while)...`);
|
|
93
|
+
try {
|
|
94
|
+
await scaffoldFrontendTemplate(configPath, template, pkg);
|
|
95
|
+
if (template.hosting === 'managed') {
|
|
96
|
+
spinner.text = 'Preparing Swell managed hosting...';
|
|
97
|
+
await finalizeManagedFrontend(frontendPath);
|
|
267
98
|
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
spinner.fail(`Error creating ${template.name} app:`);
|
|
102
|
+
this.error(error.message);
|
|
103
|
+
}
|
|
104
|
+
await normalizeFrontendManifest(frontendPath, template);
|
|
105
|
+
if (template.hosting === 'self-hosted') {
|
|
106
|
+
await addFrontendAllowedHosts(template.framework, configPath);
|
|
107
|
+
}
|
|
108
|
+
// Run install at root to hoist dependencies to workspace
|
|
109
|
+
spinner.start('Initializing workspace...');
|
|
110
|
+
try {
|
|
111
|
+
await installFrontendWorkspace(configPath, template, pkg);
|
|
112
|
+
spinner.succeed('Workspace initialized');
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
spinner.fail('Failed to initialize frontend workspace');
|
|
116
|
+
this.error(error.message);
|
|
117
|
+
}
|
|
118
|
+
swellConfig.set('frontend', {
|
|
119
|
+
...swellConfig.get('frontend'),
|
|
120
|
+
hosting: template.hosting,
|
|
121
|
+
});
|
|
122
|
+
spinner.succeed(`${template.name} initialized app in ${configPath}/frontend/`);
|
|
123
|
+
if (directCreate) {
|
|
268
124
|
this.log();
|
|
269
|
-
// Check for placeholder frontend/package.json and remove it before C3 scaffolding
|
|
270
|
-
const frontendPath = path.join(configPath, 'frontend');
|
|
271
|
-
const frontendPkgPath = path.join(frontendPath, 'package.json');
|
|
272
|
-
try {
|
|
273
|
-
const content = await fs.readFile(frontendPkgPath, 'utf8');
|
|
274
|
-
const frontendPkg = JSON.parse(content);
|
|
275
|
-
// Only remove if it matches our placeholder signature
|
|
276
|
-
if (frontendPkg.version === '0.0.0' &&
|
|
277
|
-
frontendPkg.name === 'frontend' &&
|
|
278
|
-
frontendPkg.private === true) {
|
|
279
|
-
await fs.unlink(frontendPkgPath);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// No package.json or can't read, that's fine
|
|
284
|
-
}
|
|
285
|
-
// Check if frontend folder exists and has files (user-created content)
|
|
286
|
-
try {
|
|
287
|
-
const files = await fs.readdir(frontendPath);
|
|
288
|
-
if (files.length > 0) {
|
|
289
|
-
spinner.fail('frontend/ folder is not empty. Please remove existing files before scaffolding.');
|
|
290
|
-
return {
|
|
291
|
-
created: false,
|
|
292
|
-
framework: projectType.slug,
|
|
293
|
-
provisionsStorefront: Boolean(projectType.provisionsStorefront),
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
// Folder doesn't exist, that's fine - C3 will create it
|
|
299
|
-
}
|
|
300
|
-
spinner.start(`Creating ${projectType?.name} frontend app (this may take a while)...`);
|
|
301
|
-
try {
|
|
302
|
-
await fs.mkdir(path.join(configPath, 'frontend'), {
|
|
303
|
-
recursive: true,
|
|
304
|
-
});
|
|
305
|
-
// SWELL_LOCAL_TEMPLATE_PATH overrides the scaffold for local dev — copies
|
|
306
|
-
// the directory at the path into frontend/ instead of running the install
|
|
307
|
-
// command. Used to iterate on the canonical template repo without pushing.
|
|
308
|
-
const localTemplatePath = process.env.SWELL_LOCAL_TEMPLATE_PATH;
|
|
309
|
-
if (localTemplatePath) {
|
|
310
|
-
const source = path.resolve(localTemplatePath);
|
|
311
|
-
const dest = path.join(configPath, 'frontend');
|
|
312
|
-
await fs.cp(source, dest, {
|
|
313
|
-
recursive: true,
|
|
314
|
-
filter: (src) => !src.includes('/node_modules') &&
|
|
315
|
-
!src.endsWith('/.git') &&
|
|
316
|
-
!src.includes('/dist') &&
|
|
317
|
-
!src.includes('/.wrangler'),
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
// Transform install command for the selected package manager
|
|
322
|
-
const installCommand = transformCreateCommand(projectType.installCommand, pkg);
|
|
323
|
-
await execAsync(installCommand, {
|
|
324
|
-
cwd: configPath,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
// Use this command to debug output, i.e.e when command becomes non-responsive
|
|
328
|
-
/* await this.execWithStdio(
|
|
329
|
-
configPath,
|
|
330
|
-
installCommand,
|
|
331
|
-
); */
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
spinner.fail(`Error creating ${projectType?.name} app:`);
|
|
335
|
-
const detail = [error.stdout, error.stderr]
|
|
336
|
-
.map((s) => (s || '').trim())
|
|
337
|
-
.filter(Boolean)
|
|
338
|
-
.join('\n') || (error.message ?? '').trim();
|
|
339
|
-
if (detail)
|
|
340
|
-
this.log(detail);
|
|
341
|
-
return {
|
|
342
|
-
created: false,
|
|
343
|
-
framework: projectType.slug,
|
|
344
|
-
provisionsStorefront: Boolean(projectType.provisionsStorefront),
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
// Ensure frontend package.json has correct name for workspace
|
|
348
|
-
try {
|
|
349
|
-
const pkgContent = await fs.readFile(frontendPkgPath, 'utf8');
|
|
350
|
-
const frontendPkgJson = JSON.parse(pkgContent);
|
|
351
|
-
if (frontendPkgJson.name !== 'frontend') {
|
|
352
|
-
frontendPkgJson.name = 'frontend';
|
|
353
|
-
await fs.writeFile(frontendPkgPath, JSON.stringify(frontendPkgJson, null, 2), 'utf8');
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
// Ignore if package.json doesn't exist or can't be read
|
|
358
|
-
}
|
|
359
|
-
await this.addFrontendAllowedHosts(projectType, configPath);
|
|
360
|
-
// Run install at root to hoist dependencies to workspace
|
|
361
|
-
spinner.start('Initializing workspace...');
|
|
362
|
-
try {
|
|
363
|
-
const { install } = getPackageManagerCommands(pkg);
|
|
364
|
-
await execAsync(install, { cwd: configPath });
|
|
365
|
-
spinner.succeed('Workspace initialized');
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
spinner.warn('Failed to initialize workspace');
|
|
369
|
-
}
|
|
370
|
-
spinner.succeed(`${projectType?.name} initialized app in ${configPath}/frontend/`);
|
|
371
|
-
if (directCreate) {
|
|
372
|
-
this.log();
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
created: true,
|
|
376
|
-
framework: projectType.slug,
|
|
377
|
-
provisionsStorefront: Boolean(projectType.provisionsStorefront),
|
|
378
|
-
};
|
|
379
125
|
}
|
|
380
|
-
return {
|
|
381
|
-
created: false,
|
|
382
|
-
framework: null,
|
|
383
|
-
provisionsStorefront: false,
|
|
384
|
-
};
|
|
126
|
+
return { created: true, template: template.slug };
|
|
385
127
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return frontend;
|
|
128
|
+
/** Scaffold the frontend and, when asked, its remote storefront resources. */
|
|
129
|
+
async createFrontend(swellConfig, template, packageManager, options = {}) {
|
|
130
|
+
const result = await this.scaffoldFrontend(swellConfig, template, packageManager);
|
|
131
|
+
if (!options.provisionResources || !template.provisionsStorefront) {
|
|
132
|
+
return result;
|
|
392
133
|
}
|
|
393
134
|
const resources = await ensureStorefrontResources(this.api, {
|
|
394
135
|
description: swellConfig.get('description'),
|
|
@@ -398,7 +139,7 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
398
139
|
version: swellConfig.get('version'),
|
|
399
140
|
});
|
|
400
141
|
this.log(`Storefront ${style.appConfigValue(resources.storefront.id)} created for app ${style.appConfigValue(resources.app.id)}.`);
|
|
401
|
-
return { ...
|
|
142
|
+
return { ...result, resources };
|
|
402
143
|
}
|
|
403
144
|
async createThemeApp(config, flags, installedStorefrontApp) {
|
|
404
145
|
const spinner = ora();
|
|
@@ -446,56 +187,6 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
446
187
|
}
|
|
447
188
|
return true;
|
|
448
189
|
}
|
|
449
|
-
async doesPackageManagerExist(packageManager) {
|
|
450
|
-
try {
|
|
451
|
-
await execAsync(`${packageManager} --version`);
|
|
452
|
-
return true;
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
return false;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
async execWithStdio(cwd, command, onOutput) {
|
|
459
|
-
const $$ = $({
|
|
460
|
-
cwd,
|
|
461
|
-
shell: true,
|
|
462
|
-
stderr: 'inherit',
|
|
463
|
-
stdin: 'inherit',
|
|
464
|
-
stdout: 'pipe',
|
|
465
|
-
});
|
|
466
|
-
const outStream = new Stream.Writable();
|
|
467
|
-
outStream._write = (chunk, _encoding, next) => {
|
|
468
|
-
const string = chunk.toString();
|
|
469
|
-
const out = onOutput?.(string);
|
|
470
|
-
if (out !== false) {
|
|
471
|
-
console.log(string);
|
|
472
|
-
}
|
|
473
|
-
next();
|
|
474
|
-
};
|
|
475
|
-
try {
|
|
476
|
-
await $$ `${command}`.pipeStdout?.(outStream);
|
|
477
|
-
}
|
|
478
|
-
catch (error) {
|
|
479
|
-
console.log(error);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
async findPackageManager(pkg = '') {
|
|
483
|
-
const packageManager = pkg;
|
|
484
|
-
// Determine if system has yarn or npm install
|
|
485
|
-
if (packageManager) {
|
|
486
|
-
if (await this.doesPackageManagerExist(packageManager)) {
|
|
487
|
-
return packageManager;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
if (await this.doesPackageManagerExist('npm')) {
|
|
492
|
-
return 'npm';
|
|
493
|
-
}
|
|
494
|
-
if (await this.doesPackageManagerExist('yarn')) {
|
|
495
|
-
return 'yarn';
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
190
|
async getInstalledStorefrontApps() {
|
|
500
191
|
const spinner = ora();
|
|
501
192
|
spinner.start('Retrieving installed storefront apps...');
|
|
@@ -510,131 +201,13 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
510
201
|
spinner.stop();
|
|
511
202
|
return storefrontInstalledApps;
|
|
512
203
|
}
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
if (!
|
|
516
|
-
this.error(`
|
|
204
|
+
resolveFrontendTemplate(value) {
|
|
205
|
+
const template = getFrontendTemplate(value);
|
|
206
|
+
if (!template) {
|
|
207
|
+
this.error(`Unknown frontend template: ${value}\n\nValid values: ${FRONTEND_TEMPLATE_VALUES.join(', ')}\n\nExample: ${this.commandExample} reviews --type admin --frontend astro -y`, {
|
|
517
208
|
exit: 1,
|
|
518
209
|
});
|
|
519
210
|
}
|
|
520
|
-
|
|
521
|
-
this.error(`Project type ${projectType.name} cannot be installed (legacy type)\n\nValid values: ${getFrontendProjectValidValues(true, false)}\n\nExample: ${this.commandExample} reviews --type admin --frontend astro -y`, {
|
|
522
|
-
exit: 1,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
return {
|
|
526
|
-
...projectType,
|
|
527
|
-
installCommand: projectType.installCommand || '',
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
async setupPackage(name, config, pkg) {
|
|
531
|
-
const execAsync = promisify(exec);
|
|
532
|
-
const configPath = path.dirname(config.path);
|
|
533
|
-
const packageJson = {
|
|
534
|
-
description: config.get('description'),
|
|
535
|
-
devDependencies: {
|
|
536
|
-
'@swell/app-types': '^1.2.0',
|
|
537
|
-
typescript: '^5.9.3',
|
|
538
|
-
},
|
|
539
|
-
name,
|
|
540
|
-
// Required for yarn workspaces, good practice for all package managers
|
|
541
|
-
private: true,
|
|
542
|
-
scripts: {
|
|
543
|
-
typecheck: '([ -z "$(find functions -name \'*.ts\' 2>/dev/null | head -1)" ] || tsc --noEmit) && ([ ! -f test/tsconfig.json ] || tsc --build --noEmit test) && ([ ! -f frontend/tsconfig.json ] || tsc --build --noEmit frontend)',
|
|
544
|
-
},
|
|
545
|
-
version: config.get('version'),
|
|
546
|
-
};
|
|
547
|
-
// pnpm uses pnpm-workspace.yaml instead of workspaces field in package.json
|
|
548
|
-
if (pkg !== 'pnpm') {
|
|
549
|
-
packageJson.workspaces = ['frontend'];
|
|
550
|
-
}
|
|
551
|
-
const tsConfig = {
|
|
552
|
-
compilerOptions: {
|
|
553
|
-
lib: ['esnext', 'webworker'],
|
|
554
|
-
module: 'esnext',
|
|
555
|
-
target: 'esnext',
|
|
556
|
-
moduleResolution: 'bundler',
|
|
557
|
-
types: ['@swell/app-types'],
|
|
558
|
-
},
|
|
559
|
-
exclude: ['node_modules', 'frontend', 'test', 'vitest.config.ts'],
|
|
560
|
-
};
|
|
561
|
-
await writeJsonFile(path.join(configPath, 'package.json'), packageJson);
|
|
562
|
-
await writeJsonFile(path.join(configPath, 'tsconfig.json'), tsConfig);
|
|
563
|
-
await writeFile(path.join(configPath, '.gitignore'), `node_modules\n.wrangler\n.dev.vars\n.dev.vars.*\n`);
|
|
564
|
-
// Create pnpm-workspace.yaml for pnpm (required for workspace support)
|
|
565
|
-
if (pkg === 'pnpm') {
|
|
566
|
-
await writeFile(path.join(configPath, 'pnpm-workspace.yaml'), 'packages:\n - frontend\n');
|
|
567
|
-
}
|
|
568
|
-
// Create placeholder frontend/package.json for bun and yarn
|
|
569
|
-
// These package managers require workspace directories to exist with a package.json
|
|
570
|
-
if (pkg === 'bun' || pkg === 'yarn') {
|
|
571
|
-
const frontendPath = path.join(configPath, 'frontend');
|
|
572
|
-
await fs.mkdir(frontendPath, { recursive: true });
|
|
573
|
-
await writeJsonFile(path.join(frontendPath, 'package.json'), {
|
|
574
|
-
name: 'frontend',
|
|
575
|
-
private: true,
|
|
576
|
-
version: '0.0.0',
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
// Install root dependencies using selected package manager
|
|
580
|
-
const { install } = getPackageManagerCommands(pkg);
|
|
581
|
-
await execAsync(install, { cwd: configPath });
|
|
582
|
-
}
|
|
583
|
-
async tryPackageSetup(name, config, pkg) {
|
|
584
|
-
try {
|
|
585
|
-
await this.setupPackage(name, config, pkg);
|
|
586
|
-
}
|
|
587
|
-
catch (error) {
|
|
588
|
-
this.log(`There was an error setting up the package manager: ${error.message}`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Fix wrangler.jsonc `main` field when it points to a build artifact that
|
|
593
|
-
* doesn't exist at dev time. The @cloudflare/vite-plugin validates this path
|
|
594
|
-
* at startup and crashes during `astro dev` if it can't resolve it.
|
|
595
|
-
*
|
|
596
|
-
* Only replaces the exact legacy value that C3 currently scaffolds.
|
|
597
|
-
* Once C3 updates its template, this becomes a no-op.
|
|
598
|
-
*/
|
|
599
|
-
async fixAstroWranglerEntrypoint(configPath) {
|
|
600
|
-
const filePath = path.join(configPath, 'frontend', 'wrangler.jsonc');
|
|
601
|
-
let content;
|
|
602
|
-
try {
|
|
603
|
-
content = await fs.readFile(filePath, 'utf8');
|
|
604
|
-
}
|
|
605
|
-
catch {
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
const legacy = '"main": "./dist/_worker.js/index.js"';
|
|
609
|
-
if (!content.includes(legacy)) {
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
const updated = content.replace(legacy, '"main": "@astrojs/cloudflare/entrypoints/server"');
|
|
613
|
-
await fs.writeFile(filePath, updated, 'utf8');
|
|
614
|
-
}
|
|
615
|
-
async addFrontendAllowedHosts(projectType, configPath) {
|
|
616
|
-
// Configure allowedHosts for frameworks using Vite dev server
|
|
617
|
-
// This allows tunneling services (localtunnel) to proxy requests to the local dev environment
|
|
618
|
-
switch (projectType.slug) {
|
|
619
|
-
case 'astro': {
|
|
620
|
-
await this.addAllowedHostsToAstro(configPath);
|
|
621
|
-
await this.fixAstroWranglerEntrypoint(configPath);
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
case 'angular': {
|
|
625
|
-
await this.addAllowedHostsToAngular(configPath);
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
case 'nuxt': {
|
|
629
|
-
await this.addAllowedHostsToNuxt(configPath);
|
|
630
|
-
break;
|
|
631
|
-
}
|
|
632
|
-
case 'react':
|
|
633
|
-
case 'react-storefront': {
|
|
634
|
-
await this.addAllowedHostsToVite(configPath);
|
|
635
|
-
break;
|
|
636
|
-
}
|
|
637
|
-
// Other frameworks don't require allowedHosts configuration
|
|
638
|
-
}
|
|
211
|
+
return template;
|
|
639
212
|
}
|
|
640
213
|
}
|