Skip to content

Commit

Permalink
Use magicast for astro add (#11772)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy authored Aug 28, 2024
1 parent fe80a2c commit 6272e6c
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 234 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-balloons-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Uses `magicast` to update the config for `astro add`
4 changes: 1 addition & 3 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,7 @@
"@astrojs/markdown-remark": "workspace:*",
"@astrojs/telemetry": "workspace:*",
"@babel/core": "^7.25.2",
"@babel/generator": "^7.25.5",
"@babel/parser": "^7.25.4",
"@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/traverse": "^7.25.4",
"@babel/types": "^7.25.4",
"@oslojs/encoding": "^0.4.1",
"@rollup/pluginutils": "^5.1.0",
Expand Down Expand Up @@ -164,6 +161,7 @@
"js-yaml": "^4.1.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.11",
"magicast": "^0.3.5",
"micromatch": "^4.0.8",
"mrmime": "^2.0.0",
"neotraverse": "^0.6.18",
Expand Down
16 changes: 0 additions & 16 deletions packages/astro/src/cli/add/babel.ts

This file was deleted.

35 changes: 0 additions & 35 deletions packages/astro/src/cli/add/imports.ts

This file was deleted.

214 changes: 59 additions & 155 deletions packages/astro/src/cli/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ import { ensureProcessNodeEnv, parseNpmName } from '../../core/util.js';
import { eventCliSession, telemetry } from '../../events/index.js';
import { type Flags, createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js';
import { fetchPackageJson, fetchPackageVersions } from '../install-package.js';
import { generate, parse, t, visit } from './babel.js';
import { ensureImport } from './imports.js';
import { wrapDefaultExport } from './wrapper.js';
import { loadFile, generateCode, builders, type ASTNode, type ProxifiedModule } from 'magicast';
import { getDefaultExportOptions } from 'magicast/helpers';

interface AddOptions {
flags: Flags;
Expand Down Expand Up @@ -261,29 +260,26 @@ export async function add(names: string[], { flags }: AddOptions) {
await fs.writeFile(fileURLToPath(configURL), STUBS.ASTRO_CONFIG, { encoding: 'utf-8' });
}

let ast: t.File | null = null;
let mod: ProxifiedModule<any> | undefined;
try {
ast = await parseAstroConfig(configURL);

mod = await loadFile(fileURLToPath(configURL));
logger.debug('add', 'Parsed astro config');

const defineConfig = t.identifier('defineConfig');
ensureImport(
ast,
t.importDeclaration(
[t.importSpecifier(defineConfig, defineConfig)],
t.stringLiteral('astro/config'),
),
);
wrapDefaultExport(ast, defineConfig);

if (mod.exports.default.$type !== 'function-call') {
// ensure config is wrapped with `defineConfig`
mod.imports.$prepend({ imported: 'defineConfig', from: 'astro/config' });
mod.exports.default = builders.functionCall('defineConfig', mod.exports.default);
} else if (mod.exports.default.$args[0] == null) {
// ensure first argument of `defineConfig` is not empty
mod.exports.default.$args[0] = {};
}
logger.debug('add', 'Astro config ensured `defineConfig`');

for (const integration of integrations) {
if (isAdapter(integration)) {
const officialExportName = OFFICIAL_ADAPTER_TO_IMPORT_MAP[integration.id];
if (officialExportName) {
await setAdapter(ast, integration, officialExportName);
setAdapter(mod, integration);
} else {
logger.info(
'SKIP_FORMAT',
Expand All @@ -295,7 +291,7 @@ export async function add(names: string[], { flags }: AddOptions) {
);
}
} else {
await addIntegration(ast, integration);
addIntegration(mod, integration);
}
logger.debug('add', `Astro config added integration ${integration.id}`);
}
Expand All @@ -306,11 +302,11 @@ export async function add(names: string[], { flags }: AddOptions) {

let configResult: UpdateResult | undefined;

if (ast) {
if (mod) {
try {
configResult = await updateAstroConfig({
configURL,
ast,
mod,
flags,
logger,
logAdapterInstructions: integrations.some(isAdapter),
Expand Down Expand Up @@ -390,17 +386,6 @@ function isAdapter(
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);

if (!result) throw new Error('Unknown error parsing astro config');
if (result.errors.length > 0)
throw new Error('Error parsing astro config: ' + JSON.stringify(result.errors));

return result;
}

// Convert an arbitrary NPM package name into a JS identifier
// Some examples:
// - @astrojs/image => image
Expand Down Expand Up @@ -437,130 +422,47 @@ Documentation: https://docs.astro.build/en/guides/integrations-guide/`;
return err;
}

async function addIntegration(ast: t.File, integration: IntegrationInfo) {
const integrationId = t.identifier(toIdent(integration.id));

ensureImport(
ast,
t.importDeclaration(
[t.importDefaultSpecifier(integrationId)],
t.stringLiteral(integration.packageName),
),
);
function addIntegration(mod: ProxifiedModule<any>, integration: IntegrationInfo) {
const config = getDefaultExportOptions(mod);
const integrationId = toIdent(integration.id);

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 integrationsProp = configObject.properties.find((prop) => {
if (prop.type !== 'ObjectProperty') return false;
if (prop.key.type === 'Identifier') {
if (prop.key.name === 'integrations') return true;
}
if (prop.key.type === 'StringLiteral') {
if (prop.key.value === 'integrations') return true;
}
return false;
}) as t.ObjectProperty | undefined;

const integrationCall = t.callExpression(integrationId, []);

if (!integrationsProp) {
configObject.properties.push(
t.objectProperty(t.identifier('integrations'), t.arrayExpression([integrationCall])),
);
return;
}

if (integrationsProp.value.type !== 'ArrayExpression')
throw new Error('Unable to parse integrations');

const existingIntegrationCall = integrationsProp.value.elements.find(
(expr) =>
t.isCallExpression(expr) &&
t.isIdentifier(expr.callee) &&
expr.callee.name === integrationId.name,
);

if (existingIntegrationCall) return;
if (!mod.imports.$items.some((imp) => imp.local === integrationId)) {
mod.imports.$append({ imported: integrationId, from: integration.packageName });
}

integrationsProp.value.elements.push(integrationCall);
},
});
config.integrations ??= [];
if (
!config.integrations.$ast.elements.some(
(el: ASTNode) =>
el.type === 'CallExpression' &&
el.callee.type === 'Identifier' &&
el.callee.name === integrationId,
)
) {
config.integrations.push(builders.functionCall(integrationId));
}
}

async function setAdapter(ast: t.File, adapter: IntegrationInfo, exportName: string) {
const adapterId = t.identifier(toIdent(adapter.id));
export function setAdapter(mod: ProxifiedModule<any>, adapter: IntegrationInfo) {
const config = getDefaultExportOptions(mod);
const adapterId = 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 outputProp = configObject.properties.find((prop) => {
if (prop.type !== 'ObjectProperty') return false;
if (prop.key.type === 'Identifier') {
if (prop.key.name === 'output') return true;
}
if (prop.key.type === 'StringLiteral') {
if (prop.key.value === 'output') return true;
}
return false;
}) as t.ObjectProperty | undefined;

if (!outputProp) {
configObject.properties.push(
t.objectProperty(t.identifier('output'), t.stringLiteral('server')),
);
}

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;

let adapterCall;
switch (adapter.id) {
// the node adapter requires a mode
case 'node': {
adapterCall = t.callExpression(adapterId, [
t.objectExpression([
t.objectProperty(t.identifier('mode'), t.stringLiteral('standalone')),
]),
]);
break;
}
default: {
adapterCall = t.callExpression(adapterId, []);
}
}
if (!mod.imports.$items.some((imp) => imp.local === adapterId)) {
mod.imports.$append({ imported: adapterId, from: adapter.packageName });
}

if (!adapterProp) {
configObject.properties.push(t.objectProperty(t.identifier('adapter'), adapterCall));
return;
}
if (!config.output) {
config.output = 'server';
}

adapterProp.value = adapterCall;
},
});
switch (adapter.id) {
case 'node':
config.adapter = builders.functionCall(adapterId, { mode: 'standalone' });
break;
default:
config.adapter = builders.functionCall(adapterId);
break;
}
}

const enum UpdateResult {
Expand All @@ -572,23 +474,25 @@ const enum UpdateResult {

async function updateAstroConfig({
configURL,
ast,
mod,
flags,
logger,
logAdapterInstructions,
}: {
configURL: URL;
ast: t.File;
mod: ProxifiedModule<any>;
flags: Flags;
logger: Logger;
logAdapterInstructions: boolean;
}): Promise<UpdateResult> {
const input = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' });
let output = await generate(ast);
const comment = '// https://astro.build/config';
const defaultExport = 'export default defineConfig';
output = output.replace(`\n${comment}`, '');
output = output.replace(`${defaultExport}`, `\n${comment}\n${defaultExport}`);
const output = generateCode(mod, {
format: {
objectCurlySpacing: true,
useTabs: false,
tabWidth: 2,
},
}).code;

if (input === output) {
return UpdateResult.none;
Expand Down
16 changes: 0 additions & 16 deletions packages/astro/src/cli/add/wrapper.ts

This file was deleted.

Loading

0 comments on commit 6272e6c

Please sign in to comment.