@@ -3,7 +3,7 @@ import boxen from 'boxen';
33import { diffWords } from 'diff' ;
44import { execa } from 'execa' ;
55import { 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' ;
77import ora from 'ora' ;
88import path from 'path' ;
99import 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}
3637const 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+
5059export 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+
234271async 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+
317398const 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
524667function parseIntegrationName ( spec : string ) {
0 commit comments