diff --git a/.changeset/bright-flies-argue.md b/.changeset/bright-flies-argue.md new file mode 100644 index 000000000..c66f83811 --- /dev/null +++ b/.changeset/bright-flies-argue.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +fix(drizzle): `--cwd` option in `add` command is now taken into account diff --git a/.changeset/great-mammals-buy.md b/.changeset/great-mammals-buy.md new file mode 100644 index 000000000..02a052f2d --- /dev/null +++ b/.changeset/great-mammals-buy.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +feat(drizzle): Docker Compose file is now stored in `compose.yaml` instead of `docker-compose.yml` diff --git a/packages/addons/_tests/drizzle/test.ts b/packages/addons/_tests/drizzle/test.ts index d4afdd0c0..8659dfac2 100644 --- a/packages/addons/_tests/drizzle/test.ts +++ b/packages/addons/_tests/drizzle/test.ts @@ -10,7 +10,8 @@ import drizzle from '../../drizzle/index.ts'; import { pageServer, pageComp } from './fixtures.ts'; // only linux is supported for running docker containers in github runners -const noDocker = process.env.CI && process.platform !== 'linux'; +const MUST_HAVE_DOCKER = process.env.CI && process.platform === 'linux'; +let dockerInstalled = false; const { test, testCases, prepareServer } = setupTest( { drizzle }, @@ -38,44 +39,67 @@ const { test, testCases, prepareServer } = setupTest( ); beforeAll(() => { - if (noDocker) return; + if (!MUST_HAVE_DOCKER) return; const cwd = path.dirname(fileURLToPath(import.meta.url)); - execSync('docker compose up --detach', { cwd, stdio: 'pipe' }); + + try { + execSync('docker --version', { cwd, stdio: 'pipe' }); + dockerInstalled = true; + } catch { + dockerInstalled = false; + } + + if (dockerInstalled) execSync('docker compose up --detach', { cwd, stdio: 'pipe' }); // cleans up the containers on interrupts (ctrl+c) process.addListener('SIGINT', () => { - execSync('docker compose down --volumes', { cwd, stdio: 'pipe' }); + if (dockerInstalled) execSync('docker compose down --volumes', { cwd, stdio: 'pipe' }); }); return () => { - execSync('docker compose down --volumes', { cwd, stdio: 'pipe' }); + if (dockerInstalled) execSync('docker compose down --volumes', { cwd, stdio: 'pipe' }); }; }); test.concurrent.for(testCases)( 'drizzle $kind.type $variant', async (testCase, { page, ...ctx }) => { - if (testCase.kind.options.drizzle.docker && noDocker) ctx.skip(); const cwd = ctx.cwd(testCase); - const ts = testCase.variant === 'kit-ts'; + const ts = testCase.variant.endsWith('ts'); const drizzleConfig = path.resolve(cwd, `drizzle.config.${ts ? 'ts' : 'js'}`); - const content = fs.readFileSync(drizzleConfig, 'utf8').replace(/strict: true[,\s]/, ''); - fs.writeFileSync(drizzleConfig, content, 'utf8'); + const content = fs.readFileSync(drizzleConfig, 'utf8'); + + expect(content.length, 'drizzle config should have content').toBeGreaterThan(0); + + if (MUST_HAVE_DOCKER) expect(dockerInstalled, 'docker must be installed').toBe(true); + + if (testCase.kind.options.drizzle.docker) { + const dockerCompose = path.resolve(cwd, 'compose.yaml'); + expect(fs.existsSync(dockerCompose), 'file should exist').toBe(true); + } + + const db_can_be_tested = + !testCase.kind.options.drizzle.docker || + (testCase.kind.options.drizzle.docker && dockerInstalled); + + if (db_can_be_tested) { + fs.writeFileSync(drizzleConfig, content.replace(/strict: true[,\s]/, ''), 'utf8'); - const routes = path.resolve(cwd, 'src', 'routes'); - const pagePath = path.resolve(routes, '+page.svelte'); - fs.writeFileSync(pagePath, pageComp, 'utf8'); + const routes = path.resolve(cwd, 'src', 'routes'); + const pagePath = path.resolve(routes, '+page.svelte'); + fs.writeFileSync(pagePath, pageComp, 'utf8'); - const pageServerPath = path.resolve(routes, `+page.server.${ts ? 'ts' : 'js'}`); - fs.writeFileSync(pageServerPath, pageServer, 'utf8'); + const pageServerPath = path.resolve(routes, `+page.server.${ts ? 'ts' : 'js'}`); + fs.writeFileSync(pageServerPath, pageServer, 'utf8'); - execSync('npm run db:push', { cwd, stdio: 'pipe' }); + execSync('npm run db:push', { cwd, stdio: 'pipe' }); - const { close } = await prepareServer({ cwd, page }); - // kill server process when we're done - ctx.onTestFinished(async () => await close()); + const { close } = await prepareServer({ cwd, page }); + // kill server process when we're done + ctx.onTestFinished(async () => await close()); - expect(page.locator('[data-testid]')).toBeTruthy(); + expect(page.locator('[data-testid]')).toBeTruthy(); + } } ); diff --git a/packages/addons/drizzle/index.ts b/packages/addons/drizzle/index.ts index 94cad464a..cb9a12be1 100644 --- a/packages/addons/drizzle/index.ts +++ b/packages/addons/drizzle/index.ts @@ -73,30 +73,28 @@ export default defineAddon({ shortDescription: 'database orm', homepage: 'https://orm.drizzle.team', options, - setup: ({ kit, unsupported, runsAfter, cwd, typescript }) => { + setup: ({ kit, unsupported, runsAfter }) => { runsAfter('prettier'); - const ext = typescript ? 'ts' : 'js'; - if (!kit) { - return unsupported('Requires SvelteKit'); - } + if (!kit) return unsupported('Requires SvelteKit'); + }, + run: ({ sv, typescript, options, kit, dependencyVersion, cwd, cancel }) => { + if (!kit) throw new Error('SvelteKit is required'); - const baseDBPath = path.resolve(kit.libDirectory, 'server', 'db'); + const ext = typescript ? 'ts' : 'js'; + const baseDBPath = path.resolve(cwd, kit.libDirectory, 'server', 'db'); const paths = { - 'drizzle config': path.relative(cwd, path.resolve(cwd, `drizzle.config.${ext}`)), - 'database schema': path.relative(cwd, path.resolve(baseDBPath, `schema.${ext}`)), - database: path.relative(cwd, path.resolve(baseDBPath, `index.${ext}`)) + 'drizzle config': path.resolve(cwd, `drizzle.config.${ext}`), + 'database schema': path.resolve(baseDBPath, `schema.${ext}`), + database: path.resolve(baseDBPath, `index.${ext}`) }; for (const [fileType, filePath] of Object.entries(paths)) { if (fs.existsSync(filePath)) { - unsupported(`Preexisting ${fileType} file at '${filePath}'`); + return cancel(`Preexisting ${fileType} file at '${filePath}'`); } } - }, - run: ({ sv, typescript, options, kit, dependencyVersion }) => { - const ext = typescript ? 'ts' : 'js'; - + console.log(`no preexisting files`); sv.devDependency('drizzle-orm', '^0.44.5'); sv.devDependency('drizzle-kit', '^0.31.4'); sv.devDependency('@types/node', getNodeTypesVersion()); @@ -123,7 +121,15 @@ export default defineAddon({ sv.file('.env.example', (content) => generateEnvFileContent(content, options)); if (options.docker && (options.mysql === 'mysql2' || options.postgresql === 'postgres.js')) { - sv.file('docker-compose.yml', (content) => { + const composeFileOptions = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yaml']; + let composeFile = ''; + for (const option of composeFileOptions) { + composeFile = option; + if (fs.existsSync(path.resolve(cwd, option))) break; + } + if (composeFile === '') throw new Error('unreachable state...'); + + sv.file(composeFile, (content) => { // if the file already exists, don't modify it // (in the future, we could add some tooling for modifying yaml) if (content.length > 0) return content; @@ -206,7 +212,7 @@ export default defineAddon({ }); } - sv.file(`drizzle.config.${ext}`, (content) => { + sv.file(paths['drizzle config'], (content) => { const { ast, generateCode } = parseScript(content); imports.addNamed(ast, { from: 'drizzle-kit', imports: { defineConfig: 'defineConfig' } }); @@ -234,7 +240,7 @@ export default defineAddon({ return generateCode(); }); - sv.file(`${kit?.libDirectory}/server/db/schema.${ext}`, (content) => { + sv.file(paths['database schema'], (content) => { const { ast, generateCode } = parseScript(content); let userSchemaExpression; @@ -286,7 +292,7 @@ export default defineAddon({ return generateCode(); }); - sv.file(`${kit?.libDirectory}/server/db/index.${ext}`, (content) => { + sv.file(paths['database'], (content) => { const { ast, generateCode } = parseScript(content); imports.addNamed(ast, { diff --git a/packages/cli/commands/add/index.ts b/packages/cli/commands/add/index.ts index 89d336098..cd9ffc6ee 100644 --- a/packages/cli/commands/add/index.ts +++ b/packages/cli/commands/add/index.ts @@ -205,7 +205,7 @@ export async function runAddCommand( options: Options, selectedAddonIds: string[] ): Promise<{ nextSteps: string[]; packageManager?: AgentName | null }> { - const selectedAddons: SelectedAddon[] = selectedAddonIds.map((id) => ({ + let selectedAddons: SelectedAddon[] = selectedAddonIds.map((id) => ({ type: 'official', addon: getAddonDetails(id) })); @@ -542,14 +542,30 @@ export async function runAddCommand( const details = officialDetails.concat(commDetails); const addonMap: AddonMap = Object.assign({}, ...details.map((a) => ({ [a.id]: a }))); - const { filesToFormat, pnpmBuildDependencies: addonPnpmBuildDependencies } = await applyAddons({ + const { filesToFormat, pnpmBuildDependencies, status } = await applyAddons({ workspace, addonSetupResults, addons: addonMap, options: official }); - p.log.success('Successfully setup add-ons'); + const addonSuccess: string[] = []; + for (const [addonId, info] of Object.entries(status)) { + if (info === 'success') addonSuccess.push(addonId); + else { + p.log.warn(`Canceled ${addonId}: ${info.join(', ')}`); + selectedAddons = selectedAddons.filter((a) => a.addon.id !== addonId); + } + } + + if (addonSuccess.length === 0) { + p.cancel('All selected add-ons were canceled.'); + process.exit(1); + } else if (addonSuccess.length === Object.entries(status).length) { + p.log.success('Successfully setup add-ons'); + } else { + p.log.success(`Successfully setup: ${addonSuccess.join(', ')}`); + } // prompt for package manager and install dependencies let packageManager: PackageManager | undefined; @@ -562,7 +578,7 @@ export async function runAddCommand( await addPnpmBuildDependencies(workspace.cwd, packageManager, [ 'esbuild', - ...addonPnpmBuildDependencies + ...pnpmBuildDependencies ]); await installDependencies(packageManager, options.cwd); diff --git a/packages/cli/lib/install.ts b/packages/cli/lib/install.ts index 151e24a42..aafd6446a 100644 --- a/packages/cli/lib/install.ts +++ b/packages/cli/lib/install.ts @@ -54,9 +54,11 @@ export async function applyAddons({ }: ApplyAddonOptions): Promise<{ filesToFormat: string[]; pnpmBuildDependencies: string[]; + status: Record; }> { const filesToFormat = new Set(); const allPnpmBuildDependencies: string[] = []; + const status: Record = {}; const mapped = Object.entries(addons).map(([, addon]) => addon); const ordered = orderAddons(mapped, addonSetupResults); @@ -64,7 +66,7 @@ export async function applyAddons({ for (const addon of ordered) { workspace = await createWorkspace({ ...workspace, options: options[addon.id] }); - const { files, pnpmBuildDependencies } = await runAddon({ + const { files, pnpmBuildDependencies, cancels } = await runAddon({ workspace, addon, multiple: ordered.length > 1 @@ -72,11 +74,17 @@ export async function applyAddons({ files.forEach((f) => filesToFormat.add(f)); pnpmBuildDependencies.forEach((s) => allPnpmBuildDependencies.push(s)); + if (cancels.length === 0) { + status[addon.id] = 'success'; + } else { + status[addon.id] = cancels; + } } return { filesToFormat: Array.from(filesToFormat), - pnpmBuildDependencies: allPnpmBuildDependencies + pnpmBuildDependencies: allPnpmBuildDependencies, + status }; } @@ -177,14 +185,25 @@ async function runAddon({ addon, multiple, workspace }: RunAddon) { pnpmBuildDependencies.push(pkg); } }; - await addon.run({ ...workspace, sv }); - const pkgPath = installPackages(dependencies, workspace); - files.add(pkgPath); + const cancels: string[] = []; + await addon.run({ + cancel: (reason) => { + cancels.push(reason); + }, + ...workspace, + sv + }); + + if (cancels.length === 0) { + const pkgPath = installPackages(dependencies, workspace); + files.add(pkgPath); + } return { files: Array.from(files), - pnpmBuildDependencies + pnpmBuildDependencies, + cancels }; } diff --git a/packages/core/addon/config.ts b/packages/core/addon/config.ts index 656b39b4a..9d846d260 100644 --- a/packages/core/addon/config.ts +++ b/packages/core/addon/config.ts @@ -40,7 +40,9 @@ export type Addon = { runsAfter: (addonName: string) => void; } ) => MaybePromise; - run: (workspace: Workspace & { sv: SvApi }) => MaybePromise; + run: ( + workspace: Workspace & { sv: SvApi; cancel: (reason: string) => void } + ) => MaybePromise; nextSteps?: ( data: { highlighter: Highlighter;