diff options
author | 2025-06-05 14:25:23 +0000 | |
---|---|---|
committer | 2025-06-05 14:25:23 +0000 | |
commit | e586d7d704d475afe3373a1de6ae20d504f79d6d (patch) | |
tree | 7e3fa24807cebd48a86bd40f866d792181191ee9 /packages/create-astro/src/actions | |
download | astro-e586d7d704d475afe3373a1de6ae20d504f79d6d.tar.gz astro-e586d7d704d475afe3373a1de6ae20d504f79d6d.tar.zst astro-e586d7d704d475afe3373a1de6ae20d504f79d6d.zip |
Sync from a8e1c0a7402940e0fc5beef669522b315052df1blatest
Diffstat (limited to 'packages/create-astro/src/actions')
-rw-r--r-- | packages/create-astro/src/actions/context.ts | 146 | ||||
-rw-r--r-- | packages/create-astro/src/actions/dependencies.ts | 109 | ||||
-rw-r--r-- | packages/create-astro/src/actions/git.ts | 64 | ||||
-rw-r--r-- | packages/create-astro/src/actions/help.ts | 24 | ||||
-rw-r--r-- | packages/create-astro/src/actions/intro.ts | 29 | ||||
-rw-r--r-- | packages/create-astro/src/actions/next-steps.ts | 25 | ||||
-rw-r--r-- | packages/create-astro/src/actions/project-name.ts | 74 | ||||
-rw-r--r-- | packages/create-astro/src/actions/shared.ts | 59 | ||||
-rw-r--r-- | packages/create-astro/src/actions/template.ts | 169 | ||||
-rw-r--r-- | packages/create-astro/src/actions/verify.ts | 40 |
10 files changed, 739 insertions, 0 deletions
diff --git a/packages/create-astro/src/actions/context.ts b/packages/create-astro/src/actions/context.ts new file mode 100644 index 000000000..ce4c067fd --- /dev/null +++ b/packages/create-astro/src/actions/context.ts @@ -0,0 +1,146 @@ +import os from 'node:os'; +import { type Task, prompt } from '@astrojs/cli-kit'; +import { random } from '@astrojs/cli-kit/utils'; +import arg from 'arg'; + +import getSeasonalData from '../data/seasonal.js'; +import { getName, getVersion } from '../messages.js'; + +export interface Context { + help: boolean; + prompt: typeof prompt; + cwd: string; + packageManager: string; + username: Promise<string>; + version: Promise<string>; + skipHouston: boolean; + fancy?: boolean; + add?: string[]; + dryRun?: boolean; + yes?: boolean; + projectName?: string; + template?: string; + ref: string; + install?: boolean; + git?: boolean; + typescript?: string; + stdin?: typeof process.stdin; + stdout?: typeof process.stdout; + exit(code: number): never; + welcome?: string; + hat?: string; + tie?: string; + tasks: Task[]; +} + +function getPackageTag(packageSpecifier: string | undefined): string | undefined { + switch (packageSpecifier) { + case 'alpha': + case 'beta': + case 'rc': + return packageSpecifier; + // Will fallback to latest + case undefined: + default: + return undefined; + } +} + +export async function getContext(argv: string[]): Promise<Context> { + const packageSpecifier = argv + .find((argItem) => /^(astro|create-astro)@/.exec(argItem)) + ?.split('@')[1]; + + const flags = arg( + { + '--template': String, + '--ref': String, + '--yes': Boolean, + '--no': Boolean, + '--install': Boolean, + '--no-install': Boolean, + '--git': Boolean, + '--no-git': Boolean, + '--skip-houston': Boolean, + '--dry-run': Boolean, + '--help': Boolean, + '--fancy': Boolean, + '--add': [String], + + '-y': '--yes', + '-n': '--no', + '-h': '--help', + }, + { argv, permissive: true }, + ); + + const packageManager = detectPackageManager() ?? 'npm'; + let cwd = flags['_'][0]; + let { + '--help': help = false, + '--template': template, + '--no': no, + '--yes': yes, + '--install': install, + '--no-install': noInstall, + '--git': git, + '--no-git': noGit, + '--fancy': fancy, + '--skip-houston': skipHouston, + '--dry-run': dryRun, + '--ref': ref, + '--add': add, + } = flags; + let projectName = cwd; + + if (no) { + yes = false; + if (install == undefined) install = false; + if (git == undefined) git = false; + } + + skipHouston = + ((os.platform() === 'win32' && !fancy) || skipHouston) ?? + [yes, no, install, git].some((v) => v !== undefined); + + const { messages, hats, ties } = getSeasonalData({ fancy }); + + const context: Context = { + help, + prompt, + packageManager, + username: getName(), + version: getVersion( + packageManager, + 'astro', + getPackageTag(packageSpecifier), + process.env.ASTRO_VERSION, + ), + skipHouston, + fancy, + add, + dryRun, + projectName, + template, + ref: ref ?? 'latest', + welcome: random(messages), + hat: hats ? random(hats) : undefined, + tie: ties ? random(ties) : undefined, + yes, + install: install ?? (noInstall ? false : undefined), + git: git ?? (noGit ? false : undefined), + cwd, + exit(code) { + process.exit(code); + }, + tasks: [], + }; + return context; +} + +function detectPackageManager() { + if (!process.env.npm_config_user_agent) return; + const specifier = process.env.npm_config_user_agent.split(' ')[0]; + const name = specifier.substring(0, specifier.lastIndexOf('/')); + return name === 'npminstall' ? 'cnpm' : name; +} diff --git a/packages/create-astro/src/actions/dependencies.ts b/packages/create-astro/src/actions/dependencies.ts new file mode 100644 index 000000000..d4990a8fb --- /dev/null +++ b/packages/create-astro/src/actions/dependencies.ts @@ -0,0 +1,109 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { color } from '@astrojs/cli-kit'; +import { error, info, title } from '../messages.js'; +import { shell } from '../shell.js'; +import type { Context } from './context.js'; + +export async function dependencies( + ctx: Pick< + Context, + 'install' | 'yes' | 'prompt' | 'packageManager' | 'cwd' | 'dryRun' | 'tasks' | 'add' + >, +) { + let deps = ctx.install ?? ctx.yes; + if (deps === undefined) { + ({ deps } = await ctx.prompt({ + name: 'deps', + type: 'confirm', + label: title('deps'), + message: `Install dependencies?`, + hint: 'recommended', + initial: true, + })); + ctx.install = deps; + } + + ctx.add = ctx.add?.reduce<string[]>((acc, item) => acc.concat(item.split(',')), []); + + if (ctx.dryRun) { + await info( + '--dry-run', + `Skipping dependency installation${ctx.add ? ` and adding ${ctx.add.join(', ')}` : ''}`, + ); + } else if (deps) { + ctx.tasks.push({ + pending: 'Dependencies', + start: `Dependencies installing with ${ctx.packageManager}...`, + end: 'Dependencies installed', + onError: (e) => { + error('error', e); + error( + 'error', + `Dependencies failed to install, please run ${color.bold( + ctx.packageManager + ' install', + )} to install them manually after setup.`, + ); + }, + while: () => install({ packageManager: ctx.packageManager, cwd: ctx.cwd }), + }); + + let add = ctx.add; + + if (add) { + ctx.tasks.push({ + pending: 'Integrations', + start: `Adding integrations with astro add`, + end: 'Integrations added', + onError: (e) => { + error('error', e); + error( + 'error', + `Failed to add integrations, please run ${color.bold( + `astro add ${add.join(' ')}`, + )} to install them manually after setup.`, + ); + }, + while: () => + astroAdd({ integrations: add, packageManager: ctx.packageManager, cwd: ctx.cwd }), + }); + } + } else { + await info( + ctx.yes === false ? 'deps [skip]' : 'No problem!', + 'Remember to install dependencies after setup.', + ); + } +} + +async function astroAdd({ + integrations, + packageManager, + cwd, +}: { integrations: string[]; packageManager: string; cwd: string }) { + if (packageManager === 'yarn') await ensureYarnLock({ cwd }); + return shell( + packageManager === 'npm' ? 'npx' : `${packageManager} dlx`, + ['astro add', integrations.join(' '), '-y'], + { cwd, timeout: 90_000, stdio: 'ignore' }, + ); +} + +async function install({ packageManager, cwd }: { packageManager: string; cwd: string }) { + if (packageManager === 'yarn') await ensureYarnLock({ cwd }); + return shell(packageManager, ['install'], { cwd, timeout: 90_000, stdio: 'ignore' }); +} + +/** + * Yarn Berry (PnP) versions will throw an error if there isn't an existing `yarn.lock` file + * If a `yarn.lock` file doesn't exist, this function writes an empty `yarn.lock` one. + * Unfortunately this hack is required to run `yarn install`. + * + * The empty `yarn.lock` file is immediately overwritten by the installation process. + * See https://github.com/withastro/astro/pull/8028 + */ +async function ensureYarnLock({ cwd }: { cwd: string }) { + const yarnLock = path.join(cwd, 'yarn.lock'); + if (fs.existsSync(yarnLock)) return; + return fs.promises.writeFile(yarnLock, '', { encoding: 'utf-8' }); +} diff --git a/packages/create-astro/src/actions/git.ts b/packages/create-astro/src/actions/git.ts new file mode 100644 index 000000000..ebedb8701 --- /dev/null +++ b/packages/create-astro/src/actions/git.ts @@ -0,0 +1,64 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { Context } from './context.js'; + +import { color } from '@astrojs/cli-kit'; +import { error, info, title } from '../messages.js'; +import { shell } from '../shell.js'; + +export async function git( + ctx: Pick<Context, 'cwd' | 'git' | 'yes' | 'prompt' | 'dryRun' | 'tasks'>, +) { + if (fs.existsSync(path.join(ctx.cwd, '.git'))) { + await info('Nice!', `Git has already been initialized`); + return; + } + let _git = ctx.git ?? ctx.yes; + if (_git === undefined) { + ({ git: _git } = await ctx.prompt({ + name: 'git', + type: 'confirm', + label: title('git'), + message: `Initialize a new git repository?`, + hint: 'optional', + initial: true, + })); + } + + if (ctx.dryRun) { + await info('--dry-run', `Skipping Git initialization`); + } else if (_git) { + ctx.tasks.push({ + pending: 'Git', + start: 'Git initializing...', + end: 'Git initialized', + while: () => + init({ cwd: ctx.cwd }).catch((e) => { + error('error', e); + process.exit(1); + }), + }); + } else { + await info( + ctx.yes === false ? 'git [skip]' : 'Sounds good!', + `You can always run ${color.reset('git init')}${color.dim(' manually.')}`, + ); + } +} + +async function init({ cwd }: { cwd: string }) { + try { + await shell('git', ['init'], { cwd, stdio: 'ignore' }); + await shell('git', ['add', '-A'], { cwd, stdio: 'ignore' }); + await shell( + 'git', + [ + 'commit', + '-m', + '"Initial commit from Astro"', + '--author="houston[bot] <astrobot-houston@users.noreply.github.com>"', + ], + { cwd, stdio: 'ignore' }, + ); + } catch {} +} diff --git a/packages/create-astro/src/actions/help.ts b/packages/create-astro/src/actions/help.ts new file mode 100644 index 000000000..1d5c7f609 --- /dev/null +++ b/packages/create-astro/src/actions/help.ts @@ -0,0 +1,24 @@ +import { printHelp } from '../messages.js'; + +export function help() { + printHelp({ + commandName: 'create-astro', + usage: '[dir] [...flags]', + headline: 'Scaffold Astro projects.', + tables: { + Flags: [ + ['--help (-h)', 'See all available flags.'], + ['--template <name>', 'Specify your template.'], + ['--install / --no-install', 'Install dependencies (or not).'], + ['--add <integrations>', 'Add integrations.'], + ['--git / --no-git', 'Initialize git repo (or not).'], + ['--yes (-y)', 'Skip all prompts by accepting defaults.'], + ['--no (-n)', 'Skip all prompts by declining defaults.'], + ['--dry-run', 'Walk through steps without executing.'], + ['--skip-houston', 'Skip Houston animation.'], + ['--ref', 'Choose astro branch (default: latest).'], + ['--fancy', 'Enable full Unicode support for Windows.'], + ], + }, + }); +} diff --git a/packages/create-astro/src/actions/intro.ts b/packages/create-astro/src/actions/intro.ts new file mode 100644 index 000000000..0249e63c8 --- /dev/null +++ b/packages/create-astro/src/actions/intro.ts @@ -0,0 +1,29 @@ +import type { Context } from './context.js'; + +import { color, label } from '@astrojs/cli-kit'; +import { banner, say } from '../messages.js'; + +export async function intro( + ctx: Pick<Context, 'skipHouston' | 'welcome' | 'hat' | 'tie' | 'version' | 'username' | 'fancy'>, +) { + banner(); + + if (!ctx.skipHouston) { + const { welcome, hat, tie } = ctx; + await say( + [ + [ + 'Welcome', + 'to', + label('astro', color.bgGreen, color.black), + Promise.resolve(ctx.version).then( + (version) => (version ? color.green(`v${version}`) : '') + ',', + ), + Promise.resolve(ctx.username).then((username) => `${username}!`), + ], + welcome ?? "Let's build something awesome!", + ] as string[], + { clear: true, hat, tie }, + ); + } +} diff --git a/packages/create-astro/src/actions/next-steps.ts b/packages/create-astro/src/actions/next-steps.ts new file mode 100644 index 000000000..91536cc46 --- /dev/null +++ b/packages/create-astro/src/actions/next-steps.ts @@ -0,0 +1,25 @@ +import path from 'node:path'; +import type { Context } from './context.js'; + +import { nextSteps, say } from '../messages.js'; + +export async function next( + ctx: Pick<Context, 'hat' | 'tie' | 'cwd' | 'packageManager' | 'skipHouston'>, +) { + let projectDir = path.relative(process.cwd(), ctx.cwd); + + const commandMap: { [key: string]: string } = { + npm: 'npm run dev', + bun: 'bun run dev', + yarn: 'yarn dev', + pnpm: 'pnpm dev', + }; + + const devCmd = commandMap[ctx.packageManager as keyof typeof commandMap] || 'npm run dev'; + await nextSteps({ projectDir, devCmd }); + + if (!ctx.skipHouston) { + await say(['Good luck out there, astronaut! 🚀'], { hat: ctx.hat, tie: ctx.tie }); + } + return; +} diff --git a/packages/create-astro/src/actions/project-name.ts b/packages/create-astro/src/actions/project-name.ts new file mode 100644 index 000000000..26938de80 --- /dev/null +++ b/packages/create-astro/src/actions/project-name.ts @@ -0,0 +1,74 @@ +import type { Context } from './context.js'; + +import path from 'node:path'; +import { color, generateProjectName } from '@astrojs/cli-kit'; +import { info, log, title } from '../messages.js'; + +import { isEmpty, toValidName } from './shared.js'; + +export async function projectName( + ctx: Pick<Context, 'cwd' | 'yes' | 'dryRun' | 'prompt' | 'projectName' | 'exit'>, +) { + await checkCwd(ctx.cwd); + + if (!ctx.cwd || !isEmpty(ctx.cwd)) { + if (!isEmpty(ctx.cwd)) { + await info('Hmm...', `${color.reset(`"${ctx.cwd}"`)}${color.dim(` is not empty!`)}`); + } + + if (ctx.yes) { + ctx.projectName = generateProjectName(); + ctx.cwd = `./${ctx.projectName}`; + await info('dir', `Project created at ./${ctx.projectName}`); + return; + } + + const { name } = await ctx.prompt({ + name: 'name', + type: 'text', + label: title('dir'), + message: 'Where should we create your new project?', + initial: `./${generateProjectName()}`, + validate(value: string) { + if (!isEmpty(value)) { + return `Directory is not empty!`; + } + // Check for non-printable characters + if (value.match(/[^\x20-\x7E]/g) !== null) + return `Invalid non-printable character present!`; + return true; + }, + }); + + ctx.cwd = name!.trim(); + ctx.projectName = toValidName(name!); + if (ctx.dryRun) { + await info('--dry-run', 'Skipping project naming'); + return; + } + } else { + let name = ctx.cwd; + if (name === '.' || name === './') { + const parts = process.cwd().split(path.sep); + name = parts[parts.length - 1]; + } else if (name.startsWith('./') || name.startsWith('../')) { + const parts = name.split('/'); + name = parts[parts.length - 1]; + } + ctx.projectName = toValidName(name); + } + + if (!ctx.cwd) { + ctx.exit(1); + } +} + +async function checkCwd(cwd: string | undefined) { + const empty = cwd && isEmpty(cwd); + if (empty) { + log(''); + await info('dir', `Using ${color.reset(cwd)}${color.dim(' as project directory')}`); + } + + return empty; +} diff --git a/packages/create-astro/src/actions/shared.ts b/packages/create-astro/src/actions/shared.ts new file mode 100644 index 000000000..da19677d0 --- /dev/null +++ b/packages/create-astro/src/actions/shared.ts @@ -0,0 +1,59 @@ +import fs from 'node:fs'; + +// Some existing files and directories can be safely ignored when checking if a directory is a valid project directory. +// https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7/packages/create-react-app/createReactApp.js#L907-L934 +const VALID_PROJECT_DIRECTORY_SAFE_LIST = [ + '.DS_Store', + '.git', + '.gitkeep', + '.gitattributes', + '.gitignore', + '.gitlab-ci.yml', + '.hg', + '.hgcheck', + '.hgignore', + '.idea', + '.npmignore', + '.travis.yml', + '.yarn', + '.yarnrc.yml', + 'docs', + 'LICENSE', + 'mkdocs.yml', + 'Thumbs.db', + /\.iml$/, + /^npm-debug\.log/, + /^yarn-debug\.log/, + /^yarn-error\.log/, +]; + +export function isEmpty(dirPath: string) { + if (!fs.existsSync(dirPath)) { + return true; + } + + const conflicts = fs.readdirSync(dirPath).filter((content) => { + return !VALID_PROJECT_DIRECTORY_SAFE_LIST.some((safeContent) => { + return typeof safeContent === 'string' ? content === safeContent : safeContent.test(content); + }); + }); + + return conflicts.length === 0; +} + +function isValidName(projectName: string) { + return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName); +} + +export function toValidName(projectName: string) { + if (isValidName(projectName)) return projectName; + + return projectName + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/^[._]/, '') + .replace(/[^a-z\d\-~]+/g, '-') + .replace(/^-+/, '') + .replace(/-+$/, ''); +} diff --git a/packages/create-astro/src/actions/template.ts b/packages/create-astro/src/actions/template.ts new file mode 100644 index 000000000..cf58d90a8 --- /dev/null +++ b/packages/create-astro/src/actions/template.ts @@ -0,0 +1,169 @@ +import type { Context } from './context.js'; + +import fs from 'node:fs'; +import path from 'node:path'; +import { color } from '@astrojs/cli-kit'; +import { downloadTemplate } from '@bluwy/giget-core'; +import { error, info, title } from '../messages.js'; + +export async function template( + ctx: Pick<Context, 'template' | 'prompt' | 'yes' | 'dryRun' | 'exit' | 'tasks'>, +) { + if (!ctx.template && ctx.yes) ctx.template = 'basics'; + + if (ctx.template) { + await info('tmpl', `Using ${color.reset(ctx.template)}${color.dim(' as project template')}`); + } else { + const { template: tmpl } = await ctx.prompt({ + name: 'template', + type: 'select', + label: title('tmpl'), + message: 'How would you like to start your new project?', + initial: 'basics', + choices: [ + { value: 'basics', label: 'A basic, helpful starter project', hint: '(recommended)' }, + { value: 'blog', label: 'Use blog template' }, + { value: 'starlight', label: 'Use docs (Starlight) template' }, + { value: 'minimal', label: 'Use minimal (empty) template' }, + ], + }); + ctx.template = tmpl; + } + + if (ctx.dryRun) { + await info('--dry-run', `Skipping template copying`); + } else if (ctx.template) { + ctx.tasks.push({ + pending: 'Template', + start: 'Template copying...', + end: 'Template copied', + while: () => + copyTemplate(ctx.template!, ctx as Context).catch((e) => { + if (e instanceof Error) { + error('error', e.message); + process.exit(1); + } else { + error('error', 'Unable to clone template.'); + process.exit(1); + } + }), + }); + } else { + ctx.exit(1); + } +} + +// some files are only needed for online editors when using astro.new. Remove for create-astro installs. +const FILES_TO_REMOVE = ['CHANGELOG.md', '.codesandbox']; +const FILES_TO_UPDATE = { + 'package.json': (file: string, overrides: { name: string }) => + fs.promises.readFile(file, 'utf-8').then((value) => { + // Match first indent in the file or fallback to `\t` + const indent = /(^\s+)/m.exec(value)?.[1] ?? '\t'; + return fs.promises.writeFile( + file, + JSON.stringify( + Object.assign(JSON.parse(value), Object.assign(overrides, { private: undefined })), + null, + indent, + ), + 'utf-8', + ); + }), +}; + +export function getTemplateTarget(tmpl: string, ref = 'latest') { + // Handle Starlight templates + if (tmpl.startsWith('starlight')) { + const [, starter = 'basics'] = tmpl.split('/'); + return `github:withastro/starlight/examples/${starter}`; + } + + // Handle third-party templates + const isThirdParty = tmpl.includes('/'); + if (isThirdParty) return tmpl; + + // Handle Astro templates + if (ref === 'latest') { + // `latest` ref is specially handled to route to a branch specifically + // to allow faster downloads. Otherwise giget has to download the entire + // repo and only copy a sub directory + return `github:withastro/astro#examples/${tmpl}`; + } else { + return `github:withastro/astro/examples/${tmpl}#${ref}`; + } +} + +async function copyTemplate(tmpl: string, ctx: Context) { + const templateTarget = getTemplateTarget(tmpl, ctx.ref); + // Copy + if (!ctx.dryRun) { + try { + await downloadTemplate(templateTarget, { + force: true, + cwd: ctx.cwd, + dir: '.', + }); + + // Modify the README file to reflect the correct package manager + if (ctx.packageManager !== 'npm') { + const readmePath = path.resolve(ctx.cwd, 'README.md'); + const readme = fs.readFileSync(readmePath, 'utf8'); + + // `run` is removed since it's optional in other package managers + const updatedReadme = readme + .replace(/\bnpm run\b/g, ctx.packageManager) + .replace(/\bnpm\b/g, ctx.packageManager); + + fs.writeFileSync(readmePath, updatedReadme); + } + } catch (err: any) { + // Only remove the directory if it's most likely created by us. + if (ctx.cwd !== '.' && ctx.cwd !== './' && !ctx.cwd.startsWith('../')) { + try { + fs.rmdirSync(ctx.cwd); + } catch (_) { + // Ignore any errors from removing the directory, + // make sure we throw and display the original error. + } + } + + if (err.message?.includes('404')) { + throw new Error(`Template ${color.reset(tmpl)} ${color.dim('does not exist!')}`); + } + + if (err.message) { + error('error', err.message); + } + try { + // The underlying error is often buried deep in the `cause` property + // This is in a try/catch block in case of weirdnesses in accessing the `cause` property + if ('cause' in err) { + // This is probably included in err.message, but we can log it just in case it has extra info + error('error', err.cause); + if ('cause' in err.cause) { + // Hopefully the actual fetch error message + error('error', err.cause?.cause); + } + } + } catch {} + throw new Error(`Unable to download template ${color.reset(tmpl)}`); + } + + // Post-process in parallel + const removeFiles = FILES_TO_REMOVE.map(async (file) => { + const fileLoc = path.resolve(path.join(ctx.cwd, file)); + if (fs.existsSync(fileLoc)) { + return fs.promises.rm(fileLoc, { recursive: true }); + } + }); + const updateFiles = Object.entries(FILES_TO_UPDATE).map(async ([file, update]) => { + const fileLoc = path.resolve(path.join(ctx.cwd, file)); + if (fs.existsSync(fileLoc)) { + return update(fileLoc, { name: ctx.projectName! }); + } + }); + + await Promise.all([...removeFiles, ...updateFiles]); + } +} diff --git a/packages/create-astro/src/actions/verify.ts b/packages/create-astro/src/actions/verify.ts new file mode 100644 index 000000000..7f446c869 --- /dev/null +++ b/packages/create-astro/src/actions/verify.ts @@ -0,0 +1,40 @@ +import type { Context } from './context.js'; + +import dns from 'node:dns/promises'; +import { color } from '@astrojs/cli-kit'; +import { verifyTemplate } from '@bluwy/giget-core'; +import { bannerAbort, error, info, log } from '../messages.js'; +import { getTemplateTarget } from './template.js'; + +export async function verify( + ctx: Pick<Context, 'version' | 'dryRun' | 'template' | 'ref' | 'exit'>, +) { + if (!ctx.dryRun) { + const online = await isOnline(); + if (!online) { + bannerAbort(); + log(''); + error('error', `Unable to connect to the internet.`); + ctx.exit(1); + } + } + + if (ctx.template) { + const target = getTemplateTarget(ctx.template, ctx.ref); + const ok = await verifyTemplate(target); + if (!ok) { + bannerAbort(); + log(''); + error('error', `Template ${color.reset(ctx.template)} ${color.dim('could not be found!')}`); + await info('check', 'https://astro.build/examples'); + ctx.exit(1); + } + } +} + +function isOnline(): Promise<boolean> { + return dns.lookup('github.com').then( + () => true, + () => false, + ); +} |