diff --git a/.changeset/lucky-bottles-wait.md b/.changeset/lucky-bottles-wait.md new file mode 100644 index 000000000000..271c28ed4776 --- /dev/null +++ b/.changeset/lucky-bottles-wait.md @@ -0,0 +1,23 @@ +--- +'astro': patch +'@astrojs/cloudflare': patch +'@astrojs/deno': patch +'@astrojs/image': patch +'@astrojs/lit': patch +'@astrojs/mdx': patch +'@astrojs/netlify': patch +'@astrojs/node': patch +'@astrojs/partytown': patch +'@astrojs/preact': patch +'@astrojs/prefetch': patch +'@astrojs/react': patch +'@astrojs/sitemap': patch +'@astrojs/solid-js': patch +'@astrojs/svelte': patch +'@astrojs/tailwind': patch +'@astrojs/turbolinks': patch +'@astrojs/vercel': patch +'@astrojs/vue': patch +--- + +[astro add] Support adapters and third party packages diff --git a/packages/astro/src/core/add/index.ts b/packages/astro/src/core/add/index.ts index 5a1fb9cf6c2a..dde67065312f 100644 --- a/packages/astro/src/core/add/index.ts +++ b/packages/astro/src/core/add/index.ts @@ -3,7 +3,7 @@ import boxen from 'boxen'; import { diffWords } from 'diff'; import { execa } from 'execa'; import { existsSync, promises as fs } from 'fs'; -import { bold, cyan, dim, green, magenta } from 'kleur/colors'; +import { bold, cyan, dim, green, magenta, yellow } from 'kleur/colors'; import ora from 'ora'; import path from 'path'; import preferredPM from 'preferred-pm'; @@ -32,6 +32,7 @@ export interface IntegrationInfo { id: string; packageName: string; dependencies: [name: string, version: string][]; + type: 'integration' | 'adapter'; } const ALIASES = new Map([ ['solid', 'solid-js'], @@ -47,11 +48,19 @@ module.exports = { plugins: [], }\n`; +const OFFICIAL_ADAPTER_TO_IMPORT_MAP: Record = { + 'netlify': '@astrojs/netlify/functions', + 'vercel': '@astrojs/vercel/serverless', + 'cloudflare': '@astrojs/cloudflare', + 'node': '@astrojs/node', + 'deno': '@astrojs/deno', +} + export default async function add(names: string[], { cwd, flags, logging, telemetry }: AddOptions) { if (flags.help || names.length === 0) { printHelp({ commandName: 'astro add', - usage: '[...integrations]', + usage: '[...integrations] [...adapters]', tables: { Flags: [ ['--yes', 'Accept all prompts.'], @@ -70,6 +79,11 @@ export default async function add(names: string[], { cwd, flags, logging, teleme ['partytown', 'astro add partytown'], ['sitemap', 'astro add sitemap'], ], + 'Example: Add an Adapter': [ + ['netlify', 'astro add netlify'], + ['vercel', 'astro add vercel'], + ['deno', 'astro add deno'], + ], }, description: `Check out the full integration catalog: ${cyan( 'https://astro.build/integrations' @@ -120,7 +134,20 @@ export default async function add(names: string[], { cwd, flags, logging, teleme debug('add', 'Astro config ensured `defineConfig`'); for (const integration of integrations) { - await addIntegration(ast, integration); + if (isAdapter(integration)) { + const officialExportName = OFFICIAL_ADAPTER_TO_IMPORT_MAP[integration.id]; + if (officialExportName) { + await setAdapter(ast, integration, officialExportName); + } else { + info( + logging, + null, + `\n ${magenta(`Check our deployment docs for ${bold(integration.packageName)} to update your "adapter" config.`)}` + ); + } + } else { + await addIntegration(ast, integration); + } debug('add', `Astro config added integration ${integration.id}`); } } catch (err) { @@ -133,7 +160,13 @@ export default async function add(names: string[], { cwd, flags, logging, teleme if (ast) { try { - configResult = await updateAstroConfig({ configURL, ast, flags, logging }); + configResult = await updateAstroConfig({ + configURL, + ast, + flags, + logging, + logAdapterInstructions: integrations.some(isAdapter), + }); } catch (err) { debug('add', 'Error updating astro config', err); throw createPrettyError(err as Error); @@ -231,6 +264,10 @@ export default async function add(names: string[], { cwd, flags, logging, teleme } } +function isAdapter(integration: IntegrationInfo): integration is IntegrationInfo & { type: 'adapter' } { + return integration.type === 'adapter'; +} + async function parseAstroConfig(configURL: URL): Promise { const source = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' }); const result = parse(source); @@ -314,6 +351,50 @@ async function addIntegration(ast: t.File, integration: IntegrationInfo) { }); } +async function setAdapter(ast: t.File, adapter: IntegrationInfo, exportName: string) { + const adapterId = t.identifier(toIdent(adapter.id)); + + ensureImport( + ast, + t.importDeclaration( + [t.importDefaultSpecifier(adapterId)], + t.stringLiteral(exportName) + ) + ); + + visit(ast, { + // eslint-disable-next-line @typescript-eslint/no-shadow + ExportDefaultDeclaration(path) { + if (!t.isCallExpression(path.node.declaration)) return; + + const configObject = path.node.declaration.arguments[0]; + if (!t.isObjectExpression(configObject)) return; + + let adapterProp = configObject.properties.find((prop) => { + if (prop.type !== 'ObjectProperty') return false; + if (prop.key.type === 'Identifier') { + if (prop.key.name === 'adapter') return true; + } + if (prop.key.type === 'StringLiteral') { + if (prop.key.value === 'adapter') return true; + } + return false; + }) as t.ObjectProperty | undefined; + + const adapterCall = t.callExpression(adapterId, []); + + if (!adapterProp) { + configObject.properties.push( + t.objectProperty(t.identifier('adapter'), adapterCall) + ); + return; + } + + adapterProp.value = adapterCall; + }, + }); +} + const enum UpdateResult { none, updated, @@ -326,11 +407,13 @@ async function updateAstroConfig({ ast, flags, logging, + logAdapterInstructions, }: { configURL: URL; ast: t.File; flags: yargs.Arguments; logging: LogOptions; + logAdapterInstructions: boolean; }): Promise { const input = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' }); let output = await generate(ast); @@ -378,6 +461,14 @@ async function updateAstroConfig({ `\n ${magenta('Astro will make the following changes to your config file:')}\n${message}` ); + if (logAdapterInstructions) { + info( + logging, + null, + magenta(` For complete deployment options, visit\n ${bold('https://docs.astro.build/en/guides/deploy/')}\n`) + ); + } + if (await askToContinue({ flags })) { await fs.writeFile(fileURLToPath(configURL), output, { encoding: 'utf-8' }); debug('add', `Updated astro config`); @@ -479,46 +570,98 @@ async function tryToInstallIntegrations({ } } -export async function validateIntegrations(integrations: string[]): Promise { - const spinner = ora('Resolving integrations...').start(); - const integrationEntries = await Promise.all( - integrations.map(async (integration): Promise => { - const parsed = parseIntegrationName(integration); - if (!parsed) { - spinner.fail(); - throw new Error(`${integration} does not appear to be a valid package name!`); - } +async function fetchPackageJson(scope: string | undefined, name: string, tag: string): Promise { + const packageName = `${scope ? `@${scope}/` : ''}${name}`; + const res = await fetch(`https://registry.npmjs.org/${packageName}/${tag}`) + if (res.status === 404) { + return new Error(); + } else { + return await res.json(); + } +} - let { scope = '', name, tag } = parsed; - // Allow third-party integrations starting with `astro-` namespace - if (!name.startsWith('astro-')) { - scope = `astrojs`; - } - const packageName = `${scope ? `@${scope}/` : ''}${name}`; +export async function validateIntegrations(integrations: string[]): Promise { + const spinner = ora('Resolving packages...').start(); + try { + const integrationEntries = await Promise.all( + integrations.map(async (integration): Promise => { + const parsed = parseIntegrationName(integration); + if (!parsed) { + throw new Error(`${bold(integration)} does not appear to be a valid package name!`); + } - const result = await fetch(`https://registry.npmjs.org/${packageName}/${tag}`).then((res) => { - if (res.status === 404) { - spinner.fail(); - throw new Error(`Unable to fetch ${packageName}. Does this package exist?`); + let { scope, name, tag } = parsed; + let pkgJson = null; + let pkgType: 'first-party' | 'third-party' = 'first-party'; + + if (!scope) { + const firstPartyPkgCheck = await fetchPackageJson('astrojs', name, tag); + if (firstPartyPkgCheck instanceof Error) { + spinner.warn(yellow(`${bold(integration)} is not an official Astro package. Use at your own risk!`)); + const response = await prompts({ + type: 'confirm', + name: 'askToContinue', + message: 'Continue?', + initial: true, + }); + if (!response.askToContinue) { + throw new Error(`No problem! Find our official integrations at ${cyan('https://astro.build/integrations')}`); + } + spinner.start('Resolving with third party packages...'); + pkgType = 'third-party'; + } else { + pkgJson = firstPartyPkgCheck as any; + } } - return res.json(); - }); + if (pkgType === 'third-party') { + const thirdPartyPkgCheck = await fetchPackageJson(scope, name, tag); + if (thirdPartyPkgCheck instanceof Error) { + throw new Error( + `Unable to fetch ${bold(integration)}. Does the package exist?`, + ); + } else { + pkgJson = thirdPartyPkgCheck as any; + } + } + + const resolvedScope = pkgType === 'first-party' ? 'astrojs' : scope; + const packageName = `${resolvedScope ? `@${resolvedScope}/` : ''}${name}`; + + let dependencies: IntegrationInfo['dependencies'] = [ + [pkgJson['name'], `^${pkgJson['version']}`], + ]; - let dependencies: IntegrationInfo['dependencies'] = [ - [result['name'], `^${result['version']}`], - ]; + if (pkgJson['peerDependencies']) { + for (const peer in pkgJson['peerDependencies']) { + dependencies.push([peer, pkgJson['peerDependencies'][peer]]); + } + } - if (result['peerDependencies']) { - for (const peer in result['peerDependencies']) { - dependencies.push([peer, result['peerDependencies'][peer]]); + let integrationType: IntegrationInfo['type']; + const keywords = Array.isArray(pkgJson['keywords']) ? pkgJson['keywords'] : []; + if (keywords.includes('astro-integration')) { + integrationType = 'integration'; + } else if (keywords.includes('astro-adapter')) { + integrationType = 'adapter'; + } else { + throw new Error( + `${bold(packageName)} doesn't appear to be an integration or an adapter. Find our official integrations at ${cyan('https://astro.build/integrations')}` + ); } - } - return { id: integration, packageName, dependencies }; - }) - ); - spinner.succeed(); - return integrationEntries; + return { id: integration, packageName, dependencies, type: integrationType }; + }) + ); + spinner.succeed(); + return integrationEntries; + } catch (e) { + if (e instanceof Error) { + spinner.fail(e.message); + process.exit(1); + } else { + throw e; + } + } } function parseIntegrationName(spec: string) { diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index 54856276acd6..cd4a637b7bf7 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -11,6 +11,7 @@ "url": "https://github.com/withastro/astro.git", "directory": "packages/integrations/cloudflare" }, + "keywords": ["astro-adapter"], "bugs": "https://github.com/withastro/astro/issues", "homepage": "https://astro.build", "exports": { diff --git a/packages/integrations/deno/package.json b/packages/integrations/deno/package.json index b5d6f06ea381..02ed83554578 100644 --- a/packages/integrations/deno/package.json +++ b/packages/integrations/deno/package.json @@ -11,6 +11,7 @@ "url": "https://github.com/withastro/astro.git", "directory": "packages/integrations/deno" }, + "keywords": ["astro-adapter"], "bugs": "https://github.com/withastro/astro/issues", "homepage": "https://astro.build", "exports": { diff --git a/packages/integrations/image/package.json b/packages/integrations/image/package.json index 1c3bec67ef95..5aeb3bb17287 100644 --- a/packages/integrations/image/package.json +++ b/packages/integrations/image/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/image" }, "keywords": [ + "astro-integration", "astro-component", "withastro", "image" diff --git a/packages/integrations/lit/package.json b/packages/integrations/lit/package.json index 12beab2451e7..bfda1e1af72e 100644 --- a/packages/integrations/lit/package.json +++ b/packages/integrations/lit/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/lit" }, "keywords": [ + "astro-integration", "astro-component", "renderer", "lit" diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index 285690337852..1f76f471831c 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/mdx" }, "keywords": [ + "astro-integration", "astro-component", "renderer", "mdx" diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index 2e2c15432ff4..5eee7a595b6a 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -11,6 +11,7 @@ "url": "https://github.com/withastro/astro.git", "directory": "packages/integrations/netlify" }, + "keywords": ["astro-adapter"], "bugs": "https://github.com/withastro/astro/issues", "homepage": "https://astro.build", "exports": { diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index e4a37cee3c21..de42e7d16d68 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -11,6 +11,7 @@ "url": "https://github.com/withastro/astro.git", "directory": "packages/integrations/node" }, + "keywords": ["astro-adapter"], "bugs": "https://github.com/withastro/astro/issues", "homepage": "https://astro.build", "exports": { diff --git a/packages/integrations/partytown/package.json b/packages/integrations/partytown/package.json index 2833e3d611a1..e775e30cad19 100644 --- a/packages/integrations/partytown/package.json +++ b/packages/integrations/partytown/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/partytown" }, "keywords": [ + "astro-integration", "astro-component", "analytics", "performance" diff --git a/packages/integrations/preact/package.json b/packages/integrations/preact/package.json index 426a48444409..36b04827b0bb 100644 --- a/packages/integrations/preact/package.json +++ b/packages/integrations/preact/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/preact" }, "keywords": [ + "astro-integration", "astro-component", "renderer", "preact" diff --git a/packages/integrations/prefetch/package.json b/packages/integrations/prefetch/package.json index 3b07034ebe87..4ce1db137c0c 100644 --- a/packages/integrations/prefetch/package.json +++ b/packages/integrations/prefetch/package.json @@ -11,6 +11,7 @@ "url": "https://github.com/withastro/astro.git", "directory": "packages/astro-prefetch" }, + "keywords": ["astro-integration"], "bugs": "https://github.com/withastro/astro/issues", "homepage": "https://astro.build", "exports": { diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index 0bc3937bfb46..dae26dcd699a 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/react" }, "keywords": [ + "astro-integration", "astro-component", "renderer", "react" diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json index ac98fa05c460..8aa96f33dc27 100644 --- a/packages/integrations/sitemap/package.json +++ b/packages/integrations/sitemap/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/sitemap" }, "keywords": [ + "astro-integration", "astro-component", "seo", "sitemap" diff --git a/packages/integrations/solid/package.json b/packages/integrations/solid/package.json index a3e3476595af..036ce728a371 100644 --- a/packages/integrations/solid/package.json +++ b/packages/integrations/solid/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/solid" }, "keywords": [ + "astro-integration", "astro-component", "renderer", "solid" diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json index 53ce95147aa4..755fbc8d3807 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/svelte" }, "keywords": [ + "astro-integration", "astro-component", "renderer", "svelte" diff --git a/packages/integrations/tailwind/package.json b/packages/integrations/tailwind/package.json index 0b543d9ea468..3b8d88909565 100644 --- a/packages/integrations/tailwind/package.json +++ b/packages/integrations/tailwind/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/tailwind" }, "keywords": [ + "astro-integration", "astro-component" ], "bugs": "https://github.com/withastro/astro/issues", diff --git a/packages/integrations/turbolinks/package.json b/packages/integrations/turbolinks/package.json index c3b54f1e8df4..5b7310fcafab 100644 --- a/packages/integrations/turbolinks/package.json +++ b/packages/integrations/turbolinks/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/turbolinks" }, "keywords": [ + "astro-integration", "astro-component", "performance" ], diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index 3f09baf01461..055f08b8d4e7 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -10,6 +10,7 @@ "url": "https://github.com/withastro/astro.git", "directory": "packages/integrations/vercel" }, + "keywords": ["astro-adapter"], "bugs": "https://github.com/withastro/astro/issues", "homepage": "https://astro.build", "exports": { diff --git a/packages/integrations/vue/package.json b/packages/integrations/vue/package.json index a6b2989fe9b4..03d7b7b2fe1e 100644 --- a/packages/integrations/vue/package.json +++ b/packages/integrations/vue/package.json @@ -12,6 +12,7 @@ "directory": "packages/integrations/vue" }, "keywords": [ + "astro-integration", "astro-component", "renderer", "vue"