From ac476675a9e4a5fb8035cf01bb046947a37ceefe Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Wed, 6 Nov 2024 19:44:14 -0600 Subject: [PATCH 01/18] feat(scaffolding): add hosting config files for common providers add configuration files for Vercel, Netlify, and Cloudflare Pages to simplify deployment and address CORS header issues in TutorialKit. This improves the setup experience by automating build commands and headers for common hosting providers, reducing manual configuration steps. close #234 --- _headers | 3 ++ netlify.toml | 5 +++ package.json | 3 +- packages/cli/src/commands/create/index.ts | 55 +++++++++++++++++++++++ vercel.json | 17 +++++++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 _headers create mode 100644 netlify.toml create mode 100644 vercel.json diff --git a/_headers b/_headers new file mode 100644 index 000000000..a2395ae61 --- /dev/null +++ b/_headers @@ -0,0 +1,3 @@ +/* + Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 000000000..fa8126793 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,5 @@ +[[headers]] + for = "/*" + [headers.values] + Cross-Origin-Embedder-Policy = "require-corp" + Cross-Origin-Opener-Policy = "same-origin" diff --git a/package.json b/package.json index f63d0eac9..bae972418 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "demo:build": "pnpm run build && pnpm run --filter=demo.tutorialkit.dev build", "lint": "eslint \"{packages,docs,extensions,integration}/**/*\"", "test": "pnpm run --stream --filter='@tutorialkit/*' test --run", - "test:e2e": "pnpm run --filter='./e2e' test" + "test:e2e": "pnpm run --filter='./e2e' test", + "postbuild": "cp _headers ./dist/" }, "license": "MIT", "packageManager": "pnpm@8.15.6", diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 0ac059090..3d96b9677 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -107,6 +107,24 @@ async function _createTutorial(flags: CreateOptions): Promise { const dest = await getTutorialDirectory(tutorialName, flags); const resolvedDest = path.resolve(process.cwd(), dest); + const providers = await prompts.multiselect({ + message: 'Select hosting providers for automatic configuration (use space to select):', + options: [ + { value: 'Vercel', label: 'Vercel' }, + { value: 'Netlify', label: 'Netlify' }, + { value: 'Cloudflare', label: 'Cloudflare' }, + ], + initialValues: [], + }); + + assertNotCanceled(providers); + prompts.log.info(`Configuring for: ${providers.join(', ')}`); + + await generateHostingConfig(resolvedDest, providers); + + await copyTemplate(resolvedDest, flags); + updatePackageJson(resolvedDest, tutorialName, flags); + prompts.log.info(`Scaffolding tutorial in ${chalk.blue(resolvedDest)}`); if (fs.existsSync(resolvedDest) && !flags.force) { @@ -319,3 +337,40 @@ function verifyFlags(flags: CreateOptions) { throw new Error('Cannot start project without installing dependencies.'); } } + +function generateHostingConfig(dest: string, providers: string[]) { + if (providers.includes('Vercel')) { + fs.writeFileSync( + path.join(dest, 'vercel.json'), + JSON.stringify( + { + headers: [{ source: '/(.*)', headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }] }], + }, + null, + 2, + ), + ); + } + + if (providers.includes('Netlify')) { + fs.writeFileSync( + path.join(dest, 'netlify.toml'), + `[build] + publish = "build" + command = "npm run build" + +[[headers]] + for = "/*" + [headers.values] + Access-Control-Allow-Origin = "*"`, + ); + } + + if (providers.includes('Cloudflare')) { + fs.writeFileSync( + path.join(dest, '_headers'), + `/* + Access-Control-Allow-Origin: *`, + ); + } +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 000000000..2068104d4 --- /dev/null +++ b/vercel.json @@ -0,0 +1,17 @@ +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-crop" + }, + { + "key": "Cross-Origin-Embedder-Policy", + "value": "same-origin" + } + ] + } + ] +} From 5de42a3d69f09eadac0c5e1c2d98bbf10920cb38 Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Thu, 21 Nov 2024 16:05:18 -0600 Subject: [PATCH 02/18] Attempted to fix error with locating config files --- packages/cli/src/commands/create/index.ts | 48 ++++++++++++++++++++--- packages/cli/tight-firefly/vercel.json | 13 ++++++ 2 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 packages/cli/tight-firefly/vercel.json diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 3d96b9677..3a9efb70c 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -338,10 +338,30 @@ function verifyFlags(flags: CreateOptions) { } } -function generateHostingConfig(dest: string, providers: string[]) { +async function generateHostingConfig(dest: string, providers: string[]) { + const resolvedDest = path.resolve(dest); + + if (!fs.existsSync(resolvedDest)) { + console.log(`Directory does not exist. Creating directory: ${resolvedDest}`); + fs.mkdirSync(resolvedDest, { recursive: true }); + } else { + console.log(`Directory already exists: ${resolvedDest}`); + } + + const templateDir = path.resolve(__dirname, '_template'); + console.log('Looking for template directory at:', templateDir); + + if (!fs.existsSync(templateDir)) { + console.error('Template directory does not exist at:', templateDir); + } else { + console.log('Template directory found at:', templateDir); + } + if (providers.includes('Vercel')) { + const vercelConfigPath = path.join(resolvedDest, 'vercel.json'); + console.log('Vercel config file will be written to:', vercelConfigPath); fs.writeFileSync( - path.join(dest, 'vercel.json'), + vercelConfigPath, JSON.stringify( { headers: [{ source: '/(.*)', headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }] }], @@ -353,24 +373,40 @@ function generateHostingConfig(dest: string, providers: string[]) { } if (providers.includes('Netlify')) { + const netlifyConfigPath = path.join(resolvedDest, 'netlify.toml'); + console.log('Netlify config file will be written to:', netlifyConfigPath); fs.writeFileSync( - path.join(dest, 'netlify.toml'), + netlifyConfigPath, `[build] publish = "build" command = "npm run build" -[[headers]] + [[headers]] for = "/*" [headers.values] - Access-Control-Allow-Origin = "*"`, + Access-Control-Allow-Origin = "*"`, ); } if (providers.includes('Cloudflare')) { + const cloudflareConfigPath = path.join(resolvedDest, '_headers'); + console.log('Cloudflare config file will be written to:', cloudflareConfigPath); fs.writeFileSync( - path.join(dest, '_headers'), + cloudflareConfigPath, `/* Access-Control-Allow-Origin: *`, ); } + + if (fs.existsSync(templateDir)) { + const gitignoreTemplatePath = path.join(templateDir, '.gitignore'); + + if (fs.existsSync(gitignoreTemplatePath)) { + const gitignoreDestPath = path.join(resolvedDest, '.gitignore'); + console.log('Copying .gitignore to:', gitignoreDestPath); + fs.copyFileSync(gitignoreTemplatePath, gitignoreDestPath); + } else { + console.warn('No .gitignore file found in template directory, skipping copy.'); + } + } } diff --git a/packages/cli/tight-firefly/vercel.json b/packages/cli/tight-firefly/vercel.json new file mode 100644 index 000000000..c967757fa --- /dev/null +++ b/packages/cli/tight-firefly/vercel.json @@ -0,0 +1,13 @@ +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ] + } + ] +} \ No newline at end of file From 2c062e9f81a2e1db968ca6c977006538599d0392 Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Thu, 19 Dec 2024 15:17:58 -0600 Subject: [PATCH 03/18] feat(cli): move hosting configuration generation to dedicated file moved the `generateHostingConfig` function to a new file with improved imports for hosting provider configuration files (`vercel.json`, `netlify.toml`, `_headers`). updated the function to work with the new imports and streamlined configuration generation. this change reduces clutter in the main implementation file and improves modularity by isolating hosting-related logic. closes #234 --- .../create/generate-hosting-config.ts | 52 +++++++++++++ .../commands/create/hosting-config/_headers | 0 .../create/hosting-config/netlify.toml | 0 .../create/hosting-config/vercel.json | 0 packages/cli/src/commands/create/index.ts | 74 +------------------ packages/cli/src/types.d.ts | 4 + 6 files changed, 57 insertions(+), 73 deletions(-) create mode 100644 packages/cli/src/commands/create/generate-hosting-config.ts rename _headers => packages/cli/src/commands/create/hosting-config/_headers (100%) rename netlify.toml => packages/cli/src/commands/create/hosting-config/netlify.toml (100%) rename vercel.json => packages/cli/src/commands/create/hosting-config/vercel.json (100%) create mode 100644 packages/cli/src/types.d.ts diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts new file mode 100644 index 000000000..9f4f62223 --- /dev/null +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -0,0 +1,52 @@ +/* eslint-disable prettier/prettier */ +import fs from 'node:fs'; +import path from 'node:path'; +import cloudflareConfigFile from './hosting-config/_headers?raw'; +import netlifyConfigFile from './hosting-config/netlify.toml?raw'; +import vercelConfigFile from './hosting-config/vercel.json?raw'; + +export async function generateHostingConfig(dest: string, providers: string[]) { + const resolvedDest = path.resolve(dest); + + if (!fs.existsSync(resolvedDest)) { + console.log(`Directory does not exist. Creating directory: ${resolvedDest}`); + fs.mkdirSync(resolvedDest, { recursive: true }); + } else { + console.log(`Directory already exists: ${resolvedDest}`); + } + + if (providers.includes('Vercel')) { + const vercelConfigPath = path.join(resolvedDest, 'vercel.json'); + console.log('Writing Vercel config file to:', vercelConfigPath); + fs.writeFileSync(vercelConfigPath, vercelConfigFile); + } + + if (providers.includes('Netlify')) { + const netlifyConfigPath = path.join(resolvedDest, 'netlify.toml'); + console.log('Writing Netlify config file to:', netlifyConfigPath); + fs.writeFileSync(netlifyConfigPath, netlifyConfigFile); + } + + if (providers.includes('Cloudflare')) { + const cloudflareConfigPath = path.join(resolvedDest, '_headers'); + console.log('Writing Cloudflare config file to:', cloudflareConfigPath); + fs.writeFileSync(cloudflareConfigPath, cloudflareConfigFile); + } + + const templateDir = path.resolve(__dirname, '_template'); + console.log('Looking for template directory at:', templateDir); + + if (fs.existsSync(templateDir)) { + const gitignoreTemplatePath = path.join(templateDir, '.gitignore'); + + if (fs.existsSync(gitignoreTemplatePath)) { + const gitignoreDestPath = path.join(resolvedDest, '.gitignore'); + console.log('Copying .gitignore to:', gitignoreDestPath); + fs.copyFileSync(gitignoreTemplatePath, gitignoreDestPath); + } else { + console.warn('No .gitignore file found in template directory, skipping copy.'); + } + } else { + console.warn('Template directory does not exist, skipping .gitignore copy.'); + } + } \ No newline at end of file diff --git a/_headers b/packages/cli/src/commands/create/hosting-config/_headers similarity index 100% rename from _headers rename to packages/cli/src/commands/create/hosting-config/_headers diff --git a/netlify.toml b/packages/cli/src/commands/create/hosting-config/netlify.toml similarity index 100% rename from netlify.toml rename to packages/cli/src/commands/create/hosting-config/netlify.toml diff --git a/vercel.json b/packages/cli/src/commands/create/hosting-config/vercel.json similarity index 100% rename from vercel.json rename to packages/cli/src/commands/create/hosting-config/vercel.json diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 3a9efb70c..9c01139d7 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -10,6 +10,7 @@ import { generateProjectName } from '../../utils/project.js'; import { assertNotCanceled } from '../../utils/tasks.js'; import { updateWorkspaceVersions } from '../../utils/workspace-version.js'; import { setupEnterpriseConfig } from './enterprise.js'; +import { generateHostingConfig } from './generate-hosting-config.js'; import { initGitRepo } from './git.js'; import { installAndStart } from './install-start.js'; import { DEFAULT_VALUES, type CreateOptions } from './options.js'; @@ -337,76 +338,3 @@ function verifyFlags(flags: CreateOptions) { throw new Error('Cannot start project without installing dependencies.'); } } - -async function generateHostingConfig(dest: string, providers: string[]) { - const resolvedDest = path.resolve(dest); - - if (!fs.existsSync(resolvedDest)) { - console.log(`Directory does not exist. Creating directory: ${resolvedDest}`); - fs.mkdirSync(resolvedDest, { recursive: true }); - } else { - console.log(`Directory already exists: ${resolvedDest}`); - } - - const templateDir = path.resolve(__dirname, '_template'); - console.log('Looking for template directory at:', templateDir); - - if (!fs.existsSync(templateDir)) { - console.error('Template directory does not exist at:', templateDir); - } else { - console.log('Template directory found at:', templateDir); - } - - if (providers.includes('Vercel')) { - const vercelConfigPath = path.join(resolvedDest, 'vercel.json'); - console.log('Vercel config file will be written to:', vercelConfigPath); - fs.writeFileSync( - vercelConfigPath, - JSON.stringify( - { - headers: [{ source: '/(.*)', headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }] }], - }, - null, - 2, - ), - ); - } - - if (providers.includes('Netlify')) { - const netlifyConfigPath = path.join(resolvedDest, 'netlify.toml'); - console.log('Netlify config file will be written to:', netlifyConfigPath); - fs.writeFileSync( - netlifyConfigPath, - `[build] - publish = "build" - command = "npm run build" - - [[headers]] - for = "/*" - [headers.values] - Access-Control-Allow-Origin = "*"`, - ); - } - - if (providers.includes('Cloudflare')) { - const cloudflareConfigPath = path.join(resolvedDest, '_headers'); - console.log('Cloudflare config file will be written to:', cloudflareConfigPath); - fs.writeFileSync( - cloudflareConfigPath, - `/* - Access-Control-Allow-Origin: *`, - ); - } - - if (fs.existsSync(templateDir)) { - const gitignoreTemplatePath = path.join(templateDir, '.gitignore'); - - if (fs.existsSync(gitignoreTemplatePath)) { - const gitignoreDestPath = path.join(resolvedDest, '.gitignore'); - console.log('Copying .gitignore to:', gitignoreDestPath); - fs.copyFileSync(gitignoreTemplatePath, gitignoreDestPath); - } else { - console.warn('No .gitignore file found in template directory, skipping copy.'); - } - } -} diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts new file mode 100644 index 000000000..88d404d0a --- /dev/null +++ b/packages/cli/src/types.d.ts @@ -0,0 +1,4 @@ +declare module '*?raw' { + const content: string; + export default content; +} From 986ec671ac70b365db3c1d9f23fe2c6ecbd51e80 Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Thu, 9 Jan 2025 15:45:21 -0600 Subject: [PATCH 04/18] feat(wizard): restrict hosting provider selection to a single option Adjusted the project scaffolding wizard to allow users to select only one hosting provider at a time, laying the groundwork for improved deployment configuration. Related to #234 --- packages/cli/curly-sun/vercel.json | 17 +++ packages/cli/nameless-union/vercel.json | 17 +++ .../create/generate-hosting-config.ts | 117 +++++++++++------- .../hosting-config/{_headers => _headers.txt} | 0 .../{netlify.toml => netlify_toml.txt} | 0 packages/cli/src/commands/create/index.ts | 12 +- packages/cli/super-math/vercel.json | 17 +++ packages/cli/tight-firefly/vercel.json | 13 -- 8 files changed, 129 insertions(+), 64 deletions(-) create mode 100644 packages/cli/curly-sun/vercel.json create mode 100644 packages/cli/nameless-union/vercel.json rename packages/cli/src/commands/create/hosting-config/{_headers => _headers.txt} (100%) rename packages/cli/src/commands/create/hosting-config/{netlify.toml => netlify_toml.txt} (100%) create mode 100644 packages/cli/super-math/vercel.json delete mode 100644 packages/cli/tight-firefly/vercel.json diff --git a/packages/cli/curly-sun/vercel.json b/packages/cli/curly-sun/vercel.json new file mode 100644 index 000000000..0e49a5d0c --- /dev/null +++ b/packages/cli/curly-sun/vercel.json @@ -0,0 +1,17 @@ +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-crop" + }, + { + "key": "Cross-Origin-Embedder-Policy", + "value": "same-origin" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/cli/nameless-union/vercel.json b/packages/cli/nameless-union/vercel.json new file mode 100644 index 000000000..0e49a5d0c --- /dev/null +++ b/packages/cli/nameless-union/vercel.json @@ -0,0 +1,17 @@ +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-crop" + }, + { + "key": "Cross-Origin-Embedder-Policy", + "value": "same-origin" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index 9f4f62223..744c81df9 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -1,52 +1,79 @@ -/* eslint-disable prettier/prettier */ import fs from 'node:fs'; import path from 'node:path'; -import cloudflareConfigFile from './hosting-config/_headers?raw'; -import netlifyConfigFile from './hosting-config/netlify.toml?raw'; -import vercelConfigFile from './hosting-config/vercel.json?raw'; - -export async function generateHostingConfig(dest: string, providers: string[]) { - const resolvedDest = path.resolve(dest); - - if (!fs.existsSync(resolvedDest)) { - console.log(`Directory does not exist. Creating directory: ${resolvedDest}`); - fs.mkdirSync(resolvedDest, { recursive: true }); - } else { - console.log(`Directory already exists: ${resolvedDest}`); - } - - if (providers.includes('Vercel')) { - const vercelConfigPath = path.join(resolvedDest, 'vercel.json'); - console.log('Writing Vercel config file to:', vercelConfigPath); - fs.writeFileSync(vercelConfigPath, vercelConfigFile); - } - - if (providers.includes('Netlify')) { - const netlifyConfigPath = path.join(resolvedDest, 'netlify.toml'); - console.log('Writing Netlify config file to:', netlifyConfigPath); - fs.writeFileSync(netlifyConfigPath, netlifyConfigFile); +import { fileURLToPath } from 'node:url'; +import cloudflareConfigRaw from './hosting-config/_headers.txt?raw'; +import netlifyConfigRaw from './hosting-config/netlify_toml.txt?raw'; +import vercelConfigRaw from './hosting-config/vercel.json?raw'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export async function generateHostingConfig(dest: string, provider: string) { + const resolvedDest = path.resolve(dest); + + if (!fs.existsSync(resolvedDest)) { + console.log(`Directory does not exist. Creating directory: ${resolvedDest}`); + fs.mkdirSync(resolvedDest, { recursive: true }); + } else { + console.log(`Directory already exists: ${resolvedDest}`); + } + + if (provider.includes('Vercel')) { + const vercelConfigPath = path.join(resolvedDest, 'vercel.json'); + console.log('Writing Vercel config file to:', vercelConfigPath); + + try { + const vercelConfig = typeof vercelConfigRaw === 'string' ? JSON.parse(vercelConfigRaw) : vercelConfigRaw; + fs.writeFileSync(vercelConfigPath, JSON.stringify(vercelConfig, null, 2)); + } catch (error) { + console.error('Failed to write Vercel config file:', error); } - - if (providers.includes('Cloudflare')) { - const cloudflareConfigPath = path.join(resolvedDest, '_headers'); - console.log('Writing Cloudflare config file to:', cloudflareConfigPath); - fs.writeFileSync(cloudflareConfigPath, cloudflareConfigFile); + } + + if (provider.includes('Netlify')) { + const netlifyConfigPath = path.join(resolvedDest, 'netlify.toml'); + console.log('Writing Netlify config file to:', netlifyConfigPath); + + try { + if (typeof netlifyConfigRaw !== 'string') { + throw new Error('Netlify config must be a string.'); + } + + fs.writeFileSync(netlifyConfigPath, netlifyConfigRaw); + } catch (error) { + console.error('Failed to write Netlify config file:', error); } - - const templateDir = path.resolve(__dirname, '_template'); - console.log('Looking for template directory at:', templateDir); - - if (fs.existsSync(templateDir)) { - const gitignoreTemplatePath = path.join(templateDir, '.gitignore'); - - if (fs.existsSync(gitignoreTemplatePath)) { - const gitignoreDestPath = path.join(resolvedDest, '.gitignore'); - console.log('Copying .gitignore to:', gitignoreDestPath); - fs.copyFileSync(gitignoreTemplatePath, gitignoreDestPath); - } else { - console.warn('No .gitignore file found in template directory, skipping copy.'); + } + + if (provider.includes('Cloudflare')) { + const cloudflareConfigPath = path.join(resolvedDest, '_headers'); + console.log('Writing Cloudflare config file to:', cloudflareConfigPath); + + try { + if (typeof cloudflareConfigRaw !== 'string') { + throw new Error('Cloudflare config must be a string.'); } + + fs.writeFileSync(cloudflareConfigPath, cloudflareConfigRaw); + } catch (error) { + console.error('Failed to write Cloudflare config file:', error); + } + } + + const templateDir = path.resolve(__dirname, '_template'); + console.log('Looking for template directory at:', templateDir); + + if (fs.existsSync(templateDir)) { + const gitignoreTemplatePath = path.join(templateDir, '.gitignore'); + + if (fs.existsSync(gitignoreTemplatePath)) { + const gitignoreDestPath = path.join(resolvedDest, '.gitignore'); + console.log('Copying .gitignore to:', gitignoreDestPath); + fs.copyFileSync(gitignoreTemplatePath, gitignoreDestPath); } else { - console.warn('Template directory does not exist, skipping .gitignore copy.'); + console.warn('No .gitignore file found in template directory, skipping copy.'); } - } \ No newline at end of file + } else { + console.warn('Template directory does not exist, skipping .gitignore copy.'); + } +} diff --git a/packages/cli/src/commands/create/hosting-config/_headers b/packages/cli/src/commands/create/hosting-config/_headers.txt similarity index 100% rename from packages/cli/src/commands/create/hosting-config/_headers rename to packages/cli/src/commands/create/hosting-config/_headers.txt diff --git a/packages/cli/src/commands/create/hosting-config/netlify.toml b/packages/cli/src/commands/create/hosting-config/netlify_toml.txt similarity index 100% rename from packages/cli/src/commands/create/hosting-config/netlify.toml rename to packages/cli/src/commands/create/hosting-config/netlify_toml.txt diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 9c01139d7..c7ba792d2 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -108,20 +108,20 @@ async function _createTutorial(flags: CreateOptions): Promise { const dest = await getTutorialDirectory(tutorialName, flags); const resolvedDest = path.resolve(process.cwd(), dest); - const providers = await prompts.multiselect({ - message: 'Select hosting providers for automatic configuration (use space to select):', + const provider = await prompts.select({ + message: 'Select hosting providers for automatic configuration:', options: [ { value: 'Vercel', label: 'Vercel' }, { value: 'Netlify', label: 'Netlify' }, { value: 'Cloudflare', label: 'Cloudflare' }, ], - initialValues: [], + initialValue: 'Vercel', }); - assertNotCanceled(providers); - prompts.log.info(`Configuring for: ${providers.join(', ')}`); + assertNotCanceled(provider); + prompts.log.info(`Configuring for: ${provider}`); - await generateHostingConfig(resolvedDest, providers); + await generateHostingConfig(resolvedDest, provider); await copyTemplate(resolvedDest, flags); updatePackageJson(resolvedDest, tutorialName, flags); diff --git a/packages/cli/super-math/vercel.json b/packages/cli/super-math/vercel.json new file mode 100644 index 000000000..0e49a5d0c --- /dev/null +++ b/packages/cli/super-math/vercel.json @@ -0,0 +1,17 @@ +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-crop" + }, + { + "key": "Cross-Origin-Embedder-Policy", + "value": "same-origin" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/cli/tight-firefly/vercel.json b/packages/cli/tight-firefly/vercel.json deleted file mode 100644 index c967757fa..000000000 --- a/packages/cli/tight-firefly/vercel.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "key": "Access-Control-Allow-Origin", - "value": "*" - } - ] - } - ] -} \ No newline at end of file From 686174d4d72bf9f4456f5d00d5c0df469e887ad3 Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Wed, 29 Jan 2025 17:30:46 -0600 Subject: [PATCH 05/18] feat(cli): adjusted the order in which files are generated to avoid errors --- packages/cli/src/commands/create/index.ts | 33 +++++++++++------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index c7ba792d2..92d50a6a7 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -108,24 +108,6 @@ async function _createTutorial(flags: CreateOptions): Promise { const dest = await getTutorialDirectory(tutorialName, flags); const resolvedDest = path.resolve(process.cwd(), dest); - const provider = await prompts.select({ - message: 'Select hosting providers for automatic configuration:', - options: [ - { value: 'Vercel', label: 'Vercel' }, - { value: 'Netlify', label: 'Netlify' }, - { value: 'Cloudflare', label: 'Cloudflare' }, - ], - initialValue: 'Vercel', - }); - - assertNotCanceled(provider); - prompts.log.info(`Configuring for: ${provider}`); - - await generateHostingConfig(resolvedDest, provider); - - await copyTemplate(resolvedDest, flags); - updatePackageJson(resolvedDest, tutorialName, flags); - prompts.log.info(`Scaffolding tutorial in ${chalk.blue(resolvedDest)}`); if (fs.existsSync(resolvedDest) && !flags.force) { @@ -162,6 +144,21 @@ async function _createTutorial(flags: CreateOptions): Promise { await copyTemplate(resolvedDest, flags); + const provider = await prompts.select({ + message: 'Select hosting providers for automatic configuration:', + options: [ + { value: 'Vercel', label: 'Vercel' }, + { value: 'Netlify', label: 'Netlify' }, + { value: 'Cloudflare', label: 'Cloudflare' }, + ], + initialValue: 'Vercel', + }); + + assertNotCanceled(provider); + prompts.log.info(`Configuring for: ${provider}`); + + await generateHostingConfig(resolvedDest, provider); + updatePackageJson(resolvedDest, tutorialName, flags); const selectedPackageManager = await selectPackageManager(resolvedDest, flags); From b4247e1107468642fa74b33b569e0bde5e264aac Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Thu, 6 Feb 2025 16:07:40 -0600 Subject: [PATCH 06/18] deleted tutorials that were made during testing --- packages/cli/curly-sun/vercel.json | 17 ----------------- packages/cli/nameless-union/vercel.json | 17 ----------------- packages/cli/super-math/vercel.json | 17 ----------------- 3 files changed, 51 deletions(-) delete mode 100644 packages/cli/curly-sun/vercel.json delete mode 100644 packages/cli/nameless-union/vercel.json delete mode 100644 packages/cli/super-math/vercel.json diff --git a/packages/cli/curly-sun/vercel.json b/packages/cli/curly-sun/vercel.json deleted file mode 100644 index 0e49a5d0c..000000000 --- a/packages/cli/curly-sun/vercel.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "key": "Cross-Origin-Embedder-Policy", - "value": "require-crop" - }, - { - "key": "Cross-Origin-Embedder-Policy", - "value": "same-origin" - } - ] - } - ] -} \ No newline at end of file diff --git a/packages/cli/nameless-union/vercel.json b/packages/cli/nameless-union/vercel.json deleted file mode 100644 index 0e49a5d0c..000000000 --- a/packages/cli/nameless-union/vercel.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "key": "Cross-Origin-Embedder-Policy", - "value": "require-crop" - }, - { - "key": "Cross-Origin-Embedder-Policy", - "value": "same-origin" - } - ] - } - ] -} \ No newline at end of file diff --git a/packages/cli/super-math/vercel.json b/packages/cli/super-math/vercel.json deleted file mode 100644 index 0e49a5d0c..000000000 --- a/packages/cli/super-math/vercel.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "key": "Cross-Origin-Embedder-Policy", - "value": "require-crop" - }, - { - "key": "Cross-Origin-Embedder-Policy", - "value": "same-origin" - } - ] - } - ] -} \ No newline at end of file From 4df1fc6f52fedb3545c0b3c19547840f974d0f1b Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Thu, 27 Feb 2025 17:23:18 -0600 Subject: [PATCH 07/18] fix(cli): add 'skip' option and update defaults for hosting config close #234 --- package.json | 3 +- .../create/generate-hosting-config.ts | 91 +++++++------------ packages/cli/src/commands/create/index.ts | 19 ++-- packages/cli/src/commands/create/options.ts | 1 + 4 files changed, 44 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index bae972418..f63d0eac9 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "demo:build": "pnpm run build && pnpm run --filter=demo.tutorialkit.dev build", "lint": "eslint \"{packages,docs,extensions,integration}/**/*\"", "test": "pnpm run --stream --filter='@tutorialkit/*' test --run", - "test:e2e": "pnpm run --filter='./e2e' test", - "postbuild": "cp _headers ./dist/" + "test:e2e": "pnpm run --filter='./e2e' test" }, "license": "MIT", "packageManager": "pnpm@8.15.6", diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index 744c81df9..3f76056b9 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -1,79 +1,50 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import * as prompts from '@clack/prompts'; +import chalk from 'chalk'; +import { warnLabel } from 'src/utils/messages.js'; +import { runTask } from 'src/utils/tasks.js'; import cloudflareConfigRaw from './hosting-config/_headers.txt?raw'; import netlifyConfigRaw from './hosting-config/netlify_toml.txt?raw'; import vercelConfigRaw from './hosting-config/vercel.json?raw'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +export async function generateHostingConfig(dest: string, provider: string, flags: { dryRun: boolean }) { + prompts.log.info(`${chalk.blue('Hosting Configuration')} Setting up configuration for ${provider}`); -export async function generateHostingConfig(dest: string, provider: string) { const resolvedDest = path.resolve(dest); if (!fs.existsSync(resolvedDest)) { - console.log(`Directory does not exist. Creating directory: ${resolvedDest}`); fs.mkdirSync(resolvedDest, { recursive: true }); - } else { - console.log(`Directory already exists: ${resolvedDest}`); } - if (provider.includes('Vercel')) { - const vercelConfigPath = path.join(resolvedDest, 'vercel.json'); - console.log('Writing Vercel config file to:', vercelConfigPath); - - try { - const vercelConfig = typeof vercelConfigRaw === 'string' ? JSON.parse(vercelConfigRaw) : vercelConfigRaw; - fs.writeFileSync(vercelConfigPath, JSON.stringify(vercelConfig, null, 2)); - } catch (error) { - console.error('Failed to write Vercel config file:', error); - } - } + let config; + let filename; - if (provider.includes('Netlify')) { - const netlifyConfigPath = path.join(resolvedDest, 'netlify.toml'); - console.log('Writing Netlify config file to:', netlifyConfigPath); - - try { - if (typeof netlifyConfigRaw !== 'string') { - throw new Error('Netlify config must be a string.'); - } - - fs.writeFileSync(netlifyConfigPath, netlifyConfigRaw); - } catch (error) { - console.error('Failed to write Netlify config file:', error); - } - } - - if (provider.includes('Cloudflare')) { - const cloudflareConfigPath = path.join(resolvedDest, '_headers'); - console.log('Writing Cloudflare config file to:', cloudflareConfigPath); - - try { - if (typeof cloudflareConfigRaw !== 'string') { - throw new Error('Cloudflare config must be a string.'); - } - - fs.writeFileSync(cloudflareConfigPath, cloudflareConfigRaw); - } catch (error) { - console.error('Failed to write Cloudflare config file:', error); - } + if (provider.includes('Vercel')) { + config = typeof vercelConfigRaw === 'string' ? vercelConfigRaw : JSON.stringify(vercelConfigRaw, null, 2); + filename = 'vercel.json'; + } else if (provider.includes('Netlify')) { + config = netlifyConfigRaw; + filename = 'netlify.toml'; + } else if (provider.includes('Cloudflare')) { + config = cloudflareConfigRaw; + filename = '_headers'; } - const templateDir = path.resolve(__dirname, '_template'); - console.log('Looking for template directory at:', templateDir); - - if (fs.existsSync(templateDir)) { - const gitignoreTemplatePath = path.join(templateDir, '.gitignore'); - - if (fs.existsSync(gitignoreTemplatePath)) { - const gitignoreDestPath = path.join(resolvedDest, '.gitignore'); - console.log('Copying .gitignore to:', gitignoreDestPath); - fs.copyFileSync(gitignoreTemplatePath, gitignoreDestPath); - } else { - console.warn('No .gitignore file found in template directory, skipping copy.'); - } + if (config && filename) { + await runTask({ + title: `Create hosting files for ${provider}`, + dryRun: flags.dryRun, + dryRunMessage: `${warnLabel('DRY RUN')} Skipped hosting provider config creation`, + task: async () => { + const filepath = path.join(resolvedDest, filename); + fs.writeFileSync(filepath, config); + return `Added ${filepath}`; + }, + }); } else { - console.warn('Template directory does not exist, skipping .gitignore copy.'); + prompts.log.message( + `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later. For more information see https://tutorialkit.dev/guides/deployment/#headers-configuration` + ); } } diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 92d50a6a7..122d31ce6 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -150,16 +150,14 @@ async function _createTutorial(flags: CreateOptions): Promise { { value: 'Vercel', label: 'Vercel' }, { value: 'Netlify', label: 'Netlify' }, { value: 'Cloudflare', label: 'Cloudflare' }, + { value: 'skip', label: 'Skip hosting configuration' }, ], initialValue: 'Vercel', }); - assertNotCanceled(provider); - prompts.log.info(`Configuring for: ${provider}`); + await generateHostingConfig(resolvedDest, String(provider), { dryRun: flags.dryRun }); - await generateHostingConfig(resolvedDest, provider); - - updatePackageJson(resolvedDest, tutorialName, flags); + updatePackageJson(resolvedDest, tutorialName, flags, String(provider)); const selectedPackageManager = await selectPackageManager(resolvedDest, flags); @@ -264,7 +262,7 @@ function printNextSteps(dest: string, packageManager: PackageManager, dependenci } } -function updatePackageJson(dest: string, projectName: string, flags: CreateOptions) { +function updatePackageJson(dest: string, projectName: string, flags: CreateOptions, provider: string) { if (flags.dryRun) { return; } @@ -277,7 +275,12 @@ function updatePackageJson(dest: string, projectName: string, flags: CreateOptio updateWorkspaceVersions(pkgJson.dependencies, TUTORIALKIT_VERSION); updateWorkspaceVersions(pkgJson.devDependencies, TUTORIALKIT_VERSION); - fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, undefined, 2)); + if (provider === 'Netlify' || provider === 'Cloudflare') { + pkgJson.scripts = pkgJson.scripts || {}; + pkgJson.scripts.postbuild = "cp _headers ./dist/"; + } + + fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2)); try { const pkgLockPath = path.resolve(dest, 'package-lock.json'); @@ -290,7 +293,7 @@ function updatePackageJson(dest: string, projectName: string, flags: CreateOptio defaultPackage.name = projectName; } - fs.writeFileSync(pkgLockPath, JSON.stringify(pkgLockJson, undefined, 2)); + fs.writeFileSync(pkgLockPath, JSON.stringify(pkgLockJson, null, 2)); } catch { // ignore any errors } diff --git a/packages/cli/src/commands/create/options.ts b/packages/cli/src/commands/create/options.ts index ae19d4ebf..dfeb8408a 100644 --- a/packages/cli/src/commands/create/options.ts +++ b/packages/cli/src/commands/create/options.ts @@ -25,6 +25,7 @@ export const DEFAULT_VALUES = { dryRun: false, force: false, packageManager: 'npm', + provider: 'skip', }; type Flags = Omit; From edd717d905612218c667de8cd1780292b5ebb08b Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Tue, 4 Mar 2025 19:20:44 -0600 Subject: [PATCH 08/18] chore(prettier): fix formatting errors Fixes Prettier errors to pass tests and ensure consistent code style. close #234 --- packages/cli/src/commands/create/generate-hosting-config.ts | 3 ++- packages/cli/src/commands/create/index.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index 3f76056b9..5730b4976 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -39,12 +39,13 @@ export async function generateHostingConfig(dest: string, provider: string, flag task: async () => { const filepath = path.join(resolvedDest, filename); fs.writeFileSync(filepath, config); + return `Added ${filepath}`; }, }); } else { prompts.log.message( - `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later. For more information see https://tutorialkit.dev/guides/deployment/#headers-configuration` + `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later. For more information see https://tutorialkit.dev/guides/deployment/#headers-configuration`, ); } } diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 122d31ce6..5d3540a36 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -277,7 +277,7 @@ function updatePackageJson(dest: string, projectName: string, flags: CreateOptio if (provider === 'Netlify' || provider === 'Cloudflare') { pkgJson.scripts = pkgJson.scripts || {}; - pkgJson.scripts.postbuild = "cp _headers ./dist/"; + pkgJson.scripts.postbuild = 'cp _headers ./dist/'; } fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2)); From af74cd9cd86eba50b03c3d8c436af627a34a613b Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Wed, 12 Mar 2025 15:25:25 -0500 Subject: [PATCH 09/18] feat(cli): add provider flag to hosting config prompt closes #234 --- .../create/generate-hosting-config.ts | 29 +++++++++++++++---- packages/cli/src/commands/create/index.ts | 3 +- packages/cli/tests/create-tutorial.test.ts | 10 +++---- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index 5730b4976..445fd10f4 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -4,11 +4,34 @@ import * as prompts from '@clack/prompts'; import chalk from 'chalk'; import { warnLabel } from 'src/utils/messages.js'; import { runTask } from 'src/utils/tasks.js'; +import { DEFAULT_VALUES, readFlag } from './options.js'; import cloudflareConfigRaw from './hosting-config/_headers.txt?raw'; import netlifyConfigRaw from './hosting-config/netlify_toml.txt?raw'; import vercelConfigRaw from './hosting-config/vercel.json?raw'; -export async function generateHostingConfig(dest: string, provider: string, flags: { dryRun: boolean }) { +export async function generateHostingConfig(dest: string, flags: { dryRun: boolean, provider?: string }) { + let provider = readFlag(flags, 'provider' as any); + + if (provider === undefined) { + provider = await prompts.select({ + message: 'Select hosting providers for automatic configuration:', + options: [ + { value: 'Vercel', label: 'Vercel' }, + { value: 'Netlify', label: 'Netlify' }, + { value: 'Cloudflare', label: 'Cloudflare' }, + { value: 'skip', label: 'Skip hosting configuration' }, + ], + initialValue: DEFAULT_VALUES.provider, + }); + } + + if (provider === 'skip') { + prompts.log.message( + `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later. For more information see https://tutorialkit.dev/guides/deployment/#headers-configuration`, + ); + return; + } + prompts.log.info(`${chalk.blue('Hosting Configuration')} Setting up configuration for ${provider}`); const resolvedDest = path.resolve(dest); @@ -43,9 +66,5 @@ export async function generateHostingConfig(dest: string, provider: string, flag return `Added ${filepath}`; }, }); - } else { - prompts.log.message( - `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later. For more information see https://tutorialkit.dev/guides/deployment/#headers-configuration`, - ); } } diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 5d3540a36..9ada3ea35 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -30,6 +30,7 @@ export async function createTutorial(flags: yargs.Arguments) { ['--install, --no-install', `Install dependencies (default ${chalk.yellow(DEFAULT_VALUES.install)})`], ['--start, --no-start', `Start project (default ${chalk.yellow(DEFAULT_VALUES.start)})`], ['--git, --no-git', `Initialize a local git repository (default ${chalk.yellow(DEFAULT_VALUES.git)})`], + ['--provider , --no-provider', `Select a hosting provider (default ${chalk.yellow(DEFAULT_VALUES.provider)})`], ['--dry-run', `Walk through steps without executing (default ${chalk.yellow(DEFAULT_VALUES.dryRun)})`], [ '--package-manager , -p ', @@ -155,7 +156,7 @@ async function _createTutorial(flags: CreateOptions): Promise { initialValue: 'Vercel', }); - await generateHostingConfig(resolvedDest, String(provider), { dryRun: flags.dryRun }); + await generateHostingConfig(resolvedDest, { dryRun: flags.dryRun, provider: String(provider) }); updatePackageJson(resolvedDest, tutorialName, flags, String(provider)); diff --git a/packages/cli/tests/create-tutorial.test.ts b/packages/cli/tests/create-tutorial.test.ts index 9b4ef2f5f..6fcbb7451 100644 --- a/packages/cli/tests/create-tutorial.test.ts +++ b/packages/cli/tests/create-tutorial.test.ts @@ -30,7 +30,7 @@ test('cannot create project without installing but with starting', async (contex const name = context.task.id; await expect( - execa('node', [cli, 'create', name, '--no-install', '--start'], { + execa('node', [cli, 'create', name, '--no-install', '--no-provider', '--start'], { cwd: tmpDir, }), ).rejects.toThrow('Cannot start project without installing dependencies.'); @@ -40,7 +40,7 @@ test('create a project', async (context) => { const name = context.task.id; const dest = path.join(tmpDir, name); - await execa('node', [cli, 'create', name, '--no-install', '--no-git', '--defaults'], { + await execa('node', [cli, 'create', name, '--no-install', '--no-git', '--no-provider', '--defaults'], { cwd: tmpDir, }); @@ -53,7 +53,7 @@ test('create and build a project', async (context) => { const name = context.task.id; const dest = path.join(tmpDir, name); - await execa('node', [cli, 'create', name, '--no-git', '--no-install', '--no-start', '--defaults'], { + await execa('node', [cli, 'create', name, '--no-git', '--no-install', '--no-start', '--no-provider', '--defaults'], { cwd: tmpDir, }); @@ -89,7 +89,7 @@ test('create and eject a project', async (context) => { const name = context.task.id; const dest = path.join(tmpDir, name); - await execa('node', [cli, 'create', name, '--no-git', '--no-install', '--no-start', '--defaults'], { + await execa('node', [cli, 'create', name, '--no-git', '--no-install', '--no-start', '--no-provider', '--defaults'], { cwd: tmpDir, }); @@ -117,7 +117,7 @@ test('create, eject and build a project', async (context) => { const name = context.task.id; const dest = path.join(tmpDir, name); - await execa('node', [cli, 'create', name, '--no-git', '--no-install', '--no-start', '--defaults'], { + await execa('node', [cli, 'create', name, '--no-git', '--no-install', '--no-start', '--no-provider', '--defaults'], { cwd: tmpDir, }); From facf7d2d5988ce1e872e140d065608daf983dbf2 Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Thu, 13 Mar 2025 14:58:24 -0500 Subject: [PATCH 10/18] chore(prettier): fix formatting errors close #234 --- packages/cli/src/commands/create/generate-hosting-config.ts | 6 +++--- packages/cli/src/commands/create/index.ts | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index 445fd10f4..d1ab2b9e8 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -4,14 +4,14 @@ import * as prompts from '@clack/prompts'; import chalk from 'chalk'; import { warnLabel } from 'src/utils/messages.js'; import { runTask } from 'src/utils/tasks.js'; -import { DEFAULT_VALUES, readFlag } from './options.js'; import cloudflareConfigRaw from './hosting-config/_headers.txt?raw'; import netlifyConfigRaw from './hosting-config/netlify_toml.txt?raw'; import vercelConfigRaw from './hosting-config/vercel.json?raw'; +import { DEFAULT_VALUES, readFlag } from './options.js'; -export async function generateHostingConfig(dest: string, flags: { dryRun: boolean, provider?: string }) { +export async function generateHostingConfig(dest: string, flags: { dryRun: boolean; provider?: string }) { let provider = readFlag(flags, 'provider' as any); - + if (provider === undefined) { provider = await prompts.select({ message: 'Select hosting providers for automatic configuration:', diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 9ada3ea35..a341d6349 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -30,7 +30,10 @@ export async function createTutorial(flags: yargs.Arguments) { ['--install, --no-install', `Install dependencies (default ${chalk.yellow(DEFAULT_VALUES.install)})`], ['--start, --no-start', `Start project (default ${chalk.yellow(DEFAULT_VALUES.start)})`], ['--git, --no-git', `Initialize a local git repository (default ${chalk.yellow(DEFAULT_VALUES.git)})`], - ['--provider , --no-provider', `Select a hosting provider (default ${chalk.yellow(DEFAULT_VALUES.provider)})`], + [ + '--provider , --no-provider', + `Select a hosting provider (default ${chalk.yellow(DEFAULT_VALUES.provider)})`, + ], ['--dry-run', `Walk through steps without executing (default ${chalk.yellow(DEFAULT_VALUES.dryRun)})`], [ '--package-manager , -p ', From 7c45e6e69df82b4569bd9d47c0e967ad80889088 Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Wed, 19 Mar 2025 17:12:57 -0500 Subject: [PATCH 11/18] fix(cli): ensure --no-provider skips hosting config prompt closes #234 --- .../create/generate-hosting-config.ts | 45 ++++++++++--------- packages/cli/src/commands/create/index.ts | 13 +----- packages/cli/src/commands/create/options.ts | 1 + 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index d1ab2b9e8..e7a7202b2 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -7,13 +7,13 @@ import { runTask } from 'src/utils/tasks.js'; import cloudflareConfigRaw from './hosting-config/_headers.txt?raw'; import netlifyConfigRaw from './hosting-config/netlify_toml.txt?raw'; import vercelConfigRaw from './hosting-config/vercel.json?raw'; -import { DEFAULT_VALUES, readFlag } from './options.js'; +import { DEFAULT_VALUES, readFlag, type CreateOptions } from './options.js'; -export async function generateHostingConfig(dest: string, flags: { dryRun: boolean; provider?: string }) { - let provider = readFlag(flags, 'provider' as any); +export async function generateHostingConfig(dest: string, flags: CreateOptions) { + let provider = readFlag(flags, 'provider'); if (provider === undefined) { - provider = await prompts.select({ + provider = (await prompts.select({ message: 'Select hosting providers for automatic configuration:', options: [ { value: 'Vercel', label: 'Vercel' }, @@ -22,36 +22,39 @@ export async function generateHostingConfig(dest: string, flags: { dryRun: boole { value: 'skip', label: 'Skip hosting configuration' }, ], initialValue: DEFAULT_VALUES.provider, - }); + })) as string; } if (provider === 'skip') { prompts.log.message( - `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later. For more information see https://tutorialkit.dev/guides/deployment/#headers-configuration`, + `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later.` ); - return; + return provider; } prompts.log.info(`${chalk.blue('Hosting Configuration')} Setting up configuration for ${provider}`); const resolvedDest = path.resolve(dest); - if (!fs.existsSync(resolvedDest)) { fs.mkdirSync(resolvedDest, { recursive: true }); } - let config; - let filename; + let config: string | undefined; + let filename: string | undefined; - if (provider.includes('Vercel')) { - config = typeof vercelConfigRaw === 'string' ? vercelConfigRaw : JSON.stringify(vercelConfigRaw, null, 2); - filename = 'vercel.json'; - } else if (provider.includes('Netlify')) { - config = netlifyConfigRaw; - filename = 'netlify.toml'; - } else if (provider.includes('Cloudflare')) { - config = cloudflareConfigRaw; - filename = '_headers'; + switch (provider) { + case 'Vercel': + config = typeof vercelConfigRaw === 'string' ? vercelConfigRaw : JSON.stringify(vercelConfigRaw, null, 2); + filename = 'vercel.json'; + break; + case 'Netlify': + config = netlifyConfigRaw; + filename = 'netlify.toml'; + break; + case 'Cloudflare': + config = cloudflareConfigRaw; + filename = '_headers'; + break; } if (config && filename) { @@ -62,9 +65,11 @@ export async function generateHostingConfig(dest: string, flags: { dryRun: boole task: async () => { const filepath = path.join(resolvedDest, filename); fs.writeFileSync(filepath, config); - return `Added ${filepath}`; }, }); } + + return provider; } + diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index a341d6349..468681018 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -148,18 +148,7 @@ async function _createTutorial(flags: CreateOptions): Promise { await copyTemplate(resolvedDest, flags); - const provider = await prompts.select({ - message: 'Select hosting providers for automatic configuration:', - options: [ - { value: 'Vercel', label: 'Vercel' }, - { value: 'Netlify', label: 'Netlify' }, - { value: 'Cloudflare', label: 'Cloudflare' }, - { value: 'skip', label: 'Skip hosting configuration' }, - ], - initialValue: 'Vercel', - }); - - await generateHostingConfig(resolvedDest, { dryRun: flags.dryRun, provider: String(provider) }); + const provider = await generateHostingConfig(resolvedDest, flags); updatePackageJson(resolvedDest, tutorialName, flags, String(provider)); diff --git a/packages/cli/src/commands/create/options.ts b/packages/cli/src/commands/create/options.ts index dfeb8408a..c5be2f758 100644 --- a/packages/cli/src/commands/create/options.ts +++ b/packages/cli/src/commands/create/options.ts @@ -12,6 +12,7 @@ export interface CreateOptions { defaults?: boolean; packageManager?: string; force?: boolean; + provider?: string; } const __dirname = path.dirname(fileURLToPath(import.meta.url)); From bbc7ec988385ff65779103337f58a42cdaa8b8ca Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Wed, 19 Mar 2025 17:18:29 -0500 Subject: [PATCH 12/18] chore(prettier): fix formatting errors close #234 --- .../create/generate-hosting-config.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index e7a7202b2..8c6cb899f 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -27,14 +27,20 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) if (provider === 'skip') { prompts.log.message( - `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later.` + `${chalk.blue( + 'hosting provider config [skip]' + )} You can configure hosting provider settings manually later.` ); + return provider; } - prompts.log.info(`${chalk.blue('Hosting Configuration')} Setting up configuration for ${provider}`); + prompts.log.info( + `${chalk.blue('Hosting Configuration')} Setting up configuration for ${provider}` + ); const resolvedDest = path.resolve(dest); + if (!fs.existsSync(resolvedDest)) { fs.mkdirSync(resolvedDest, { recursive: true }); } @@ -43,18 +49,24 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) let filename: string | undefined; switch (provider) { - case 'Vercel': - config = typeof vercelConfigRaw === 'string' ? vercelConfigRaw : JSON.stringify(vercelConfigRaw, null, 2); + case 'Vercel': { + config = + typeof vercelConfigRaw === 'string' + ? vercelConfigRaw + : JSON.stringify(vercelConfigRaw, null, 2); filename = 'vercel.json'; break; - case 'Netlify': + } + case 'Netlify': { config = netlifyConfigRaw; filename = 'netlify.toml'; break; - case 'Cloudflare': + } + case 'Cloudflare': { config = cloudflareConfigRaw; filename = '_headers'; break; + } } if (config && filename) { @@ -72,4 +84,3 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) return provider; } - From 13103d9fe5813f58c1f9bb0739f3c4e6cc155d7b Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Wed, 19 Mar 2025 17:24:42 -0500 Subject: [PATCH 13/18] chore(prettier): fix formatting errors close #234 --- .../src/commands/create/generate-hosting-config.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index 8c6cb899f..e5e53d6a9 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -27,17 +27,13 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) if (provider === 'skip') { prompts.log.message( - `${chalk.blue( - 'hosting provider config [skip]' - )} You can configure hosting provider settings manually later.` + `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later.`, ); return provider; } - prompts.log.info( - `${chalk.blue('Hosting Configuration')} Setting up configuration for ${provider}` - ); + prompts.log.info(`${chalk.blue('Hosting Configuration')} Setting up configuration for ${provider}`); const resolvedDest = path.resolve(dest); @@ -50,10 +46,7 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) switch (provider) { case 'Vercel': { - config = - typeof vercelConfigRaw === 'string' - ? vercelConfigRaw - : JSON.stringify(vercelConfigRaw, null, 2); + config = typeof vercelConfigRaw === 'string' ? vercelConfigRaw : JSON.stringify(vercelConfigRaw, null, 2); filename = 'vercel.json'; break; } @@ -77,6 +70,7 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) task: async () => { const filepath = path.join(resolvedDest, filename); fs.writeFileSync(filepath, config); + return `Added ${filepath}`; }, }); From 675c5fbcd2830e17e076eac133ad8127223c41e1 Mon Sep 17 00:00:00 2001 From: RonithManikonda <111534854+RonithManikonda@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:07:05 -0500 Subject: [PATCH 14/18] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ari Perkkiƶ --- .../cli/src/commands/create/generate-hosting-config.ts | 10 +++++----- packages/cli/src/commands/create/index.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index e5e53d6a9..e35e3a378 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -25,7 +25,7 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) })) as string; } - if (provider === 'skip') { + if (!provider || provider === 'skip') { prompts.log.message( `${chalk.blue('hosting provider config [skip]')} You can configure hosting provider settings manually later.`, ); @@ -44,18 +44,18 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) let config: string | undefined; let filename: string | undefined; - switch (provider) { - case 'Vercel': { + switch (provider.toLowerCase()) { + case 'vercel': { config = typeof vercelConfigRaw === 'string' ? vercelConfigRaw : JSON.stringify(vercelConfigRaw, null, 2); filename = 'vercel.json'; break; } - case 'Netlify': { + case 'netlify': { config = netlifyConfigRaw; filename = 'netlify.toml'; break; } - case 'Cloudflare': { + case 'cloudflare': { config = cloudflareConfigRaw; filename = '_headers'; break; diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index 468681018..df30c4a1a 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -268,7 +268,7 @@ function updatePackageJson(dest: string, projectName: string, flags: CreateOptio updateWorkspaceVersions(pkgJson.dependencies, TUTORIALKIT_VERSION); updateWorkspaceVersions(pkgJson.devDependencies, TUTORIALKIT_VERSION); - if (provider === 'Netlify' || provider === 'Cloudflare') { + if (provider.toLowerCase() === 'cloudflare') { pkgJson.scripts = pkgJson.scripts || {}; pkgJson.scripts.postbuild = 'cp _headers ./dist/'; } From a1771dbb16d0eea674a2c4f19a8947032952cdd7 Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Wed, 26 Mar 2025 16:18:41 -0500 Subject: [PATCH 15/18] test(cli): add tests for Netlify, Cloudflare, and Vercel providers --- packages/cli/tests/create-tutorial.test.ts | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/cli/tests/create-tutorial.test.ts b/packages/cli/tests/create-tutorial.test.ts index 6fcbb7451..40226ec7d 100644 --- a/packages/cli/tests/create-tutorial.test.ts +++ b/packages/cli/tests/create-tutorial.test.ts @@ -49,6 +49,49 @@ test('create a project', async (context) => { expect(projectFiles.map(normaliseSlash).sort()).toMatchSnapshot(); }); +test('create a project with Netlify as provider', async (context) => { + const name = context.task.id; + const dest = path.join(tmpDir, name); + + await execa('node', [cli, 'create', name, '--no-install', '--no-git', '--defaults', '--provider', 'netlify'], { + cwd: tmpDir, + }); + + const projectFiles = await fs.readdir(dest, { recursive: true }); + expect(projectFiles).toContain('netlify.toml'); +}); + +test('create a project with Cloudflare as provider', async (context) => { + const name = context.task.id; + const dest = path.join(tmpDir, name); + + await execa('node', [cli, 'create', name, '--no-install', '--no-git', '--defaults', '--provider', 'cloudflare'], { + cwd: tmpDir, + }); + + const projectFiles = await fs.readdir(dest, { recursive: true }); + expect(projectFiles).toContain('wrangler.toml'); + + const packageJson = await fs.readFile(`${dest}/package.json`, 'utf8'); + const json = JSON.parse(packageJson); + + expect(json).toHaveProperty('scripts'); + expect(json.scripts).toHaveProperty('postbuild'); + expect(json.scripts.postbuild).toBe('cp _headers ./dist/'); +}); + +test('create a project with Vercel as provider', async (context) => { + const name = context.task.id; + const dest = path.join(tmpDir, name); + + await execa('node', [cli, 'create', name, '--no-install', '--no-git', '--defaults', '--provider', 'vercel'], { + cwd: tmpDir, + }); + + const projectFiles = await fs.readdir(dest, { recursive: true }); + expect(projectFiles).toContain('vercel.json'); +}); + test('create and build a project', async (context) => { const name = context.task.id; const dest = path.join(tmpDir, name); From 8abfb9728e12794e1803686215ab75d082a5c947 Mon Sep 17 00:00:00 2001 From: RonithManikonda Date: Wed, 26 Mar 2025 16:33:12 -0500 Subject: [PATCH 16/18] test(cli): updated test for Cloudflare to reflect functionality --- packages/cli/tests/create-tutorial.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/tests/create-tutorial.test.ts b/packages/cli/tests/create-tutorial.test.ts index 40226ec7d..5ef22bf35 100644 --- a/packages/cli/tests/create-tutorial.test.ts +++ b/packages/cli/tests/create-tutorial.test.ts @@ -70,7 +70,7 @@ test('create a project with Cloudflare as provider', async (context) => { }); const projectFiles = await fs.readdir(dest, { recursive: true }); - expect(projectFiles).toContain('wrangler.toml'); + expect(projectFiles).toContain('_headers'); const packageJson = await fs.readFile(`${dest}/package.json`, 'utf8'); const json = JSON.parse(packageJson); From 9dcefcb74f99c3fd3c2ec88f3cf1b1231541cacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 31 Mar 2025 10:41:36 +0300 Subject: [PATCH 17/18] Update packages/cli/src/commands/create/index.ts --- packages/cli/src/commands/create/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/create/index.ts b/packages/cli/src/commands/create/index.ts index df30c4a1a..cd7fefbc1 100644 --- a/packages/cli/src/commands/create/index.ts +++ b/packages/cli/src/commands/create/index.ts @@ -150,7 +150,7 @@ async function _createTutorial(flags: CreateOptions): Promise { const provider = await generateHostingConfig(resolvedDest, flags); - updatePackageJson(resolvedDest, tutorialName, flags, String(provider)); + updatePackageJson(resolvedDest, tutorialName, flags, provider); const selectedPackageManager = await selectPackageManager(resolvedDest, flags); From 97a237d4cedf66cad6fc5f90730f66ea0adba3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 31 Mar 2025 10:49:05 +0300 Subject: [PATCH 18/18] fix: handle `--no-provider` value --- .../cli/src/commands/create/generate-hosting-config.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/create/generate-hosting-config.ts b/packages/cli/src/commands/create/generate-hosting-config.ts index e35e3a378..16f1dd594 100644 --- a/packages/cli/src/commands/create/generate-hosting-config.ts +++ b/packages/cli/src/commands/create/generate-hosting-config.ts @@ -10,10 +10,10 @@ import vercelConfigRaw from './hosting-config/vercel.json?raw'; import { DEFAULT_VALUES, readFlag, type CreateOptions } from './options.js'; export async function generateHostingConfig(dest: string, flags: CreateOptions) { - let provider = readFlag(flags, 'provider'); + let provider: string | false | symbol = readFlag(flags, 'provider'); if (provider === undefined) { - provider = (await prompts.select({ + provider = await prompts.select({ message: 'Select hosting providers for automatic configuration:', options: [ { value: 'Vercel', label: 'Vercel' }, @@ -22,7 +22,11 @@ export async function generateHostingConfig(dest: string, flags: CreateOptions) { value: 'skip', label: 'Skip hosting configuration' }, ], initialValue: DEFAULT_VALUES.provider, - })) as string; + }); + } + + if (typeof provider !== 'string') { + provider = 'skip'; } if (!provider || provider === 'skip') {