Skip to content

Commit

Permalink
feat(cli): scaffold out astro add command (withastro#2849)
Browse files Browse the repository at this point in the history
* feat(cli): scaffold out `astro add` command

* added first babel transforms

* Format output

* Added changes confirmation

* Error flow

* Add dependencies

* feat(cli): astro add cleanup pass

* feat: add support for tailwind

* chore: update lockfile

* fix: types

* chore: rever @proload/core bump

* chore: add changeset

* chore: rollback dep update

* Added spinners

* chore: remove extra deps

* Removed extra argument

* Use `execa` instead of `exec`

* Changed how lines are trimmed within diffLines

* refactor: move add to core

* refactor: remove old add entrypoint

* refactor: simplify wording

* feat: improve diff

* feat: improve diff and logging, add interactive prompt when no args passed

* Formatted files

* Added --yes

* feat: improve logging for install command

* Fixed execa

* Added help message to add

* refactor: extract consts to own file

* feat: remove implicit projectRoot behavior

* feat: improve error handling, existing integrations

* fix(tailwind): ensure existing tailwind config is not overwritten

* refactor: prefer cwd to projectRoot flag

* chore: add refactor notes

* refactor: throw createPrettyError > implicit bail

* refactor: cleanup language

* feat(cli): prompt user before generating tailwind config

* fix(cli): update config generation to use cwd

* fix: resolve root from cwd

* chore: update changelog

Co-authored-by: JuanM04 <[email protected]>
  • Loading branch information
natemoo-re and JuanM04 authored Mar 25, 2022
1 parent 21d617e commit 2d12171
Show file tree
Hide file tree
Showing 9 changed files with 696 additions and 59 deletions.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,17 @@
"@astrojs/prism": "0.4.1-next.0",
"@astrojs/webapi": "^0.11.0",
"@babel/core": "^7.17.8",
"@babel/generator": "^7.17.7",
"@babel/parser": "^7.17.8",
"@babel/traverse": "^7.17.3",
"@proload/core": "^0.2.2",
"@proload/plugin-tsm": "^0.1.1",
"@web/parse5-utils": "^1.3.0",
"boxen": "^6.2.1",
"ci-info": "^3.3.0",
"common-ancestor-path": "^1.0.1",
"debug": "^4.3.4",
"diff": "^5.0.0",
"eol": "^0.9.1",
"es-module-lexer": "^0.10.4",
"esbuild": "0.14.25",
Expand All @@ -99,11 +103,14 @@
"magic-string": "^0.25.9",
"micromorph": "^0.1.2",
"mime": "^3.0.0",
"ora": "^6.1.0",
"parse5": "^6.0.1",
"path-to-regexp": "^6.2.0",
"postcss": "^8.4.12",
"postcss-load-config": "^3.1.3",
"preferred-pm": "^3.0.3",
"prismjs": "^1.27.0",
"prompts": "^2.4.2",
"rehype-slug": "^5.0.1",
"resolve": "^1.22.0",
"rollup": "^2.70.1",
Expand All @@ -126,16 +133,19 @@
"devDependencies": {
"@babel/types": "^7.17.0",
"@types/babel__core": "^7.1.19",
"@types/babel__generator": "^7.6.4",
"@types/babel__traverse": "^7.14.2",
"@types/chai": "^4.3.0",
"@types/common-ancestor-path": "^1.0.0",
"@types/connect": "^3.4.35",
"@types/debug": "^4.1.7",
"@types/diff": "^5.0.2",
"@types/estree": "^0.0.51",
"@types/html-escaper": "^3.0.0",
"@types/mime": "^2.0.3",
"@types/mocha": "^9.1.0",
"@types/parse5": "^6.0.3",
"@types/prettier": "^2.4.4",
"@types/resolve": "^1.20.1",
"@types/rimraf": "^3.0.2",
"@types/send": "^0.17.1",
Expand Down
88 changes: 31 additions & 57 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,31 @@ import yargs from 'yargs-parser';
import { z } from 'zod';
import { defaultLogDestination } from '../core/logger.js';
import build from '../core/build/index.js';
import add from '../core/add/index.js';
import devServer from '../core/dev/index.js';
import preview from '../core/preview/index.js';
import { check } from './check.js';
import { formatConfigError, loadConfig } from '../core/config.js';
import { pad } from '../core/dev/util.js';
import { printHelp } from '../core/messages.js';

type Arguments = yargs.Arguments;
type CLICommand = 'help' | 'version' | 'dev' | 'build' | 'preview' | 'reload' | 'check';
type CLICommand = 'help' | 'version' | 'add' | 'dev' | 'build' | 'preview' | 'reload' | 'check';

/** Display --help flag */
function printHelp() {
linebreak();
headline('astro', 'Futuristic web development tool.');
linebreak();
title('Commands');
table(
[
function printAstroHelp() {
printHelp({
commandName: 'astro',
headline: 'Futuristic web development tool.',
commands: [
['add', 'Add an integration to your configuration.'],
['dev', 'Run Astro in development mode.'],
['build', 'Build a pre-compiled production-ready site.'],
['preview', 'Preview your build locally before deploying.'],
['check', 'Check your project for errors.'],
['--version', 'Show the version number and exit.'],
['--help', 'Show this help message.'],
],
{ padding: 28, prefix: ' astro ' }
);
linebreak();
title('Flags');
table(
[
flags: [
['--host [optional IP]', 'Expose server on network'],
['--config <path>', 'Specify the path to the Astro config file.'],
['--project-root <path>', 'Specify the path to the project root folder.'],
Expand All @@ -48,39 +43,7 @@ function printHelp() {
['--verbose', 'Enable verbose logging'],
['--silent', 'Disable logging'],
],
{ padding: 28, prefix: ' ' }
);

// Logging utils
function linebreak() {
console.log();
}

function headline(name: string, tagline: string) {
console.log(` ${colors.bgGreen(colors.black(` ${name} `))} ${colors.green(`v${process.env.PACKAGE_VERSION ?? ''}`)} ${tagline}`);
}
function title(label: string) {
console.log(` ${colors.bgWhite(colors.black(` ${label} `))}`);
}
function table(rows: [string, string][], opts: { padding: number; prefix: string }) {
const split = rows.some((row) => {
const message = `${opts.prefix}${' '.repeat(opts.padding)}${row[1]}`;
return message.length > process.stdout.columns;
});
for (const row of rows) {
row.forEach((col, i) => {
if (i === 0) {
process.stdout.write(`${opts.prefix}${colors.bold(pad(`${col}`, opts.padding - opts.prefix.length))}`);
} else {
if (split) {
process.stdout.write('\n ');
}
process.stdout.write(colors.dim(col) + '\n');
}
});
}
return '';
}
});
}

/** Display --version flag */
Expand All @@ -93,15 +56,15 @@ async function printVersion() {

/** Determine which command the user requested */
function resolveCommand(flags: Arguments): CLICommand {
if (flags.version) {
return 'version';
} else if (flags.help) {
return 'help';
}
const cmd = flags._[2] as string;
if (cmd === 'add') return 'add';

if (flags.version) return 'version';
else if (flags.help) return 'help';

const supportedCommands = new Set(['dev', 'build', 'preview', 'check']);
if (supportedCommands.has(cmd)) {
return cmd as 'dev' | 'build' | 'preview' | 'check';
return cmd as CLICommand;
}
return 'help';
}
Expand All @@ -110,11 +73,11 @@ function resolveCommand(flags: Arguments): CLICommand {
export async function cli(args: string[]) {
const flags = yargs(args);
const cmd = resolveCommand(flags);
const projectRoot = flags.projectRoot || flags._[3];
const projectRoot = flags.projectRoot;

switch (cmd) {
case 'help':
printHelp();
printAstroHelp();
return process.exit(0);
case 'version':
await printVersion();
Expand All @@ -135,13 +98,25 @@ export async function cli(args: string[]) {

let config: AstroConfig;
try {
// Note: ideally, `loadConfig` would return the config AND its filePath
// For now, `add` has to resolve the config again internally
config = await loadConfig({ cwd: projectRoot, flags });
} catch (err) {
throwAndExit(err);
return;
}

switch (cmd) {
case 'add': {
try {
const packages = flags._.slice(3) as string[];
await add(packages, { cwd: projectRoot, flags, logging });
process.exit(0);
} catch (err) {
throwAndExit(err);
}
return;
}
case 'dev': {
try {
await devServer(config, { logging });
Expand All @@ -150,7 +125,6 @@ export async function cli(args: string[]) {
} catch (err) {
throwAndExit(err);
}

return;
}

Expand Down
17 changes: 17 additions & 0 deletions src/core/add/babel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import traverse from '@babel/traverse';
import generator from '@babel/generator';
import * as t from '@babel/types';
import parser from '@babel/parser';

// @ts-ignore @babel/traverse isn't ESM and needs this trick
export const visit = traverse.default as typeof traverse;
export { t };

export async function generate(ast: t.File) {
// @ts-ignore @babel/generator isn't ESM and needs this trick
const astToText = generator.default as typeof generator;
const { code } = astToText(ast);
return code;
}

export const parse = (code: string) => parser.parse(code, { sourceType: 'unambiguous', plugins: ['typescript'] });
26 changes: 26 additions & 0 deletions src/core/add/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const FIRST_PARTY_FRAMEWORKS = [
{ value: 'react', title: 'React' },
{ value: 'preact', title: 'Preact' },
{ value: 'vue', title: 'Vue' },
{ value: 'svelte', title: 'Svelte' },
{ value: 'solid-js', title: 'Solid' },
{ value: 'lit', title: 'Lit' },
];
export const FIRST_PARTY_ADDONS = [
{ value: 'tailwind', title: 'Tailwind' },
{ value: 'turbolinks', title: 'Turbolinks' },
{ value: 'partytown', title: 'Partytown' },
{ value: 'sitemap', title: 'Sitemap' },
];
export const ALIASES = new Map([
['solid', 'solid-js'],
['tailwindcss', 'tailwind'],
]);
export const CONFIG_STUB = `import { defineConfig } from 'astro/config';\n\nexport default defineConfig({});`;
export const TAILWIND_CONFIG_STUB = `module.exports = {
content: [],
theme: {
extend: {},
},
plugins: [],
}\n`;
35 changes: 35 additions & 0 deletions src/core/add/imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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);
},
});
}
Loading

0 comments on commit 2d12171

Please sign in to comment.