Skip to content

Commit b012ee5

Browse files
authored
[astro add] Support adapters and third party packages (#3854)
* feat: support adapters and third part integrations by keywords * refactor: add keywords to all official integrations * docs: add adapter ex to astro add help * nit: clarify astro add usage * nit: highlight link * fix: use process.exit(1) on error * chore: changeset * nit: bold integration name * fix: log install instructions for adapters instead * nit: change to logAdapterConfigInstructions * Revert "fix: log install instructions for adapters instead" This reverts commit 1a459f1. * feat: add hardcoded adapter export map * refactor: inline adapter config log
1 parent 6258cd1 commit b012ee5

File tree

20 files changed

+221
-37
lines changed

20 files changed

+221
-37
lines changed

.changeset/lucky-bottles-wait.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'astro': patch
3+
'@astrojs/cloudflare': patch
4+
'@astrojs/deno': patch
5+
'@astrojs/image': patch
6+
'@astrojs/lit': patch
7+
'@astrojs/mdx': patch
8+
'@astrojs/netlify': patch
9+
'@astrojs/node': patch
10+
'@astrojs/partytown': patch
11+
'@astrojs/preact': patch
12+
'@astrojs/prefetch': patch
13+
'@astrojs/react': patch
14+
'@astrojs/sitemap': patch
15+
'@astrojs/solid-js': patch
16+
'@astrojs/svelte': patch
17+
'@astrojs/tailwind': patch
18+
'@astrojs/turbolinks': patch
19+
'@astrojs/vercel': patch
20+
'@astrojs/vue': patch
21+
---
22+
23+
[astro add] Support adapters and third party packages

packages/astro/src/core/add/index.ts

Lines changed: 180 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import boxen from 'boxen';
33
import { diffWords } from 'diff';
44
import { execa } from 'execa';
55
import { existsSync, promises as fs } from 'fs';
6-
import { bold, cyan, dim, green, magenta } from 'kleur/colors';
6+
import { bold, cyan, dim, green, magenta, yellow } from 'kleur/colors';
77
import ora from 'ora';
88
import path from 'path';
99
import preferredPM from 'preferred-pm';
@@ -32,6 +32,7 @@ export interface IntegrationInfo {
3232
id: string;
3333
packageName: string;
3434
dependencies: [name: string, version: string][];
35+
type: 'integration' | 'adapter';
3536
}
3637
const ALIASES = new Map([
3738
['solid', 'solid-js'],
@@ -47,11 +48,19 @@ module.exports = {
4748
plugins: [],
4849
}\n`;
4950

51+
const OFFICIAL_ADAPTER_TO_IMPORT_MAP: Record<string, string> = {
52+
'netlify': '@astrojs/netlify/functions',
53+
'vercel': '@astrojs/vercel/serverless',
54+
'cloudflare': '@astrojs/cloudflare',
55+
'node': '@astrojs/node',
56+
'deno': '@astrojs/deno',
57+
}
58+
5059
export default async function add(names: string[], { cwd, flags, logging, telemetry }: AddOptions) {
5160
if (flags.help || names.length === 0) {
5261
printHelp({
5362
commandName: 'astro add',
54-
usage: '[...integrations]',
63+
usage: '[...integrations] [...adapters]',
5564
tables: {
5665
Flags: [
5766
['--yes', 'Accept all prompts.'],
@@ -70,6 +79,11 @@ export default async function add(names: string[], { cwd, flags, logging, teleme
7079
['partytown', 'astro add partytown'],
7180
['sitemap', 'astro add sitemap'],
7281
],
82+
'Example: Add an Adapter': [
83+
['netlify', 'astro add netlify'],
84+
['vercel', 'astro add vercel'],
85+
['deno', 'astro add deno'],
86+
],
7387
},
7488
description: `Check out the full integration catalog: ${cyan(
7589
'https://astro.build/integrations'
@@ -120,7 +134,20 @@ export default async function add(names: string[], { cwd, flags, logging, teleme
120134
debug('add', 'Astro config ensured `defineConfig`');
121135

122136
for (const integration of integrations) {
123-
await addIntegration(ast, integration);
137+
if (isAdapter(integration)) {
138+
const officialExportName = OFFICIAL_ADAPTER_TO_IMPORT_MAP[integration.id];
139+
if (officialExportName) {
140+
await setAdapter(ast, integration, officialExportName);
141+
} else {
142+
info(
143+
logging,
144+
null,
145+
`\n ${magenta(`Check our deployment docs for ${bold(integration.packageName)} to update your "adapter" config.`)}`
146+
);
147+
}
148+
} else {
149+
await addIntegration(ast, integration);
150+
}
124151
debug('add', `Astro config added integration ${integration.id}`);
125152
}
126153
} catch (err) {
@@ -133,7 +160,13 @@ export default async function add(names: string[], { cwd, flags, logging, teleme
133160

134161
if (ast) {
135162
try {
136-
configResult = await updateAstroConfig({ configURL, ast, flags, logging });
163+
configResult = await updateAstroConfig({
164+
configURL,
165+
ast,
166+
flags,
167+
logging,
168+
logAdapterInstructions: integrations.some(isAdapter),
169+
});
137170
} catch (err) {
138171
debug('add', 'Error updating astro config', err);
139172
throw createPrettyError(err as Error);
@@ -231,6 +264,10 @@ export default async function add(names: string[], { cwd, flags, logging, teleme
231264
}
232265
}
233266

267+
function isAdapter(integration: IntegrationInfo): integration is IntegrationInfo & { type: 'adapter' } {
268+
return integration.type === 'adapter';
269+
}
270+
234271
async function parseAstroConfig(configURL: URL): Promise<t.File> {
235272
const source = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' });
236273
const result = parse(source);
@@ -314,6 +351,50 @@ async function addIntegration(ast: t.File, integration: IntegrationInfo) {
314351
});
315352
}
316353

354+
async function setAdapter(ast: t.File, adapter: IntegrationInfo, exportName: string) {
355+
const adapterId = t.identifier(toIdent(adapter.id));
356+
357+
ensureImport(
358+
ast,
359+
t.importDeclaration(
360+
[t.importDefaultSpecifier(adapterId)],
361+
t.stringLiteral(exportName)
362+
)
363+
);
364+
365+
visit(ast, {
366+
// eslint-disable-next-line @typescript-eslint/no-shadow
367+
ExportDefaultDeclaration(path) {
368+
if (!t.isCallExpression(path.node.declaration)) return;
369+
370+
const configObject = path.node.declaration.arguments[0];
371+
if (!t.isObjectExpression(configObject)) return;
372+
373+
let adapterProp = configObject.properties.find((prop) => {
374+
if (prop.type !== 'ObjectProperty') return false;
375+
if (prop.key.type === 'Identifier') {
376+
if (prop.key.name === 'adapter') return true;
377+
}
378+
if (prop.key.type === 'StringLiteral') {
379+
if (prop.key.value === 'adapter') return true;
380+
}
381+
return false;
382+
}) as t.ObjectProperty | undefined;
383+
384+
const adapterCall = t.callExpression(adapterId, []);
385+
386+
if (!adapterProp) {
387+
configObject.properties.push(
388+
t.objectProperty(t.identifier('adapter'), adapterCall)
389+
);
390+
return;
391+
}
392+
393+
adapterProp.value = adapterCall;
394+
},
395+
});
396+
}
397+
317398
const enum UpdateResult {
318399
none,
319400
updated,
@@ -326,11 +407,13 @@ async function updateAstroConfig({
326407
ast,
327408
flags,
328409
logging,
410+
logAdapterInstructions,
329411
}: {
330412
configURL: URL;
331413
ast: t.File;
332414
flags: yargs.Arguments;
333415
logging: LogOptions;
416+
logAdapterInstructions: boolean;
334417
}): Promise<UpdateResult> {
335418
const input = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' });
336419
let output = await generate(ast);
@@ -378,6 +461,14 @@ async function updateAstroConfig({
378461
`\n ${magenta('Astro will make the following changes to your config file:')}\n${message}`
379462
);
380463

464+
if (logAdapterInstructions) {
465+
info(
466+
logging,
467+
null,
468+
magenta(` For complete deployment options, visit\n ${bold('https://docs.astro.build/en/guides/deploy/')}\n`)
469+
);
470+
}
471+
381472
if (await askToContinue({ flags })) {
382473
await fs.writeFile(fileURLToPath(configURL), output, { encoding: 'utf-8' });
383474
debug('add', `Updated astro config`);
@@ -479,46 +570,98 @@ async function tryToInstallIntegrations({
479570
}
480571
}
481572

482-
export async function validateIntegrations(integrations: string[]): Promise<IntegrationInfo[]> {
483-
const spinner = ora('Resolving integrations...').start();
484-
const integrationEntries = await Promise.all(
485-
integrations.map(async (integration): Promise<IntegrationInfo> => {
486-
const parsed = parseIntegrationName(integration);
487-
if (!parsed) {
488-
spinner.fail();
489-
throw new Error(`${integration} does not appear to be a valid package name!`);
490-
}
573+
async function fetchPackageJson(scope: string | undefined, name: string, tag: string): Promise<object | Error> {
574+
const packageName = `${scope ? `@${scope}/` : ''}${name}`;
575+
const res = await fetch(`https://registry.npmjs.org/${packageName}/${tag}`)
576+
if (res.status === 404) {
577+
return new Error();
578+
} else {
579+
return await res.json();
580+
}
581+
}
491582

492-
let { scope = '', name, tag } = parsed;
493-
// Allow third-party integrations starting with `astro-` namespace
494-
if (!name.startsWith('astro-')) {
495-
scope = `astrojs`;
496-
}
497-
const packageName = `${scope ? `@${scope}/` : ''}${name}`;
583+
export async function validateIntegrations(integrations: string[]): Promise<IntegrationInfo[]> {
584+
const spinner = ora('Resolving packages...').start();
585+
try {
586+
const integrationEntries = await Promise.all(
587+
integrations.map(async (integration): Promise<IntegrationInfo> => {
588+
const parsed = parseIntegrationName(integration);
589+
if (!parsed) {
590+
throw new Error(`${bold(integration)} does not appear to be a valid package name!`);
591+
}
498592

499-
const result = await fetch(`https://registry.npmjs.org/${packageName}/${tag}`).then((res) => {
500-
if (res.status === 404) {
501-
spinner.fail();
502-
throw new Error(`Unable to fetch ${packageName}. Does this package exist?`);
593+
let { scope, name, tag } = parsed;
594+
let pkgJson = null;
595+
let pkgType: 'first-party' | 'third-party' = 'first-party';
596+
597+
if (!scope) {
598+
const firstPartyPkgCheck = await fetchPackageJson('astrojs', name, tag);
599+
if (firstPartyPkgCheck instanceof Error) {
600+
spinner.warn(yellow(`${bold(integration)} is not an official Astro package. Use at your own risk!`));
601+
const response = await prompts({
602+
type: 'confirm',
603+
name: 'askToContinue',
604+
message: 'Continue?',
605+
initial: true,
606+
});
607+
if (!response.askToContinue) {
608+
throw new Error(`No problem! Find our official integrations at ${cyan('https://astro.build/integrations')}`);
609+
}
610+
spinner.start('Resolving with third party packages...');
611+
pkgType = 'third-party';
612+
} else {
613+
pkgJson = firstPartyPkgCheck as any;
614+
}
503615
}
504-
return res.json();
505-
});
616+
if (pkgType === 'third-party') {
617+
const thirdPartyPkgCheck = await fetchPackageJson(scope, name, tag);
618+
if (thirdPartyPkgCheck instanceof Error) {
619+
throw new Error(
620+
`Unable to fetch ${bold(integration)}. Does the package exist?`,
621+
);
622+
} else {
623+
pkgJson = thirdPartyPkgCheck as any;
624+
}
625+
}
626+
627+
const resolvedScope = pkgType === 'first-party' ? 'astrojs' : scope;
628+
const packageName = `${resolvedScope ? `@${resolvedScope}/` : ''}${name}`;
629+
630+
let dependencies: IntegrationInfo['dependencies'] = [
631+
[pkgJson['name'], `^${pkgJson['version']}`],
632+
];
506633

507-
let dependencies: IntegrationInfo['dependencies'] = [
508-
[result['name'], `^${result['version']}`],
509-
];
634+
if (pkgJson['peerDependencies']) {
635+
for (const peer in pkgJson['peerDependencies']) {
636+
dependencies.push([peer, pkgJson['peerDependencies'][peer]]);
637+
}
638+
}
510639

511-
if (result['peerDependencies']) {
512-
for (const peer in result['peerDependencies']) {
513-
dependencies.push([peer, result['peerDependencies'][peer]]);
640+
let integrationType: IntegrationInfo['type'];
641+
const keywords = Array.isArray(pkgJson['keywords']) ? pkgJson['keywords'] : [];
642+
if (keywords.includes('astro-integration')) {
643+
integrationType = 'integration';
644+
} else if (keywords.includes('astro-adapter')) {
645+
integrationType = 'adapter';
646+
} else {
647+
throw new Error(
648+
`${bold(packageName)} doesn't appear to be an integration or an adapter. Find our official integrations at ${cyan('https://astro.build/integrations')}`
649+
);
514650
}
515-
}
516651

517-
return { id: integration, packageName, dependencies };
518-
})
519-
);
520-
spinner.succeed();
521-
return integrationEntries;
652+
return { id: integration, packageName, dependencies, type: integrationType };
653+
})
654+
);
655+
spinner.succeed();
656+
return integrationEntries;
657+
} catch (e) {
658+
if (e instanceof Error) {
659+
spinner.fail(e.message);
660+
process.exit(1);
661+
} else {
662+
throw e;
663+
}
664+
}
522665
}
523666

524667
function parseIntegrationName(spec: string) {

packages/integrations/cloudflare/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"url": "https://github.com/withastro/astro.git",
1212
"directory": "packages/integrations/cloudflare"
1313
},
14+
"keywords": ["astro-adapter"],
1415
"bugs": "https://github.com/withastro/astro/issues",
1516
"homepage": "https://astro.build",
1617
"exports": {

packages/integrations/deno/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"url": "https://github.com/withastro/astro.git",
1212
"directory": "packages/integrations/deno"
1313
},
14+
"keywords": ["astro-adapter"],
1415
"bugs": "https://github.com/withastro/astro/issues",
1516
"homepage": "https://astro.build",
1617
"exports": {

packages/integrations/image/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"directory": "packages/integrations/image"
1313
},
1414
"keywords": [
15+
"astro-integration",
1516
"astro-component",
1617
"withastro",
1718
"image"

packages/integrations/lit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"directory": "packages/integrations/lit"
1313
},
1414
"keywords": [
15+
"astro-integration",
1516
"astro-component",
1617
"renderer",
1718
"lit"

packages/integrations/mdx/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"directory": "packages/integrations/mdx"
1313
},
1414
"keywords": [
15+
"astro-integration",
1516
"astro-component",
1617
"renderer",
1718
"mdx"

packages/integrations/netlify/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"url": "https://github.com/withastro/astro.git",
1212
"directory": "packages/integrations/netlify"
1313
},
14+
"keywords": ["astro-adapter"],
1415
"bugs": "https://github.com/withastro/astro/issues",
1516
"homepage": "https://astro.build",
1617
"exports": {

packages/integrations/node/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"url": "https://github.com/withastro/astro.git",
1212
"directory": "packages/integrations/node"
1313
},
14+
"keywords": ["astro-adapter"],
1415
"bugs": "https://github.com/withastro/astro/issues",
1516
"homepage": "https://astro.build",
1617
"exports": {

packages/integrations/partytown/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"directory": "packages/integrations/partytown"
1313
},
1414
"keywords": [
15+
"astro-integration",
1516
"astro-component",
1617
"analytics",
1718
"performance"

0 commit comments

Comments
 (0)