From d59d340ff23ed12edeaad873dbe9a7c7384fefc5 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:49:26 -0400 Subject: [PATCH 01/49] use `none` instead of `null` --- packages/create/index.ts | 4 ++-- packages/create/scripts/build-templates.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/create/index.ts b/packages/create/index.ts index 2e880991d..a6e4dd1b4 100644 --- a/packages/create/index.ts +++ b/packages/create/index.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { mkdirp, copy, dist } from './utils'; export type TemplateType = 'default' | 'skeleton' | 'skeletonlib'; -export type LanguageType = 'typescript' | 'checkjs' | null; +export type LanguageType = 'typescript' | 'checkjs' | 'none'; export type Options = { name: string; @@ -16,7 +16,7 @@ export type File = { contents: string; }; -export type Condition = Exclude; +export type Condition = Exclude; export type Common = { files: Array<{ diff --git a/packages/create/scripts/build-templates.js b/packages/create/scripts/build-templates.js index 52d8c71ae..a6335c897 100644 --- a/packages/create/scripts/build-templates.js +++ b/packages/create/scripts/build-templates.js @@ -80,7 +80,7 @@ async function generate_templates(dist, shared) { const types = { typescript: [], checkjs: [], - null: [] + none: [] }; const files = glob('**/*', { cwd, filesOnly: true, dot: true }); @@ -121,7 +121,7 @@ async function generate_templates(dist, shared) { contents: js }); - types.null.push({ + types.none.push({ name: name.replace(/\.ts$/, '.js'), contents: strip_jsdoc(js) }); @@ -186,7 +186,7 @@ async function generate_templates(dist, shared) { contents: js_contents }); - types.null.push({ + types.none.push({ name, contents: strip_jsdoc(js_contents) }); @@ -208,8 +208,8 @@ async function generate_templates(dist, shared) { JSON.stringify(types.checkjs, null, '\t') ); fs.writeFileSync( - path.join(dir, 'files.types=null.json'), - JSON.stringify(types.null, null, '\t') + path.join(dir, 'files.types=none.json'), + JSON.stringify(types.none, null, '\t') ); } } From 116cccd86f5b019f203b42e02da81aef786ae30d Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:11:51 -0400 Subject: [PATCH 02/49] change `default` to `demo` --- packages/create/index.ts | 4 ++-- .../svelte.config.js | 0 .../svelte.config.js | 0 .../svelte.config.js | 0 .../create/templates/{default => demo}/.gitignore | 0 packages/create/templates/{default => demo}/.ignore | 0 .../create/templates/{default => demo}/.meta.json | 0 packages/create/templates/{default => demo}/.npmrc | 0 .../create/templates/{default => demo}/README.md | 0 .../create/templates/{default => demo}/netlify.toml | 0 .../create/templates/{default => demo}/package.json | 0 .../{default => demo}/package.template.json | 0 .../create/templates/{default => demo}/src/app.css | 0 .../create/templates/{default => demo}/src/app.d.ts | 0 .../create/templates/{default => demo}/src/app.html | 0 .../{default => demo}/src/lib/images/github.svg | 0 .../src/lib/images/svelte-logo.svg | 0 .../src/lib/images/svelte-welcome.png | Bin .../src/lib/images/svelte-welcome.webp | Bin .../{default => demo}/src/routes/+layout.svelte | 0 .../{default => demo}/src/routes/+page.svelte | 0 .../templates/{default => demo}/src/routes/+page.ts | 0 .../{default => demo}/src/routes/Counter.svelte | 0 .../{default => demo}/src/routes/Header.svelte | 0 .../{default => demo}/src/routes/about/+page.svelte | 0 .../{default => demo}/src/routes/about/+page.ts | 0 .../src/routes/sverdle/+page.server.ts | 0 .../src/routes/sverdle/+page.svelte | 0 .../{default => demo}/src/routes/sverdle/game.ts | 0 .../src/routes/sverdle/how-to-play/+page.svelte | 0 .../src/routes/sverdle/how-to-play/+page.ts | 0 .../src/routes/sverdle/reduced-motion.ts | 0 .../src/routes/sverdle/words.server.ts | 0 .../templates/{default => demo}/static/favicon.png | Bin .../templates/{default => demo}/static/robots.txt | 0 .../templates/{default => demo}/svelte.config.js | 0 .../templates/{default => demo}/tsconfig.json | 0 .../create/templates/{default => demo}/vercel.json | 0 .../templates/{default => demo}/vite.config.js | 0 .../templates/{default => demo}/wrangler.toml | 0 40 files changed, 2 insertions(+), 2 deletions(-) rename packages/create/shared/{+default+checkjs => +demo+checkjs}/svelte.config.js (100%) rename packages/create/shared/{+default+typescript => +demo+typescript}/svelte.config.js (100%) rename packages/create/shared/{+default-typescript => +demo-typescript}/svelte.config.js (100%) rename packages/create/templates/{default => demo}/.gitignore (100%) rename packages/create/templates/{default => demo}/.ignore (100%) rename packages/create/templates/{default => demo}/.meta.json (100%) rename packages/create/templates/{default => demo}/.npmrc (100%) rename packages/create/templates/{default => demo}/README.md (100%) rename packages/create/templates/{default => demo}/netlify.toml (100%) rename packages/create/templates/{default => demo}/package.json (100%) rename packages/create/templates/{default => demo}/package.template.json (100%) rename packages/create/templates/{default => demo}/src/app.css (100%) rename packages/create/templates/{default => demo}/src/app.d.ts (100%) rename packages/create/templates/{default => demo}/src/app.html (100%) rename packages/create/templates/{default => demo}/src/lib/images/github.svg (100%) rename packages/create/templates/{default => demo}/src/lib/images/svelte-logo.svg (100%) rename packages/create/templates/{default => demo}/src/lib/images/svelte-welcome.png (100%) rename packages/create/templates/{default => demo}/src/lib/images/svelte-welcome.webp (100%) rename packages/create/templates/{default => demo}/src/routes/+layout.svelte (100%) rename packages/create/templates/{default => demo}/src/routes/+page.svelte (100%) rename packages/create/templates/{default => demo}/src/routes/+page.ts (100%) rename packages/create/templates/{default => demo}/src/routes/Counter.svelte (100%) rename packages/create/templates/{default => demo}/src/routes/Header.svelte (100%) rename packages/create/templates/{default => demo}/src/routes/about/+page.svelte (100%) rename packages/create/templates/{default => demo}/src/routes/about/+page.ts (100%) rename packages/create/templates/{default => demo}/src/routes/sverdle/+page.server.ts (100%) rename packages/create/templates/{default => demo}/src/routes/sverdle/+page.svelte (100%) rename packages/create/templates/{default => demo}/src/routes/sverdle/game.ts (100%) rename packages/create/templates/{default => demo}/src/routes/sverdle/how-to-play/+page.svelte (100%) rename packages/create/templates/{default => demo}/src/routes/sverdle/how-to-play/+page.ts (100%) rename packages/create/templates/{default => demo}/src/routes/sverdle/reduced-motion.ts (100%) rename packages/create/templates/{default => demo}/src/routes/sverdle/words.server.ts (100%) rename packages/create/templates/{default => demo}/static/favicon.png (100%) rename packages/create/templates/{default => demo}/static/robots.txt (100%) rename packages/create/templates/{default => demo}/svelte.config.js (100%) rename packages/create/templates/{default => demo}/tsconfig.json (100%) rename packages/create/templates/{default => demo}/vercel.json (100%) rename packages/create/templates/{default => demo}/vite.config.js (100%) rename packages/create/templates/{default => demo}/wrangler.toml (100%) diff --git a/packages/create/index.ts b/packages/create/index.ts index a6e4dd1b4..60bab9cc8 100644 --- a/packages/create/index.ts +++ b/packages/create/index.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { mkdirp, copy, dist } from './utils'; -export type TemplateType = 'default' | 'skeleton' | 'skeletonlib'; +export type TemplateType = 'demo' | 'skeleton' | 'skeletonlib'; export type LanguageType = 'typescript' | 'checkjs' | 'none'; export type Options = { @@ -93,7 +93,7 @@ function write_common_files(cwd: string, options: Options, name: string) { } function matches_condition(condition: Condition, options: Options) { - if (condition === 'default' || condition === 'skeleton' || condition === 'skeletonlib') { + if (condition === 'demo' || condition === 'skeleton' || condition === 'skeletonlib') { return options.template === condition; } if (condition === 'typescript' || condition === 'checkjs') { diff --git a/packages/create/shared/+default+checkjs/svelte.config.js b/packages/create/shared/+demo+checkjs/svelte.config.js similarity index 100% rename from packages/create/shared/+default+checkjs/svelte.config.js rename to packages/create/shared/+demo+checkjs/svelte.config.js diff --git a/packages/create/shared/+default+typescript/svelte.config.js b/packages/create/shared/+demo+typescript/svelte.config.js similarity index 100% rename from packages/create/shared/+default+typescript/svelte.config.js rename to packages/create/shared/+demo+typescript/svelte.config.js diff --git a/packages/create/shared/+default-typescript/svelte.config.js b/packages/create/shared/+demo-typescript/svelte.config.js similarity index 100% rename from packages/create/shared/+default-typescript/svelte.config.js rename to packages/create/shared/+demo-typescript/svelte.config.js diff --git a/packages/create/templates/default/.gitignore b/packages/create/templates/demo/.gitignore similarity index 100% rename from packages/create/templates/default/.gitignore rename to packages/create/templates/demo/.gitignore diff --git a/packages/create/templates/default/.ignore b/packages/create/templates/demo/.ignore similarity index 100% rename from packages/create/templates/default/.ignore rename to packages/create/templates/demo/.ignore diff --git a/packages/create/templates/default/.meta.json b/packages/create/templates/demo/.meta.json similarity index 100% rename from packages/create/templates/default/.meta.json rename to packages/create/templates/demo/.meta.json diff --git a/packages/create/templates/default/.npmrc b/packages/create/templates/demo/.npmrc similarity index 100% rename from packages/create/templates/default/.npmrc rename to packages/create/templates/demo/.npmrc diff --git a/packages/create/templates/default/README.md b/packages/create/templates/demo/README.md similarity index 100% rename from packages/create/templates/default/README.md rename to packages/create/templates/demo/README.md diff --git a/packages/create/templates/default/netlify.toml b/packages/create/templates/demo/netlify.toml similarity index 100% rename from packages/create/templates/default/netlify.toml rename to packages/create/templates/demo/netlify.toml diff --git a/packages/create/templates/default/package.json b/packages/create/templates/demo/package.json similarity index 100% rename from packages/create/templates/default/package.json rename to packages/create/templates/demo/package.json diff --git a/packages/create/templates/default/package.template.json b/packages/create/templates/demo/package.template.json similarity index 100% rename from packages/create/templates/default/package.template.json rename to packages/create/templates/demo/package.template.json diff --git a/packages/create/templates/default/src/app.css b/packages/create/templates/demo/src/app.css similarity index 100% rename from packages/create/templates/default/src/app.css rename to packages/create/templates/demo/src/app.css diff --git a/packages/create/templates/default/src/app.d.ts b/packages/create/templates/demo/src/app.d.ts similarity index 100% rename from packages/create/templates/default/src/app.d.ts rename to packages/create/templates/demo/src/app.d.ts diff --git a/packages/create/templates/default/src/app.html b/packages/create/templates/demo/src/app.html similarity index 100% rename from packages/create/templates/default/src/app.html rename to packages/create/templates/demo/src/app.html diff --git a/packages/create/templates/default/src/lib/images/github.svg b/packages/create/templates/demo/src/lib/images/github.svg similarity index 100% rename from packages/create/templates/default/src/lib/images/github.svg rename to packages/create/templates/demo/src/lib/images/github.svg diff --git a/packages/create/templates/default/src/lib/images/svelte-logo.svg b/packages/create/templates/demo/src/lib/images/svelte-logo.svg similarity index 100% rename from packages/create/templates/default/src/lib/images/svelte-logo.svg rename to packages/create/templates/demo/src/lib/images/svelte-logo.svg diff --git a/packages/create/templates/default/src/lib/images/svelte-welcome.png b/packages/create/templates/demo/src/lib/images/svelte-welcome.png similarity index 100% rename from packages/create/templates/default/src/lib/images/svelte-welcome.png rename to packages/create/templates/demo/src/lib/images/svelte-welcome.png diff --git a/packages/create/templates/default/src/lib/images/svelte-welcome.webp b/packages/create/templates/demo/src/lib/images/svelte-welcome.webp similarity index 100% rename from packages/create/templates/default/src/lib/images/svelte-welcome.webp rename to packages/create/templates/demo/src/lib/images/svelte-welcome.webp diff --git a/packages/create/templates/default/src/routes/+layout.svelte b/packages/create/templates/demo/src/routes/+layout.svelte similarity index 100% rename from packages/create/templates/default/src/routes/+layout.svelte rename to packages/create/templates/demo/src/routes/+layout.svelte diff --git a/packages/create/templates/default/src/routes/+page.svelte b/packages/create/templates/demo/src/routes/+page.svelte similarity index 100% rename from packages/create/templates/default/src/routes/+page.svelte rename to packages/create/templates/demo/src/routes/+page.svelte diff --git a/packages/create/templates/default/src/routes/+page.ts b/packages/create/templates/demo/src/routes/+page.ts similarity index 100% rename from packages/create/templates/default/src/routes/+page.ts rename to packages/create/templates/demo/src/routes/+page.ts diff --git a/packages/create/templates/default/src/routes/Counter.svelte b/packages/create/templates/demo/src/routes/Counter.svelte similarity index 100% rename from packages/create/templates/default/src/routes/Counter.svelte rename to packages/create/templates/demo/src/routes/Counter.svelte diff --git a/packages/create/templates/default/src/routes/Header.svelte b/packages/create/templates/demo/src/routes/Header.svelte similarity index 100% rename from packages/create/templates/default/src/routes/Header.svelte rename to packages/create/templates/demo/src/routes/Header.svelte diff --git a/packages/create/templates/default/src/routes/about/+page.svelte b/packages/create/templates/demo/src/routes/about/+page.svelte similarity index 100% rename from packages/create/templates/default/src/routes/about/+page.svelte rename to packages/create/templates/demo/src/routes/about/+page.svelte diff --git a/packages/create/templates/default/src/routes/about/+page.ts b/packages/create/templates/demo/src/routes/about/+page.ts similarity index 100% rename from packages/create/templates/default/src/routes/about/+page.ts rename to packages/create/templates/demo/src/routes/about/+page.ts diff --git a/packages/create/templates/default/src/routes/sverdle/+page.server.ts b/packages/create/templates/demo/src/routes/sverdle/+page.server.ts similarity index 100% rename from packages/create/templates/default/src/routes/sverdle/+page.server.ts rename to packages/create/templates/demo/src/routes/sverdle/+page.server.ts diff --git a/packages/create/templates/default/src/routes/sverdle/+page.svelte b/packages/create/templates/demo/src/routes/sverdle/+page.svelte similarity index 100% rename from packages/create/templates/default/src/routes/sverdle/+page.svelte rename to packages/create/templates/demo/src/routes/sverdle/+page.svelte diff --git a/packages/create/templates/default/src/routes/sverdle/game.ts b/packages/create/templates/demo/src/routes/sverdle/game.ts similarity index 100% rename from packages/create/templates/default/src/routes/sverdle/game.ts rename to packages/create/templates/demo/src/routes/sverdle/game.ts diff --git a/packages/create/templates/default/src/routes/sverdle/how-to-play/+page.svelte b/packages/create/templates/demo/src/routes/sverdle/how-to-play/+page.svelte similarity index 100% rename from packages/create/templates/default/src/routes/sverdle/how-to-play/+page.svelte rename to packages/create/templates/demo/src/routes/sverdle/how-to-play/+page.svelte diff --git a/packages/create/templates/default/src/routes/sverdle/how-to-play/+page.ts b/packages/create/templates/demo/src/routes/sverdle/how-to-play/+page.ts similarity index 100% rename from packages/create/templates/default/src/routes/sverdle/how-to-play/+page.ts rename to packages/create/templates/demo/src/routes/sverdle/how-to-play/+page.ts diff --git a/packages/create/templates/default/src/routes/sverdle/reduced-motion.ts b/packages/create/templates/demo/src/routes/sverdle/reduced-motion.ts similarity index 100% rename from packages/create/templates/default/src/routes/sverdle/reduced-motion.ts rename to packages/create/templates/demo/src/routes/sverdle/reduced-motion.ts diff --git a/packages/create/templates/default/src/routes/sverdle/words.server.ts b/packages/create/templates/demo/src/routes/sverdle/words.server.ts similarity index 100% rename from packages/create/templates/default/src/routes/sverdle/words.server.ts rename to packages/create/templates/demo/src/routes/sverdle/words.server.ts diff --git a/packages/create/templates/default/static/favicon.png b/packages/create/templates/demo/static/favicon.png similarity index 100% rename from packages/create/templates/default/static/favicon.png rename to packages/create/templates/demo/static/favicon.png diff --git a/packages/create/templates/default/static/robots.txt b/packages/create/templates/demo/static/robots.txt similarity index 100% rename from packages/create/templates/default/static/robots.txt rename to packages/create/templates/demo/static/robots.txt diff --git a/packages/create/templates/default/svelte.config.js b/packages/create/templates/demo/svelte.config.js similarity index 100% rename from packages/create/templates/default/svelte.config.js rename to packages/create/templates/demo/svelte.config.js diff --git a/packages/create/templates/default/tsconfig.json b/packages/create/templates/demo/tsconfig.json similarity index 100% rename from packages/create/templates/default/tsconfig.json rename to packages/create/templates/demo/tsconfig.json diff --git a/packages/create/templates/default/vercel.json b/packages/create/templates/demo/vercel.json similarity index 100% rename from packages/create/templates/default/vercel.json rename to packages/create/templates/demo/vercel.json diff --git a/packages/create/templates/default/vite.config.js b/packages/create/templates/demo/vite.config.js similarity index 100% rename from packages/create/templates/default/vite.config.js rename to packages/create/templates/demo/vite.config.js diff --git a/packages/create/templates/default/wrangler.toml b/packages/create/templates/demo/wrangler.toml similarity index 100% rename from packages/create/templates/default/wrangler.toml rename to packages/create/templates/demo/wrangler.toml From 77920b03b6264ce03dc5fd6d933e58126b14aca3 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:16:11 -0400 Subject: [PATCH 03/49] initial `create` command --- packages/cli/commands/create.ts | 113 ++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 packages/cli/commands/create.ts diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts new file mode 100644 index 000000000..7b22ebede --- /dev/null +++ b/packages/cli/commands/create.ts @@ -0,0 +1,113 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import * as v from 'valibot'; +import { Command, Option } from 'commander'; +import * as p from '@svelte-cli/clack-prompts'; +import { + create as createKit, + templates, + type LanguageType, + type TemplateType +} from '@svelte-cli/create'; +import { wrap } from '../common.js'; + +const langs = ['typescript', 'checkjs', 'none'] as const; +const templateChoices = templates.map((t) => t.name); +const langOption = new Option('--check-types ', 'add type checking').choices(langs); +const templateOption = new Option('--template ', 'template to scaffold').choices( + templateChoices +); + +const OptionsSchema = v.strictObject({ + checkTypes: v.optional(v.picklist(langs)), + adders: v.boolean(), + template: v.optional(v.picklist(templateChoices)) +}); +type Options = v.InferOutput; + +export const create = new Command('create') + .description('scaffolds a new SvelteKit project') + .argument('[path]', 'where the project will be created', process.cwd()) + .addOption(langOption) + .addOption(templateOption) + .option('--no-adders', 'skips interactive adder installer') + .action((projectPath: string, opts) => { + const options = v.parse(OptionsSchema, opts); + wrap(async () => { + await createProject(projectPath, options); + }); + }); + +async function createProject(cwd: string, options: Options) { + const relativePath = path.relative(process.cwd(), cwd) || './'; + + const { directory, template, language } = await p.group( + { + directory: async () => { + if (relativePath !== './') return relativePath; + return p.text({ + message: 'Where should we create your project?', + placeholder: ` (hit Enter to use '${relativePath}')`, + defaultValue: relativePath + }); + }, + force: async ({ results: { directory } }) => { + if (fs.existsSync(directory!) && fs.readdirSync(directory!).length > 0) { + const force = await p.confirm({ + message: 'Directory not empty. Continue?', + initialValue: false + }); + if (p.isCancel(force) || !force) { + p.cancel('Exiting.'); + process.exit(0); + } + } + }, + template: async () => { + if (options.template) return options.template; + return p.select({ + message: 'Which Svelte app template', + initialValue: 'demo', + options: templates.map((t) => ({ label: t.title, value: t.name, hint: t.description })) + }); + }, + language: async () => { + if (options.checkTypes) return options.checkTypes; + return p.select({ + message: 'Add type checking with Typescript?', + initialValue: 'typescript', + options: [ + { label: 'Yes, using Typescript syntax', value: 'typescript' }, + { label: 'Yes, using Javascript with JSDoc comments', value: 'checkjs' }, + { label: 'No', value: 'none' } + ] + }); + } + }, + { + onCancel: () => { + p.cancel('Exiting.'); + process.exit(0); + } + } + ); + + const initSpinner = p.spinner(); + initSpinner.start('Initializing template'); + + createKit(directory, { + name: path.basename(path.resolve(directory)), + template, + types: language + }); + + initSpinner.stop('Project created'); + + if (options.adders) { + // TODO: ask about adders + } + + return { + directory: path.join(process.cwd(), directory) + }; +} From 1505c33210917647b111f1a8e3c0aea73f6ee8b0 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:34:59 -0400 Subject: [PATCH 04/49] not needed --- packages/adders/drizzle/index.ts | 2 -- packages/adders/eslint/index.ts | 2 -- packages/adders/mdsvex/index.ts | 2 -- packages/adders/playwright/index.ts | 2 -- packages/adders/prettier/index.ts | 2 -- packages/adders/routify/index.ts | 2 -- packages/adders/storybook/index.ts | 2 -- packages/adders/tailwindcss/index.ts | 2 -- packages/adders/vitest/index.ts | 2 -- 9 files changed, 18 deletions(-) diff --git a/packages/adders/drizzle/index.ts b/packages/adders/drizzle/index.ts index 4549a9e82..68a875ddd 100644 --- a/packages/adders/drizzle/index.ts +++ b/packages/adders/drizzle/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { checks } from './config/checks.js'; diff --git a/packages/adders/eslint/index.ts b/packages/adders/eslint/index.ts index 4549a9e82..68a875ddd 100644 --- a/packages/adders/eslint/index.ts +++ b/packages/adders/eslint/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { checks } from './config/checks.js'; diff --git a/packages/adders/mdsvex/index.ts b/packages/adders/mdsvex/index.ts index 24861cbd4..339956e44 100644 --- a/packages/adders/mdsvex/index.ts +++ b/packages/adders/mdsvex/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { tests } from './config/tests.js'; diff --git a/packages/adders/playwright/index.ts b/packages/adders/playwright/index.ts index 4549a9e82..68a875ddd 100644 --- a/packages/adders/playwright/index.ts +++ b/packages/adders/playwright/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { checks } from './config/checks.js'; diff --git a/packages/adders/prettier/index.ts b/packages/adders/prettier/index.ts index 4549a9e82..68a875ddd 100644 --- a/packages/adders/prettier/index.ts +++ b/packages/adders/prettier/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { checks } from './config/checks.js'; diff --git a/packages/adders/routify/index.ts b/packages/adders/routify/index.ts index 24861cbd4..339956e44 100644 --- a/packages/adders/routify/index.ts +++ b/packages/adders/routify/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { tests } from './config/tests.js'; diff --git a/packages/adders/storybook/index.ts b/packages/adders/storybook/index.ts index 24861cbd4..339956e44 100644 --- a/packages/adders/storybook/index.ts +++ b/packages/adders/storybook/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { tests } from './config/tests.js'; diff --git a/packages/adders/tailwindcss/index.ts b/packages/adders/tailwindcss/index.ts index 4549a9e82..68a875ddd 100644 --- a/packages/adders/tailwindcss/index.ts +++ b/packages/adders/tailwindcss/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { checks } from './config/checks.js'; diff --git a/packages/adders/vitest/index.ts b/packages/adders/vitest/index.ts index 4549a9e82..68a875ddd 100644 --- a/packages/adders/vitest/index.ts +++ b/packages/adders/vitest/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { defineAdder } from '@svelte-cli/core'; import { adder } from './config/adder.js'; import { checks } from './config/checks.js'; From e570384138b0386df4076af74f0f5f2c52ac34cf Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:35:45 -0400 Subject: [PATCH 05/49] move config directly into `adders` --- .../{config => adders/_config}/categories.ts | 5 +- .../adders => adders/_config}/community.ts | 0 packages/adders/_config/index.ts | 3 ++ packages/adders/_config/official.ts | 53 +++++++++++++++++++ packages/adders/index.ts | 22 +------- packages/config/README.md | 3 -- packages/config/adders/official.ts | 11 ---- packages/config/index.ts | 12 ----- packages/config/package.json | 24 --------- packages/config/tsconfig.json | 8 --- 10 files changed, 60 insertions(+), 81 deletions(-) rename packages/{config => adders/_config}/categories.ts (97%) rename packages/{config/adders => adders/_config}/community.ts (100%) create mode 100644 packages/adders/_config/index.ts create mode 100644 packages/adders/_config/official.ts delete mode 100644 packages/config/README.md delete mode 100644 packages/config/adders/official.ts delete mode 100644 packages/config/index.ts delete mode 100644 packages/config/package.json delete mode 100644 packages/config/tsconfig.json diff --git a/packages/config/categories.ts b/packages/adders/_config/categories.ts similarity index 97% rename from packages/config/categories.ts rename to packages/adders/_config/categories.ts index 66aa35255..bab9c4a77 100644 --- a/packages/config/categories.ts +++ b/packages/adders/_config/categories.ts @@ -1,10 +1,11 @@ +export type CategoryKeys = 'codeQuality' | 'css' | 'db' | 'testing' | 'additional'; + export type CategoryInfo = { - id: string; + id: CategoryKeys; name: string; description: string; }; -export type CategoryKeys = 'codeQuality' | 'css' | 'db' | 'testing' | 'additional'; export type CategoryDetails = Record; export type AdderCategories = Record; diff --git a/packages/config/adders/community.ts b/packages/adders/_config/community.ts similarity index 100% rename from packages/config/adders/community.ts rename to packages/adders/_config/community.ts diff --git a/packages/adders/_config/index.ts b/packages/adders/_config/index.ts new file mode 100644 index 000000000..ce8e749e5 --- /dev/null +++ b/packages/adders/_config/index.ts @@ -0,0 +1,3 @@ +export { adderIds, adderCategories, getAdderConfig, getAdderDetails } from './official'; +export { categories, type CategoryKeys, type CategoryInfo } from './categories'; +export { communityAdders } from './community'; diff --git a/packages/adders/_config/official.ts b/packages/adders/_config/official.ts new file mode 100644 index 000000000..60562d089 --- /dev/null +++ b/packages/adders/_config/official.ts @@ -0,0 +1,53 @@ +import type { AdderCategories } from './categories'; +import type { AdderWithoutExplicitArgs } from '@svelte-cli/core'; + +// adders +import drizzle from '../drizzle'; +import eslint from '../eslint'; +import mdsvex from '../mdsvex'; +import playwright from '../playwright'; +import prettier from '../prettier'; +import routify from '../routify'; +import storybook from '../storybook'; +import tailwindcss from '../tailwindcss'; +import vitest from '../vitest'; + +export const adderCategories: AdderCategories = { + codeQuality: ['prettier', 'eslint'], + testing: ['vitest', 'playwright'], + css: ['tailwindcss'], + db: ['drizzle'], + additional: ['storybook', 'mdsvex', 'routify'] +}; + +export const adderIds: string[] = Object.values(adderCategories).flatMap((x) => x); + +export function getAdderDetails(name: string): AdderWithoutExplicitArgs { + switch (name) { + case 'drizzle': + return drizzle as AdderWithoutExplicitArgs; + case 'eslint': + return eslint; + case 'mdsvex': + return mdsvex; + case 'playwright': + return playwright; + case 'prettier': + return prettier; + case 'routify': + return routify; + case 'storybook': + return storybook; + case 'tailwindcss': + return tailwindcss as AdderWithoutExplicitArgs; + case 'vitest': + return vitest; + default: + throw new Error(`invalid adder name: ${name}`); + } +} + +export function getAdderConfig(name: string) { + const adder = getAdderDetails(name); + return adder.config; +} diff --git a/packages/adders/index.ts b/packages/adders/index.ts index 6559ce82e..a779c487a 100644 --- a/packages/adders/index.ts +++ b/packages/adders/index.ts @@ -1,21 +1 @@ -import type { AdderConfig, AdderWithoutExplicitArgs, Question } from '@svelte-cli/core'; - -export async function getAdderDetails(name: string) { - const adder: { default: AdderWithoutExplicitArgs } = await import(`./${name}/index.ts`); - - return adder.default; -} - -export async function getAdderConfig(name: string) { - // Mainly used by the website - // Either vite / rollup or esbuild are not able to process the shebangs - // present on the `index.js` file. That's why we directly import the configuration - // for the website here, as this is the only important part. - - const adder: Promise<{ adder: AdderConfig> }> = await import( - `./${name}/config/adder.ts` - ); - const { adder: adderConfig } = await adder; - - return adderConfig; -} +export * from './_config/index'; diff --git a/packages/config/README.md b/packages/config/README.md deleted file mode 100644 index 0fd236505..000000000 --- a/packages/config/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @svelte-cli/config - -This package provides configurations for adders and categories. This package also includes community adders. diff --git a/packages/config/adders/official.ts b/packages/config/adders/official.ts deleted file mode 100644 index f59d1083e..000000000 --- a/packages/config/adders/official.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { AdderCategories } from '../categories.js'; - -export const adderCategories: AdderCategories = { - codeQuality: ['prettier', 'eslint'], - testing: ['vitest', 'playwright'], - css: ['tailwindcss'], - db: ['drizzle'], - additional: ['storybook', 'mdsvex', 'routify'] -}; - -export const adderIds: string[] = Object.values(adderCategories).flatMap((x) => x); diff --git a/packages/config/index.ts b/packages/config/index.ts deleted file mode 100644 index 3a799ab3e..000000000 --- a/packages/config/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { adderIds, adderCategories } from './adders/official'; -import { categories, type CategoryKeys, type CategoryInfo } from './categories'; -import { communityAdders } from './adders/community'; - -export { - adderIds, - categories, - adderCategories, - communityAdders, - type CategoryKeys, - type CategoryInfo -}; diff --git a/packages/config/package.json b/packages/config/package.json deleted file mode 100644 index 9cd9a46a6..000000000 --- a/packages/config/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@svelte-cli/config", - "private": true, - "version": "1.5.2", - "license": "MIT", - "type": "module", - "scripts": { - "lint": "prettier --check . --config ../../.prettierrc --ignore-path ../../.gitignore --ignore-path .gitignore", - "format": "pnpm lint --write", - "check": "tsc" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }, - "bugs": "https://github.com/sveltejs/cli/issues", - "repository": { - "type": "git", - "url": "https://github.com/sveltejs/cli/tree/main/packages/config" - }, - "keywords": [] -} diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json deleted file mode 100644 index a1fcf6c9b..000000000 --- a/packages/config/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "checkJs": false, - "isolatedDeclarations": true, - "declaration": true - } -} From 9de4bd828a4d0100da7eda2eb4ee3f68afb7f612 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:37:01 -0400 Subject: [PATCH 06/49] missed a spot --- packages/create/scripts/update-template-repo-contents.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create/scripts/update-template-repo-contents.js b/packages/create/scripts/update-template-repo-contents.js index cd9eb56f0..703a05c89 100644 --- a/packages/create/scripts/update-template-repo-contents.js +++ b/packages/create/scripts/update-template-repo-contents.js @@ -15,7 +15,7 @@ fs.readdirSync(repo).forEach((file) => { create(repo, { name: 'kit-template-default', - template: 'default', + template: 'demo', types: 'checkjs' }); From 48d6173415db506f58c71bc01778dd1b1644e38b Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:07:31 -0400 Subject: [PATCH 07/49] run adder wizard after `create` --- packages/cli/commands/create.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 7b22ebede..95ddd4a2a 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -10,6 +10,7 @@ import { type TemplateType } from '@svelte-cli/create'; import { wrap } from '../common.js'; +import { runAddCommand } from './add.js'; const langs = ['typescript', 'checkjs', 'none'] as const; const templateChoices = templates.map((t) => t.name); @@ -18,6 +19,7 @@ const templateOption = new Option('--template ', 'template to scaffold').c templateChoices ); +const ProjectPathSchema = v.string(); const OptionsSchema = v.strictObject({ checkTypes: v.optional(v.picklist(langs)), adders: v.boolean(), @@ -31,20 +33,19 @@ export const create = new Command('create') .addOption(langOption) .addOption(templateOption) .option('--no-adders', 'skips interactive adder installer') - .action((projectPath: string, opts) => { + .action((projectPath, opts) => { + const cwd = v.parse(ProjectPathSchema, projectPath); const options = v.parse(OptionsSchema, opts); wrap(async () => { - await createProject(projectPath, options); + await createProject(cwd, options); }); }); async function createProject(cwd: string, options: Options) { - const relativePath = path.relative(process.cwd(), cwd) || './'; - const { directory, template, language } = await p.group( { directory: async () => { - if (relativePath !== './') return relativePath; + const relativePath = path.relative(process.cwd(), cwd) || './'; return p.text({ message: 'Where should we create your project?', placeholder: ` (hit Enter to use '${relativePath}')`, @@ -86,7 +87,7 @@ async function createProject(cwd: string, options: Options) { }, { onCancel: () => { - p.cancel('Exiting.'); + p.cancel('Operation cancelled.'); process.exit(0); } } @@ -95,8 +96,9 @@ async function createProject(cwd: string, options: Options) { const initSpinner = p.spinner(); initSpinner.start('Initializing template'); - createKit(directory, { - name: path.basename(path.resolve(directory)), + const projectPath = path.resolve(directory); + createKit(projectPath, { + name: path.basename(projectPath), template, types: language }); @@ -104,10 +106,11 @@ async function createProject(cwd: string, options: Options) { initSpinner.stop('Project created'); if (options.adders) { - // TODO: ask about adders + const config = { cwd: projectPath, default: false, install: true, preconditions: true }; + await runAddCommand(config, []); } return { - directory: path.join(process.cwd(), directory) + directory: projectPath }; } From 9c316d1a494e2c798008e5826a45e8938df8e899 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:09:48 -0400 Subject: [PATCH 08/49] initial `add` cmd implementation --- packages/cli/commands/add.ts | 359 +++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 packages/cli/commands/add.ts diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts new file mode 100644 index 000000000..84eda9a1c --- /dev/null +++ b/packages/cli/commands/add.ts @@ -0,0 +1,359 @@ +import * as v from 'valibot'; +import { Argument, Command, Option } from 'commander'; +import * as p from '@svelte-cli/clack-prompts'; +import pc from 'picocolors'; +import { executeCli, formatFiles, suggestInstallingDependencies, wrap } from '../common.js'; +import { adderCategories, categories, adderIds, communityAdders } from '@svelte-cli/adders'; +import { getAdderConfig, getAdderDetails } from '../../adders/index.js'; +import { + createOrUpdateFiles, + createWorkspace, + installPackages, + remoteControl +} from '@svelte-cli/core/internal'; +import { + type ExternalAdderConfig, + type InlineAdderConfig, + type OptionDefinition, + type OptionValues +} from '@svelte-cli/core'; + +const AddersSchema = v.array(v.string()); +const OptionsSchema = v.strictObject({ + cwd: v.string(), + install: v.boolean(), + preconditions: v.boolean(), + default: v.boolean() + // community: AddersSchema +}); +type Options = v.InferOutput; + +const adderDetails = adderIds.map((id) => getAdderDetails(id)); +const aliases = adderDetails.map((c) => c.config.metadata.alias).filter((v) => v !== undefined); + +// const communityOptions = new Option('--community [adder...]', 'community adders to install') +//.choices(communityAdders.map((a) => a.id)); + +const adderArg = new Argument('[adder...]', 'adders to install'); + +export const add = new Command('add') + .description('Applies specified adders into a project') + .addArgument(adderArg) + .option('--cwd ', 'path to working directory', process.cwd()) + .option('--no-install', 'skips installing dependencies') + .option('--no-preconditions', 'skips validating preconditions') + .option('--default', 'applies default adder options for unspecified options', false) + // .addOption(communityOptions) + .action(async (adderArgs, opts) => { + const adders = v.parse(AddersSchema, adderArgs); + const options = v.parse(OptionsSchema, opts); + + const invalidAdders = adders.filter((a) => !adderIds.includes(a) && !aliases.includes(a)); + if (invalidAdders.length > 0) { + console.error(`Invalid adders specified: ${invalidAdders.join(', ')}`); + process.exit(1); + } + + const deduped = transformAliases(adders); + + await wrap(async () => await runAddCommand(options, deduped)); + }); + +export async function runAddCommand(options: Options, adders: string[]): Promise { + const selectedAdders = adders.map((id) => getAdderDetails(id)); + // prompt which adders to apply + if (selectedAdders.length === 0) { + const adderOptions: Record> = {}; + const workspace = createWorkspace(options.cwd); + const projectType = workspace.kit ? 'kit' : 'svelte'; + for (const { id, name } of Object.values(categories)) { + const category = adderCategories[id]; + const categoryOptions = category + .map((id) => { + const config = getAdderConfig(id); + // we'll only display adders within their respective project types + if (projectType === 'kit' && !config.metadata.environments.kit) return; + if (projectType === 'svelte' && !config.metadata.environments.svelte) return; + + return { label: config.metadata.name, value: config.metadata.id }; + }) + .filter((c) => !!c); + + if (categoryOptions.length > 0) { + adderOptions[name] = categoryOptions; + } + } + + const selected = await p.groupMultiselect({ + message: 'What would you like to add to your project?', + options: adderOptions, + spacedGroups: true, + selectableGroups: false, + required: false + }); + if (p.isCancel(selected)) { + p.cancel('Operation cancelled.'); + process.exit(1); + } + + selected.forEach((id) => selectedAdders.push(getAdderDetails(id))); + } + + // run precondition checks + if (options.preconditions) { + // TODO: add global checks + const fails: Array<{ name: string; message?: string }> = []; + const preconditions = selectedAdders + .flatMap((c) => c.checks.preconditions) + .filter((p) => p !== undefined); + for (const condition of preconditions) { + const { message, success } = await condition.run(); + if (!success) fails.push({ name: condition.name, message }); + } + + if (fails.length > 0) { + const message = fails + .map(({ name, message }) => pc.yellow(`${name} (${message})`)) + .join('\n- '); + p.note(`- ${message}`, 'Preconditions not met'); + + const force = await p.confirm({ + message: 'Preconditions failed. Do you wish to continue?' + }); + if (p.isCancel(force) || !force) { + p.cancel('Operation cancelled.'); + process.exit(1); + } + } + } + + const official: AdderOption = {}; + const community = {}; + + // TODO: apply specified options from flags + + // apply defaults to unspecified options + if (options.default) { + for (const adder of selectedAdders) { + const adderId = adder.config.metadata.id; + official[adderId] ??= {}; + for (const [id, question] of Object.entries(adder.config.options)) { + official[adderId][id] ??= question.default; + } + } + } + + // ask remaining questions + for (const adder of selectedAdders) { + const adderId = adder.config.metadata.id; + const questionPrefix = selectedAdders.length > 1 ? `${adder.config.metadata.name}: ` : ''; + official[adderId] ??= {}; + for (const [questionId, question] of Object.entries(adder.config.options)) { + const shouldAsk = question.condition?.(official[adderId]); + if (shouldAsk === false || official[adderId][questionId] !== undefined) continue; + + let answer; + const message = questionPrefix + question.question; + if (question.type === 'boolean') { + answer = await p.confirm({ message, initialValue: question.default }); + } + if (question.type === 'select') { + answer = await p.select({ + message, + initialValue: question.default, + options: question.options + }); + } + if (question.type === 'string' || question.type === 'number') { + answer = await p.text({ + message, + initialValue: question.default.toString() + }); + } + if (p.isCancel(answer)) { + p.cancel('Operation cancelled.'); + process.exit(1); + } + + official[adderId][questionId] = answer; + } + } + + // apply adders + let filesToFormat: string[] = []; + if (Object.keys({ ...official, ...community }).length > 0) { + const adderSpinner = p.spinner(); + adderSpinner.start('Applying adders'); + filesToFormat = await installAdders({ cwd: options.cwd, official, community }); + adderSpinner.stop('Successfully installed adders'); + } + + // TODO: run postconditions? + + // install dependencies + let depsInstalled; + if (options.install) { + depsInstalled = await suggestInstallingDependencies(options.cwd); + } + + // format modified/created files with prettier (if available) + const workspace = createWorkspace(options.cwd); + if (depsInstalled === 'installed' && workspace.prettier) { + const formatSpinner = p.spinner(); + formatSpinner.start('Formatting modified files'); + try { + await formatFiles(options.cwd, filesToFormat); + formatSpinner.stop('Successfully formatted modified files'); + } catch (e) { + formatSpinner.stop('Failed to format files'); + if (e instanceof Error) p.log.error(e.message); + } + } + + // print next steps + const nextStepsMsg = selectedAdders + .filter((a) => a.config.integrationType === 'inline' && a.config.nextSteps) + .map((a) => a.config as InlineAdderConfig) + .map((config) => { + const metadata = config.metadata; + let adderMessage = ''; + if (selectedAdders.length > 1) { + adderMessage = `${pc.green(metadata.name)}:\n`; + } + + const adderNextSteps = config.nextSteps!({ + options: official[metadata.id], + cwd: options.cwd, + colors: pc, + docs: metadata.website?.documentation + }); + adderMessage += `- ${adderNextSteps.join('\n- ')}`; + return adderMessage; + }) + .join('\n\n'); + if (nextStepsMsg) p.note(nextStepsMsg, 'Next steps'); +} + +type AdderId = string; +type QuestionValues = OptionValues; +export type AdderOption = Record; + +export type InstallAdderOptions = { + cwd: string; + official?: AdderOption; + community?: AdderOption; +}; + +/** + * Installs adders + * @param options {InstallAdderOptions} + * @returns a list of paths of modified files + */ +export async function installAdders({ + cwd, + official = {} +}: InstallAdderOptions): Promise { + const adderDetails = Object.keys(official).map((id) => getAdderDetails(id)); + + // adders might specify that they should be executed after another adder. + // this orders the adders to (ideally) have adders without dependencies run first + // and adders with dependencies runs later on, based on the adders they depend on. + // based on https://stackoverflow.com/a/72030336/16075084 + adderDetails.sort((a, b) => { + if (!a.config.runsAfter) return -1; + if (!b.config.runsAfter) return 1; + + return a.config.runsAfter.includes(b.config.metadata.id) + ? 1 + : b.config.runsAfter.includes(a.config.metadata.id) + ? -1 + : 0; + }); + + // apply adders + const filesToFormat = new Set(); + for (const { config } of adderDetails) { + const adderId = config.metadata.id; + const workspace = createWorkspace(cwd); + + workspace.options = official[adderId]; + + // execute adders + if (config.integrationType === 'inline') { + const pkgPath = installPackages(config, workspace); + filesToFormat.add(pkgPath); + const changedFiles = createOrUpdateFiles(config.files, workspace); + changedFiles.forEach((file) => filesToFormat.add(file)); + } else if (config.integrationType === 'external') { + await processExternalAdder(config, cwd); + } else { + throw new Error('Unknown integration type'); + } + } + + return Array.from(filesToFormat); +} + +async function processExternalAdder( + config: ExternalAdderConfig, + cwd: string +) { + if (!remoteControl) console.log('Executing external command'); + + try { + await executeCli('npx', config.command.split(' '), cwd, { + env: Object.assign(process.env, config.environment ?? {}), + stdio: remoteControl ? 'pipe' : 'inherit' + }); + } catch (error) { + const typedError = error as Error; + throw new Error('Failed executing external command: ' + typedError.message); + } +} + +/** + * Dedupes and transforms aliases into their respective adder id + */ +function transformAliases(ids: string[]): string[] { + const set = new Set(); + for (const id of ids) { + if (aliases.includes(id)) { + const adder = adderDetails.find((a) => a.config.metadata.alias === id)!; + set.add(adder.config.metadata.id); + } else { + set.add(id); + } + } + return Array.from(set); +} + +// other possivel variations +// npx sv add drizzle tailwindcss --options postgresql,postgres.js,no-docker no-typography +// npx sv add drizzle=postgresql,postgres.js,no-docker tailwindcss=no-typography +// `sv add drizzle tailwindcss --drizzle db:postgresql client:postgres.js docker:false --tailwindcss typography:false` + +// TODO: PoC for `sv add --drizzle=postgresql,postgres.js,no-docker --tailwindcss=no-typography` +// const addersOptions: Option[] = []; +// for (const id of adderIds) { +// const details = getAdderDetails(id); +// if (Object.values(details.config.options).length === 0) continue; + +// const option = new Option(`--${id} `).argParser((value) => value.split(',')); +// const choices: string[] = []; +// for (const [key, question] of Object.entries(details.config.options)) { +// if (question.type === 'boolean') { +// const values = [key, `no-${key}`]; +// choices.push(...values); +// } +// if (question.type === 'select') { +// const values = question.options.map((o) => o.value); +// choices.push(...values); +// } +// } +// option.choices(choices); +// addersOptions.push(option); +// } + +// for (const option of addersOptions) { +// add.addOption(option); +// } From f47ce639947b6441cc9ecf505c8922f1f5a9665a Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:44:15 -0400 Subject: [PATCH 09/49] add global preconditions --- packages/cli/commands/add.ts | 18 +++- packages/cli/common.ts | 170 +++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 packages/cli/common.ts diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 84eda9a1c..d5d8a0147 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -2,7 +2,13 @@ import * as v from 'valibot'; import { Argument, Command, Option } from 'commander'; import * as p from '@svelte-cli/clack-prompts'; import pc from 'picocolors'; -import { executeCli, formatFiles, suggestInstallingDependencies, wrap } from '../common.js'; +import { + executeCli, + formatFiles, + getGlobalPreconditions, + suggestInstallingDependencies, + wrap +} from '../common.js'; import { adderCategories, categories, adderIds, communityAdders } from '@svelte-cli/adders'; import { getAdderConfig, getAdderDetails } from '../../adders/index.js'; import { @@ -101,11 +107,17 @@ export async function runAddCommand(options: Options, adders: string[]): Promise // run precondition checks if (options.preconditions) { - // TODO: add global checks - const fails: Array<{ name: string; message?: string }> = []; const preconditions = selectedAdders .flatMap((c) => c.checks.preconditions) .filter((p) => p !== undefined); + + // add global checks + const { kit } = createWorkspace(options.cwd); + const projectType = kit ? 'kit' : 'svelte'; + const globalPreconditions = getGlobalPreconditions(options.cwd, projectType, selectedAdders); + preconditions.unshift(...globalPreconditions.preconditions); + + const fails: Array<{ name: string; message?: string }> = []; for (const condition of preconditions) { const { message, success } = await condition.run(); if (!success) fails.push({ name: condition.name, message }); diff --git a/packages/cli/common.ts b/packages/cli/common.ts new file mode 100644 index 000000000..1cae3812b --- /dev/null +++ b/packages/cli/common.ts @@ -0,0 +1,170 @@ +import { spawn, type ChildProcess } from 'node:child_process'; +import pc from 'picocolors'; +import * as p from '@svelte-cli/clack-prompts'; +import { detect } from 'package-manager-detector'; +import { COMMANDS, AGENTS, type Agent } from 'package-manager-detector/agents'; +import pkg from './package.json'; +import type { AdderWithoutExplicitArgs } from '@svelte-cli/core'; + +type MaybePromise = () => Promise | void; + +export async function wrap(action: MaybePromise) { + p.intro(`Welcome to the Svelte CLI! ${pc.gray(`(v${pkg.version})`)}`); + await action(); + p.outro("You're all set!"); +} + +export async function executeCli( + command: string, + commandArgs: string[], + cwd: string, + options?: { + onData?: (data: string, program: ChildProcess, resolve: (value?: any) => any) => void; + stdio?: 'pipe' | 'inherit'; + env?: Record; + } +): Promise { + const stdio = options?.stdio ?? 'pipe'; + const env = options?.env ?? process.env; + + const program = spawn(command, commandArgs, { stdio, shell: true, cwd, env }); + + return await new Promise((resolve, reject) => { + let errorText = ''; + program.stderr?.on('data', (data: Buffer) => { + const value = data.toString(); + errorText += value; + }); + + program.stdout?.on('data', (data: Buffer) => { + const value = data.toString(); + options?.onData?.(value, program, resolve); + }); + + program.on('exit', (code) => { + if (code == 0) { + resolve(undefined); + } else { + reject(new Error(errorText)); + } + }); + }); +} + +export async function formatFiles(cwd: string, paths: string[]): Promise { + await executeCli('npx', ['prettier', '--write', '--ignore-unknown', ...paths], cwd, { + stdio: 'pipe' + }); +} + +type PMOptions = Array<{ value: Agent | undefined; label: Agent | 'None' }>; +export async function suggestInstallingDependencies(cwd: string): Promise<'installed' | 'skipped'> { + const detectedPm = await detect({ cwd }); + let selectedPm = detectedPm.agent; + + const options: PMOptions = AGENTS.filter((agent) => !agent.includes('@')).map((pm) => ({ + value: pm, + label: pm + })); + options.unshift({ label: 'None', value: undefined }); + + if (!selectedPm) { + const pm = await p.select({ + message: 'Which package manager do you want to install dependencies with?', + options, + initialValue: undefined + }); + if (p.isCancel(pm)) { + p.cancel('Operation cancelled.'); + process.exit(1); + } + + selectedPm = pm; + } + + if (!selectedPm || !COMMANDS[selectedPm]) { + return 'skipped'; + } + + const loadingSpinner = p.spinner(); + loadingSpinner.start('Installing dependencies...'); + + const installCommand = COMMANDS[selectedPm].install; + const [pm, install] = installCommand.split(' '); + await installDependencies(pm, [install], cwd); + + loadingSpinner.stop('Successfully installed dependencies'); + return 'installed'; +} + +async function installDependencies(command: string, args: string[], workingDirectory: string) { + try { + await executeCli(command, args, workingDirectory); + } catch (error) { + const typedError = error as Error; + throw new Error('unable to install dependencies: ' + typedError.message); + } +} + +export type ProjectType = 'svelte' | 'kit'; + +export function getGlobalPreconditions( + cwd: string, + projectType: ProjectType, + adders: AdderWithoutExplicitArgs[] +) { + return { + name: 'global checks', + preconditions: [ + { + name: 'clean working directory', + run: async () => { + let outputText = ''; + + try { + // If a user has pending git changes the output of the following command will list + // all files that have been added/modified/deleted and thus the output will not be empty. + // In case the output of the command below is an empty text, we can safely assume + // there are no pending changes. If the below command is run outside of a git repository, + // git will exit with a failing exit code, which will trigger the catch statement. + // also see https://remarkablemark.org/blog/2017/10/12/check-git-dirty/#git-status + await executeCli('git', ['status', '--short'], cwd, { + onData: (data) => { + outputText += data; + } + }); + + if (outputText) { + return { success: false, message: 'Found modified files' }; + } + + return { success: true, message: undefined }; + } catch { + return { success: true, message: 'Not a git repository' }; + } + } + }, + { + name: 'supported environments', + run: () => { + const addersForInvalidEnvironment = adders.filter((a) => { + const supportedEnvironments = a.config.metadata.environments; + if (projectType == 'kit' && !supportedEnvironments.kit) return true; + if (projectType == 'svelte' && !supportedEnvironments.svelte) return true; + + return false; + }); + + if (addersForInvalidEnvironment.length == 0) { + return { success: true, message: undefined }; + } + + const messages = addersForInvalidEnvironment.map( + (a) => `"${a.config.metadata.name}" does not support "${projectType}"` + ); + return { success: false, message: messages.join(' / ') }; + } + } + ] + }; +} From 6d5678b5a37768d2276f50b5b1bc61f4189cada6 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:44:36 -0400 Subject: [PATCH 10/49] tweak types --- packages/adders/common.ts | 4 ++-- packages/adders/drizzle/config/adder.ts | 14 ++++++-------- packages/adders/drizzle/config/tests.ts | 16 +++++++--------- packages/adders/eslint/config/adder.ts | 12 ++++++------ packages/adders/mdsvex/config/tests.ts | 20 ++++++++++---------- packages/adders/playwright/config/adder.ts | 4 ++-- packages/adders/routify/config/adder.ts | 2 +- packages/adders/tailwindcss/config/adder.ts | 16 ++++++++-------- packages/adders/tailwindcss/config/tests.ts | 18 ++++++------------ packages/adders/vitest/config/adder.ts | 4 ++-- 10 files changed, 50 insertions(+), 60 deletions(-) diff --git a/packages/adders/common.ts b/packages/adders/common.ts index 2e7759d43..5e881e456 100644 --- a/packages/adders/common.ts +++ b/packages/adders/common.ts @@ -1,4 +1,4 @@ -import type { ScriptFileEditorArgs } from '@svelte-cli/core'; +import type { ScriptFileEditor } from '@svelte-cli/core'; import type { Question } from '../core/internal'; export function addEslintConfigPrettier({ @@ -6,7 +6,7 @@ export function addEslintConfigPrettier({ imports, exports, common -}: ScriptFileEditorArgs>) { +}: ScriptFileEditor>) { // if a default import for `eslint-plugin-svelte` already exists, then we'll use their specifier's name instead const importNodes = ast.body.filter((n) => n.type === 'ImportDeclaration'); const sveltePluginImport = importNodes.find( diff --git a/packages/adders/drizzle/config/adder.ts b/packages/adders/drizzle/config/adder.ts index aae8c2499..b49361846 100644 --- a/packages/adders/drizzle/config/adder.ts +++ b/packages/adders/drizzle/config/adder.ts @@ -1,4 +1,4 @@ -import { defineAdderConfig, dedent, type TextFileEditorArgs } from '@svelte-cli/core'; +import { defineAdderConfig, dedent, type TextFileEditor } from '@svelte-cli/core'; import { options as availableOptions } from './options'; const PORTS = { @@ -151,7 +151,7 @@ export const adder = defineAdderConfig({ } }, { - name: ({ typescript }) => `drizzle.config.${typescript.installed ? 'ts' : 'js'}`, + name: ({ typescript }) => `drizzle.config.${typescript ? 'ts' : 'js'}`, contentType: 'script', content: ({ options, ast, common, exports, typescript, imports, object }) => { imports.addNamed(ast, 'drizzle-kit', { defineConfig: 'defineConfig' }); @@ -175,9 +175,7 @@ export const adder = defineAdderConfig({ : undefined; object.properties(objExpression, { - schema: common.createLiteral( - `./src/lib/server/db/schema.${typescript.installed ? 'ts' : 'js'}` - ), + schema: common.createLiteral(`./src/lib/server/db/schema.${typescript ? 'ts' : 'js'}`), dbCredentials: object.create({ url: common.expressionFromString('process.env.DATABASE_URL'), authToken @@ -198,7 +196,7 @@ export const adder = defineAdderConfig({ }, { name: ({ kit, typescript }) => - `${kit.libDirectory}/server/db/schema.${typescript.installed ? 'ts' : 'js'}`, + `${kit?.libDirectory}/server/db/schema.${typescript ? 'ts' : 'js'}`, contentType: 'script', content: ({ ast, exports, imports, options, common, variables }) => { let userSchemaExpression; @@ -251,7 +249,7 @@ export const adder = defineAdderConfig({ }, { name: ({ kit, typescript }) => - `${kit.libDirectory}/server/db/index.${typescript.installed ? 'ts' : 'js'}`, + `${kit?.libDirectory}/server/db/index.${typescript ? 'ts' : 'js'}`, contentType: 'script', content: ({ ast, exports, imports, options, common, functions, variables }) => { imports.addNamed(ast, '$env/dynamic/private', { env: 'env' }); @@ -337,7 +335,7 @@ export const adder = defineAdderConfig({ } }); -function generateEnvFileContent({ content, options }: TextFileEditorArgs) { +function generateEnvFileContent({ content, options }: TextFileEditor) { const DB_URL_KEY = 'DATABASE_URL'; if (options.docker) { // we'll prefill with the default docker db credentials diff --git a/packages/adders/drizzle/config/tests.ts b/packages/adders/drizzle/config/tests.ts index 480b590d8..7db9171fd 100644 --- a/packages/adders/drizzle/config/tests.ts +++ b/packages/adders/drizzle/config/tests.ts @@ -18,9 +18,9 @@ export const tests = defineAdderTests({ ], files: [ { - name: ({ kit }) => `${kit.routesDirectory}/+page.svelte`, + name: ({ kit }) => `${kit?.routesDirectory}/+page.svelte`, contentType: 'svelte', - condition: ({ kit }) => kit.installed, + condition: ({ kit }) => Boolean(kit), content: ({ html, js }) => { js.common.addFromString(js.ast, 'export let data;'); html.addFromRawHtml( @@ -35,9 +35,9 @@ export const tests = defineAdderTests({ }, { name: ({ kit, typescript }) => - `${kit.routesDirectory}/+page.server.${typescript.installed ? 'ts' : 'js'}`, + `${kit?.routesDirectory}/+page.server.${typescript ? 'ts' : 'js'}`, contentType: 'script', - condition: ({ kit }) => kit.installed, + condition: ({ kit }) => Boolean(kit), content: ({ ast, common, typescript }) => { common.addFromString( ast, @@ -53,9 +53,7 @@ export const tests = defineAdderTests({ return { users }; }; - function insertUser(${ - typescript.installed ? 'value: typeof user.$inferInsert' : 'value' - }) { + function insertUser(${typescript ? 'value: typeof user.$inferInsert' : 'value'}) { return db.insert(user).values(value); } ` @@ -64,9 +62,9 @@ export const tests = defineAdderTests({ }, { // override the config so we can remove strict mode - name: ({ typescript }) => `drizzle.config.${typescript.installed ? 'ts' : 'js'}`, + name: ({ typescript }) => `drizzle.config.${typescript ? 'ts' : 'js'}`, contentType: 'text', - condition: ({ kit }) => kit.installed, + condition: ({ kit }) => Boolean(kit), content: ({ content }) => { return content.replace('strict: true,', ''); } diff --git a/packages/adders/eslint/config/adder.ts b/packages/adders/eslint/config/adder.ts index c0b82dba2..cdca2a493 100644 --- a/packages/adders/eslint/config/adder.ts +++ b/packages/adders/eslint/config/adder.ts @@ -26,14 +26,14 @@ export const adder = defineAdderConfig({ name: 'typescript-eslint', version: '^8.0.0', dev: true, - condition: ({ typescript }) => typescript.installed + condition: ({ typescript }) => typescript }, { name: 'eslint-plugin-svelte', version: '^2.36.0', dev: true }, { name: 'eslint-config-prettier', version: '^9.1.0', dev: true, - condition: ({ prettier }) => prettier.installed + condition: ({ prettier }) => prettier } ], files: [ @@ -69,7 +69,7 @@ export const adder = defineAdderConfig({ const jsConfig = common.expressionFromString('js.configs.recommended'); array.push(eslintConfigs, jsConfig); - if (typescript.installed) { + if (typescript) { const tsConfig = common.expressionFromString('ts.configs.recommended'); array.push(eslintConfigs, common.createSpreadElement(tsConfig)); } @@ -90,7 +90,7 @@ export const adder = defineAdderConfig({ }); array.push(eslintConfigs, globalsConfig); - if (typescript.installed) { + if (typescript) { const svelteTSParserConfig = object.create({ files: common.expressionFromString('["**/*.svelte"]'), languageOptions: object.create({ @@ -118,7 +118,7 @@ export const adder = defineAdderConfig({ common.addJsDocTypeComment(defaultExport.astNode, "import('eslint').Linter.Config[]"); // imports - if (typescript.installed) imports.addDefault(ast, 'typescript-eslint', 'ts'); + if (typescript) imports.addDefault(ast, 'typescript-eslint', 'ts'); imports.addDefault(ast, 'globals', 'globals'); imports.addDefault(ast, 'eslint-plugin-svelte', 'svelte'); imports.addDefault(ast, '@eslint/js', 'js'); @@ -127,7 +127,7 @@ export const adder = defineAdderConfig({ { name: () => 'eslint.config.js', contentType: 'script', - condition: ({ prettier }) => prettier.installed, + condition: ({ prettier }) => prettier, content: addEslintConfigPrettier } ] diff --git a/packages/adders/mdsvex/config/tests.ts b/packages/adders/mdsvex/config/tests.ts index 64f6424c1..65ee1cdba 100644 --- a/packages/adders/mdsvex/config/tests.ts +++ b/packages/adders/mdsvex/config/tests.ts @@ -1,7 +1,7 @@ import { defineAdderTests, - type SvelteFileEditorArgs, - type TextFileEditorArgs, + type SvelteFileEditor, + type TextFileEditor, type OptionDefinition } from '@svelte-cli/core'; import { options } from './options'; @@ -9,28 +9,28 @@ import { options } from './options'; export const tests = defineAdderTests({ files: [ { - name: ({ kit }) => `${kit.routesDirectory}/+page.svelte`, + name: ({ kit }) => `${kit?.routesDirectory}/+page.svelte`, contentType: 'svelte', content: useMarkdownFile, - condition: ({ kit }) => kit.installed + condition: ({ kit }) => Boolean(kit) }, { name: () => 'src/App.svelte', contentType: 'svelte', content: useMarkdownFile, - condition: ({ kit }) => !kit.installed + condition: ({ kit }) => !kit }, { - name: ({ kit }) => `${kit.routesDirectory}/Demo.svx`, + name: ({ kit }) => `${kit?.routesDirectory}/Demo.svx`, contentType: 'text', content: addMarkdownFile, - condition: ({ kit }) => kit.installed + condition: ({ kit }) => Boolean(kit) }, { name: () => 'src/Demo.svx', contentType: 'text', content: addMarkdownFile, - condition: ({ kit }) => !kit.installed + condition: ({ kit }) => !kit } ], options, @@ -47,7 +47,7 @@ export const tests = defineAdderTests({ ] }); -function addMarkdownFile(editor: TextFileEditorArgs) { +function addMarkdownFile(editor: TextFileEditor) { // example taken from website: https://mdsvex.pngwn.io return ( editor.content + @@ -65,7 +65,7 @@ Markdown is pretty good but sometimes you just need more. ); } -function useMarkdownFile({ js, html }: SvelteFileEditorArgs) { +function useMarkdownFile({ js, html }: SvelteFileEditor) { js.imports.addDefault(js.ast, './Demo.svx', 'Demo'); const div = html.div({ class: 'mdsvex' }); diff --git a/packages/adders/playwright/config/adder.ts b/packages/adders/playwright/config/adder.ts index 3fd455fdb..f404b5cfe 100644 --- a/packages/adders/playwright/config/adder.ts +++ b/packages/adders/playwright/config/adder.ts @@ -42,7 +42,7 @@ export const adder = defineAdderConfig({ } }, { - name: ({ typescript }) => `e2e/demo.test.${typescript.installed ? 'ts' : 'js'}`, + name: ({ typescript }) => `e2e/demo.test.${typescript ? 'ts' : 'js'}`, contentType: 'text', content: ({ content }) => { if (content) return content; @@ -58,7 +58,7 @@ export const adder = defineAdderConfig({ } }, { - name: ({ typescript }) => `playwright.config.${typescript.installed ? 'ts' : 'js'}`, + name: ({ typescript }) => `playwright.config.${typescript ? 'ts' : 'js'}`, contentType: 'script', content: ({ ast, imports, exports, common, object }) => { const defineConfig = common.expressionFromString('defineConfig({})'); diff --git a/packages/adders/routify/config/adder.ts b/packages/adders/routify/config/adder.ts index 4cec02249..69c2df68f 100644 --- a/packages/adders/routify/config/adder.ts +++ b/packages/adders/routify/config/adder.ts @@ -18,7 +18,7 @@ export const adder = defineAdderConfig({ packages: [{ name: '@roxi/routify', version: 'next', dev: true }], files: [ { - name: ({ typescript }) => `vite.config.${typescript.installed ? 'ts' : 'js'}`, + name: ({ typescript }) => `vite.config.${typescript ? 'ts' : 'js'}`, contentType: 'script', content: ({ ast, array, object, functions, imports, exports }) => { const vitePluginName = 'routify'; diff --git a/packages/adders/tailwindcss/config/adder.ts b/packages/adders/tailwindcss/config/adder.ts index 947b2a352..f371ad520 100644 --- a/packages/adders/tailwindcss/config/adder.ts +++ b/packages/adders/tailwindcss/config/adder.ts @@ -29,12 +29,12 @@ export const adder = defineAdderConfig({ name: 'prettier-plugin-tailwindcss', version: '^0.6.5', dev: true, - condition: ({ prettier }) => prettier.installed + condition: ({ prettier }) => prettier } ], files: [ { - name: ({ typescript }) => `tailwind.config.${typescript.installed ? 'ts' : 'js'}`, + name: ({ typescript }) => `tailwind.config.${typescript ? 'ts' : 'js'}`, contentType: 'script', content: ({ options, @@ -49,14 +49,14 @@ export const adder = defineAdderConfig({ }) => { let root; const rootExport = object.createEmpty(); - if (typescript.installed) { + if (typescript) { imports.addNamed(ast, 'tailwindcss', { Config: 'Config' }, true); root = common.typeAnnotateExpression(rootExport, 'Config'); } const { astNode: exportDeclaration } = exports.defaultExport(ast, root ?? rootExport); - if (!typescript.installed) + if (!typescript) common.addJsDocTypeComment(exportDeclaration, "import('tailwindcss').Config"); const contentArray = object.property(rootExport, 'content', array.createEmpty()); @@ -117,10 +117,10 @@ export const adder = defineAdderConfig({ content: ({ js }) => { js.imports.addEmpty(js.ast, './app.css'); }, - condition: ({ kit }) => !kit.installed + condition: ({ kit }) => !kit }, { - name: ({ kit }) => `${kit.routesDirectory}/+layout.svelte`, + name: ({ kit }) => `${kit?.routesDirectory}/+layout.svelte`, contentType: 'svelte', content: ({ js, html }) => { js.imports.addEmpty(js.ast, '../app.css'); @@ -129,7 +129,7 @@ export const adder = defineAdderConfig({ html.ast.childNodes.push(slot); } }, - condition: ({ kit }) => kit.installed + condition: ({ kit }) => Boolean(kit) }, { name: () => '.prettierrc', @@ -142,7 +142,7 @@ export const adder = defineAdderConfig({ if (!plugins.includes(PLUGIN_NAME)) plugins.push(PLUGIN_NAME); }, - condition: ({ prettier }) => prettier.installed + condition: ({ prettier }) => prettier } ] }); diff --git a/packages/adders/tailwindcss/config/tests.ts b/packages/adders/tailwindcss/config/tests.ts index 891d12b8d..2df21ab2f 100644 --- a/packages/adders/tailwindcss/config/tests.ts +++ b/packages/adders/tailwindcss/config/tests.ts @@ -1,8 +1,4 @@ -import { - defineAdderTests, - type OptionDefinition, - type SvelteFileEditorArgs -} from '@svelte-cli/core'; +import { defineAdderTests, type OptionDefinition, type SvelteFileEditor } from '@svelte-cli/core'; import { options } from './options'; const divId = 'myDiv'; @@ -11,13 +7,13 @@ const typographyDivId = 'myTypographyDiv'; export const tests = defineAdderTests({ files: [ { - name: ({ kit }) => `${kit.routesDirectory}/+page.svelte`, + name: ({ kit }) => `${kit?.routesDirectory}/+page.svelte`, contentType: 'svelte', content: (editor) => { prepareCoreTest(editor); if (editor.options.typography) prepareTypographyTest(editor); }, - condition: ({ kit }) => kit.installed + condition: ({ kit }) => Boolean(kit) }, { name: () => 'src/App.svelte', @@ -26,7 +22,7 @@ export const tests = defineAdderTests({ prepareCoreTest(editor); if (editor.options.typography) prepareTypographyTest(editor); }, - condition: ({ kit }) => !kit.installed + condition: ({ kit }) => !kit } ], options, @@ -56,14 +52,12 @@ export const tests = defineAdderTests({ ] }); -function prepareCoreTest({ html }: SvelteFileEditorArgs) { +function prepareCoreTest({ html }: SvelteFileEditor) { const div = html.div({ class: 'bg-slate-600 border-gray-50 border-4 mt-1', id: divId }); html.appendElement(html.ast.childNodes, div); } -function prepareTypographyTest({ - html -}: SvelteFileEditorArgs) { +function prepareTypographyTest({ html }: SvelteFileEditor) { const div = html.element('p', { class: 'text-lg text-right line-through', id: typographyDivId }); html.appendElement(html.ast.childNodes, div); } diff --git a/packages/adders/vitest/config/adder.ts b/packages/adders/vitest/config/adder.ts index c9d9bdf79..34f433080 100644 --- a/packages/adders/vitest/config/adder.ts +++ b/packages/adders/vitest/config/adder.ts @@ -32,7 +32,7 @@ export const adder = defineAdderConfig({ } }, { - name: ({ typescript }) => `src/demo.spec.${typescript.installed ? 'ts' : 'js'}`, + name: ({ typescript }) => `src/demo.spec.${typescript ? 'ts' : 'js'}`, contentType: 'text', content: ({ content }) => { if (content) return content; @@ -49,7 +49,7 @@ export const adder = defineAdderConfig({ } }, { - name: ({ typescript }) => `vite.config.${typescript.installed ? 'ts' : 'js'}`, + name: ({ typescript }) => `vite.config.${typescript ? 'ts' : 'js'}`, contentType: 'script', content: ({ ast, imports, exports, common, object }) => { // find `defineConfig` import declaration for "vite" From bd546da66356aa62c68d6819f18c72ce42443a50 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:24:15 -0400 Subject: [PATCH 11/49] tweak --- packages/cli/commands/add.ts | 14 +++++++------- packages/core/adder/remoteControl.ts | 19 ------------------- packages/core/env.ts | 1 + 3 files changed, 8 insertions(+), 26 deletions(-) delete mode 100644 packages/core/adder/remoteControl.ts create mode 100644 packages/core/env.ts diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index d5d8a0147..f31dbd75f 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -15,7 +15,7 @@ import { createOrUpdateFiles, createWorkspace, installPackages, - remoteControl + TESTING } from '@svelte-cli/core/internal'; import { type ExternalAdderConfig, @@ -127,10 +127,12 @@ export async function runAddCommand(options: Options, adders: string[]): Promise const message = fails .map(({ name, message }) => pc.yellow(`${name} (${message})`)) .join('\n- '); + p.note(`- ${message}`, 'Preconditions not met'); const force = await p.confirm({ - message: 'Preconditions failed. Do you wish to continue?' + message: 'Preconditions failed. Do you wish to continue?', + initialValue: false }); if (p.isCancel(force) || !force) { p.cancel('Operation cancelled.'); @@ -194,10 +196,8 @@ export async function runAddCommand(options: Options, adders: string[]): Promise // apply adders let filesToFormat: string[] = []; if (Object.keys({ ...official, ...community }).length > 0) { - const adderSpinner = p.spinner(); - adderSpinner.start('Applying adders'); filesToFormat = await installAdders({ cwd: options.cwd, official, community }); - adderSpinner.stop('Successfully installed adders'); + p.log.success('Successfully installed adders'); } // TODO: run postconditions? @@ -310,12 +310,12 @@ async function processExternalAdder( config: ExternalAdderConfig, cwd: string ) { - if (!remoteControl) console.log('Executing external command'); + if (!TESTING) p.log.message('Executing external command'); try { await executeCli('npx', config.command.split(' '), cwd, { env: Object.assign(process.env, config.environment ?? {}), - stdio: remoteControl ? 'pipe' : 'inherit' + stdio: TESTING ? 'pipe' : 'inherit' }); } catch (error) { const typedError = error as Error; diff --git a/packages/core/adder/remoteControl.ts b/packages/core/adder/remoteControl.ts deleted file mode 100644 index f10a4803a..000000000 --- a/packages/core/adder/remoteControl.ts +++ /dev/null @@ -1,19 +0,0 @@ -let remoteControlled = false; - -export type RemoteControlOptions = { - workingDirectory: string; - isTesting: boolean; - adderOptions: Record>; -}; - -export function enable(): void { - remoteControlled = true; -} - -export function isRemoteControlled(): boolean { - return remoteControlled; -} - -export function disable(): void { - remoteControlled = false; -} diff --git a/packages/core/env.ts b/packages/core/env.ts new file mode 100644 index 000000000..61d54dfd0 --- /dev/null +++ b/packages/core/env.ts @@ -0,0 +1 @@ +export const TESTING: boolean = process.env.CI?.toLowerCase() === 'true'; From 33808427a85823c31e5ad83e2a33e1e1c181658e Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:03:07 -0400 Subject: [PATCH 12/49] validate workspace --- packages/cli/commands/add.ts | 12 ++++++++++-- packages/cli/common.ts | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index f31dbd75f..4a90310b5 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -1,3 +1,5 @@ +import fs from 'node:fs'; +import path from 'node:path'; import * as v from 'valibot'; import { Argument, Command, Option } from 'commander'; import * as p from '@svelte-cli/clack-prompts'; @@ -54,6 +56,14 @@ export const add = new Command('add') const adders = v.parse(AddersSchema, adderArgs); const options = v.parse(OptionsSchema, opts); + // TODO: maybe use `detectSvelteDirectory`? + // validate workspace + const pkgPath = path.join(options.cwd, 'package.json'); + if (!fs.existsSync(pkgPath)) { + console.error(`Invalid workspace: '${pkgPath}' does not exist`); + process.exit(1); + } + const invalidAdders = adders.filter((a) => !adderIds.includes(a) && !aliases.includes(a)); if (invalidAdders.length > 0) { console.error(`Invalid adders specified: ${invalidAdders.join(', ')}`); @@ -200,8 +210,6 @@ export async function runAddCommand(options: Options, adders: string[]): Promise p.log.success('Successfully installed adders'); } - // TODO: run postconditions? - // install dependencies let depsInstalled; if (options.install) { diff --git a/packages/cli/common.ts b/packages/cli/common.ts index 1cae3812b..bd791df8b 100644 --- a/packages/cli/common.ts +++ b/packages/cli/common.ts @@ -149,8 +149,8 @@ export function getGlobalPreconditions( run: () => { const addersForInvalidEnvironment = adders.filter((a) => { const supportedEnvironments = a.config.metadata.environments; - if (projectType == 'kit' && !supportedEnvironments.kit) return true; - if (projectType == 'svelte' && !supportedEnvironments.svelte) return true; + if (projectType === 'kit' && !supportedEnvironments.kit) return true; + if (projectType === 'svelte' && !supportedEnvironments.svelte) return true; return false; }); From 6b5b33dd82e3f2910287574c0d58fb0504232f26 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:03:34 -0400 Subject: [PATCH 13/49] update entry --- packages/cli/index.ts | 70 ++++----------------------------------- packages/cli/package.json | 7 +++- 2 files changed, 12 insertions(+), 65 deletions(-) diff --git a/packages/cli/index.ts b/packages/cli/index.ts index 9b99d74c3..040f50cc5 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -1,68 +1,10 @@ #!/usr/bin/env node -import { remoteControl, executeAdders, prompts } from '@svelte-cli/core/internal'; import pkg from './package.json'; -import type { - AdderDetails, - AddersToApplySelectorParams, - ExecutingAdderInfo, - Question -} from '@svelte-cli/core'; -import { adderCategories, categories, adderIds, type CategoryKeys } from '@svelte-cli/config'; -import { getAdderDetails } from '@svelte-cli/adders'; +import { program } from 'commander'; +import { add } from './commands/add.js'; +import { create } from './commands/create.js'; -void executeCli(); - -async function executeCli() { - remoteControl.enable(); - - const adderDetails: Array>> = []; - - for (const adderName of adderIds) { - const adder = await getAdderDetails(adderName); - adderDetails.push({ config: adder.config, checks: adder.checks }); - } - - const executingAdderInfo: ExecutingAdderInfo = { - name: pkg.name, - version: pkg.version - }; - - await executeAdders(adderDetails, executingAdderInfo, undefined, selectAddersToApply); - - remoteControl.disable(); -} - -type AdderOption = { value: string; label: string; hint: string }; -async function selectAddersToApply({ projectType, addersMetadata }: AddersToApplySelectorParams) { - const promptOptions: Record = {}; - - for (const [categoryId, adderIds] of Object.entries(adderCategories)) { - const categoryDetails = categories[categoryId as CategoryKeys]; - const options: AdderOption[] = []; - const adders = addersMetadata.filter((x) => adderIds.includes(x.id)); - - for (const adder of adders) { - // if we detected a kit project, and the adder is not available for kit, ignore it. - if (projectType === 'kit' && !adder.environments.kit) continue; - // if we detected a svelte project, and the adder is not available for svelte, ignore it. - if (projectType === 'svelte' && !adder.environments.svelte) continue; - - options.push({ - label: adder.name, - value: adder.id, - hint: adder.website?.documentation || '' - }); - } - - if (options.length > 0) { - promptOptions[categoryDetails.name] = options; - } - } - const selectedAdders = await prompts.groupedMultiSelectPrompt( - 'What would you like to add to your project?', - promptOptions - ); - - return selectedAdders; -} +program.name(pkg.name).version(pkg.version, '-v'); +program.addCommand(create).addCommand(add); +program.parse(); diff --git a/packages/cli/package.json b/packages/cli/package.json index c53cfe09a..d0e0527dd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -26,7 +26,12 @@ }, "devDependencies": { "@svelte-cli/adders": "workspace:*", - "@svelte-cli/config": "workspace:*" + "@svelte-cli/clack-prompts": "workspace:*", + "@svelte-cli/create": "workspace:*", + "commander": "^12.1.0", + "package-manager-detector": "^0.1.0", + "picocolors": "^1.0.1", + "valibot": "^0.39.0" }, "scripts": { "lint": "prettier --check . --config ../../.prettierrc --ignore-path ../../.gitignore --ignore-path .gitignore", From 767af1b9e730bca62ed6407f9c6a96a3a780a819 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:06:00 -0400 Subject: [PATCH 14/49] no longer necessary --- packages/core/adder/execute.ts | 370 -------------------------- packages/core/adder/nextSteps.ts | 37 --- packages/core/adder/postconditions.ts | 73 ----- packages/core/adder/preconditions.ts | 153 ----------- packages/core/utils/cli.ts | 38 --- packages/core/utils/common.ts | 37 --- packages/core/utils/dependencies.ts | 56 ---- packages/core/utils/prompts.ts | 119 --------- 8 files changed, 883 deletions(-) delete mode 100644 packages/core/adder/execute.ts delete mode 100644 packages/core/adder/nextSteps.ts delete mode 100644 packages/core/adder/postconditions.ts delete mode 100644 packages/core/adder/preconditions.ts delete mode 100644 packages/core/utils/cli.ts delete mode 100644 packages/core/utils/common.ts delete mode 100644 packages/core/utils/dependencies.ts delete mode 100644 packages/core/utils/prompts.ts diff --git a/packages/core/adder/execute.ts b/packages/core/adder/execute.ts deleted file mode 100644 index 78a9fbf20..000000000 --- a/packages/core/adder/execute.ts +++ /dev/null @@ -1,370 +0,0 @@ -import path from 'node:path'; -import * as pc from 'picocolors'; -import { serializeJson } from '@svelte-cli/ast-tooling'; -import { commonFilePaths, format, writeFile } from '../files/utils'; -import { createProject, detectSvelteDirectory } from '../utils/create-project'; -import { createOrUpdateFiles } from '../files/processors'; -import { getPackageJson } from '../utils/common'; -import { - type Workspace, - createEmptyWorkspace, - populateWorkspaceDetails, - addPropertyToWorkspaceOption -} from '../utils/workspace'; -import { - type OptionDefinition, - ensureCorrectOptionTypes as validateOptionTypes, - prepareAndParseCliOptions, - extractCommonCliOptions, - extractAdderCliOptions, - type AvailableCliOptionValues, - requestMissingOptionsFromUser -} from './options'; -import type { - AdderCheckConfig, - AdderConfig, - AdderConfigMetadata, - ExternalAdderConfig, - InlineAdderConfig -} from './config'; -import type { RemoteControlOptions } from './remoteControl'; -import { suggestInstallingDependencies } from '../utils/dependencies'; -import { validatePreconditions } from './preconditions'; -import { endPrompts, startPrompts } from '../utils/prompts'; -import { checkPostconditions, printUnmetPostconditions } from './postconditions'; -import { displayNextSteps } from './nextSteps'; -import { spinner, log, cancel } from '@svelte-cli/clack-prompts'; -import { executeCli } from '../utils/cli'; - -export type ProjectType = 'svelte' | 'kit'; - -export type AdderDetails = { - config: AdderConfig; - checks: AdderCheckConfig; -}; - -export type ExecutingAdderInfo = { - name: string; - version: string; -}; - -export type AddersToApplySelectorParams = { - projectType: ProjectType; - addersMetadata: AdderConfigMetadata[]; -}; -export type AddersToApplySelector = (params: AddersToApplySelectorParams) => Promise; - -export type AddersExecutionPlan = { - createProject: boolean; - commonCliOptions: AvailableCliOptionValues; - cliOptionsByAdderId: Record>; - workingDirectory: string; - selectAddersToApply?: AddersToApplySelector; -}; - -export async function executeAdder( - adderDetails: AdderDetails, - executingAdderInfo: ExecutingAdderInfo, - remoteControlOptions: RemoteControlOptions | undefined = undefined -): Promise { - await executeAdders([adderDetails], executingAdderInfo, remoteControlOptions); -} - -export async function executeAdders( - adderDetails: Array>, - executingAdder: ExecutingAdderInfo, - remoteControlOptions: RemoteControlOptions | undefined = undefined, - selectAddersToApply: AddersToApplySelector | undefined = undefined, - cwd: string | undefined = undefined -): Promise { - try { - const adderDetailsByAdderId: Map> = new Map(); - adderDetails.map((x) => adderDetailsByAdderId.set(x.config.metadata.id, x)); - - const remoteControlled = remoteControlOptions !== undefined; - const isTesting = remoteControlled && remoteControlOptions.isTesting; - - const cliOptions = !isTesting ? prepareAndParseCliOptions(adderDetails) : {}; - const commonCliOptions = extractCommonCliOptions(cliOptions); - const cliOptionsByAdderId = - (!isTesting - ? extractAdderCliOptions(cliOptions, adderDetails) - : remoteControlOptions.adderOptions) ?? {}; - validateOptionTypes(adderDetails, cliOptionsByAdderId); - - let workingDirectory: string | null = cwd ?? null; - if (isTesting && !workingDirectory) workingDirectory = remoteControlOptions.workingDirectory; - else if (!workingDirectory) workingDirectory = determineWorkingDirectory(commonCliOptions.path); - workingDirectory = await detectSvelteDirectory(workingDirectory); - const createProject = workingDirectory == null; - if (!workingDirectory) workingDirectory = process.cwd(); - - const executionPlan: AddersExecutionPlan = { - workingDirectory, - createProject, - commonCliOptions, - cliOptionsByAdderId, - selectAddersToApply - }; - - await executePlan(executionPlan, executingAdder, adderDetails, remoteControlOptions); - } catch (e) { - if (e instanceof Error) cancel(e.message); - else cancel('Something went wrong.'); - console.error(e); - process.exit(1); - } -} - -async function executePlan( - executionPlan: AddersExecutionPlan, - executingAdder: ExecutingAdderInfo, - adderDetails: Array>, - remoteControlOptions: RemoteControlOptions | undefined -) { - const remoteControlled = remoteControlOptions !== undefined; - const isTesting = remoteControlled && remoteControlOptions.isTesting; - const isRunningCli = adderDetails.length > 1; - - if (!isTesting) { - console.log(pc.gray(`${executingAdder.name} version ${executingAdder.version}\n`)); - startPrompts('Welcome to Svelte Add!'); - } - - // create project if required - if (executionPlan.createProject) { - const cwd = executionPlan.commonCliOptions.path ?? executionPlan.workingDirectory; - const { projectCreated, directory } = await createProject(cwd); - if (!projectCreated) return; - executionPlan.workingDirectory = directory; - } - - const workspace = createEmptyWorkspace(); - populateWorkspaceDetails(workspace, executionPlan.workingDirectory); - const projectType: ProjectType = workspace.kit.installed ? 'kit' : 'svelte'; - - // select appropriate adders - let userSelectedAdders = executionPlan.commonCliOptions.adders ?? []; - if (userSelectedAdders.length == 0 && isRunningCli) { - // if the user has not selected any adders via the cli and we are currently executing for more than one adder - // the user should have the possibility to select the adders he want's to add. - if (!executionPlan.selectAddersToApply) - throw new Error('selectAddersToApply must be provided!'); - - const addersMetadata = adderDetails.map((x) => x.config.metadata); - userSelectedAdders = await executionPlan.selectAddersToApply({ - projectType, - addersMetadata - }); - } else if (userSelectedAdders.length == 0 && !isRunningCli) { - // if we are executing only one adder, then we can safely assume that this adder should be added - userSelectedAdders = [adderDetails[0].config.metadata.id]; - } - const isApplyingMultipleAdders = userSelectedAdders.length > 1; - - // remove unselected adder data - const addersToRemove = adderDetails.filter( - (x) => !userSelectedAdders.includes(x.config.metadata.id) - ); - for (const adderToRemove of addersToRemove) { - const adderId = adderToRemove.config.metadata.id; - - delete executionPlan.cliOptionsByAdderId[adderId]; - } - adderDetails = adderDetails.filter((x) => userSelectedAdders.includes(x.config.metadata.id)); - - // preconditions - if (!executionPlan.commonCliOptions.skipPreconditions) - await validatePreconditions( - adderDetails, - executingAdder.name, - executionPlan.workingDirectory, - isTesting, - projectType - ); - - // applies the default option value to missing adder's cli options - if (executionPlan.commonCliOptions.default) { - for (const adder of adderDetails) { - const adderId = adder.config.metadata.id; - for (const [option, value] of Object.entries(adder.config.options)) { - executionPlan.cliOptionsByAdderId[adderId][option] ??= value.default; - } - } - } - - // ask the user questions about unselected options - await requestMissingOptionsFromUser(adderDetails, executionPlan); - - // adders might specify that they should be executed after another adder. - // this orders the adders to (ideally) have adders without dependencies run first - // and adders with dependencies runs later on, based on the adders they depend on. - // based on https://stackoverflow.com/a/72030336/16075084 - adderDetails = adderDetails.sort((a, b) => { - if (!a.config.runsAfter) return -1; - if (!b.config.runsAfter) return 1; - - return a.config.runsAfter.includes(b.config.metadata.id) - ? 1 - : b.config.runsAfter.includes(a.config.metadata.id) - ? -1 - : 0; - }); - - // apply the adders - const unmetPostconditions: string[] = []; - const filesToFormat = new Set(); - for (const { config, checks } of adderDetails) { - const adderId = config.metadata.id; - - const adderWorkspace = createEmptyWorkspace(); - populateWorkspaceDetails(adderWorkspace, executionPlan.workingDirectory); - if (executionPlan.cliOptionsByAdderId) { - for (const [key, value] of Object.entries(executionPlan.cliOptionsByAdderId[adderId])) { - addPropertyToWorkspaceOption(adderWorkspace, key, value); - } - } - - const isInstall = true; - if (config.integrationType === 'inline') { - const localConfig = config as InlineAdderConfig; - const changedFiles = await processInlineAdder(localConfig, adderWorkspace, isInstall); - changedFiles.forEach((file) => filesToFormat.add(file)); - } else if (config.integrationType === 'external') { - await processExternalAdder(config, executionPlan.workingDirectory, isTesting); - } else { - throw new Error('Unknown integration type'); - } - - const unmetAdderPostconditions = await checkPostconditions( - config, - checks, - adderWorkspace, - isApplyingMultipleAdders - ); - unmetPostconditions.push(...unmetAdderPostconditions); - } - - if (isTesting && unmetPostconditions.length > 0) { - throw new Error('Postconditions not met: ' + unmetPostconditions.join(' / ')); - } else if (unmetPostconditions.length > 0) { - printUnmetPostconditions(unmetPostconditions); - } - - // reload workspace as adders might have changed i.e. dependencies - populateWorkspaceDetails(workspace, executionPlan.workingDirectory); - - let installStatus; - if (!remoteControlled && !executionPlan.commonCliOptions.skipInstall) - installStatus = await suggestInstallingDependencies(executionPlan.workingDirectory); - - if (installStatus === 'installed' && workspace.prettier.installed) { - const formatSpinner = spinner(); - formatSpinner.start('Formatting modified files'); - try { - await format(workspace, Array.from(filesToFormat)); - formatSpinner.stop('Successfully formatted modified files'); - } catch (e) { - formatSpinner.stop('Failed to format files'); - if (e instanceof Error) log.error(e.message); - } - } - - if (!isTesting) { - displayNextSteps(adderDetails, isApplyingMultipleAdders, executionPlan); - endPrompts("You're all set!"); - } -} - -async function processInlineAdder( - config: InlineAdderConfig, - workspace: Workspace, - isInstall: boolean -) { - const pkgPath = installPackages(config, workspace); - const updatedOrCreatedFiles = createOrUpdateFiles(config.files, workspace); - await runHooks(config, workspace, isInstall); - - const changedFiles = [pkgPath, ...updatedOrCreatedFiles]; - return changedFiles; -} - -async function processExternalAdder( - config: ExternalAdderConfig, - workingDirectory: string, - isTesting: boolean -) { - if (!isTesting) console.log('Executing external command'); - - if (!config.environment) config.environment = {}; - - try { - await executeCli('npx', config.command.split(' '), workingDirectory, { - env: Object.assign(process.env, config.environment), - stdio: isTesting ? 'pipe' : 'inherit' - }); - } catch (error) { - const typedError = error as Error; - throw new Error('Failed executing external command: ' + typedError.message); - } -} - -export function determineWorkingDirectory(directory: string | undefined): string { - let cwd = directory ?? process.cwd(); - if (!path.isAbsolute(cwd)) { - cwd = path.join(process.cwd(), cwd); - } - - return cwd; -} - -export function installPackages( - config: InlineAdderConfig, - workspace: Workspace -): string { - const { text: originalText, data } = getPackageJson(workspace); - - for (const dependency of config.packages) { - if (dependency.condition && !dependency.condition(workspace)) { - continue; - } - - if (dependency.dev) { - if (!data.devDependencies) { - data.devDependencies = {}; - } - - data.devDependencies[dependency.name] = dependency.version; - } else { - if (!data.dependencies) { - data.dependencies = {}; - } - - data.dependencies[dependency.name] = dependency.version; - } - } - - if (data.dependencies) data.dependencies = alphabetizeProperties(data.dependencies); - if (data.devDependencies) data.devDependencies = alphabetizeProperties(data.devDependencies); - - writeFile(workspace, commonFilePaths.packageJsonFilePath, serializeJson(originalText, data)); - return commonFilePaths.packageJsonFilePath; -} - -function alphabetizeProperties(obj: Record) { - const orderedObj: Record = {}; - const sortedEntries = Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)); - for (const [key, value] of sortedEntries) { - orderedObj[key] = value; - } - return orderedObj; -} - -async function runHooks( - config: InlineAdderConfig, - workspace: Workspace, - isInstall: boolean -) { - if (isInstall && config.installHook) await config.installHook(workspace); - else if (!isInstall && config.uninstallHook) await config.uninstallHook(workspace); -} diff --git a/packages/core/adder/nextSteps.ts b/packages/core/adder/nextSteps.ts deleted file mode 100644 index fb7359482..000000000 --- a/packages/core/adder/nextSteps.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { messagePrompt } from '../utils/prompts'; -import type { InlineAdderConfig } from './config'; -import type { AdderDetails, AddersExecutionPlan } from './execute'; -import type { OptionDefinition, OptionValues } from './options'; -import pc from 'picocolors'; - -export function displayNextSteps( - adderDetails: Array>, - multipleAdders: boolean, - executionPlan: AddersExecutionPlan -): void { - const allAddersMessage = adderDetails - .filter((x) => x.config.integrationType == 'inline' && x.config.nextSteps) - .map((x) => x.config as InlineAdderConfig) - .map((x) => { - // only doing this to narrow the type, `nextSteps` should already exist here - if (!x.nextSteps) return ''; - const metadata = x.metadata; - let adderMessage = ''; - if (multipleAdders) { - adderMessage = `${pc.green(metadata.name)}:\n`; - } - - const options = executionPlan.cliOptionsByAdderId[x.metadata.id] as OptionValues; - - const adderNextSteps = x.nextSteps({ - options, - cwd: executionPlan.workingDirectory, - colors: pc, - docs: x.metadata.website?.documentation - }); - adderMessage += `- ${adderNextSteps.join('\n- ')}`; - return adderMessage; - }) - .join('\n\n'); - if (allAddersMessage) messagePrompt('Next steps', allAddersMessage); -} diff --git a/packages/core/adder/postconditions.ts b/packages/core/adder/postconditions.ts deleted file mode 100644 index 5df71a0b6..000000000 --- a/packages/core/adder/postconditions.ts +++ /dev/null @@ -1,73 +0,0 @@ -import dedent from 'dedent'; -import * as pc from 'picocolors'; -import { messagePrompt } from '../utils/prompts'; -import { fileExistsWorkspace, readFile } from '../files/utils'; -import type { Workspace } from '../utils/workspace'; -import type { AdderCheckConfig, AdderConfig } from './config'; -import type { OptionDefinition } from './options'; - -export type PreconditionParameters = { - workspace: Workspace; - fileExists: (path: string) => void; - fileContains: (path: string, expectedContent: string) => void; -}; -export type Postcondition = { - name: string; - run: (params: PreconditionParameters) => Promise | void; -}; - -export async function checkPostconditions( - config: AdderConfig, - checks: AdderCheckConfig, - workspace: Workspace, - multipleAdders: boolean -): Promise { - const postconditions = checks.postconditions ?? []; - const unmetPostconditions: string[] = []; - - for (const postcondition of postconditions) { - try { - await postcondition.run({ - workspace, - fileExists: (path) => fileExists(path, workspace), - fileContains: (path, expectedContent) => fileContains(path, workspace, expectedContent) - }); - } catch (error) { - const typedError = error as Error; - const message = `${postcondition.name} (${typedError.message})`; - unmetPostconditions.push(`${multipleAdders ? config.metadata.id + ': ' : ''}${message}`); - } - } - - return unmetPostconditions; -} - -function fileExists(path: string, workspace: Workspace) { - if (fileExistsWorkspace(workspace, path)) return; - - throw new Error(`File "${path}" does not exists`); -} - -function fileContains( - path: string, - workspace: Workspace, - expectedContent: string -): void { - fileExists(path, workspace); - - const content = readFile(workspace, path); - if (content && content.includes(expectedContent)) return; - - throw new Error(`File "${path}" does not contain "${expectedContent}"`); -} - -export function printUnmetPostconditions(unmetPostconditions: string[]): void { - const postconditionList = unmetPostconditions.map((x) => pc.yellow(`- ${x}`)).join('\n'); - const additionalText = dedent` - Postconditions are not supposed to fail. - Please open an issue providing the full console output: - https://github.com/sveltejs/cli/issues/new/choose - `; - - messagePrompt('Postconditions not met', `${postconditionList}\n\n${additionalText}`); -} diff --git a/packages/core/adder/preconditions.ts b/packages/core/adder/preconditions.ts deleted file mode 100644 index 5c0cf099b..000000000 --- a/packages/core/adder/preconditions.ts +++ /dev/null @@ -1,153 +0,0 @@ -import * as pc from 'picocolors'; -import { booleanPrompt, endPrompts, messagePrompt } from '../utils/prompts'; -import { executeCli } from '../utils/cli'; -import type { AdderDetails, ProjectType } from './execute'; -import type { Precondition } from './config'; -import type { OptionDefinition } from './options'; - -function getGlobalPreconditions( - executingCli: string, - workingDirectory: string, - adderDetails: Array>, - projectType: ProjectType -): { name: string; preconditions: Precondition[] | undefined } { - return { - name: executingCli, - preconditions: [ - { - name: 'clean working directory', - run: async () => { - let outputText = ''; - - try { - // If a user has pending git changes the output of the following command will list - // all files that have been added/modified/deleted and thus the output will not be empty. - // In case the output of the command below is an empty text, we can safely assume - // there are no pending changes. If the below command is run outside of a git repository, - // git will exit with a failing exit code, which will trigger the catch statement. - // also see https://remarkablemark.org/blog/2017/10/12/check-git-dirty/#git-status - await executeCli('git', ['status', '--short'], workingDirectory, { - onData: (data) => { - outputText += data; - } - }); - - if (outputText) { - return { success: false, message: 'Found modified files' }; - } - - return { success: true, message: undefined }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - return { success: true, message: 'Not a git repository' }; - } - } - }, - { - name: 'supported environments', - run: () => { - const addersForInvalidEnvironment = adderDetails.filter((x) => { - const supportedEnvironments = x.config.metadata.environments; - if (projectType == 'kit' && !supportedEnvironments.kit) return true; - if (projectType == 'svelte' && !supportedEnvironments.svelte) return true; - - return false; - }); - - if (addersForInvalidEnvironment.length == 0) { - return { success: true, message: undefined }; - } - - const messages = addersForInvalidEnvironment.map( - (adder) => - `"${adder.config.metadata.name}" does not support "${projectType.toString()}"` - ); - return { success: false, message: messages.join(' / ') }; - } - } - ] - }; -} - -export async function validatePreconditions( - adderDetails: Array>, - executingCliName: string, - workingDirectory: string, - isTesting: boolean, - projectType: ProjectType -): Promise { - const multipleAdders = adderDetails.length > 1; - let allPreconditionsPassed = true; - const preconditionLog: string[] = []; - - const adderPreconditions = adderDetails.map(({ config, checks }) => { - return { - name: config.metadata.name, - preconditions: checks.preconditions - }; - }); - const combinedPreconditions = isTesting - ? adderPreconditions - : [ - getGlobalPreconditions(executingCliName, workingDirectory, adderDetails, projectType), - ...adderPreconditions - ]; - - for (const { name, preconditions } of combinedPreconditions) { - if (!preconditions) continue; - - for (const precondition of preconditions) { - let message; - let preconditionPassed; - try { - const result = await precondition.run(); - - if (result.success) { - message = precondition.name; - preconditionPassed = true; - } else { - preconditionPassed = false; - message = `${precondition.name} (${result.message ?? 'No failure message provided'})`; - } - } catch (error) { - const errorString = error as string; - preconditionPassed = false; - message = precondition.name + ` (Unexpected failure: ${errorString})`; - } - - if (!preconditionPassed) { - if (multipleAdders) { - message = `${name}: ${message}`; - } - - message = pc.yellow(message); - preconditionLog.push(message); - } - - if (!preconditionPassed) allPreconditionsPassed = false; - } - } - - if (allPreconditionsPassed) { - return; - } - - if (isTesting) { - throw new Error(`Preconditions failed: ${preconditionLog.join(' / ')}`); - } - - const allMessages = preconditionLog.map((msg) => `- ${msg}`).join('\n'); - - messagePrompt('Preconditions not met', allMessages); - - await askUserToContinueWithFailedPreconditions(); -} - -export async function askUserToContinueWithFailedPreconditions(): Promise { - const result = await booleanPrompt('Preconditions failed. Do you wish to continue?', false); - - if (!result) { - endPrompts('Exiting.'); - process.exit(); - } -} diff --git a/packages/core/utils/cli.ts b/packages/core/utils/cli.ts deleted file mode 100644 index 474d666d8..000000000 --- a/packages/core/utils/cli.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { spawn, type ChildProcess } from 'node:child_process'; - -export async function executeCli( - command: string, - commandArgs: string[], - cwd: string, - options?: { - onData?: (data: string, program: ChildProcess, resolve: (value?: any) => any) => void; - stdio?: 'pipe' | 'inherit'; - env?: Record; - } -): Promise { - const stdio = options?.stdio ?? 'pipe'; - const env = options?.env ?? process.env; - - const program = spawn(command, commandArgs, { stdio, shell: true, cwd, env }); - - return await new Promise((resolve, reject) => { - let errorText = ''; - program.stderr?.on('data', (data: Buffer) => { - const value = data.toString(); - errorText += value; - }); - - program.stdout?.on('data', (data: Buffer) => { - const value = data.toString(); - options?.onData?.(value, program, resolve); - }); - - program.on('exit', (code) => { - if (code == 0) { - resolve(undefined); - } else { - reject(new Error(errorText)); - } - }); - }); -} diff --git a/packages/core/utils/common.ts b/packages/core/utils/common.ts deleted file mode 100644 index 6f2481a9b..000000000 --- a/packages/core/utils/common.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { parseJson } from '@svelte-cli/ast-tooling'; -import { commonFilePaths, readFile } from '../files/utils'; -import type { WorkspaceWithoutExplicitArgs } from './workspace'; - -export type Package = { - name: string; - version: string; - dependencies?: Record; - devDependencies?: Record; - bugs?: string; - repository?: { type: string; url: string }; - keywords?: string[]; -}; - -export function getPackageJson(workspace: WorkspaceWithoutExplicitArgs): { - text: string; - data: Package; -} { - const packageText = readFile(workspace, commonFilePaths.packageJsonFilePath); - if (!packageText) { - return { - text: '', - data: { - dependencies: {}, - devDependencies: {}, - name: '', - version: '' - } - }; - } - - const packageJson: Package = parseJson(packageText) as Package; - return { - text: packageText, - data: packageJson - }; -} diff --git a/packages/core/utils/dependencies.ts b/packages/core/utils/dependencies.ts deleted file mode 100644 index 070e2543d..000000000 --- a/packages/core/utils/dependencies.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { selectPrompt } from './prompts'; -import { detect } from 'package-manager-detector'; -import { COMMANDS } from 'package-manager-detector/agents'; -import { spinner } from '@svelte-cli/clack-prompts'; -import { executeCli } from './cli'; - -type PackageManager = (typeof packageManagers)[number] | undefined; -const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'] as const; - -/** - * @param workingDirectory - * @returns the install status of dependencies - */ -export async function suggestInstallingDependencies( - workingDirectory: string -): Promise<'installed' | 'skipped'> { - const detectedPm = await detect({ cwd: workingDirectory }); - let selectedPm = detectedPm.agent; - - selectedPm ??= await selectPrompt( - 'Which package manager do you want to install dependencies with?', - undefined, - [ - { - label: 'None', - value: undefined - }, - ...packageManagers.map((x) => { - return { label: x, value: x as PackageManager }; - }) - ] - ); - - if (!selectedPm || !COMMANDS[selectedPm]) { - return 'skipped'; - } - - const loadingSpinner = spinner(); - loadingSpinner.start('Installing dependencies...'); - - const installCommand = COMMANDS[selectedPm].install; - const [pm, install] = installCommand.split(' '); - await installDependencies(pm, [install], workingDirectory); - - loadingSpinner.stop('Successfully installed dependencies'); - return 'installed'; -} - -async function installDependencies(command: string, args: string[], workingDirectory: string) { - try { - await executeCli(command, args, workingDirectory); - } catch (error) { - const typedError = error as Error; - throw new Error('unable to install dependencies: ' + typedError.message); - } -} diff --git a/packages/core/utils/prompts.ts b/packages/core/utils/prompts.ts deleted file mode 100644 index bb254a581..000000000 --- a/packages/core/utils/prompts.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - cancel, - intro, - isCancel, - outro, - select, - text, - multiselect, - note, - groupMultiselect, - confirm -} from '@svelte-cli/clack-prompts'; - -type Primitive = Readonly; -export type PromptOption = Value extends Primitive - ? { - value: Value; - label?: string; - hint?: string; - } - : { - value: Value; - label: string; - hint?: string; - }; - -export function startPrompts(message: string): void { - intro(message); -} - -export function endPrompts(message: string): void { - outro(message); -} - -export async function confirmPrompt(message: string, initialValue: boolean): Promise { - const value = await confirm({ - message, - initialValue - }); - return cancelIfRequired(value); -} - -export async function booleanPrompt(question: string, initialValue: boolean): Promise { - return selectPrompt(question, initialValue, [ - { label: 'Yes', value: true }, - { label: 'No', value: false } - ]); -} - -export async function selectPrompt( - question: string, - initialValue: T, - options: Array> -): Promise { - const value = await select({ - message: question, - options, - initialValue - }); - - return cancelIfRequired(value); -} - -export async function textPrompt( - question: string, - placeholder: string = '', - initialValue: string = '' -): Promise { - const value = await text({ - message: question, - placeholder, - initialValue - }); - - const result = cancelIfRequired(value); - return result; -} - -export async function multiSelectPrompt( - question: string, - options: Array> -): Promise { - const value = await multiselect({ - message: question, - options, - required: false - }); - - return cancelIfRequired(value); -} - -export async function groupedMultiSelectPrompt( - question: string, - options: Record>> -): Promise { - const value = await groupMultiselect({ - message: question, - options, - required: false, - selectableGroups: false, - spacedGroups: true - }); - - return cancelIfRequired(value); -} - -export function messagePrompt(title: string, content: string): void { - note(content, title); -} - -function cancelIfRequired(value: T): T extends symbol ? never : T { - if (typeof value === 'symbol' || isCancel(value)) { - cancel('Operation cancelled.'); - process.exit(0); - } - - // @ts-expect-error hacking it to never return a symbol. there's probably a better way, but this works for now. - return value; -} From 13a8f8874d02c725e3f06dad318209db9bf2cd47 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:20:45 -0400 Subject: [PATCH 15/49] try to reduce core to its essentials --- packages/core/adder/config.ts | 21 +- packages/core/adder/options.ts | 267 +------------------- packages/core/files/processors.ts | 194 +++++++------- packages/core/files/utils.ts | 91 ++++++- packages/core/{utils => files}/workspace.ts | 114 +++------ packages/core/index.ts | 22 +- packages/core/internal.ts | 41 +-- packages/core/package.json | 3 - packages/core/utils/create-project.ts | 102 +------- 9 files changed, 241 insertions(+), 614 deletions(-) rename packages/core/{utils => files}/workspace.ts (52%) diff --git a/packages/core/adder/config.ts b/packages/core/adder/config.ts index 3fdef2e2e..5044086fa 100644 --- a/packages/core/adder/config.ts +++ b/packages/core/adder/config.ts @@ -1,16 +1,14 @@ -import type { +import type { OptionDefinition, OptionValues, Question } from './options.js'; +import type { FileType } from '../files/processors.js'; +import type { Workspace } from '../files/workspace.js'; +import type { Colors } from 'picocolors/types.js'; + +export type { CssAstEditor, HtmlAstEditor, JsAstEditor, SvelteAstEditor } from '@svelte-cli/ast-manipulation'; -import type { OptionDefinition, OptionValues, Question } from './options.js'; -import type { FileTypes } from '../files/processors.js'; -import type { Workspace } from '../utils/workspace.js'; -import type { Postcondition } from './postconditions.js'; -import type { Colors } from 'picocolors/types.js'; - -export type { CssAstEditor, HtmlAstEditor, JsAstEditor, SvelteAstEditor }; export type ConditionDefinition = ( Workspace: Workspace @@ -54,15 +52,13 @@ export type BaseAdderConfig = { export type InlineAdderConfig = BaseAdderConfig & { integrationType: 'inline'; packages: Array>; - files: Array>; + files: Array>; nextSteps?: (data: { options: OptionValues; cwd: string; colors: Colors; docs: string | undefined; }) => string[]; - installHook?: (workspace: Workspace) => Promise; - uninstallHook?: (workspace: Workspace) => Promise; }; export type ExternalAdderConfig = BaseAdderConfig & { @@ -113,7 +109,7 @@ export type TestDefinition = { }; export type AdderTestConfig = { - files: Array>; + files: Array>; options: Args; optionValues: Array>; runSynchronously?: boolean; @@ -141,7 +137,6 @@ export type Precondition = { export type AdderCheckConfig = { options: Args; preconditions?: Precondition[]; - postconditions?: Array>; }; export function defineAdderChecks( diff --git a/packages/core/adder/options.ts b/packages/core/adder/options.ts index a729ace0f..25d83a8bf 100644 --- a/packages/core/adder/options.ts +++ b/packages/core/adder/options.ts @@ -1,7 +1,3 @@ -import { type OptionValues as CliOptionValues, program } from 'commander'; -import { booleanPrompt, selectPrompt, textPrompt, type PromptOption } from '../utils/prompts'; -import type { AdderDetails, AddersExecutionPlan } from './execute'; - export type BooleanQuestion = { type: 'boolean'; default: boolean; @@ -20,13 +16,12 @@ export type NumberQuestion = { export type SelectQuestion = { type: 'select'; default: Value; - options: Array>; + options: Array<{ value: Value; label?: string; hint?: string }>; }; export type BaseQuestion = { question: string; // TODO: we want this to be akin to OptionValues so that the options can be inferred - condition?: (options: OptionValues) => boolean; }; @@ -45,263 +40,3 @@ export type OptionValues = { ? Value : never; }; - -export type AvailableCliOptionKeys = keyof AvailableCliOptionKeyTypes; -export type AvailableCliOptionKeyTypes = { - default: boolean; - path: string; - skipPreconditions: boolean; - skipInstall: boolean; -}; - -export type AvailableCliOptionValues = { - [K in AvailableCliOptionKeys]?: AvailableCliOptionKeyTypes[K]; -} & { adders?: string[] }; - -export type AvailableCliOption = { - cliArg: string; - processedCliArg: string; // `commander` will transform the cli name if the arg names contains `-` - description: string; - allowShorthand: boolean; -} & (BooleanQuestion | StringQuestion); -export type AvailableCliOptions = Record; - -export const availableCliOptions: AvailableCliOptions = { - default: { - cliArg: 'default', - processedCliArg: 'default', - type: 'boolean', - default: false, - description: 'Installs default adder options for unspecified options', - allowShorthand: true - }, - path: { - cliArg: 'path', - processedCliArg: 'path', - type: 'string', - default: './', - description: 'Path to working directory', - allowShorthand: false - }, - skipPreconditions: { - cliArg: 'skip-preconditions', - processedCliArg: 'skipPreconditions', - type: 'boolean', - default: false, - description: 'Skips validating preconditions before running the adder', - allowShorthand: true - }, - skipInstall: { - cliArg: 'skip-install', - processedCliArg: 'skipInstall', - type: 'boolean', - default: false, - description: 'Skips installing dependencies after applying the adder', - allowShorthand: true - } -}; - -export function prepareAndParseCliOptions( - adderDetails: Array> -): CliOptionValues { - const multipleAdders = adderDetails.length > 1; - - for (const option of Object.values(availableCliOptions)) { - if (option.allowShorthand) { - program.option(`--${option.cliArg} [${option.type}]`, option.description); - } else { - program.option(`--${option.cliArg} <${option.type}>`, option.description); - } - } - - if (multipleAdders) { - program.argument('[adders...]', 'List of adders to install'); - } - - const addersWithOptions = adderDetails.filter((x) => Object.keys(x.config.options).length > 0); - - for (const { config } of addersWithOptions) { - for (const optionKey of Object.keys(config.options)) { - const option = config.options[optionKey]; - - let optionString; - if (multipleAdders) { - optionString = `--${config.metadata.id}-${optionKey} [${option.type}]`; - } else { - optionString = `--${optionKey} [${option.type}]`; - } - - program.option(optionString, option.question); - } - } - - program.parse(); - const options = program.opts(); - - if (multipleAdders) { - let selectedAdderIds = program.args ?? []; - - // replace aliases with adder ids - selectedAdderIds = selectedAdderIds.map((id) => { - const adder = adderDetails.find(({ config }) => config.metadata?.alias === id); - return adder ? adder.config.metadata.id : id; - }); - - validateAdders(adderDetails, selectedAdderIds); - - options.adder = selectedAdderIds; - } - - return options; -} - -function validateAdders( - adderDetails: Array>, - selectedAdderIds: string[] -) { - const validAdderIds = adderDetails.map((x) => x.config.metadata.id); - const invalidAdders = selectedAdderIds.filter((x) => !validAdderIds.includes(x)); - - if (invalidAdders.length > 0) { - console.error( - `Invalid adder${invalidAdders.length > 1 ? 's' : ''} selected:`, - invalidAdders.join(', ') - ); - process.exit(1); - } -} - -export function ensureCorrectOptionTypes( - adderDetails: Array>, - cliOptionsByAdderId: Record> -): void { - let foundInvalidType = false; - - for (const { config } of adderDetails) { - const adderId = config.metadata.id; - - for (const optionKey of Object.keys(config.options)) { - const option = config.options[optionKey]; - const value = cliOptionsByAdderId[adderId][optionKey]; - - if (value == undefined) { - continue; - } else if (option.type == 'boolean' && typeof value == 'boolean') { - continue; - } else if (option.type == 'number' && typeof value == 'number') { - continue; - } else if ( - option.type == 'number' && - typeof value == 'string' && - typeof parseInt(value) == 'number' && - !isNaN(parseInt(value)) - ) { - cliOptionsByAdderId[adderId][optionKey] = parseInt(value); - continue; - } else if (option.type == 'string' && typeof value == 'string') { - continue; - } else if (option.type === 'select') { - continue; - } - - foundInvalidType = true; - console.log( - `Option ${optionKey} needs to be of type ${option.type} but was of type ${typeof value}!` - ); - } - } - - if (foundInvalidType) { - console.log('Found invalid option type. Exiting.'); - process.exit(0); - } -} - -export function extractCommonCliOptions(cliOptions: CliOptionValues): AvailableCliOptionValues { - const typedOption = (name: string) => cliOptions[name] as T; - - const commonOptions: AvailableCliOptionValues = { - default: typedOption(availableCliOptions.default.processedCliArg), - path: typedOption(availableCliOptions.path.processedCliArg), - skipInstall: typedOption(availableCliOptions.skipInstall.processedCliArg), - skipPreconditions: typedOption(availableCliOptions.skipPreconditions.processedCliArg), - adders: typedOption('adder') - }; - - return commonOptions; -} - -export function extractAdderCliOptions( - cliOptions: CliOptionValues, - adderDetails: Array> -): Record> { - const multipleAdders = adderDetails.length > 1; - - const options: Record> = {}; - for (const { config } of adderDetails) { - const adderId = config.metadata.id; - options[adderId] = {}; - - for (const optionKey of Object.keys(config.options)) { - let cliOptionKey = optionKey; - - if (multipleAdders) cliOptionKey = `${adderId}${upperCaseFirstLetter(cliOptionKey)}`; - - let optionValue = cliOptions[cliOptionKey] as unknown; - if (optionValue === 'true') optionValue = true; - else if (optionValue === 'false') optionValue = false; - - options[adderId][optionKey] = optionValue; - } - } - - return options; -} - -function upperCaseFirstLetter(string: string) { - return string.charAt(0).toLocaleUpperCase() + string.slice(1); -} - -export async function requestMissingOptionsFromUser( - adderDetails: Array>, - executionPlan: AddersExecutionPlan -): Promise { - for (const { config } of adderDetails) { - const adderId = config.metadata.id; - const questionPrefix = adderDetails.length > 1 ? `${config.metadata.name}: ` : ''; - - for (const optionKey of Object.keys(config.options)) { - const option = config.options[optionKey]; - const selectedValues = executionPlan.cliOptionsByAdderId[adderId]; - const skipQuestion = option.condition?.(selectedValues) === false; - - if (!selectedValues || skipQuestion) continue; - - let optionValue = selectedValues[optionKey]; - - // if the option already has an value, ignore it and continue - if (optionValue !== undefined) continue; - - if (option.type == 'number' || option.type == 'string') { - optionValue = await textPrompt( - questionPrefix + option.question, - 'Not sure', - option.default.toString() - ); - } else if (option.type == 'boolean') { - optionValue = await booleanPrompt(questionPrefix + option.question, option.default); - } else if (option.type == 'select') { - optionValue = await selectPrompt( - questionPrefix + option.question, - option.default, - option.options - ); - } - - if (optionValue === 'true') optionValue = true; - if (optionValue === 'false') optionValue = false; - - selectedValues[optionKey] = optionValue; - } - } -} diff --git a/packages/core/files/processors.ts b/packages/core/files/processors.ts index 7f5e7bd8b..920204c10 100644 --- a/packages/core/files/processors.ts +++ b/packages/core/files/processors.ts @@ -22,64 +22,54 @@ import { import { fileExistsWorkspace, readFile, writeFile } from './utils'; import type { ConditionDefinition } from '../adder/config'; import type { OptionDefinition } from '../adder/options'; -import type { Workspace } from '../utils/workspace'; +import type { Workspace } from './workspace'; -export type BaseFile = { - name: (options: Workspace) => string; - condition?: ConditionDefinition; -}; - -export type ScriptFileEditorArgs = JsAstEditor & Workspace; -export type ScriptFileType = { - contentType: 'script'; - content: (editor: ScriptFileEditorArgs) => void; -}; -export type ScriptFile = ScriptFileType & BaseFile; +export type CssFileEditor = Workspace & CssAstEditor; +export type HtmlFileEditor = Workspace & HtmlAstEditor; +export type JsonFileEditor = Workspace & { data: any }; +export type ScriptFileEditor = Workspace & JsAstEditor; +export type SvelteFileEditor = Workspace & SvelteAstEditor; +export type TextFileEditor = Workspace & { content: string }; -export type TextFileEditorArgs = { - content: string; -} & Workspace; -export type TextFileType = { - contentType: 'text'; - content: (editor: TextFileEditorArgs) => string; +type CssFile = { + contentType: 'css'; + content: (editor: CssFileEditor) => void; }; -export type TextFile = TextFileType & BaseFile; - -export type SvelteFileEditorArgs = SvelteAstEditor & Workspace; -export type SvelteFileType = { - contentType: 'svelte'; - content: (editor: SvelteFileEditorArgs) => void; +type HtmlFile = { + contentType: 'html'; + content: (editor: HtmlFileEditor) => void; }; -export type SvelteFile = SvelteFileType & BaseFile; - -export type JsonFileEditorArgs = { data: any } & Workspace; -export type JsonFileType = { +type JsonFile = { contentType: 'json'; - content: (editor: JsonFileEditorArgs) => void; + content: (editor: JsonFileEditor) => void; }; -export type JsonFile = JsonFileType & BaseFile; - -export type HtmlFileEditorArgs = HtmlAstEditor & Workspace; -export type HtmlFileType = { - contentType: 'html'; - content: (editor: HtmlFileEditorArgs) => void; +type ScriptFile = { + contentType: 'script'; + content: (editor: ScriptFileEditor) => void; }; -export type HtmlFile = HtmlFileType & BaseFile; - -export type CssFileEditorArgs = CssAstEditor & Workspace; -export type CssFileType = { - contentType: 'css'; - content: (editor: CssFileEditorArgs) => void; +type SvelteFile = { + contentType: 'svelte'; + content: (editor: SvelteFileEditor) => void; +}; +type TextFile = { + contentType: 'text'; + content: (editor: TextFileEditor) => string; }; -export type CssFile = CssFileType & BaseFile; -export type FileTypes = +type ParsedFile = + | CssFile + | HtmlFile + | JsonFile | ScriptFile - | TextFile | SvelteFile - | JsonFile - | HtmlFile - | CssFile; + | TextFile; + +type BaseFile = { + name: (options: Workspace) => string; + condition?: ConditionDefinition; +}; + +export type FileType = BaseFile & ParsedFile; /** * @param files @@ -87,7 +77,7 @@ export type FileTypes = * @returns a list of paths of changed or created files */ export function createOrUpdateFiles( - files: Array>, + files: Array>, workspace: Workspace ): string[] { const changedFiles = []; @@ -98,26 +88,25 @@ export function createOrUpdateFiles( } const exists = fileExistsWorkspace(workspace, fileDetails.name(workspace)); + let content = exists ? readFile(workspace, fileDetails.name(workspace)) : ''; - let content = ''; - if (!exists) { - content = ''; - } else { - content = readFile(workspace, fileDetails.name(workspace)); + if (fileDetails.contentType === 'css') { + content = handleCssFile(content, fileDetails, workspace); } - - if (fileDetails.contentType == 'script') { + if (fileDetails.contentType === 'html') { + content = handleHtmlFile(content, fileDetails, workspace); + } + if (fileDetails.contentType === 'json') { + content = handleJsonFile(content, fileDetails, workspace); + } + if (fileDetails.contentType === 'script') { content = handleScriptFile(content, fileDetails, workspace); - } else if (fileDetails.contentType == 'text') { - content = handleTextFile(content, fileDetails, workspace); - } else if (fileDetails.contentType == 'svelte') { + } + if (fileDetails.contentType === 'svelte') { content = handleSvelteFile(content, fileDetails, workspace); - } else if (fileDetails.contentType == 'json') { - content = handleJsonFile(content, fileDetails, workspace); - } else if (fileDetails.contentType == 'css') { - content = handleCssFile(content, fileDetails, workspace); - } else if (fileDetails.contentType == 'html') { - content = handleHtmlFile(content, fileDetails, workspace); + } + if (fileDetails.contentType === 'text') { + content = handleTextFile(content, fileDetails, workspace); } writeFile(workspace, fileDetails.name(workspace), content); @@ -131,78 +120,87 @@ export function createOrUpdateFiles( return changedFiles; } -function handleHtmlFile( +function handleCssFile( content: string, - fileDetails: HtmlFileType, + fileDetails: CssFile, workspace: Workspace ) { - const ast = parseHtml(content); - fileDetails.content({ ...getHtmlAstEditor(ast), ...workspace }); - content = serializeHtml(ast); + const ast = parsePostcss(content); + ast.raws.semicolon = true; // always add the optional semicolon + const editor = getCssAstEditor(ast); + + fileDetails.content({ ...editor, ...workspace }); + content = serializePostcss(ast); return content; } -function handleCssFile( +function handleHtmlFile( content: string, - fileDetails: CssFileType, + fileDetails: HtmlFile, workspace: Workspace ) { - const ast = parsePostcss(content); - ast.raws.semicolon = true; // always add the optional semicolon - fileDetails.content({ ...getCssAstEditor(ast), ...workspace }); - content = serializePostcss(ast); + const ast = parseHtml(content); + const editor = getHtmlAstEditor(ast); + + fileDetails.content({ ...editor, ...workspace }); + content = serializeHtml(ast); return content; } function handleJsonFile( content: string, - fileDetails: JsonFileType, + fileDetails: JsonFile, workspace: Workspace ) { if (!content) content = '{}'; - const data: unknown = parseJson(content); + const data = parseJson(content); + fileDetails.content({ data, ...workspace }); content = serializeJson(content, data); return content; } -function handleSvelteFile( +function handleScriptFile( content: string, - fileDetails: SvelteFileType, + fileDetails: ScriptFile, workspace: Workspace ) { - const { jsAst, htmlAst, cssAst } = parseSvelteFile(content); + const ast = parseScript(content); + const editor = getJsAstEditor(ast); fileDetails.content({ - js: getJsAstEditor(jsAst), - html: getHtmlAstEditor(htmlAst), - css: getCssAstEditor(cssAst), + ...editor, ...workspace }); - - return serializeSvelteFile({ jsAst, htmlAst, cssAst }); -} - -function handleTextFile( - content: string, - fileDetails: TextFileType, - workspace: Workspace -) { - content = fileDetails.content({ content, ...workspace }); + content = serializeScript(ast); return content; } -function handleScriptFile( +function handleSvelteFile( content: string, - fileDetails: ScriptFileType, + fileDetails: SvelteFile, workspace: Workspace ) { - const ast = parseScript(content); + const { cssAst, htmlAst, jsAst } = parseSvelteFile(content); + const css = getCssAstEditor(cssAst); + const html = getHtmlAstEditor(htmlAst); + const js = getJsAstEditor(jsAst); fileDetails.content({ - ...getJsAstEditor(ast), + css, + html, + js, ...workspace }); - content = serializeScript(ast); + + return serializeSvelteFile({ cssAst, htmlAst, jsAst }); +} + +function handleTextFile( + content: string, + fileDetails: TextFile, + workspace: Workspace +) { + content = fileDetails.content({ content, ...workspace }); return content; } diff --git a/packages/core/files/utils.ts b/packages/core/files/utils.ts index 27f56eb3c..19cae9480 100644 --- a/packages/core/files/utils.ts +++ b/packages/core/files/utils.ts @@ -1,7 +1,43 @@ import fs from 'node:fs'; import path from 'node:path'; -import { executeCli } from '../utils/cli'; -import type { WorkspaceWithoutExplicitArgs } from '../utils/workspace'; +import { parseJson, serializeJson } from '@svelte-cli/ast-tooling'; +import type { InlineAdderConfig } from '../adder/config.js'; +import type { OptionDefinition } from '../adder/options.js'; +import type { Workspace, WorkspaceWithoutExplicitArgs } from './workspace.js'; + +export type Package = { + name: string; + version: string; + dependencies?: Record; + devDependencies?: Record; + bugs?: string; + repository?: { type: string; url: string }; + keywords?: string[]; +}; + +export function getPackageJson(workspace: WorkspaceWithoutExplicitArgs): { + text: string; + data: Package; +} { + const packageText = readFile(workspace, commonFilePaths.packageJsonFilePath); + if (!packageText) { + return { + text: '', + data: { + dependencies: {}, + devDependencies: {}, + name: '', + version: '' + } + }; + } + + const packageJson = parseJson(packageText) as Package; + return { + text: packageText, + data: packageJson + }; +} export function readFile(workspace: WorkspaceWithoutExplicitArgs, filePath: string): string { const fullFilePath = getFilePath(workspace.cwd, filePath); @@ -15,6 +51,48 @@ export function readFile(workspace: WorkspaceWithoutExplicitArgs, filePath: stri return text; } +export function installPackages( + config: InlineAdderConfig, + workspace: Workspace +): string { + const { text: originalText, data } = getPackageJson(workspace); + + for (const dependency of config.packages) { + if (dependency.condition && !dependency.condition(workspace)) { + continue; + } + + if (dependency.dev) { + if (!data.devDependencies) { + data.devDependencies = {}; + } + + data.devDependencies[dependency.name] = dependency.version; + } else { + if (!data.dependencies) { + data.dependencies = {}; + } + + data.dependencies[dependency.name] = dependency.version; + } + } + + if (data.dependencies) data.dependencies = alphabetizeProperties(data.dependencies); + if (data.devDependencies) data.devDependencies = alphabetizeProperties(data.devDependencies); + + writeFile(workspace, commonFilePaths.packageJsonFilePath, serializeJson(originalText, data)); + return commonFilePaths.packageJsonFilePath; +} + +function alphabetizeProperties(obj: Record) { + const orderedObj: Record = {}; + const sortedEntries = Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)); + for (const [key, value] of sortedEntries) { + orderedObj[key] = value; + } + return orderedObj; +} + export function writeFile( workspace: WorkspaceWithoutExplicitArgs, filePath: string, @@ -44,15 +122,6 @@ export function getFilePath(cwd: string, fileName: string): string { return path.join(cwd, fileName); } -export async function format( - workspace: WorkspaceWithoutExplicitArgs, - paths: string[] -): Promise { - await executeCli('npx', ['prettier', '--write', '--ignore-unknown', ...paths], workspace.cwd, { - stdio: 'pipe' - }); -} - export const commonFilePaths = { packageJsonFilePath: 'package.json', svelteConfigFilePath: 'svelte.config.js' diff --git a/packages/core/utils/workspace.ts b/packages/core/files/workspace.ts similarity index 52% rename from packages/core/utils/workspace.ts rename to packages/core/files/workspace.ts index e0e79f41f..fd6914b25 100644 --- a/packages/core/utils/workspace.ts +++ b/packages/core/files/workspace.ts @@ -1,111 +1,63 @@ import fs from 'node:fs'; import path from 'node:path'; -import { type AstTypes, parseScript } from '@svelte-cli/ast-tooling'; -import { getPackageJson } from './common'; -import { commonFilePaths, findUp, readFile } from '../files/utils'; import { getJsAstEditor } from '@svelte-cli/ast-manipulation'; +import { type AstTypes, parseScript } from '@svelte-cli/ast-tooling'; +import { TESTING } from '../env'; +import { commonFilePaths, findUp, getPackageJson, readFile } from './utils'; import type { OptionDefinition, OptionValues, Question } from '../adder/options'; -import { remoteControl } from '../internal'; - -export type PrettierData = { - installed: boolean; -}; - -export type TypescriptData = { - installed: boolean; -}; - -export type SvelteKitData = { - installed: boolean; - libDirectory: string; - routesDirectory: string; -}; export type Workspace = { options: OptionValues; cwd: string; - prettier: PrettierData; - typescript: TypescriptData; - kit: SvelteKitData; dependencies: Record; + prettier: boolean; + typescript: boolean; + kit: { libDirectory: string; routesDirectory: string } | undefined; }; export type WorkspaceWithoutExplicitArgs = Workspace>; -export function createEmptyWorkspace(): Workspace { +export function createEmptyWorkspace() { return { options: {}, cwd: '', - prettier: { - installed: false - }, - typescript: { - installed: false - }, - kit: { - installed: false, - routesDirectory: 'src/routes', - libDirectory: 'src/lib' - } + prettier: false, + typescript: false, + kit: undefined } as Workspace; } -export function addPropertyToWorkspaceOption( - workspace: WorkspaceWithoutExplicitArgs, - optionKey: string, - value: unknown -): void { - if (value === 'true') { - value = true; - } - - if (value === 'false') { - value = false; - } - - Object.defineProperty(workspace.options, optionKey, { - value, - writable: true, - enumerable: true, - configurable: true - }); -} - -export function populateWorkspaceDetails( - workspace: WorkspaceWithoutExplicitArgs, - workingDirectory: string -): void { - workspace.cwd = workingDirectory; +export function createWorkspace(cwd: string): Workspace { + const workspace = createEmptyWorkspace(); + workspace.cwd = cwd; const tsConfigFileName = 'tsconfig.json'; const viteConfigFileName = 'vite.config.ts'; - let usesTypescript = fs.existsSync(path.join(workingDirectory, viteConfigFileName)); + let usesTypescript = fs.existsSync(path.join(cwd, viteConfigFileName)); - if (remoteControl.isRemoteControlled()) { - // while executing tests, we only look into the direct `workingDirectory` + if (TESTING) { + // while executing tests, we only look into the direct `cwd` // as we might detect the monorepo `tsconfig.json` otherwise. - usesTypescript ||= fs.existsSync(path.join(workingDirectory, tsConfigFileName)); + usesTypescript ||= fs.existsSync(path.join(cwd, tsConfigFileName)); } else { - usesTypescript ||= findUp(workingDirectory, tsConfigFileName); + usesTypescript ||= findUp(cwd, tsConfigFileName); } const { data: packageJson } = getPackageJson(workspace); - if (packageJson.devDependencies) { - workspace.typescript.installed = usesTypescript; - workspace.prettier.installed = 'prettier' in packageJson.devDependencies; - workspace.kit.installed = '@sveltejs/kit' in packageJson.devDependencies; - workspace.dependencies = { ...packageJson.devDependencies, ...packageJson.dependencies }; - for (const [key, value] of Object.entries(workspace.dependencies)) { - // removes the version ranges (e.g. `^` is removed from: `^9.0.0`) - workspace.dependencies[key] = value.replaceAll(/[^\d|.]/g, ''); - } + + workspace.dependencies = { ...packageJson.devDependencies, ...packageJson.dependencies }; + workspace.typescript = usesTypescript; + workspace.prettier = 'prettier' in workspace.dependencies; + if ('@sveltejs/kit' in workspace.dependencies) workspace.kit = parseKitOptions(workspace); + for (const [key, value] of Object.entries(workspace.dependencies)) { + // removes the version ranges (e.g. `^` is removed from: `^9.0.0`) + workspace.dependencies[key] = value.replaceAll(/[^\d|.]/g, ''); } - parseSvelteConfigIntoWorkspace(workspace); + return workspace; } -export function parseSvelteConfigIntoWorkspace(workspace: WorkspaceWithoutExplicitArgs): void { - if (!workspace.kit.installed) return; +function parseKitOptions(workspace: WorkspaceWithoutExplicitArgs) { const configText = readFile(workspace, commonFilePaths.svelteConfigFilePath); const ast = parseScript(configText); const editor = getJsAstEditor(ast); @@ -146,10 +98,8 @@ export function parseSvelteConfigIntoWorkspace(workspace: WorkspaceWithoutExplic const routes = editor.object.property(files, 'routes', editor.common.createLiteral()); const lib = editor.object.property(files, 'lib', editor.common.createLiteral()); - if (routes.value) { - workspace.kit.routesDirectory = routes.value as string; - } - if (lib.value) { - workspace.kit.libDirectory = lib.value as string; - } + const routesDirectory = (routes.value as string) || 'src/routes'; + const libDirectory = (lib.value as string) || 'src/lib'; + + return { routesDirectory, libDirectory }; } diff --git a/packages/core/index.ts b/packages/core/index.ts index 0021a8ff1..64ccac6e8 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -1,28 +1,14 @@ -import { +export { defineAdderConfig, defineAdderTests, defineAdder, defineAdderOptions, defineAdderChecks } from './adder/config'; -import { executeCli } from './utils/cli'; -import { log } from '@svelte-cli/clack-prompts'; -import * as colors from 'picocolors'; -import dedent from 'dedent'; - -export { - defineAdderConfig, - defineAdder, - defineAdderTests, - defineAdderOptions, - defineAdderChecks, - executeCli, - dedent, - log, - colors -}; +export { log } from '@svelte-cli/clack-prompts'; +export { default as colors } from 'picocolors'; +export { default as dedent } from 'dedent'; export type * from './files/processors'; -export type * from './adder/execute'; export type * from './adder/options'; export type * from './adder/config'; diff --git a/packages/core/internal.ts b/packages/core/internal.ts index e24ce634a..26699618d 100644 --- a/packages/core/internal.ts +++ b/packages/core/internal.ts @@ -1,35 +1,6 @@ -import * as remoteControl from './adder/remoteControl'; -import { - executeAdder, - executeAdders, - determineWorkingDirectory, - type AddersToApplySelectorParams, - type AdderDetails, - type ExecutingAdderInfo -} from './adder/execute'; -import { createOrUpdateFiles } from './files/processors'; -import { createEmptyWorkspace, populateWorkspaceDetails } from './utils/workspace'; -import { suggestInstallingDependencies } from './utils/dependencies'; -import { availableCliOptions, type AvailableCliOptions, type Question } from './adder/options'; -import * as prompts from './utils/prompts'; - -export { - remoteControl, - createOrUpdateFiles, - createEmptyWorkspace, - executeAdder, - executeAdders, - populateWorkspaceDetails, - determineWorkingDirectory, - prompts, - suggestInstallingDependencies, - availableCliOptions -}; - -export type { - AvailableCliOptions, - AddersToApplySelectorParams, - AdderDetails, - Question, - ExecutingAdderInfo -}; +export { installPackages } from './files/utils'; +export { createOrUpdateFiles } from './files/processors'; +export { createWorkspace, type Workspace } from './files/workspace'; +export { detectSvelteDirectory } from './utils/create-project.js'; +export { TESTING } from './env'; +export type { Question } from './adder/options'; diff --git a/packages/core/package.json b/packages/core/package.json index fb114d641..12281c2b7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,10 +23,7 @@ ], "devDependencies": { "@svelte-cli/clack-prompts": "workspace:*", - "@svelte-cli/create": "workspace:*", - "commander": "^12.1.0", "dedent": "^1.5.3", - "package-manager-detector": "^0.1.0", "picocolors": "^1.0.1" }, "dependencies": { diff --git a/packages/core/utils/create-project.ts b/packages/core/utils/create-project.ts index b510a1161..42b5e020b 100644 --- a/packages/core/utils/create-project.ts +++ b/packages/core/utils/create-project.ts @@ -1,108 +1,34 @@ import fs from 'node:fs'; import path from 'node:path'; -import * as p from './prompts'; -import { commonFilePaths } from '../files/utils'; -import { getPackageJson } from './common'; -import { createEmptyWorkspace } from './workspace'; -import { spinner } from '@svelte-cli/clack-prompts'; -import { create, type LanguageType, templates } from '@svelte-cli/create'; +import { commonFilePaths, getPackageJson } from '../files/utils'; +import { createEmptyWorkspace } from '../files/workspace'; -export async function detectSvelteDirectory(directoryPath: string): Promise { - if (!directoryPath) return null; +export function detectSvelteDirectory(cwd: string): string | undefined { + if (!cwd) return; - const packageJsonPath = path.join(directoryPath, commonFilePaths.packageJsonFilePath); - const parentDirectoryPath = path.normalize(path.join(directoryPath, '..')); - const isRoot = parentDirectoryPath == directoryPath; + const packageJsonPath = path.join(cwd, commonFilePaths.packageJsonFilePath); + const parentDirectoryPath = path.normalize(path.join(cwd, '..')); + const isRoot = parentDirectoryPath == cwd; - if (!isRoot && !fs.existsSync(directoryPath)) { - return await detectSvelteDirectory(parentDirectoryPath); + if (!isRoot && !fs.existsSync(cwd)) { + return detectSvelteDirectory(parentDirectoryPath); } if (!isRoot && !fs.existsSync(packageJsonPath)) { - return await detectSvelteDirectory(parentDirectoryPath); + return detectSvelteDirectory(parentDirectoryPath); } if (isRoot && !fs.existsSync(packageJsonPath)) { - return null; + return; } const emptyWorkspace = createEmptyWorkspace(); - emptyWorkspace.cwd = directoryPath; + emptyWorkspace.cwd = cwd; const { data: packageJson } = getPackageJson(emptyWorkspace); if (packageJson.devDependencies && 'svelte' in packageJson.devDependencies) { - return directoryPath; + return cwd; } else if (!isRoot) { - return await detectSvelteDirectory(parentDirectoryPath); + return detectSvelteDirectory(parentDirectoryPath); } - - return null; -} - -export async function createProject(cwd: string): Promise<{ - projectCreated: boolean; - directory: string; -}> { - const createNewProject = await p.booleanPrompt('Create new Project?', true); - if (!createNewProject) { - p.endPrompts('Exiting.'); - process.exit(0); - } - - let relativePath = path.relative(process.cwd(), cwd); - if (!relativePath) { - relativePath = './'; - } - - let directory = await p.textPrompt( - 'Where should we create your project?', - ` (hit Enter to use '${relativePath}')` - ); - if (!directory) { - directory = relativePath; - } - - if (fs.existsSync(directory) && fs.readdirSync(directory).length > 0) { - const force = await p.confirmPrompt('Directory not empty. Continue?', false); - if (!force) { - p.endPrompts('Exiting.'); - process.exit(0); - } - } - - const options = templates.map((t) => ({ label: t.title, value: t.name, hint: t.description })); - const template = await p.selectPrompt('Which Svelte app template', 'default', options); - - const language = await p.selectPrompt( - 'Add type checking with Typescript?', - 'typescript', - [ - { label: 'Yes, using Typescript syntax', value: 'typescript' }, - { label: 'Yes, using Javascript with JSDoc comments', value: 'checkjs' }, - { label: 'No', value: null } - ] - ); - - const loadingSpinner = spinner(); - loadingSpinner.start('Initializing template'); - - try { - create(directory, { - name: path.basename(path.resolve(directory)), - template, - types: language - }); - } catch (error) { - loadingSpinner.stop('Failed initializing template!'); - const typedError = error as Error; - console.log('cancelled or failed ' + typedError.message); - return { projectCreated: false, directory: '' }; - } - - loadingSpinner.stop('Project created'); - - return { - projectCreated: true, - directory: path.join(process.cwd(), directory) - }; } From 4e8acbbddd23f4b5e13e9ef8eeefa399bce35d72 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:21:31 -0400 Subject: [PATCH 16/49] tweaks --- package.json | 2 +- packages/create/package.json | 2 -- pnpm-lock.yaml | 44 +++++++++++++++++++++++----------- rollup.config.js | 3 +-- scripts/get-deps-to-publish.js | 2 +- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index a78f5d3bc..dc7ff2ced 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "rollup": "^4.20.0", "rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-preserve-shebangs": "^0.2.0", - "typescript": "^5.3.3", + "typescript": "^5.5.4", "typescript-eslint": "^8.0.0", "unplugin-isolated-decl": "^0.4.5" }, diff --git a/packages/create/package.json b/packages/create/package.json index 5c70a4afc..53a539bc8 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -20,8 +20,6 @@ }, "license": "MIT", "homepage": "https://kit.svelte.dev", - "main": "./dist/index.js", - "types": "types/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61e160da5..c265b9ed8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,7 +54,7 @@ importers: specifier: ^0.2.0 version: 0.2.0(rollup@4.21.0) typescript: - specifier: ^5.3.3 + specifier: ^5.5.4 version: 5.5.4 typescript-eslint: specifier: ^8.0.0 @@ -151,11 +151,24 @@ importers: '@svelte-cli/adders': specifier: workspace:* version: link:../adders - '@svelte-cli/config': + '@svelte-cli/clack-prompts': specifier: workspace:* - version: link:../config - - packages/config: {} + version: link:../clack-prompts + '@svelte-cli/create': + specifier: workspace:* + version: link:../create + commander: + specifier: ^12.1.0 + version: 12.1.0 + package-manager-detector: + specifier: ^0.1.0 + version: 0.1.2 + picocolors: + specifier: ^1.0.1 + version: 1.0.1 + valibot: + specifier: ^0.39.0 + version: 0.39.0(typescript@5.5.4) packages/core: dependencies: @@ -169,18 +182,9 @@ importers: '@svelte-cli/clack-prompts': specifier: workspace:* version: link:../clack-prompts - '@svelte-cli/create': - specifier: workspace:* - version: link:../create - commander: - specifier: ^12.1.0 - version: 12.1.0 dedent: specifier: ^1.5.3 version: 1.5.3 - package-manager-detector: - specifier: ^0.1.0 - version: 0.1.2 picocolors: specifier: ^1.0.1 version: 1.0.1 @@ -2000,6 +2004,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@0.39.0: + resolution: {integrity: sha512-d+vE8SDRNy9zKg6No5MHz2tdz8H6CW8X3OdqYdmlhnoqQmEoM6Hu0hJUrZv3tPSVrzZkIIMCtdCQtMzcM6NCWw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vite-node@2.0.5: resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3927,6 +3939,10 @@ snapshots: util-deprecate@1.0.2: {} + valibot@0.39.0(typescript@5.5.4): + optionalDependencies: + typescript: 5.5.4 + vite-node@2.0.5(@types/node@22.4.1): dependencies: cac: 6.7.14 diff --git a/rollup.config.js b/rollup.config.js index 545fe1e68..8ad5ed6f2 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -45,7 +45,7 @@ function getConfig(project) { async writeBundle() { console.log('building templates'); const start = performance.now(); - await buildTemplates(path.resolve('packages', 'core', 'dist')); + await buildTemplates(path.resolve('packages', 'cli', 'dist')); const end = performance.now(); console.log(`finished building templates: ${Math.round(end - start)}ms`); } @@ -78,7 +78,6 @@ export default [ getConfig('clack-prompts'), getConfig('ast-tooling'), getConfig('ast-manipulation'), - getConfig('config'), getConfig('create'), getConfig('core'), getConfig('cli') diff --git a/scripts/get-deps-to-publish.js b/scripts/get-deps-to-publish.js index d4693140a..17c9dd8e3 100644 --- a/scripts/get-deps-to-publish.js +++ b/scripts/get-deps-to-publish.js @@ -14,7 +14,7 @@ if (!process.env.CHANGED_DIRS) throw new Error('CHANGED_DIRS is missing'); const json = execSync('pnpm -r list --only-projects --json').toString('utf8'); const repoPackages = - /** @type {Array }>} */ ( + /** @type {Array }>} */ ( JSON.parse(json) ); From efa766a2d6fbad533372db4eaaa5c74aaa2a5433 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 16:39:05 -0400 Subject: [PATCH 17/49] log errors --- packages/cli/commands/add.ts | 12 +++++++----- packages/cli/commands/create.ts | 4 ++-- packages/cli/common.ts | 14 ++++++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 4a90310b5..204ba1eea 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -9,7 +9,7 @@ import { formatFiles, getGlobalPreconditions, suggestInstallingDependencies, - wrap + runCommand } from '../common.js'; import { adderCategories, categories, adderIds, communityAdders } from '@svelte-cli/adders'; import { getAdderConfig, getAdderDetails } from '../../adders/index.js'; @@ -51,8 +51,9 @@ export const add = new Command('add') .option('--no-install', 'skips installing dependencies') .option('--no-preconditions', 'skips validating preconditions') .option('--default', 'applies default adder options for unspecified options', false) + // TODO: community adders // .addOption(communityOptions) - .action(async (adderArgs, opts) => { + .action((adderArgs, opts) => { const adders = v.parse(AddersSchema, adderArgs); const options = v.parse(OptionsSchema, opts); @@ -70,9 +71,10 @@ export const add = new Command('add') process.exit(1); } - const deduped = transformAliases(adders); - - await wrap(async () => await runAddCommand(options, deduped)); + const adderIds = transformAliases(adders); + runCommand(async () => { + await runAddCommand(options, adderIds); + }); }); export async function runAddCommand(options: Options, adders: string[]): Promise { diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 95ddd4a2a..6ea482487 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -9,7 +9,7 @@ import { type LanguageType, type TemplateType } from '@svelte-cli/create'; -import { wrap } from '../common.js'; +import { runCommand } from '../common.js'; import { runAddCommand } from './add.js'; const langs = ['typescript', 'checkjs', 'none'] as const; @@ -36,7 +36,7 @@ export const create = new Command('create') .action((projectPath, opts) => { const cwd = v.parse(ProjectPathSchema, projectPath); const options = v.parse(OptionsSchema, opts); - wrap(async () => { + runCommand(async () => { await createProject(cwd, options); }); }); diff --git a/packages/cli/common.ts b/packages/cli/common.ts index bd791df8b..61ad356d9 100644 --- a/packages/cli/common.ts +++ b/packages/cli/common.ts @@ -8,10 +8,16 @@ import type { AdderWithoutExplicitArgs } from '@svelte-cli/core'; type MaybePromise = () => Promise | void; -export async function wrap(action: MaybePromise) { - p.intro(`Welcome to the Svelte CLI! ${pc.gray(`(v${pkg.version})`)}`); - await action(); - p.outro("You're all set!"); +export async function runCommand(action: MaybePromise) { + try { + p.intro(`Welcome to the Svelte CLI! ${pc.gray(`(v${pkg.version})`)}`); + await action(); + p.outro("You're all set!"); + } catch (e) { + if (e instanceof Error) { + p.cancel(e.message); + } + } } export async function executeCli( From 37b1912d9e047147d097f0e4338ed267ed76de2d Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:45:38 -0400 Subject: [PATCH 18/49] word --- packages/cli/commands/add.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 204ba1eea..cc6c80aae 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -349,7 +349,7 @@ function transformAliases(ids: string[]): string[] { return Array.from(set); } -// other possivel variations +// other possible variations // npx sv add drizzle tailwindcss --options postgresql,postgres.js,no-docker no-typography // npx sv add drizzle=postgresql,postgres.js,no-docker tailwindcss=no-typography // `sv add drizzle tailwindcss --drizzle db:postgresql client:postgres.js docker:false --tailwindcss typography:false` From f15ba4191746878a9b9d00aff962471fee4e0cc3 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:49:54 -0400 Subject: [PATCH 19/49] add community adders --- packages/cli/commands/add.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index cc6c80aae..c83441b96 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -31,17 +31,14 @@ const OptionsSchema = v.strictObject({ cwd: v.string(), install: v.boolean(), preconditions: v.boolean(), - default: v.boolean() - // community: AddersSchema + default: v.boolean(), + community: AddersSchema }); type Options = v.InferOutput; const adderDetails = adderIds.map((id) => getAdderDetails(id)); const aliases = adderDetails.map((c) => c.config.metadata.alias).filter((v) => v !== undefined); -// const communityOptions = new Option('--community [adder...]', 'community adders to install') -//.choices(communityAdders.map((a) => a.id)); - const adderArg = new Argument('[adder...]', 'adders to install'); export const add = new Command('add') @@ -51,8 +48,7 @@ export const add = new Command('add') .option('--no-install', 'skips installing dependencies') .option('--no-preconditions', 'skips validating preconditions') .option('--default', 'applies default adder options for unspecified options', false) - // TODO: community adders - // .addOption(communityOptions) + .option('--community [adder...]', 'community adders to install') .action((adderArgs, opts) => { const adders = v.parse(AddersSchema, adderArgs); const options = v.parse(OptionsSchema, opts); @@ -71,9 +67,9 @@ export const add = new Command('add') process.exit(1); } - const adderIds = transformAliases(adders); + const dedupedIds = transformAliases(adders); runCommand(async () => { - await runAddCommand(options, adderIds); + await runAddCommand(options, dedupedIds); }); }); From 25786e6af2457c354917f84f9d7167cc262f2a27 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:50:18 -0400 Subject: [PATCH 20/49] lint --- packages/cli/commands/add.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index c83441b96..232b2cd17 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import * as v from 'valibot'; -import { Argument, Command, Option } from 'commander'; +import { Argument, Command } from 'commander'; import * as p from '@svelte-cli/clack-prompts'; import pc from 'picocolors'; import { @@ -11,7 +11,7 @@ import { suggestInstallingDependencies, runCommand } from '../common.js'; -import { adderCategories, categories, adderIds, communityAdders } from '@svelte-cli/adders'; +import { adderCategories, categories, adderIds } from '@svelte-cli/adders'; import { getAdderConfig, getAdderDetails } from '../../adders/index.js'; import { createOrUpdateFiles, From 93364288632d00e4c497e59b729614615e65f710 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:50:53 -0400 Subject: [PATCH 21/49] todo --- packages/cli/commands/add.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 232b2cd17..1e0bb732d 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -208,6 +208,8 @@ export async function runAddCommand(options: Options, adders: string[]): Promise p.log.success('Successfully installed adders'); } + // TODO: apply community adders + // install dependencies let depsInstalled; if (options.install) { From 1b3eb614e72ded654350fb4d21a19b9e87679565 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:52:56 -0400 Subject: [PATCH 22/49] types --- packages/cli/commands/create.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 6ea482487..ec2f3b065 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -106,8 +106,10 @@ async function createProject(cwd: string, options: Options) { initSpinner.stop('Project created'); if (options.adders) { - const config = { cwd: projectPath, default: false, install: true, preconditions: true }; - await runAddCommand(config, []); + await runAddCommand( + { cwd: projectPath, default: false, install: true, preconditions: true, community: [] }, + [] + ); } return { From 876b5dc38c55517d8509c5cfdf5425d0d99f69de Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:01:32 -0400 Subject: [PATCH 23/49] am i going insane? --- rollup.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup.config.js b/rollup.config.js index 8ad5ed6f2..36f94f320 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,7 +9,7 @@ import dts from 'unplugin-isolated-decl/rollup'; import esbuild from 'rollup-plugin-esbuild'; import { buildTemplates } from '@svelte-cli/create/build'; -/** @import { Package } from "./packages/core/utils/common.js" */ +/** @import { Package } from "./packages/core/files/utils.js" */ /** @import { Plugin, RollupOptions } from "rollup" */ /** @typedef {Package & { peerDependencies: Record }} PackageJson */ From 2cf6fe1d33911fa05d959e966e1ea01260f5c4a6 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:08:30 -0400 Subject: [PATCH 24/49] yea ok. no idea why this is only failing in CI. disabling you for now --- eslint.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index f7ee066d1..e165f9fde 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,8 +17,8 @@ export default [ }, rules: { '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/require-await': 'error' + '@typescript-eslint/no-unused-expressions': 'off' + // '@typescript-eslint/require-await': 'error' } }, { From 962c85ab32efd2748a5c70de6c95f11535541104 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:19:50 -0400 Subject: [PATCH 25/49] tweaks and expose findUp --- packages/core/files/utils.ts | 24 +++++++++---------- packages/core/files/workspace.ts | 10 ++++---- packages/core/internal.ts | 3 +-- packages/core/utils/create-project.ts | 34 --------------------------- 4 files changed, 17 insertions(+), 54 deletions(-) delete mode 100644 packages/core/utils/create-project.ts diff --git a/packages/core/files/utils.ts b/packages/core/files/utils.ts index 19cae9480..ba5793e5e 100644 --- a/packages/core/files/utils.ts +++ b/packages/core/files/utils.ts @@ -19,7 +19,7 @@ export function getPackageJson(workspace: WorkspaceWithoutExplicitArgs): { text: string; data: Package; } { - const packageText = readFile(workspace, commonFilePaths.packageJsonFilePath); + const packageText = readFile(workspace, commonFilePaths.packageJson); if (!packageText) { return { text: '', @@ -80,8 +80,8 @@ export function installPackages( if (data.dependencies) data.dependencies = alphabetizeProperties(data.dependencies); if (data.devDependencies) data.devDependencies = alphabetizeProperties(data.devDependencies); - writeFile(workspace, commonFilePaths.packageJsonFilePath, serializeJson(originalText, data)); - return commonFilePaths.packageJsonFilePath; + writeFile(workspace, commonFilePaths.packageJson, serializeJson(originalText, data)); + return commonFilePaths.packageJson; } function alphabetizeProperties(obj: Record) { @@ -123,25 +123,27 @@ export function getFilePath(cwd: string, fileName: string): string { } export const commonFilePaths = { - packageJsonFilePath: 'package.json', - svelteConfigFilePath: 'svelte.config.js' -}; + packageJson: 'package.json', + svelteConfig: 'svelte.config.js', + tsconfig: 'tsconfig.json', + viteConfigTS: 'vite.config.ts' +} as const; -export function findUp(searchPath: string, fileName: string, maxDepth?: number): boolean { +export function findUp(searchPath: string, fileName: string, maxDepth = -1): string | undefined { // partially sourced from https://github.com/privatenumber/get-tsconfig/blob/9e78ec52d450d58743439358dd88e2066109743f/src/utils/find-up.ts#L5 let depth = 0; - while (!maxDepth || depth < maxDepth) { + while (maxDepth < 0 || depth < maxDepth) { const configPath = path.posix.join(searchPath, fileName); try { // `access` throws an exception if the file could not be found fs.accessSync(configPath); - return true; + return configPath; } catch { const parentPath = path.dirname(searchPath); if (parentPath === searchPath) { // root directory - return false; + return; } searchPath = parentPath; @@ -149,6 +151,4 @@ export function findUp(searchPath: string, fileName: string, maxDepth?: number): depth++; } - - return false; } diff --git a/packages/core/files/workspace.ts b/packages/core/files/workspace.ts index fd6914b25..0e9ae8f17 100644 --- a/packages/core/files/workspace.ts +++ b/packages/core/files/workspace.ts @@ -31,16 +31,14 @@ export function createWorkspace(cwd: string): Wor const workspace = createEmptyWorkspace(); workspace.cwd = cwd; - const tsConfigFileName = 'tsconfig.json'; - const viteConfigFileName = 'vite.config.ts'; - let usesTypescript = fs.existsSync(path.join(cwd, viteConfigFileName)); + let usesTypescript = fs.existsSync(path.join(cwd, commonFilePaths.viteConfigTS)); if (TESTING) { // while executing tests, we only look into the direct `cwd` // as we might detect the monorepo `tsconfig.json` otherwise. - usesTypescript ||= fs.existsSync(path.join(cwd, tsConfigFileName)); + usesTypescript ||= fs.existsSync(path.join(cwd, commonFilePaths.tsconfig)); } else { - usesTypescript ||= findUp(cwd, tsConfigFileName); + usesTypescript ||= findUp(cwd, commonFilePaths.tsconfig) !== undefined; } const { data: packageJson } = getPackageJson(workspace); @@ -58,7 +56,7 @@ export function createWorkspace(cwd: string): Wor } function parseKitOptions(workspace: WorkspaceWithoutExplicitArgs) { - const configText = readFile(workspace, commonFilePaths.svelteConfigFilePath); + const configText = readFile(workspace, commonFilePaths.svelteConfig); const ast = parseScript(configText); const editor = getJsAstEditor(ast); diff --git a/packages/core/internal.ts b/packages/core/internal.ts index 26699618d..5dc258273 100644 --- a/packages/core/internal.ts +++ b/packages/core/internal.ts @@ -1,6 +1,5 @@ -export { installPackages } from './files/utils'; +export { installPackages, findUp } from './files/utils'; export { createOrUpdateFiles } from './files/processors'; export { createWorkspace, type Workspace } from './files/workspace'; -export { detectSvelteDirectory } from './utils/create-project.js'; export { TESTING } from './env'; export type { Question } from './adder/options'; diff --git a/packages/core/utils/create-project.ts b/packages/core/utils/create-project.ts deleted file mode 100644 index 42b5e020b..000000000 --- a/packages/core/utils/create-project.ts +++ /dev/null @@ -1,34 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { commonFilePaths, getPackageJson } from '../files/utils'; -import { createEmptyWorkspace } from '../files/workspace'; - -export function detectSvelteDirectory(cwd: string): string | undefined { - if (!cwd) return; - - const packageJsonPath = path.join(cwd, commonFilePaths.packageJsonFilePath); - const parentDirectoryPath = path.normalize(path.join(cwd, '..')); - const isRoot = parentDirectoryPath == cwd; - - if (!isRoot && !fs.existsSync(cwd)) { - return detectSvelteDirectory(parentDirectoryPath); - } - - if (!isRoot && !fs.existsSync(packageJsonPath)) { - return detectSvelteDirectory(parentDirectoryPath); - } - - if (isRoot && !fs.existsSync(packageJsonPath)) { - return; - } - - const emptyWorkspace = createEmptyWorkspace(); - emptyWorkspace.cwd = cwd; - const { data: packageJson } = getPackageJson(emptyWorkspace); - - if (packageJson.devDependencies && 'svelte' in packageJson.devDependencies) { - return cwd; - } else if (!isRoot) { - return detectSvelteDirectory(parentDirectoryPath); - } -} From bae23965fe3b6d306d28006f9eb9080d7556db88 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:27:20 -0400 Subject: [PATCH 26/49] infer workspace if we're in a child directory of one --- packages/cli/commands/add.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 1e0bb732d..cf6e10fe1 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -1,7 +1,6 @@ -import fs from 'node:fs'; import path from 'node:path'; import * as v from 'valibot'; -import { Argument, Command } from 'commander'; +import { Command } from 'commander'; import * as p from '@svelte-cli/clack-prompts'; import pc from 'picocolors'; import { @@ -16,6 +15,7 @@ import { getAdderConfig, getAdderDetails } from '../../adders/index.js'; import { createOrUpdateFiles, createWorkspace, + findUp, installPackages, TESTING } from '@svelte-cli/core/internal'; @@ -39,28 +39,30 @@ type Options = v.InferOutput; const adderDetails = adderIds.map((id) => getAdderDetails(id)); const aliases = adderDetails.map((c) => c.config.metadata.alias).filter((v) => v !== undefined); -const adderArg = new Argument('[adder...]', 'adders to install'); +// infers the workspace cwd if a `package.json` resides in a parent directory +const defaultPkgPath = findUp(process.cwd(), 'package.json'); +const defaultCwd = defaultPkgPath ? path.dirname(defaultPkgPath) : undefined; export const add = new Command('add') .description('Applies specified adders into a project') - .addArgument(adderArg) - .option('--cwd ', 'path to working directory', process.cwd()) + .argument('[adder...]', 'adders to install') + .option('--cwd ', 'path to working directory', defaultCwd) .option('--no-install', 'skips installing dependencies') .option('--no-preconditions', 'skips validating preconditions') .option('--default', 'applies default adder options for unspecified options', false) - .option('--community [adder...]', 'community adders to install') + .option('--community ', 'community adders to install', []) .action((adderArgs, opts) => { - const adders = v.parse(AddersSchema, adderArgs); - const options = v.parse(OptionsSchema, opts); - - // TODO: maybe use `detectSvelteDirectory`? // validate workspace - const pkgPath = path.join(options.cwd, 'package.json'); - if (!fs.existsSync(pkgPath)) { - console.error(`Invalid workspace: '${pkgPath}' does not exist`); + if (opts.cwd === undefined) { + console.error( + 'Invalid workspace: Please verify that you are inside of a Svelte project. You can also specify the working directory with `--cwd `' + ); process.exit(1); } + const adders = v.parse(AddersSchema, adderArgs); + const options = v.parse(OptionsSchema, opts); + const invalidAdders = adders.filter((a) => !adderIds.includes(a) && !aliases.includes(a)); if (invalidAdders.length > 0) { console.error(`Invalid adders specified: ${invalidAdders.join(', ')}`); From 2e89184f41e966c853062d818a1b0aff9a307a52 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:41:34 -0400 Subject: [PATCH 27/49] only format if there are files to format --- packages/cli/commands/add.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index cf6e10fe1..27cae4fb4 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -213,14 +213,14 @@ export async function runAddCommand(options: Options, adders: string[]): Promise // TODO: apply community adders // install dependencies - let depsInstalled; + let depsStatus; if (options.install) { - depsInstalled = await suggestInstallingDependencies(options.cwd); + depsStatus = await suggestInstallingDependencies(options.cwd); } // format modified/created files with prettier (if available) const workspace = createWorkspace(options.cwd); - if (depsInstalled === 'installed' && workspace.prettier) { + if (filesToFormat.length > 0 && depsStatus === 'installed' && workspace.prettier) { const formatSpinner = p.spinner(); formatSpinner.start('Formatting modified files'); try { @@ -320,7 +320,7 @@ async function processExternalAdder( config: ExternalAdderConfig, cwd: string ) { - if (!TESTING) p.log.message('Executing external command'); + if (!TESTING) p.log.message(`Executing external command ${pc.gray(`(${config.metadata.id})`)}`); try { await executeCli('npx', config.command.split(' '), cwd, { From 79469a004c3c6ab872ab57bef066d0e7a5dbffef Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:56:44 -0400 Subject: [PATCH 28/49] apply code review suggestions --- packages/adders/_config/index.ts | 2 +- packages/adders/_config/official.ts | 62 ++++++++++++++--------------- packages/cli/commands/add.ts | 4 +- packages/cli/commands/create.ts | 2 +- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/packages/adders/_config/index.ts b/packages/adders/_config/index.ts index ce8e749e5..68870628c 100644 --- a/packages/adders/_config/index.ts +++ b/packages/adders/_config/index.ts @@ -1,3 +1,3 @@ -export { adderIds, adderCategories, getAdderConfig, getAdderDetails } from './official'; +export { adderIds, adderCategories, getAdderDetails } from './official'; export { categories, type CategoryKeys, type CategoryInfo } from './categories'; export { communityAdders } from './community'; diff --git a/packages/adders/_config/official.ts b/packages/adders/_config/official.ts index 60562d089..4cf938bf4 100644 --- a/packages/adders/_config/official.ts +++ b/packages/adders/_config/official.ts @@ -12,42 +12,40 @@ import storybook from '../storybook'; import tailwindcss from '../tailwindcss'; import vitest from '../vitest'; -export const adderCategories: AdderCategories = { - codeQuality: ['prettier', 'eslint'], - testing: ['vitest', 'playwright'], - css: ['tailwindcss'], - db: ['drizzle'], - additional: ['storybook', 'mdsvex', 'routify'] +const categories = { + codeQuality: [prettier, eslint], + testing: [vitest, playwright], + css: [tailwindcss], + db: [drizzle], + additional: [storybook, mdsvex, routify] }; -export const adderIds: string[] = Object.values(adderCategories).flatMap((x) => x); +export const adderCategories: AdderCategories = getCategoriesById(); -export function getAdderDetails(name: string): AdderWithoutExplicitArgs { - switch (name) { - case 'drizzle': - return drizzle as AdderWithoutExplicitArgs; - case 'eslint': - return eslint; - case 'mdsvex': - return mdsvex; - case 'playwright': - return playwright; - case 'prettier': - return prettier; - case 'routify': - return routify; - case 'storybook': - return storybook; - case 'tailwindcss': - return tailwindcss as AdderWithoutExplicitArgs; - case 'vitest': - return vitest; - default: - throw new Error(`invalid adder name: ${name}`); +function getCategoriesById(): AdderCategories { + const adderCategories: any = {}; + for (const [key, adders] of Object.entries(categories)) { + adderCategories[key] = adders.map((a) => a.config.metadata.id); } + return adderCategories; } -export function getAdderConfig(name: string) { - const adder = getAdderDetails(name); - return adder.config; +export const adderIds: string[] = Object.values(adderCategories).flatMap((x) => x); + +const adderDetails = { + drizzle, + eslint, + mdsvex, + playwright, + prettier, + routify, + storybook, + tailwindcss, + vitest +}; + +export function getAdderDetails(name: string): AdderWithoutExplicitArgs { + const details = (adderDetails as any)[name]; + if (!details) throw new Error(`invalid adder name: ${name}`); + return details; } diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 27cae4fb4..893637b01 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -11,7 +11,7 @@ import { runCommand } from '../common.js'; import { adderCategories, categories, adderIds } from '@svelte-cli/adders'; -import { getAdderConfig, getAdderDetails } from '../../adders/index.js'; +import { getAdderDetails } from '../../adders/index.js'; import { createOrUpdateFiles, createWorkspace, @@ -86,7 +86,7 @@ export async function runAddCommand(options: Options, adders: string[]): Promise const category = adderCategories[id]; const categoryOptions = category .map((id) => { - const config = getAdderConfig(id); + const config = getAdderDetails(id).config; // we'll only display adders within their respective project types if (projectType === 'kit' && !config.metadata.environments.kit) return; if (projectType === 'svelte' && !config.metadata.environments.svelte) return; diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index ec2f3b065..e14ae4e68 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -47,7 +47,7 @@ async function createProject(cwd: string, options: Options) { directory: async () => { const relativePath = path.relative(process.cwd(), cwd) || './'; return p.text({ - message: 'Where should we create your project?', + message: 'Where should the project be created?', placeholder: ` (hit Enter to use '${relativePath}')`, defaultValue: relativePath }); From 5fc5a3d446686769d9ccc75093ebbfb9ab235d33 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Mon, 2 Sep 2024 17:08:30 +0200 Subject: [PATCH 29/49] re-enable linting rule and fix errors --- eslint.config.js | 4 ++-- packages/cli/commands/create.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index e165f9fde..f7ee066d1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,8 +17,8 @@ export default [ }, rules: { '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/no-unused-expressions': 'off' - // '@typescript-eslint/require-await': 'error' + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/require-await': 'error' } }, { diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index e14ae4e68..1425ab375 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -44,7 +44,7 @@ export const create = new Command('create') async function createProject(cwd: string, options: Options) { const { directory, template, language } = await p.group( { - directory: async () => { + directory: () => { const relativePath = path.relative(process.cwd(), cwd) || './'; return p.text({ message: 'Where should the project be created?', @@ -64,7 +64,7 @@ async function createProject(cwd: string, options: Options) { } } }, - template: async () => { + template: () => { if (options.template) return options.template; return p.select({ message: 'Which Svelte app template', @@ -72,7 +72,7 @@ async function createProject(cwd: string, options: Options) { options: templates.map((t) => ({ label: t.title, value: t.name, hint: t.description })) }); }, - language: async () => { + language: () => { if (options.checkTypes) return options.checkTypes; return p.select({ message: 'Add type checking with Typescript?', From e64a46bcefc117408b79c7e6409084db1a62f718 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:34:10 -0400 Subject: [PATCH 30/49] fix type --- packages/cli/commands/create.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 1425ab375..ac0e56164 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -65,7 +65,7 @@ async function createProject(cwd: string, options: Options) { } }, template: () => { - if (options.template) return options.template; + if (options.template) return Promise.resolve(options.template); return p.select({ message: 'Which Svelte app template', initialValue: 'demo', @@ -73,7 +73,7 @@ async function createProject(cwd: string, options: Options) { }); }, language: () => { - if (options.checkTypes) return options.checkTypes; + if (options.checkTypes) return Promise.resolve(options.checkTypes); return p.select({ message: 'Add type checking with Typescript?', initialValue: 'typescript', From fbe1cfbb02e4cfe6ad0a0606ceb9289c8a0e44ce Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:37:29 -0400 Subject: [PATCH 31/49] derive adder details from categories --- packages/adders/_config/official.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/adders/_config/official.ts b/packages/adders/_config/official.ts index 4cf938bf4..beaf278bc 100644 --- a/packages/adders/_config/official.ts +++ b/packages/adders/_config/official.ts @@ -32,20 +32,13 @@ function getCategoriesById(): AdderCategories { export const adderIds: string[] = Object.values(adderCategories).flatMap((x) => x); -const adderDetails = { - drizzle, - eslint, - mdsvex, - playwright, - prettier, - routify, - storybook, - tailwindcss, - vitest -}; +const adderDetails = Object.values(categories).flat(); export function getAdderDetails(name: string): AdderWithoutExplicitArgs { - const details = (adderDetails as any)[name]; - if (!details) throw new Error(`invalid adder name: ${name}`); - return details; + const details = adderDetails.find((a) => a.config.metadata.id === name); + if (!details) { + throw new Error(`invalid adder name: ${name}`); + } + + return details as AdderWithoutExplicitArgs; } From be078ad2162b7bea84b96ee4ac8db7b154575f50 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:43:46 -0400 Subject: [PATCH 32/49] dont prompt if a directory is specified --- packages/cli/commands/create.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index ac0e56164..3bb08a5a8 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -45,11 +45,13 @@ async function createProject(cwd: string, options: Options) { const { directory, template, language } = await p.group( { directory: () => { - const relativePath = path.relative(process.cwd(), cwd) || './'; + const relativePath = path.relative(process.cwd(), cwd); + if (relativePath) return Promise.resolve(relativePath); + const defaultPath = './'; return p.text({ message: 'Where should the project be created?', - placeholder: ` (hit Enter to use '${relativePath}')`, - defaultValue: relativePath + placeholder: ` (hit Enter to use '${defaultPath}')`, + defaultValue: defaultPath }); }, force: async ({ results: { directory } }) => { From 3dd2d0ab5d616cc82544fa4b61654bf0f0b15c24 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:44:53 -0400 Subject: [PATCH 33/49] wording --- packages/cli/commands/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 3bb08a5a8..831b2d914 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -49,7 +49,7 @@ async function createProject(cwd: string, options: Options) { if (relativePath) return Promise.resolve(relativePath); const defaultPath = './'; return p.text({ - message: 'Where should the project be created?', + message: 'Where would you like your project to be created?', placeholder: ` (hit Enter to use '${defaultPath}')`, defaultValue: defaultPath }); From a4ae9f5165504b0faa6924bfd3880f1d3e85a39c Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:55:59 -0400 Subject: [PATCH 34/49] prompt to install deps when `--no-adders` is set --- packages/cli/commands/create.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 831b2d914..ff3ebe835 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -9,7 +9,7 @@ import { type LanguageType, type TemplateType } from '@svelte-cli/create'; -import { runCommand } from '../common.js'; +import { runCommand, suggestInstallingDependencies } from '../common.js'; import { runAddCommand } from './add.js'; const langs = ['typescript', 'checkjs', 'none'] as const; @@ -23,6 +23,7 @@ const ProjectPathSchema = v.string(); const OptionsSchema = v.strictObject({ checkTypes: v.optional(v.picklist(langs)), adders: v.boolean(), + install: v.boolean(), template: v.optional(v.picklist(templateChoices)) }); type Options = v.InferOutput; @@ -33,6 +34,7 @@ export const create = new Command('create') .addOption(langOption) .addOption(templateOption) .option('--no-adders', 'skips interactive adder installer') + .option('--no-install', 'skips installing dependencies') .action((projectPath, opts) => { const cwd = v.parse(ProjectPathSchema, projectPath); const options = v.parse(OptionsSchema, opts); @@ -109,9 +111,19 @@ async function createProject(cwd: string, options: Options) { if (options.adders) { await runAddCommand( - { cwd: projectPath, default: false, install: true, preconditions: true, community: [] }, + { + cwd: projectPath, + default: false, + install: options.install, + preconditions: true, + community: [] + }, [] ); + } else { + // `runAddCommand` includes the installing dependencies prompt. if it's skipped, + // then we'll prompt to install dependencies here + await suggestInstallingDependencies(projectPath); } return { From 21d892fcb151bfb4e62bdf6c59ab1f3bada3ebdd Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:59:03 -0400 Subject: [PATCH 35/49] skip install if specified --- packages/cli/commands/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index ff3ebe835..befbf0b37 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -120,7 +120,7 @@ async function createProject(cwd: string, options: Options) { }, [] ); - } else { + } else if (options.install) { // `runAddCommand` includes the installing dependencies prompt. if it's skipped, // then we'll prompt to install dependencies here await suggestInstallingDependencies(projectPath); From bfe8153855ba322eff0145f2df80d0255cbd6c32 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:12:33 -0400 Subject: [PATCH 36/49] add `sv` to the root workspace --- package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/package.json b/package.json index a68dd23ca..f09402a0a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "rollup": "^4.20.0", "rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-preserve-shebangs": "^0.2.0", + "sv": "workspace:*", "typescript": "^5.5.4", "typescript-eslint": "^8.0.0", "unplugin-isolated-decl": "^0.4.7" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 107d9bf92..c79a62145 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: rollup-plugin-preserve-shebangs: specifier: ^0.2.0 version: 0.2.0(rollup@4.21.0) + sv: + specifier: workspace:* + version: link:packages/cli typescript: specifier: ^5.5.4 version: 5.5.4 From 13c0409010efaa3e2a2f1e657ae0aa58530d6127 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:44:06 -0400 Subject: [PATCH 37/49] initial adder option flags implementation --- packages/cli/commands/add.ts | 137 +++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 40 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 893637b01..2a5a7e9dd 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import * as v from 'valibot'; -import { Command } from 'commander'; +import { Command, Option } from 'commander'; import * as p from '@svelte-cli/clack-prompts'; import pc from 'picocolors'; import { @@ -27,17 +27,23 @@ import { } from '@svelte-cli/core'; const AddersSchema = v.array(v.string()); +const AdderOptionFlagsSchema = v.object({ + tailwindcss: v.optional(v.array(v.string())), + drizzle: v.optional(v.array(v.string())) +}); const OptionsSchema = v.strictObject({ cwd: v.string(), install: v.boolean(), preconditions: v.boolean(), default: v.boolean(), - community: AddersSchema + community: AddersSchema, + ...AdderOptionFlagsSchema.entries }); type Options = v.InferOutput; const adderDetails = adderIds.map((id) => getAdderDetails(id)); const aliases = adderDetails.map((c) => c.config.metadata.alias).filter((v) => v !== undefined); +const addersOptions = getAdderOptionFlags(); // infers the workspace cwd if a `package.json` resides in a parent directory const defaultPkgPath = findUp(process.cwd(), 'package.json'); @@ -46,7 +52,7 @@ const defaultCwd = defaultPkgPath ? path.dirname(defaultPkgPath) : undefined; export const add = new Command('add') .description('Applies specified adders into a project') .argument('[adder...]', 'adders to install') - .option('--cwd ', 'path to working directory', defaultCwd) + .option('-C, --cwd ', 'path to working directory', defaultCwd) .option('--no-install', 'skips installing dependencies') .option('--no-preconditions', 'skips validating preconditions') .option('--default', 'applies default adder options for unspecified options', false) @@ -69,14 +75,71 @@ export const add = new Command('add') process.exit(1); } - const dedupedIds = transformAliases(adders); + const selectedAdders = transformAliases(adders); runCommand(async () => { - await runAddCommand(options, dedupedIds); + await runAddCommand(options, selectedAdders); }); }); +// adds adder specific option flags to the `add` command +for (const option of addersOptions) { + add.addOption(option); +} + export async function runAddCommand(options: Options, adders: string[]): Promise { const selectedAdders = adders.map((id) => getAdderDetails(id)); + const official: AdderOption = {}; + const community: AdderOption = {}; + + // apply specified options from flags + for (const adderOption of addersOptions) { + const adderId = adderOption.attributeName() as keyof Options; + const specifiedOptions = options[adderId] as string[] | undefined; + if (!specifiedOptions) continue; + + const details = getAdderDetails(adderId); + if (!selectedAdders.includes(details)) { + selectedAdders.push(details); + } + + const optionEntries = Object.entries(details.config.options); + for (const specifiedOption of specifiedOptions) { + // figure out which option it belongs to + const optionEntry = optionEntries.find(([id, question]) => { + if (question.type === 'boolean') { + return id === specifiedOption || `no-${id}` === specifiedOption; + } + if (question.type === 'select') { + return question.options.some((o) => o.value === specifiedOption); + } + }); + if (!optionEntry) { + const choices = getOptionChoices(adderId).join(', '); + throw new Error( + `Operation failed.\n\nInvalid '--${adderId}' option: '${specifiedOption}'\nAvailable options: ${choices}` + ); + } + + official[adderId] ??= {}; + const [questionId, question] = optionEntry; + + // validate that there are no conflicts + let existingOption = official[adderId][questionId]; + if (existingOption !== undefined) { + if (typeof existingOption === 'boolean') { + // need to transform the boolean back to `no-{id}` or `{id}` + existingOption = existingOption ? questionId : `no-${questionId}`; + } + throw new Error( + `Conflicting '--${adderId}' option: '${specifiedOption}' conflicts with '${existingOption}'` + ); + } + + official[adderId][questionId] = + question.type === 'boolean' ? !specifiedOption.startsWith('no-') : specifiedOption; + } + } + // prompt which adders to apply if (selectedAdders.length === 0) { const adderOptions: Record> = {}; @@ -151,11 +214,6 @@ export async function runAddCommand(options: Options, adders: string[]): Promise } } - const official: AdderOption = {}; - const community = {}; - - // TODO: apply specified options from flags - // apply defaults to unspecified options if (options.default) { for (const adder of selectedAdders) { @@ -349,33 +407,32 @@ function transformAliases(ids: string[]): string[] { return Array.from(set); } -// other possible variations -// npx sv add drizzle tailwindcss --options postgresql,postgres.js,no-docker no-typography -// npx sv add drizzle=postgresql,postgres.js,no-docker tailwindcss=no-typography -// `sv add drizzle tailwindcss --drizzle db:postgresql client:postgres.js docker:false --tailwindcss typography:false` - -// TODO: PoC for `sv add --drizzle=postgresql,postgres.js,no-docker --tailwindcss=no-typography` -// const addersOptions: Option[] = []; -// for (const id of adderIds) { -// const details = getAdderDetails(id); -// if (Object.values(details.config.options).length === 0) continue; - -// const option = new Option(`--${id} `).argParser((value) => value.split(',')); -// const choices: string[] = []; -// for (const [key, question] of Object.entries(details.config.options)) { -// if (question.type === 'boolean') { -// const values = [key, `no-${key}`]; -// choices.push(...values); -// } -// if (question.type === 'select') { -// const values = question.options.map((o) => o.value); -// choices.push(...values); -// } -// } -// option.choices(choices); -// addersOptions.push(option); -// } - -// for (const option of addersOptions) { -// add.addOption(option); -// } +function getAdderOptionFlags(): Option[] { + const options: Option[] = []; + for (const id of adderIds) { + const details = getAdderDetails(id); + if (Object.values(details.config.options).length === 0) continue; + + const option = new Option(`--${id} `).argParser((value) => value.split(',')); + // .choices(getChoices(id)); + option.hideHelp(); + options.push(option); + } + return options; +} + +function getOptionChoices(adderId: string): string[] { + const details = getAdderDetails(adderId); + const choices: string[] = []; + for (const [key, question] of Object.entries(details.config.options)) { + if (question.type === 'boolean') { + const values = [key, `no-${key}`]; + choices.push(...values); + } + if (question.type === 'select') { + const values = question.options.map((o) => o.value); + choices.push(...values); + } + } + return choices; +} From 93c84b246214e6c03de84accae43461af540c41b Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:47:49 -0400 Subject: [PATCH 38/49] tweak error --- packages/cli/commands/add.ts | 2 +- packages/cli/common.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 2a5a7e9dd..3171aaad8 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -116,7 +116,7 @@ export async function runAddCommand(options: Options, adders: string[]): Promise if (!optionEntry) { const choices = getOptionChoices(adderId).join(', '); throw new Error( - `Operation failed.\n\nInvalid '--${adderId}' option: '${specifiedOption}'\nAvailable options: ${choices}` + `Invalid '--${adderId}' option: '${specifiedOption}'\nAvailable options: ${choices}` ); } diff --git a/packages/cli/common.ts b/packages/cli/common.ts index 61ad356d9..8eddd0ae6 100644 --- a/packages/cli/common.ts +++ b/packages/cli/common.ts @@ -14,8 +14,9 @@ export async function runCommand(action: MaybePromise) { await action(); p.outro("You're all set!"); } catch (e) { + p.cancel('Operation failed.'); if (e instanceof Error) { - p.cancel(e.message); + console.error(e.message); } } } From 5c21342a5bcdde535b51ba4c0b7a7c4e0fd50a67 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:49:47 -0400 Subject: [PATCH 39/49] fix order --- packages/adders/_config/categories.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/adders/_config/categories.ts b/packages/adders/_config/categories.ts index bab9c4a77..7e2bb8a74 100644 --- a/packages/adders/_config/categories.ts +++ b/packages/adders/_config/categories.ts @@ -26,14 +26,14 @@ export const categories: CategoryDetails = { name: 'CSS', description: 'Can be used to style your components' }, - additional: { - id: 'additional', - name: 'Additional functionality', - description: '' - }, db: { id: 'db', name: 'Database', description: '' + }, + additional: { + id: 'additional', + name: 'Additional functionality', + description: '' } }; From ec6828db098c325af5a8b24604bde2ff7778c029 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:05:47 -0400 Subject: [PATCH 40/49] validate workspace when `--cwd` is specified --- packages/cli/commands/add.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 3171aaad8..694b5bd01 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import * as v from 'valibot'; import { Command, Option } from 'commander'; @@ -64,6 +65,12 @@ export const add = new Command('add') 'Invalid workspace: Please verify that you are inside of a Svelte project. You can also specify the working directory with `--cwd `' ); process.exit(1); + } else if (!fs.existsSync(path.resolve(opts.cwd, 'package.json'))) { + // when `--cwd` is specified, we'll validate that it's a valid workspace + console.error( + `Invalid workspace: Path '${path.resolve(opts.cwd)}' is not a valid workspace.` + ); + process.exit(1); } const adders = v.parse(AddersSchema, adderArgs); From ebb4f8c57c897e7975a7514752cd50855253b2ad Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:24:56 -0400 Subject: [PATCH 41/49] adjust create template order --- packages/cli/commands/create.ts | 2 +- packages/create/index.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index befbf0b37..d2eba3202 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -72,7 +72,7 @@ async function createProject(cwd: string, options: Options) { if (options.template) return Promise.resolve(options.template); return p.select({ message: 'Which Svelte app template', - initialValue: 'demo', + initialValue: 'skeleton', options: templates.map((t) => ({ label: t.title, value: t.name, hint: t.description })) }); }, diff --git a/packages/create/index.ts b/packages/create/index.ts index 60bab9cc8..46210dfcd 100644 --- a/packages/create/index.ts +++ b/packages/create/index.ts @@ -2,9 +2,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { mkdirp, copy, dist } from './utils'; -export type TemplateType = 'demo' | 'skeleton' | 'skeletonlib'; +export type TemplateType = (typeof templateTypes)[number]; export type LanguageType = 'typescript' | 'checkjs' | 'none'; +const templateTypes = ['skeleton', 'skeletonlib', 'demo'] as const; + export type Options = { name: string; template: TemplateType; @@ -35,7 +37,7 @@ export function create(cwd: string, options: Options): void { } export type TemplateMetadata = { name: TemplateType; title: string; description: string }; -export const templates: TemplateMetadata[] = fs.readdirSync(dist('templates')).map((dir) => { +export const templates: TemplateMetadata[] = templateTypes.map((dir) => { const meta_file = dist(`templates/${dir}/meta.json`); const { title, description } = JSON.parse(fs.readFileSync(meta_file, 'utf8')); From 38a62ea99157464f619e5e7e102c369abb469a0f Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:15:56 -0400 Subject: [PATCH 42/49] add basic choices to help cmd --- packages/cli/commands/add.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts index 694b5bd01..6f5db8fe8 100644 --- a/packages/cli/commands/add.ts +++ b/packages/cli/commands/add.ts @@ -420,9 +420,10 @@ function getAdderOptionFlags(): Option[] { const details = getAdderDetails(id); if (Object.values(details.config.options).length === 0) continue; - const option = new Option(`--${id} `).argParser((value) => value.split(',')); - // .choices(getChoices(id)); - option.hideHelp(); + const choices = getOptionChoices(id).join(', '); + const option = new Option(`--${id} `, `(choices: ${choices})`).argParser((value) => + value.split(',') + ); options.push(option); } return options; From b26c388296b113815c3d65cdd301791e3b49722a Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:00:52 -0400 Subject: [PATCH 43/49] set initial value of the install prompt to the current pm --- packages/cli/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/common.ts b/packages/cli/common.ts index 8eddd0ae6..180f3ae44 100644 --- a/packages/cli/common.ts +++ b/packages/cli/common.ts @@ -79,7 +79,7 @@ export async function suggestInstallingDependencies(cwd: string): Promise<'insta const pm = await p.select({ message: 'Which package manager do you want to install dependencies with?', options, - initialValue: undefined + initialValue: process.env.npm_config_user_agent as Agent | undefined }); if (p.isCancel(pm)) { p.cancel('Operation cancelled.'); From 0df1af78a52926fb5b0bc774e2583d27bd980bf0 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:31:21 -0400 Subject: [PATCH 44/49] tweak --- packages/cli/common.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/cli/common.ts b/packages/cli/common.ts index 180f3ae44..566bb46ee 100644 --- a/packages/cli/common.ts +++ b/packages/cli/common.ts @@ -79,7 +79,7 @@ export async function suggestInstallingDependencies(cwd: string): Promise<'insta const pm = await p.select({ message: 'Which package manager do you want to install dependencies with?', options, - initialValue: process.env.npm_config_user_agent as Agent | undefined + initialValue: getUserAgent() }); if (p.isCancel(pm)) { p.cancel('Operation cancelled.'); @@ -104,6 +104,15 @@ export async function suggestInstallingDependencies(cwd: string): Promise<'insta return 'installed'; } +function getUserAgent(): Agent | undefined { + const userAgent = process.env.npm_config_user_agent; + if (!userAgent) return undefined; + const pmSpec = userAgent.split(' ')[0]; + const separatorPos = pmSpec.lastIndexOf('/'); + const name = pmSpec.substring(0, separatorPos); + return name as Agent; +} + async function installDependencies(command: string, args: string[], workingDirectory: string) { try { await executeCli(command, args, workingDirectory); From bc4dfdb53f268174f58df21a36df4b8bcaaacfe1 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:08:25 -0400 Subject: [PATCH 45/49] unnecessary --- packages/create/utils.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/create/utils.ts b/packages/create/utils.ts index 8021bd23a..bd5531a3a 100644 --- a/packages/create/utils.ts +++ b/packages/create/utils.ts @@ -44,21 +44,3 @@ export function dist(path: string): string { new URL(`./${!insideDistFolder ? 'dist/' : ''}${path}`, import.meta.url).href ); } - -export const package_manager: string = get_package_manager() || 'npm'; - -/** - * Supports npm, pnpm, Yarn, cnpm, bun and any other package manager that sets the - * npm_config_user_agent env variable. - * Thanks to https://github.com/zkochan/packages/tree/main/which-pm-runs for this code! - */ -function get_package_manager() { - if (!process.env.npm_config_user_agent) { - return undefined; - } - const user_agent = process.env.npm_config_user_agent; - const pm_spec = user_agent.split(' ')[0]; - const separator_pos = pm_spec.lastIndexOf('/'); - const name = pm_spec.substring(0, separator_pos); - return name === 'npminstall' ? 'cnpm' : name; -} From ccc8cc90d5f774a49c407eb0ea72ff0b95334847 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:09:21 -0400 Subject: [PATCH 46/49] print next steps for `create` --- packages/cli/commands/create.ts | 29 +++++++++++++++++++++++++++-- packages/cli/common.ts | 4 ++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index d2eba3202..8a2818b88 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -3,13 +3,14 @@ import path from 'node:path'; import * as v from 'valibot'; import { Command, Option } from 'commander'; import * as p from '@svelte-cli/clack-prompts'; +import pc from 'picocolors'; import { create as createKit, templates, type LanguageType, type TemplateType } from '@svelte-cli/create'; -import { runCommand, suggestInstallingDependencies } from '../common.js'; +import { packageManager, runCommand, suggestInstallingDependencies } from '../common.js'; import { runAddCommand } from './add.js'; const langs = ['typescript', 'checkjs', 'none'] as const; @@ -39,7 +40,31 @@ export const create = new Command('create') const cwd = v.parse(ProjectPathSchema, projectPath); const options = v.parse(OptionsSchema, opts); runCommand(async () => { - await createProject(cwd, options); + const { directory } = await createProject(cwd, options); + const highlight = (str: string) => pc.bold(pc.cyan(str)); + + let i = 1; + const initialSteps = []; + const relative = path.relative(process.cwd(), directory); + const pm = packageManager ?? 'npm'; + if (relative !== '') { + initialSteps.push(`${i++}: ${highlight(`cd ${relative}`)}`); + } + if (!packageManager) { + initialSteps.push(`${i++}: ${highlight(`${pm} install`)}`); + } + + const steps = [ + ...initialSteps, + `${i++}: ${highlight('git init && git add -A && git commit -m "Initial commit"')} (optional)`, + `${i++}: ${highlight(`${pm} run dev -- --open`)}`, + '', + `To close the dev server, hit ${highlight('Ctrl-C')}`, + '', + `Stuck? Visit us at ${pc.cyan('https://svelte.dev/chat')}` + ].map((msg) => pc.reset(msg)); + + p.note(steps.join('\n'), 'Project next steps'); }); }); diff --git a/packages/cli/common.ts b/packages/cli/common.ts index 566bb46ee..6c4fec642 100644 --- a/packages/cli/common.ts +++ b/packages/cli/common.ts @@ -8,6 +8,8 @@ import type { AdderWithoutExplicitArgs } from '@svelte-cli/core'; type MaybePromise = () => Promise | void; +export let packageManager: string | undefined = getUserAgent(); + export async function runCommand(action: MaybePromise) { try { p.intro(`Welcome to the Svelte CLI! ${pc.gray(`(v${pkg.version})`)}`); @@ -100,6 +102,8 @@ export async function suggestInstallingDependencies(cwd: string): Promise<'insta const [pm, install] = installCommand.split(' '); await installDependencies(pm, [install], cwd); + packageManager = pm; + loadingSpinner.stop('Successfully installed dependencies'); return 'installed'; } From 31aff01d180697e89529058c6cb9cfd472eecb45 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:15:12 -0400 Subject: [PATCH 47/49] i borked it - fixed --- packages/cli/commands/create.ts | 8 +++++++- packages/cli/common.ts | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 8a2818b88..f5cf51d41 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -10,7 +10,12 @@ import { type LanguageType, type TemplateType } from '@svelte-cli/create'; -import { packageManager, runCommand, suggestInstallingDependencies } from '../common.js'; +import { + getUserAgent, + packageManager, + runCommand, + suggestInstallingDependencies +} from '../common.js'; import { runAddCommand } from './add.js'; const langs = ['typescript', 'checkjs', 'none'] as const; @@ -51,6 +56,7 @@ export const create = new Command('create') initialSteps.push(`${i++}: ${highlight(`cd ${relative}`)}`); } if (!packageManager) { + const pm = getUserAgent() ?? 'npm'; initialSteps.push(`${i++}: ${highlight(`${pm} install`)}`); } diff --git a/packages/cli/common.ts b/packages/cli/common.ts index 6c4fec642..9aa868a99 100644 --- a/packages/cli/common.ts +++ b/packages/cli/common.ts @@ -8,7 +8,7 @@ import type { AdderWithoutExplicitArgs } from '@svelte-cli/core'; type MaybePromise = () => Promise | void; -export let packageManager: string | undefined = getUserAgent(); +export let packageManager: string | undefined; export async function runCommand(action: MaybePromise) { try { @@ -108,7 +108,7 @@ export async function suggestInstallingDependencies(cwd: string): Promise<'insta return 'installed'; } -function getUserAgent(): Agent | undefined { +export function getUserAgent(): Agent | undefined { const userAgent = process.env.npm_config_user_agent; if (!userAgent) return undefined; const pmSpec = userAgent.split(' ')[0]; From 7ef6540e50a41d837811f3316813792bc85542ba Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:16:35 -0400 Subject: [PATCH 48/49] ugh --- packages/cli/commands/create.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index f5cf51d41..c8d97af26 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -51,12 +51,11 @@ export const create = new Command('create') let i = 1; const initialSteps = []; const relative = path.relative(process.cwd(), directory); - const pm = packageManager ?? 'npm'; + const pm = getUserAgent() ?? 'npm'; if (relative !== '') { initialSteps.push(`${i++}: ${highlight(`cd ${relative}`)}`); } if (!packageManager) { - const pm = getUserAgent() ?? 'npm'; initialSteps.push(`${i++}: ${highlight(`${pm} install`)}`); } From e3950c5cd61f08802bf1f6905cd65b35c898da15 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:18:22 -0400 Subject: [PATCH 49/49] silly --- packages/cli/commands/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index c8d97af26..a7ebce1ea 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -51,7 +51,7 @@ export const create = new Command('create') let i = 1; const initialSteps = []; const relative = path.relative(process.cwd(), directory); - const pm = getUserAgent() ?? 'npm'; + const pm = packageManager ?? getUserAgent() ?? 'npm'; if (relative !== '') { initialSteps.push(`${i++}: ${highlight(`cd ${relative}`)}`); }