diff --git a/package.json b/package.json index 5fc799a4e..f09402a0a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "rollup": "^4.20.0", "rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-preserve-shebangs": "^0.2.0", - "typescript": "^5.3.3", + "sv": "workspace:*", + "typescript": "^5.5.4", "typescript-eslint": "^8.0.0", "unplugin-isolated-decl": "^0.4.7" }, 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..7e2bb8a74 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; @@ -25,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: '' } }; 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..68870628c --- /dev/null +++ b/packages/adders/_config/index.ts @@ -0,0 +1,3 @@ +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 new file mode 100644 index 000000000..beaf278bc --- /dev/null +++ b/packages/adders/_config/official.ts @@ -0,0 +1,44 @@ +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'; + +const categories = { + codeQuality: [prettier, eslint], + testing: [vitest, playwright], + css: [tailwindcss], + db: [drizzle], + additional: [storybook, mdsvex, routify] +}; + +export const adderCategories: AdderCategories = getCategoriesById(); + +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 const adderIds: string[] = Object.values(adderCategories).flatMap((x) => x); + +const adderDetails = Object.values(categories).flat(); + +export function getAdderDetails(name: string): AdderWithoutExplicitArgs { + const details = adderDetails.find((a) => a.config.metadata.id === name); + if (!details) { + throw new Error(`invalid adder name: ${name}`); + } + + return details as AdderWithoutExplicitArgs; +} 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/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/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/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/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/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/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/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/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/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/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/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/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/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" 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'; diff --git a/packages/cli/commands/add.ts b/packages/cli/commands/add.ts new file mode 100644 index 000000000..6f5db8fe8 --- /dev/null +++ b/packages/cli/commands/add.ts @@ -0,0 +1,446 @@ +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 pc from 'picocolors'; +import { + executeCli, + formatFiles, + getGlobalPreconditions, + suggestInstallingDependencies, + runCommand +} from '../common.js'; +import { adderCategories, categories, adderIds } from '@svelte-cli/adders'; +import { getAdderDetails } from '../../adders/index.js'; +import { + createOrUpdateFiles, + createWorkspace, + findUp, + installPackages, + TESTING +} 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 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, + ...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'); +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('-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) + .option('--community ', 'community adders to install', []) + .action((adderArgs, opts) => { + // validate workspace + 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); + } 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); + 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 selectedAdders = transformAliases(adders); + runCommand(async () => { + 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( + `Invalid '--${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> = {}; + 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 = 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; + + 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) { + 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 }); + } + + 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?', + initialValue: false + }); + if (p.isCancel(force) || !force) { + p.cancel('Operation cancelled.'); + process.exit(1); + } + } + } + + // 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) { + filesToFormat = await installAdders({ cwd: options.cwd, official, community }); + p.log.success('Successfully installed adders'); + } + + // TODO: apply community adders + + // install dependencies + let depsStatus; + if (options.install) { + depsStatus = await suggestInstallingDependencies(options.cwd); + } + + // format modified/created files with prettier (if available) + const workspace = createWorkspace(options.cwd); + if (filesToFormat.length > 0 && depsStatus === '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 (!TESTING) p.log.message(`Executing external command ${pc.gray(`(${config.metadata.id})`)}`); + + try { + await executeCli('npx', config.command.split(' '), cwd, { + env: Object.assign(process.env, config.environment ?? {}), + stdio: TESTING ? '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); +} + +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 choices = getOptionChoices(id).join(', '); + const option = new Option(`--${id} `, `(choices: ${choices})`).argParser((value) => + value.split(',') + ); + 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; +} diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts new file mode 100644 index 000000000..a7ebce1ea --- /dev/null +++ b/packages/cli/commands/create.ts @@ -0,0 +1,162 @@ +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 pc from 'picocolors'; +import { + create as createKit, + templates, + type LanguageType, + type TemplateType +} from '@svelte-cli/create'; +import { + getUserAgent, + packageManager, + runCommand, + suggestInstallingDependencies +} from '../common.js'; +import { runAddCommand } from './add.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 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; + +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') + .option('--no-install', 'skips installing dependencies') + .action((projectPath, opts) => { + const cwd = v.parse(ProjectPathSchema, projectPath); + const options = v.parse(OptionsSchema, opts); + runCommand(async () => { + 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 ?? getUserAgent() ?? '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'); + }); + }); + +async function createProject(cwd: string, options: Options) { + const { directory, template, language } = await p.group( + { + directory: () => { + const relativePath = path.relative(process.cwd(), cwd); + if (relativePath) return Promise.resolve(relativePath); + const defaultPath = './'; + return p.text({ + message: 'Where would you like your project to be created?', + placeholder: ` (hit Enter to use '${defaultPath}')`, + defaultValue: defaultPath + }); + }, + 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: () => { + if (options.template) return Promise.resolve(options.template); + return p.select({ + message: 'Which Svelte app template', + initialValue: 'skeleton', + options: templates.map((t) => ({ label: t.title, value: t.name, hint: t.description })) + }); + }, + language: () => { + if (options.checkTypes) return Promise.resolve(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('Operation cancelled.'); + process.exit(0); + } + } + ); + + const initSpinner = p.spinner(); + initSpinner.start('Initializing template'); + + const projectPath = path.resolve(directory); + createKit(projectPath, { + name: path.basename(projectPath), + template, + types: language + }); + + initSpinner.stop('Project created'); + + if (options.adders) { + await runAddCommand( + { + cwd: projectPath, + default: false, + install: options.install, + preconditions: true, + community: [] + }, + [] + ); + } 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); + } + + return { + directory: projectPath + }; +} diff --git a/packages/cli/common.ts b/packages/cli/common.ts new file mode 100644 index 000000000..9aa868a99 --- /dev/null +++ b/packages/cli/common.ts @@ -0,0 +1,190 @@ +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 let packageManager: string | undefined; + +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) { + p.cancel('Operation failed.'); + if (e instanceof Error) { + console.error(e.message); + } + } +} + +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: getUserAgent() + }); + 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); + + packageManager = pm; + + loadingSpinner.stop('Successfully installed dependencies'); + return 'installed'; +} + +export 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); + } 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(' / ') }; + } + } + ] + }; +} 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", 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 - } -} 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/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/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/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/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'; 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..ba5793e5e 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.packageJson); + 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.packageJson, serializeJson(originalText, data)); + return commonFilePaths.packageJson; +} + +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,35 +122,28 @@ 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' -}; + 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; @@ -80,6 +151,4 @@ export function findUp(searchPath: string, fileName: string, maxDepth?: number): depth++; } - - return false; } diff --git a/packages/core/utils/workspace.ts b/packages/core/files/workspace.ts similarity index 50% rename from packages/core/utils/workspace.ts rename to packages/core/files/workspace.ts index e0e79f41f..0e9ae8f17 100644 --- a/packages/core/utils/workspace.ts +++ b/packages/core/files/workspace.ts @@ -1,112 +1,62 @@ 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, commonFilePaths.viteConfigTS)); - 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, commonFilePaths.tsconfig)); } else { - usesTypescript ||= findUp(workingDirectory, tsConfigFileName); + usesTypescript ||= findUp(cwd, commonFilePaths.tsconfig) !== undefined; } 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; - const configText = readFile(workspace, commonFilePaths.svelteConfigFilePath); +function parseKitOptions(workspace: WorkspaceWithoutExplicitArgs) { + const configText = readFile(workspace, commonFilePaths.svelteConfig); const ast = parseScript(configText); const editor = getJsAstEditor(ast); @@ -146,10 +96,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..5dc258273 100644 --- a/packages/core/internal.ts +++ b/packages/core/internal.ts @@ -1,35 +1,5 @@ -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, findUp } from './files/utils'; +export { createOrUpdateFiles } from './files/processors'; +export { createWorkspace, type Workspace } from './files/workspace'; +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/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/create-project.ts b/packages/core/utils/create-project.ts deleted file mode 100644 index b510a1161..000000000 --- a/packages/core/utils/create-project.ts +++ /dev/null @@ -1,108 +0,0 @@ -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'; - -export async function detectSvelteDirectory(directoryPath: string): Promise { - if (!directoryPath) return null; - - const packageJsonPath = path.join(directoryPath, commonFilePaths.packageJsonFilePath); - const parentDirectoryPath = path.normalize(path.join(directoryPath, '..')); - const isRoot = parentDirectoryPath == directoryPath; - - if (!isRoot && !fs.existsSync(directoryPath)) { - return await detectSvelteDirectory(parentDirectoryPath); - } - - if (!isRoot && !fs.existsSync(packageJsonPath)) { - return await detectSvelteDirectory(parentDirectoryPath); - } - - if (isRoot && !fs.existsSync(packageJsonPath)) { - return null; - } - - const emptyWorkspace = createEmptyWorkspace(); - emptyWorkspace.cwd = directoryPath; - const { data: packageJson } = getPackageJson(emptyWorkspace); - - if (packageJson.devDependencies && 'svelte' in packageJson.devDependencies) { - return directoryPath; - } else if (!isRoot) { - return await 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) - }; -} 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; -} diff --git a/packages/create/index.ts b/packages/create/index.ts index 2e880991d..46210dfcd 100644 --- a/packages/create/index.ts +++ b/packages/create/index.ts @@ -2,8 +2,10 @@ import fs from 'node:fs'; 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 TemplateType = (typeof templateTypes)[number]; +export type LanguageType = 'typescript' | 'checkjs' | 'none'; + +const templateTypes = ['skeleton', 'skeletonlib', 'demo'] as const; export type Options = { name: string; @@ -16,7 +18,7 @@ export type File = { contents: string; }; -export type Condition = Exclude; +export type Condition = Exclude; export type Common = { files: Array<{ @@ -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')); @@ -93,7 +95,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/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/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') ); } } 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' }); 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 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; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7289a3e1..c79a62145 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,8 +53,11 @@ 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.3.3 + specifier: ^5.5.4 version: 5.5.4 typescript-eslint: specifier: ^8.0.0 @@ -151,11 +154,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 +185,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 @@ -2043,6 +2050,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} @@ -4006,6 +4021,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..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 */ @@ -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) );