@swell/cli 2.9.18 → 2.9.19

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.
@@ -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 { FrontendProjectTypes, getFrontendProjectValidValues, getAllConfigPaths, getFrontendProjectSlugs, writeFile, writeJsonFile, } from './lib/apps/index.js';
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: `Framework: ${getFrontendProjectSlugs(true, false).join(' | ')}`,
52
- options: getFrontendProjectSlugs(true, false),
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, require all arguments',
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 createFrontendApp(swellConfig, flags, directCreate, kind) {
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
- // use 'none' by default for non-interactive mode
223
- const inputFrontend = flags.yes ? flags.frontend || 'none' : flags.frontend;
224
- const appType = swellConfig.get('type');
225
- const frameworkType = inputFrontend ||
226
- (await select({
227
- choices: [
228
- ...(directCreate
229
- ? []
230
- : [
231
- {
232
- name: 'Do not install a frontend framework',
233
- value: null,
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
- // Determine package manager - 'none' is not valid for frontend scaffolding
254
- let pkg = (flags.pkg || 'npm');
255
- if (flags.pkg === 'none') {
256
- this.log(`\n${style.dim('Note: --pkg none is not available for frontend scaffolding, using npm.')}`);
257
- pkg = 'npm';
258
- // Ensure root package.json exists (skipped when --pkg none)
259
- const rootPkgPath = path.join(configPath, 'package.json');
260
- try {
261
- await fs.access(rootPkgPath);
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
- async createStorefrontApp(swellConfig, flags, options = {}) {
387
- const frontend = await this.createFrontendApp(swellConfig, flags, options.directCreate, 'storefront');
388
- if (!options.provisionResources ||
389
- !frontend.created ||
390
- !frontend.provisionsStorefront) {
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 { ...frontend, resources };
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
- getProjectType(frameworkType) {
514
- const projectType = FrontendProjectTypes.find((type) => type.slug === frameworkType);
515
- if (!projectType) {
516
- this.error(`Could not find project type: ${frameworkType}\n\nValid values: ${getFrontendProjectValidValues(true, false)}\n\nExample: ${this.commandExample} reviews --type admin --frontend astro -y`, {
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
- if (!projectType.installCommand) {
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
  }