Skip to content

Commit

Permalink
[astro add] Support adapters and third party packages (withastro#3854)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bholmesdev authored Jul 8, 2022
1 parent aecb154 commit 57319c9
Showing 1 changed file with 180 additions and 37 deletions.
217 changes: 180 additions & 37 deletions src/core/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'],
Expand All @@ -47,11 +48,19 @@ module.exports = {
plugins: [],
}\n`;

const OFFICIAL_ADAPTER_TO_IMPORT_MAP: Record<string, string> = {
'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.'],
Expand All @@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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<t.File> {
const source = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' });
const result = parse(source);
Expand Down Expand Up @@ -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,
Expand All @@ -326,11 +407,13 @@ async function updateAstroConfig({
ast,
flags,
logging,
logAdapterInstructions,
}: {
configURL: URL;
ast: t.File;
flags: yargs.Arguments;
logging: LogOptions;
logAdapterInstructions: boolean;
}): Promise<UpdateResult> {
const input = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' });
let output = await generate(ast);
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -479,46 +570,98 @@ async function tryToInstallIntegrations({
}
}

export async function validateIntegrations(integrations: string[]): Promise<IntegrationInfo[]> {
const spinner = ora('Resolving integrations...').start();
const integrationEntries = await Promise.all(
integrations.map(async (integration): Promise<IntegrationInfo> => {
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<object | Error> {
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<IntegrationInfo[]> {
const spinner = ora('Resolving packages...').start();
try {
const integrationEntries = await Promise.all(
integrations.map(async (integration): Promise<IntegrationInfo> => {
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) {
Expand Down

0 comments on commit 57319c9

Please sign in to comment.