From 72ef7ae64a6d0fd17077bf6920ce11613116b659 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 25 Mar 2022 16:26:55 -0500 Subject: [PATCH] feat(cli): scaffold out `astro add` command (#2849) * 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 --- .changeset/brave-rings-jump.md | 9 + examples/with-tailwindcss/tailwind.config.cjs | 7 + packages/astro/package.json | 10 + packages/astro/src/cli/index.ts | 88 ++-- packages/astro/src/core/add/babel.ts | 17 + packages/astro/src/core/add/consts.ts | 26 + packages/astro/src/core/add/imports.ts | 35 ++ packages/astro/src/core/add/index.ts | 459 ++++++++++++++++++ packages/astro/src/core/add/wrapper.ts | 11 + packages/astro/src/core/config.ts | 23 +- packages/astro/src/core/messages.ts | 86 +++- pnpm-lock.yaml | 105 +++- 12 files changed, 792 insertions(+), 84 deletions(-) create mode 100644 .changeset/brave-rings-jump.md create mode 100644 examples/with-tailwindcss/tailwind.config.cjs create mode 100644 packages/astro/src/core/add/babel.ts create mode 100644 packages/astro/src/core/add/consts.ts create mode 100644 packages/astro/src/core/add/imports.ts create mode 100644 packages/astro/src/core/add/index.ts create mode 100644 packages/astro/src/core/add/wrapper.ts diff --git a/.changeset/brave-rings-jump.md b/.changeset/brave-rings-jump.md new file mode 100644 index 000000000..1fbb5c90d --- /dev/null +++ b/.changeset/brave-rings-jump.md @@ -0,0 +1,9 @@ +--- +'astro': minor +--- + +Introduce new `astro add` command to automatically configure integrations. + +```shell +npx astro add +``` diff --git a/examples/with-tailwindcss/tailwind.config.cjs b/examples/with-tailwindcss/tailwind.config.cjs new file mode 100644 index 000000000..81cb91aee --- /dev/null +++ b/examples/with-tailwindcss/tailwind.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + content: [], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/packages/astro/package.json b/packages/astro/package.json index 23926f8d0..da89cddd9 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -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", @@ -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", @@ -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", diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index ffa1071cc..fa6fc7547 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -8,23 +8,23 @@ 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.'], @@ -32,12 +32,7 @@ function printHelp() { ['--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 ', 'Specify the path to the Astro config file.'], ['--project-root ', 'Specify the path to the project root folder.'], @@ -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 */ @@ -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'; } @@ -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(); @@ -135,6 +98,8 @@ 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); @@ -142,6 +107,16 @@ export async function cli(args: string[]) { } 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 }); @@ -150,7 +125,6 @@ export async function cli(args: string[]) { } catch (err) { throwAndExit(err); } - return; } diff --git a/packages/astro/src/core/add/babel.ts b/packages/astro/src/core/add/babel.ts new file mode 100644 index 000000000..8ec31cd46 --- /dev/null +++ b/packages/astro/src/core/add/babel.ts @@ -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'] }); diff --git a/packages/astro/src/core/add/consts.ts b/packages/astro/src/core/add/consts.ts new file mode 100644 index 000000000..7eb64b4db --- /dev/null +++ b/packages/astro/src/core/add/consts.ts @@ -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`; diff --git a/packages/astro/src/core/add/imports.ts b/packages/astro/src/core/add/imports.ts new file mode 100644 index 000000000..bae8c7443 --- /dev/null +++ b/packages/astro/src/core/add/imports.ts @@ -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); + }, + }); +} diff --git a/packages/astro/src/core/add/index.ts b/packages/astro/src/core/add/index.ts new file mode 100644 index 000000000..4175cbf2f --- /dev/null +++ b/packages/astro/src/core/add/index.ts @@ -0,0 +1,459 @@ +import type yargs from 'yargs-parser'; +import path from 'path'; +import { existsSync, promises as fs } from 'fs'; +import { execa } from 'execa'; +import { fileURLToPath, pathToFileURL } from 'url'; +import { diffWords } from 'diff'; +import boxen from 'boxen'; +import prompts from 'prompts'; +import preferredPM from 'preferred-pm'; +import ora from 'ora'; +import { resolveConfigURL } from '../config.js'; +import { apply as applyPolyfill } from '../polyfill.js'; +import { error, info, debug, LogOptions } from '../logger.js'; +import { printHelp } from '../messages.js'; +import * as msg from '../messages.js'; +import * as CONSTS from './consts.js'; +import { dim, red, cyan, green, magenta, bold } from 'kleur/colors'; +import { parseNpmName } from '../util.js'; +import { wrapDefaultExport } from './wrapper.js'; +import { ensureImport } from './imports.js'; +import { t, parse, visit, generate } from './babel.js'; + +export interface AddOptions { + logging: LogOptions; + flags: yargs.Arguments; + cwd?: string; +} + +export interface IntegrationInfo { + id: string; + packageName: string; + dependencies: [name: string, version: string][]; +} + +export default async function add(names: string[], { cwd, flags, logging }: AddOptions) { + if (flags.help) { + printHelp({ + commandName: 'astro add', + usage: '[FLAGS] [INTEGRATIONS...]', + flags: [ + ['--yes', 'Add the integration without user interaction.'], + ['--help', 'Show this help message.'], + ], + }); + return; + } + let configURL: URL | undefined; + const root = pathToFileURL(cwd ? path.resolve(cwd) : process.cwd()); + // TODO: improve error handling for invalid configs + configURL = await resolveConfigURL({ cwd, flags }); + + if (configURL?.pathname.endsWith('package.json')) { + throw new Error(`Unable to use astro add with package.json#astro configuration! Try migrating to \`astro.config.mjs\` and try again.`); + } + applyPolyfill(); + + if (names.length === 0) { + const response = await prompts([ + { + type: 'multiselect', + name: 'frameworks', + message: 'What frameworks would you like to enable?', + instructions: '\n Space to select. Return to submit', + choices: CONSTS.FIRST_PARTY_FRAMEWORKS, + }, + { + type: 'multiselect', + name: 'addons', + message: 'What additional integrations would you like to enable?', + instructions: '\n Space to select. Return to submit', + choices: CONSTS.FIRST_PARTY_ADDONS, + }, + ]); + + if (!response.frameworks && !response.addons) { + info(logging, null, msg.cancelled(`Integrations skipped.`, `You can always run ${cyan('astro add')} later!`)); + return; + } + const selected = [response.frameworks ?? [], response.addons ?? []].flat(1); + if (selected.length === 0) { + error(logging, null, `\n${red('No integrations specified!')}\n${dim('Try running')} astro add again.`); + return; + } + names = selected; + } + + // Some packages might have a common alias! We normalize those here. + names = names.map((name) => (CONSTS.ALIASES.has(name) ? CONSTS.ALIASES.get(name)! : name)); + + if (configURL) { + debug('add', `Found config at ${configURL}`); + } else { + info(logging, 'add', `Unable to locate a config file, generating one for you.`); + configURL = new URL('./astro.config.mjs', root); + await fs.writeFile(fileURLToPath(configURL), CONSTS.CONFIG_STUB, { encoding: 'utf-8' }); + } + + const integrations = await validateIntegrations(names); + + let ast: t.File | null = null; + try { + ast = await parseAstroConfig(configURL); + + 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); + + debug('add', 'Astro config ensured `defineConfig`'); + + for (const integration of integrations) { + await addIntegration(ast, integration); + debug('add', `Astro config added integration ${integration.id}`); + } + } catch (err) { + debug('add', 'Error parsing/modifying astro config: ', err); + throw createPrettyError(err as Error); + } + + let configResult: UpdateResult | undefined; + let installResult: UpdateResult | undefined; + + if (ast) { + try { + configResult = await updateAstroConfig({ configURL, ast, flags, logging }); + } catch (err) { + debug('add', 'Error updating astro config', err); + throw createPrettyError(err as Error); + } + } + + switch (configResult) { + case UpdateResult.cancelled: { + info(logging, null, msg.cancelled(`Your configuration has ${bold('NOT')} been updated.`)); + return; + } + case UpdateResult.none: { + const pkgURL = new URL('./package.json', configURL); + if (existsSync(fileURLToPath(pkgURL))) { + const { dependencies = {}, devDependencies = {} } = await fs.readFile(fileURLToPath(pkgURL)).then(res => JSON.parse(res.toString())); + const deps = Object.keys(Object.assign(dependencies, devDependencies)); + const missingDeps = integrations.filter(integration => !deps.includes(integration.packageName)); + if (missingDeps.length === 0) { + info(logging, null, msg.success(`Configuration up-to-date.`)); + return; + } + } + + info(logging, null, msg.success(`Configuration up-to-date.`)); + break; + } + } + + installResult = await tryToInstallIntegrations({ integrations, cwd, flags, logging }); + + switch (installResult) { + case UpdateResult.updated: { + const len = integrations.length; + if (integrations.find((integration) => integration.id === 'tailwind')) { + const possibleConfigFiles = ['./tailwind.config.cjs', './tailwind.config.mjs', './tailwind.config.js'].map(p => fileURLToPath(new URL(p, configURL))); + let alreadyConfigured = false; + for (const possibleConfigPath of possibleConfigFiles) { + if (existsSync(possibleConfigPath)) { + alreadyConfigured = true; + break; + } + } + if (!alreadyConfigured) { + info(logging, null, `\n ${magenta(`Astro will generate a minimal ${bold('./tailwind.config.cjs')} file.`)}\n`); + if (await askToContinue({ flags })) { + await fs.writeFile(fileURLToPath(new URL('./tailwind.config.cjs', configURL)), CONSTS.TAILWIND_CONFIG_STUB, { encoding: 'utf-8' }); + debug('add', `Generated default ./tailwind.config.cjs file`); + } + } else { + debug('add', `Using existing Tailwind configuration`); + } + } + const list = integrations.map(integration => ` - ${integration.packageName}`).join('\n') + info(logging, null, msg.success(`Added the following integration${len === 1 ? '' : 's'} to your project:\n${list}`)); + return; + } + case UpdateResult.cancelled: { + info(logging, null, msg.cancelled(`Dependencies ${bold('NOT')} installed.`, `Be sure to install them manually before continuing!`)); + return; + } + case UpdateResult.failure: { + throw createPrettyError(new Error(`Unable to install dependencies`)); + } + } +} + +async function parseAstroConfig(configURL: URL): Promise { + 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; +} + +const toIdent = (name: string) => { + if (name.includes('-')) { + return name.split('-')[0]; + } + return name; +}; + +function createPrettyError(err: Error) { + err.message = `Astro could not update your astro.config.js file safely. +Reason: ${err.message} + +You will need to add these integration(s) manually. +Documentation: https://next--astro-docs-2.netlify.app/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))); + + 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; + + integrationsProp.value.elements.push(integrationCall); + }, + }); +} + +const enum UpdateResult { + none, + updated, + cancelled, + failure, +} + +async function updateAstroConfig({ configURL, ast, flags, logging }: { configURL: URL; ast: t.File; flags: yargs.Arguments; logging: LogOptions }): Promise { + 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(` ${comment}`, ''); + output = output.replace(`${defaultExport}`, `\n${comment}\n${defaultExport}`); + + if (input === output) { + return UpdateResult.none; + } + + let changes = []; + for (const change of diffWords(input, output)) { + let lines = change.value + .trim() + .split('\n') + .slice(0, change.count) + if (lines.length === 0) continue; + if (change.added) { + if (!change.value.trim()) continue; + changes.push(change.value); + } + } + if (changes.length === 0) { + return UpdateResult.none; + } + + let diffed = output; + for (let newContent of changes) { + const coloredOutput = newContent + .split('\n') + .map((ln) => (ln ? green(ln) : '')) + .join('\n'); + diffed = diffed.replace(newContent, coloredOutput); + } + + const message = `\n${boxen(diffed, { margin: 0.5, padding: 0.5, borderStyle: 'round', title: configURL.pathname.split('/').pop() })}\n`; + + info(logging, null, `\n ${magenta('Astro will make the following changes to your config file:')}\n${message}`); + + if (await askToContinue({ flags })) { + await fs.writeFile(fileURLToPath(configURL), output, { encoding: 'utf-8' }); + debug('add', `Updated astro config`); + return UpdateResult.updated; + } else { + return UpdateResult.cancelled; + } +} + +interface InstallCommand { + pm: string; + command: string; + flags: string[]; + dependencies: string[]; +} +async function getInstallIntegrationsCommand({ integrations, cwd = process.cwd() }: { integrations: IntegrationInfo[]; cwd?: string }): Promise { + const pm = await preferredPM(cwd); + debug('add', `package manager: ${JSON.stringify(pm)}`); + if (!pm) return null; + + let dependencies = integrations + .map<[string, string | null][]>((i) => [[i.packageName, null], ...i.dependencies]) + .flat(1) + .filter((dep, i, arr) => arr.findIndex((d) => d[0] === dep[0]) === i) + .map(([name, version]) => (version === null ? name : `${name}@${version}`)) + .sort(); + + switch (pm.name) { + case 'npm': + return { pm: 'npm', command: 'install', flags: ['--save-dev'], dependencies }; + case 'yarn': + return { pm: 'yarn', command: 'add', flags: ['--dev'], dependencies }; + case 'pnpm': + return { pm: 'pnpm', command: 'install', flags: ['--save-dev'], dependencies }; + default: + return null; + } +} + +async function tryToInstallIntegrations({ + integrations, + cwd, + flags, + logging, +}: { + integrations: IntegrationInfo[]; + cwd?: string; + flags: yargs.Arguments; + logging: LogOptions; +}): Promise { + const installCommand = await getInstallIntegrationsCommand({ integrations, cwd }); + + if (installCommand === null) { + info(logging, null); + return UpdateResult.none; + } else { + const coloredOutput = `${bold(installCommand.pm)} ${installCommand.command} ${installCommand.flags.join(' ')} ${cyan(installCommand.dependencies.join(' '))}`; + const message = `\n${boxen(coloredOutput, { margin: 0.5, padding: 0.5, borderStyle: 'round' })}\n`; + info( + logging, + null, + `\n ${magenta('Astro will run the following command:')}\n ${dim('If you skip this step, you can always run it yourself later')}\n${message}` + ); + + if (await askToContinue({ flags })) { + const spinner = ora('Installing dependencies...').start(); + try { + await execa(installCommand.pm, [installCommand.command, ...installCommand.flags, ...installCommand.dependencies], { cwd }); + spinner.succeed(); + return UpdateResult.updated; + } catch (err) { + debug('add', 'Error installing dependencies', err); + spinner.fail(); + return UpdateResult.failure; + } + } else { + return UpdateResult.cancelled; + } + } +} + +export async function validateIntegrations(integrations: string[]): Promise { + const spinner = ora('Resolving integrations...').start(); + const integrationEntries = await Promise.all( + integrations.map(async (integration): Promise => { + const parsed = parseIntegrationName(integration); + if (!parsed) { + spinner.fail(); + throw new Error(`${integration} does not appear to be a valid package name!`); + } + + let { scope = '', name, tag } = parsed; + // Allow third-party integrations starting with `astro-` namespace + if (!name.startsWith('astro-')) { + scope = `astrojs`; + } + const packageName = `${scope ? `@${scope}/` : ''}${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?`); + } + return res.json(); + }); + + let dependencies: IntegrationInfo['dependencies'] = [[result['name'], `^${result['version']}`]]; + + if (result['peerDependencies']) { + for (const peer in result['peerDependencies']) { + dependencies.push([peer, result['peerDependencies'][peer]]); + } + } + + return { id: integration, packageName, dependencies }; + }) + ); + spinner.succeed(); + return integrationEntries; +} + +function parseIntegrationName(spec: string) { + const result = parseNpmName(spec); + if (!result) return; + let { scope, name } = result; + let tag = 'latest'; + if (scope) { + name = name.replace(scope + '/', ''); + } + if (name.includes('@')) { + const tagged = name.split('@'); + name = tagged[0]; + tag = tagged[1]; + } + return { scope, name, tag }; +} + +async function askToContinue({ flags }: { flags: yargs.Arguments }): Promise { + if (flags.yes) return true; + + const response = await prompts({ + type: 'confirm', + name: 'askToContinue', + message: 'Continue?', + initial: true, + }); + + return Boolean(response.askToContinue); +} diff --git a/packages/astro/src/core/add/wrapper.ts b/packages/astro/src/core/add/wrapper.ts new file mode 100644 index 000000000..a8f6b3bc8 --- /dev/null +++ b/packages/astro/src/core/add/wrapper.ts @@ -0,0 +1,11 @@ +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/packages/astro/src/core/config.ts b/packages/astro/src/core/config.ts index 26ebc0931..7bc121963 100644 --- a/packages/astro/src/core/config.ts +++ b/packages/astro/src/core/config.ts @@ -11,7 +11,6 @@ import load from '@proload/core'; import loadTypeScript from '@proload/plugin-tsm'; import postcssrc from 'postcss-load-config'; import { arraify, isObject } from './util.js'; -import ssgAdapter from '../adapter-ssg/index.js'; load.use([loadTypeScript]); @@ -266,6 +265,28 @@ interface LoadConfigOptions { flags?: Flags; } +/** + * Resolve the file URL of the user's `astro.config.js|cjs|mjs|ts` file + * Note: currently the same as loadConfig but only returns the `filePath` + * instead of the resolved config + */ +export async function resolveConfigURL(configOptions: LoadConfigOptions): Promise { + const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd(); + const flags = resolveFlags(configOptions.flags || {}); + let userConfigPath: string | undefined; + + if (flags?.config) { + userConfigPath = /^\.*\//.test(flags.config) ? flags.config : `./${flags.config}`; + userConfigPath = fileURLToPath(new URL(userConfigPath, `file://${root}/`)); + } + // Automatically load config file using Proload + // If `userConfigPath` is `undefined`, Proload will search for `astro.config.[cm]?[jt]s` + const config = await load('astro', { mustExist: false, cwd: root, filePath: userConfigPath }); + if (config) { + return pathToFileURL(config.filePath); + } +} + /** Attempt to load an `astro.config.mjs` file */ export async function loadConfig(configOptions: LoadConfigOptions): Promise { const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd(); diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index c84a454d1..e3a7741c4 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -3,7 +3,7 @@ */ import stripAnsi from 'strip-ansi'; -import { bold, dim, red, green, underline, yellow, bgYellow, cyan, bgGreen, black } from 'kleur/colors'; +import { bold, dim, red, green, underline, yellow, bgYellow, cyan, bgGreen, black, bgRed, bgWhite } from 'kleur/colors'; import { pad, emoji, getLocalAddress, getNetworkLogging } from './dev/util.js'; import os from 'os'; import type { AddressInfo } from 'net'; @@ -87,6 +87,36 @@ export function prerelease({ currentVersion }: { currentVersion: string }) { return [headline, warning, ''].map((msg) => ` ${msg}`).join('\n'); } +export function success(message: string, tip?: string) { + const badge = bgGreen(black(` success `)); + const headline = green(message); + const footer = tip ? `\n ▶ ${tip}` : undefined; + return ['', badge, headline, footer] + .filter((v) => v !== undefined) + .map((msg) => ` ${msg}`) + .join('\n'); +} + +export function failure(message: string, tip?: string) { + const badge = bgRed(black(` error `)); + const headline = red(message); + const footer = tip ? `\n ▶ ${tip}` : undefined; + return ['', badge, headline, footer] + .filter((v) => v !== undefined) + .map((msg) => ` ${msg}`) + .join('\n'); +} + +export function cancelled(message: string, tip?: string) { + const badge = bgYellow(black(` cancelled `)); + const headline = yellow(message); + const footer = tip ? `\n ▶ ${tip}` : undefined; + return ['', badge, headline, footer] + .filter((v) => v !== undefined) + .map((msg) => ` ${msg}`) + .join('\n'); +} + /** Display port in use */ export function portInUse({ port }: { port: number }): string { return `Port ${port} in use. Trying a new one…`; @@ -102,3 +132,57 @@ export function err(error: Error): string { stack = stack.slice(split).replace(/^\n+/, ''); return `${message}\n${dim(stack)}`; } + +export function printHelp({ + commandName, + headline, + usage, + commands, + flags, +}: { + commandName: string; + headline?: string; + usage?: string; + commands?: [command: string, help: string][]; + flags?: [flag: string, help: string][]; +}) { + const linebreak = () => ''; + const title = (label: string) => ` ${bgWhite(black(` ${label} `))}`; + const 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; + }); + + let raw = ''; + + for (const row of rows) { + raw += `${opts.prefix}${bold(pad(`${row[0]}`, opts.padding - opts.prefix.length))}`; + if (split) raw += '\n '; + raw += dim(row[1]) + '\n'; + } + + return raw.slice(0, -1); // remove latest \n + }; + + let message = []; + + if (headline) { + message.push(linebreak(), ` ${bgGreen(black(` ${commandName} `))} ${green(`v${process.env.PACKAGE_VERSION ?? ''}`)} ${headline}`); + } + + if (usage) { + message.push(linebreak(), ` ${green(commandName)} ${bold(usage)}`); + } + + if (commands) { + message.push(linebreak(), title('Commands'), table(commands, { padding: 28, prefix: ' astro ' })); + } + + if (flags) { + message.push(linebreak(), title('Flags'), table(flags, { padding: 28, prefix: ' ' })); + } + + // eslint-disable-next-line no-console + console.log(message.join('\n')); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20e1d39d9..29351650e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -448,32 +448,39 @@ importers: '@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 '@babel/types': ^7.17.0 '@proload/core': ^0.2.2 '@proload/plugin-tsm': ^0.1.1 '@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 '@types/yargs-parser': ^21.0.0 '@web/parse5-utils': ^1.3.0 astro-scripts: workspace:* + boxen: ^6.2.1 chai: ^4.3.6 cheerio: ^1.0.0-rc.10 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 @@ -489,11 +496,14 @@ importers: micromorph: ^0.1.2 mime: ^3.0.0 mocha: ^9.2.2 + 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 @@ -520,13 +530,17 @@ importers: '@astrojs/prism': link:../astro-prism '@astrojs/webapi': link:../webapi '@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_@proload+core@0.2.2 '@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 @@ -540,11 +554,14 @@ importers: 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 @@ -566,16 +583,19 @@ importers: 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 @@ -3821,6 +3841,10 @@ packages: resolution: {integrity: sha512-CL7y71j2zaDmtPLD5Xq5S1Gv2dFoHl0/GBZm6s39Mj/ls28L3NzAOqf7H4H0/2TNVMgMjMVf9CAFYSjmXhi3bw==} dev: false + /@types/diff/5.0.2: + resolution: {integrity: sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==} + dev: true + /@types/estree-jsx/0.0.1: resolution: {integrity: sha512-gcLAYiMfQklDCPjQegGn0TBAn9it05ISEsEhlKQUddIk7o2XDokOcTN7HBO8tznM0D9dGezvHEfRZBfZf6me0A==} dependencies: @@ -3936,6 +3960,10 @@ packages: /@types/parse5/6.0.3: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + /@types/prettier/2.4.4: + resolution: {integrity: sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA==} + dev: true + /@types/prismjs/1.26.0: resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} dev: true @@ -4460,6 +4488,12 @@ packages: '@algolia/transporter': 4.13.0 dev: false + /ansi-align/3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + dependencies: + string-width: 4.2.3 + dev: false + /ansi-colors/4.1.1: resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} engines: {node: '>=6'} @@ -4473,7 +4507,6 @@ packages: /ansi-regex/5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true /ansi-regex/6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} @@ -4492,6 +4525,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles/6.1.0: + resolution: {integrity: sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==} + engines: {node: '>=12'} + dev: false + /anymatch/3.1.2: resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} engines: {node: '>= 8'} @@ -4706,6 +4744,20 @@ packages: /boolbase/1.0.0: resolution: {integrity: sha1-aN/1++YMUes3cl6p4+0xDcwed24=} + /boxen/6.2.1: + resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.12.1 + widest-line: 4.0.1 + wrap-ansi: 8.0.1 + dev: false + /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -4807,7 +4859,6 @@ packages: /camelcase/6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - dev: true /caniuse-lite/1.0.30001320: resolution: {integrity: sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==} @@ -4939,6 +4990,11 @@ packages: engines: {node: '>=6'} dev: true + /cli-boxes/3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + dev: false + /cli-cursor/4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5423,7 +5479,6 @@ packages: /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true /emoji-regex/9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -6244,7 +6299,6 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - dev: true /find-up/5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -6252,14 +6306,12 @@ packages: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - dev: true /find-yarn-workspace-root2/1.2.16: resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} dependencies: micromatch: 4.0.5 pkg-dir: 4.2.0 - dev: true /flat-cache/3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} @@ -6938,7 +6990,6 @@ packages: /is-fullwidth-code-point/3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true /is-generator-function/1.0.10: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} @@ -7294,7 +7345,6 @@ packages: js-yaml: 3.14.1 pify: 4.0.1 strip-bom: 3.0.0 - dev: true /local-pkg/0.4.1: resolution: {integrity: sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==} @@ -7306,14 +7356,12 @@ packages: engines: {node: '>=8'} dependencies: p-locate: 4.1.0 - dev: true /locate-path/6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} dependencies: p-locate: 5.0.0 - dev: true /lodash.debounce/4.0.8: resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=} @@ -8260,28 +8308,24 @@ packages: engines: {node: '>=6'} dependencies: p-try: 2.2.0 - dev: true /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 - dev: true /p-locate/4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} dependencies: p-limit: 2.3.0 - dev: true /p-locate/5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} dependencies: p-limit: 3.1.0 - dev: true /p-map/2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} @@ -8298,7 +8342,6 @@ packages: /p-try/2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - dev: true /pac-proxy-agent/5.0.0: resolution: {integrity: sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==} @@ -8378,7 +8421,6 @@ packages: /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - dev: true /path-is-absolute/1.0.1: resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} @@ -8423,14 +8465,12 @@ packages: /pify/4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - dev: true /pkg-dir/4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} dependencies: find-up: 4.1.0 - dev: true /postcss-js/4.0.0_postcss@8.4.12: resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==} @@ -8520,7 +8560,6 @@ packages: find-yarn-workspace-root2: 1.2.16 path-exists: 4.0.0 which-pm: 2.0.0 - dev: true /prelude-ls/1.1.2: resolution: {integrity: sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=} @@ -9391,7 +9430,6 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true /string-width/5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} @@ -9470,7 +9508,6 @@ packages: engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true /strip-ansi/7.0.1: resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} @@ -9487,7 +9524,6 @@ packages: /strip-bom/3.0.0: resolution: {integrity: sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=} engines: {node: '>=4'} - dev: true /strip-bom/4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} @@ -9871,7 +9907,7 @@ packages: engines: {node: '>=12'} hasBin: true dependencies: - esbuild: 0.14.25 + esbuild: 0.14.27 dev: false /tsutils/3.21.0_typescript@4.6.3: @@ -10065,6 +10101,11 @@ packages: engines: {node: '>=8'} dev: true + /type-fest/2.12.1: + resolution: {integrity: sha512-AiknQSEqKVGDDjtZqeKrUoTlcj7FKhupmnVUgz6KoOKtvMwRGE6hUNJ/nVear+h7fnUPO1q/htSkYKb1pyntkQ==} + engines: {node: '>=12.20'} + dev: false + /typescript/4.6.3: resolution: {integrity: sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==} engines: {node: '>=4.2.0'} @@ -10562,7 +10603,6 @@ packages: dependencies: load-yaml-file: 0.2.0 path-exists: 4.0.0 - dev: true /which-typed-array/1.1.7: resolution: {integrity: sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==} @@ -10597,6 +10637,13 @@ packages: string-width: 1.0.2 dev: true + /widest-line/4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + dev: false + /word-wrap/1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} @@ -10770,6 +10817,15 @@ packages: strip-ansi: 6.0.1 dev: true + /wrap-ansi/8.0.1: + resolution: {integrity: sha512-QFF+ufAqhoYHvoHdajT/Po7KoXVBPXS2bgjIam5isfWJPfIOnQZ50JtUiVvCv/sjgacf3yRrt2ZKUZ/V4itN4g==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.1.0 + string-width: 5.1.2 + strip-ansi: 7.0.1 + dev: false + /wrappy/1.0.2: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} @@ -10866,7 +10922,6 @@ packages: /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - dev: true /zod/3.14.2: resolution: {integrity: sha512-iF+wrtzz7fQfkmn60PG6XFxaWBhYYKzp2i+nv24WbLUWb2JjymdkHlzBwP0erpc78WotwP5g9AAu7Sk8GWVVNw==}