summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/eight-balloons-cover.md5
-rw-r--r--packages/astro/package.json4
-rw-r--r--packages/astro/src/cli/add/babel.ts16
-rw-r--r--packages/astro/src/cli/add/imports.ts35
-rw-r--r--packages/astro/src/cli/add/index.ts214
-rw-r--r--packages/astro/src/cli/add/wrapper.ts16
-rw-r--r--pnpm-lock.yaml21
7 files changed, 77 insertions, 234 deletions
diff --git a/.changeset/eight-balloons-cover.md b/.changeset/eight-balloons-cover.md
new file mode 100644
index 000000000..ea6364668
--- /dev/null
+++ b/.changeset/eight-balloons-cover.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Uses `magicast` to update the config for `astro add`
diff --git a/packages/astro/package.json b/packages/astro/package.json
index ea1a562e0..d8a9c7c02 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -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",
@@ -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",
diff --git a/packages/astro/src/cli/add/babel.ts b/packages/astro/src/cli/add/babel.ts
deleted file mode 100644
index facaabd54..000000000
--- a/packages/astro/src/cli/add/babel.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import generator from '@babel/generator';
-import parser from '@babel/parser';
-import traverse from '@babel/traverse';
-import * as t from '@babel/types';
-
-export const visit = traverse.default;
-export { t };
-
-export async function generate(ast: t.File) {
- const astToText = generator.default;
- const { code } = astToText(ast);
- return code;
-}
-
-export const parse = (code: string) =>
- parser.parse(code, { sourceType: 'unambiguous', plugins: ['typescript'] });
diff --git a/packages/astro/src/cli/add/imports.ts b/packages/astro/src/cli/add/imports.ts
deleted file mode 100644
index 375ca1dd8..000000000
--- a/packages/astro/src/cli/add/imports.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { t, visit } from './babel.js';
-
-export function ensureImport(root: t.File, importDeclaration: t.ImportDeclaration) {
- let specifiersToFind = [...importDeclaration.specifiers];
-
- visit(root, {
- ImportDeclaration(path) {
- if (path.node.source.value === importDeclaration.source.value) {
- path.node.specifiers.forEach((specifier) =>
- specifiersToFind.forEach((specifierToFind, i) => {
- if (specifier.type !== specifierToFind.type) return;
- if (specifier.local.name === specifierToFind.local.name) {
- specifiersToFind.splice(i, 1);
- }
- }),
- );
- }
- },
- });
-
- if (specifiersToFind.length === 0) return;
-
- visit(root, {
- Program(path) {
- const declaration = t.importDeclaration(specifiersToFind, importDeclaration.source);
- const latestImport = path
- .get('body')
- .filter((statement) => statement.isImportDeclaration())
- .pop();
-
- if (latestImport) latestImport.insertAfter(declaration);
- else path.unshiftContainer('body', declaration);
- },
- });
-}
diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts
index f710184d2..91d5b5435 100644
--- a/packages/astro/src/cli/add/index.ts
+++ b/packages/astro/src/cli/add/index.ts
@@ -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;
@@ -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',
@@ -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}`);
}
@@ -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),
@@ -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
@@ -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 {
@@ -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;
diff --git a/packages/astro/src/cli/add/wrapper.ts b/packages/astro/src/cli/add/wrapper.ts
deleted file mode 100644
index c86e87698..000000000
--- a/packages/astro/src/cli/add/wrapper.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { t, visit } from './babel.js';
-
-export function wrapDefaultExport(ast: t.File, functionIdentifier: t.Identifier) {
- visit(ast, {
- ExportDefaultDeclaration(path) {
- if (!t.isExpression(path.node.declaration)) return;
- if (
- t.isCallExpression(path.node.declaration) &&
- t.isIdentifier(path.node.declaration.callee) &&
- path.node.declaration.callee.name === functionIdentifier.name
- )
- return;
- path.node.declaration = t.callExpression(functionIdentifier, [path.node.declaration]);
- },
- });
-}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 093d3615e..7bae5f21d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -570,18 +570,9 @@ importers:
'@babel/core':
specifier: ^7.25.2
version: 7.25.2
- '@babel/generator':
- specifier: ^7.25.5
- version: 7.25.5
- '@babel/parser':
- specifier: ^7.25.4
- version: 7.25.4
'@babel/plugin-transform-react-jsx':
specifier: ^7.25.2
version: 7.25.2(@babel/core@7.25.2)
- '@babel/traverse':
- specifier: ^7.25.4
- version: 7.25.4
'@babel/types':
specifier: ^7.25.4
version: 7.25.4
@@ -681,6 +672,9 @@ importers:
magic-string:
specifier: ^0.30.11
version: 0.30.11
+ magicast:
+ specifier: ^0.3.5
+ version: 0.3.5
micromatch:
specifier: ^4.0.8
version: 4.0.8
@@ -9491,6 +9485,9 @@ packages:
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
engines: {node: '>=12'}
+ magicast@0.3.5:
+ resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+
make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
@@ -15570,6 +15567,12 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
+ magicast@0.3.5:
+ dependencies:
+ '@babel/parser': 7.25.4
+ '@babel/types': 7.25.4
+ source-map-js: 1.2.0
+
make-dir@3.1.0:
dependencies:
semver: 6.3.1