diff --git a/src/build/mod.ts b/src/build/mod.ts index 4ebd3638..40148600 100644 --- a/src/build/mod.ts +++ b/src/build/mod.ts @@ -25,6 +25,7 @@ import { migrateDev } from "../migrate/dev.ts"; import { compileModuleTypeHelper } from "./gen.ts"; import { migrateDeploy } from "../migrate/deploy.ts"; import { ensurePostgresRunning } from "../utils/postgres_daemon.ts"; +import { generateClient } from "../migrate/generate.ts"; /** * Which format to use for building. @@ -257,12 +258,18 @@ async function buildSteps( // Do not alter migrations, only deploy them await migrateDeploy(project, [module]); } + + // Generate client + await generateClient(project, [module]); }, }); // Run one migration at a time since Prisma is interactive await waitForBuildPromises(buildState); } + + // Wait for promise since we can't run multiple `migrateDev` commands at once + await waitForBuildPromises(buildState); } buildStep(buildState, { diff --git a/src/cli/commands/clean.ts b/src/cli/commands/clean.ts new file mode 100644 index 00000000..57b1269c --- /dev/null +++ b/src/cli/commands/clean.ts @@ -0,0 +1,12 @@ +import { Command } from "../deps.ts"; +import { GlobalOpts, initProject } from "../common.ts"; +import { cleanProject } from "../../project/project.ts"; + +export const cleanCommand = new Command() + .description("Removes all build artifacts") + .action( + async (opts) => { + const project = await initProject(opts); + await cleanProject(project); + }, + ); diff --git a/src/cli/main.ts b/src/cli/main.ts index 80c0f368..db8bb158 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -13,6 +13,7 @@ import { createCommand } from "./commands/create.ts"; import { lintCommand } from "./commands/lint.ts"; import { formatCommand } from "./commands/format.ts"; import { initCommand } from "./commands/init.ts"; +import { cleanCommand } from "./commands/clean.ts"; const command = new Command(); command.action(() => command.showHelp()) @@ -21,11 +22,12 @@ command.action(() => command.showHelp()) .command("create", createCommand) .command("start", startCommand) .command("test", testCommand) - .command("db", dbCommand) + .command("database, db", dbCommand) .command("sdk", sdkCommand) .command("format, fmt", formatCommand) .command("lint", lintCommand) .command("build", buildCommand) + .command("clean", cleanCommand) .command("help", new HelpCommand().global()) .command("completions", new CompletionsCommand()) .error((error, cmd) => { diff --git a/src/migrate/deploy.ts b/src/migrate/deploy.ts index 8f2719ea..5364b32f 100644 --- a/src/migrate/deploy.ts +++ b/src/migrate/deploy.ts @@ -1,3 +1,5 @@ +// Deploys SQL migrations. See `dev.ts` to generate migrations. +// // Wrapper around `prisma migrate deploy` import { Module, Project } from "../project/mod.ts"; diff --git a/src/migrate/dev.ts b/src/migrate/dev.ts index 84bbcb04..f7ef1cd7 100644 --- a/src/migrate/dev.ts +++ b/src/migrate/dev.ts @@ -1,9 +1,8 @@ -// Generates SQL migrations & generates client library. +// Generates & deploys SQL migrations. See `deploy.ts` to only deploy migrations. // // Wrapper around `prisma migrate dev` -import { copy, exists, resolve } from "../deps.ts"; -import { buildPrismaPackage } from "./build_prisma_esm.ts"; +import { assert, copy, exists, resolve } from "../deps.ts"; import { Module, Project } from "../project/mod.ts"; import { forEachPrismaSchema } from "./mod.ts"; @@ -16,10 +15,12 @@ export async function migrateDev( modules: Module[], opts: MigrateDevOpts, ) { + assert(modules.every(m => ("local" in m.registry.config)), "Only local modules can run migrateDev because it generates migration files"); + await forEachPrismaSchema( project, modules, - async ({ databaseUrl, module, tempDir, generatedClientDir }) => { + async ({ databaseUrl, module, tempDir }) => { // Generate migrations & client const status = await new Deno.Command("deno", { args: [ @@ -28,6 +29,7 @@ export async function migrateDev( "npm:prisma@5.9.1", "migrate", "dev", + "--skip-generate", ...(opts.createOnly ? ["--create-only"] : []), ], cwd: tempDir, @@ -36,149 +38,21 @@ export async function migrateDev( stderr: "inherit", env: { DATABASE_URL: databaseUrl, - PRISMA_CLIENT_FORCE_WASM: "true", }, }).output(); if (!status.success) { throw new Error("Failed to generate migrations"); } - if (!opts.createOnly) { - // Specify the path to the library & binary types - await (async () => { - for ( - const filename of [ - "index.d.ts", - "default.d.ts", - "wasm.d.ts", - "edge.d.ts", - ] - ) { - const filePath = resolve(generatedClientDir, filename); - let content = await Deno.readTextFile(filePath); - const replaceLineA = - `import * as runtime from './runtime/library.js'`; - const replaceLineB = - `import * as runtime from './runtime/binary.js'`; - content = content - .replace( - replaceLineA, - `// @deno-types="./runtime/library.d.ts"\n${replaceLineA}`, - ) - .replace( - replaceLineB, - `// @deno-types="./runtime/binary.d.ts"\n${replaceLineB}`, - ) - .replace(/from '.\/default'/g, `from './default.d.ts'`); - await Deno.writeTextFile(filePath, content); - } - })(); - - // Compile the ESM library - buildPrismaPackage( - generatedClientDir, - resolve(generatedClientDir, "esm.js"), - ); - } - // Copy back migrations dir + // + // Copy for both `path` (that we'll use later in this script) and + // `sourcePath` (which is the original module's source) const tempMigrationsDir = resolve(tempDir, "migrations"); - const migrationsDir = resolve(module.path, "db", "migrations"); if (await exists(tempMigrationsDir)) { - await copy(tempMigrationsDir, migrationsDir, { overwrite: true }); + await copy(tempMigrationsDir, resolve(module.path, "db", "migrations"), { overwrite: true }); + await copy(tempMigrationsDir, resolve(module.sourcePath, "db", "migrations"), { overwrite: true }); } }, ); } - -// export async function __TEMP__migrateDev( -// project: Project, -// module: Module, -// opts: MigrateDevOpts, -// ) { -// const databaseUrl = "postgres://username:password@localhost:5432/database"; -// // Setup database -// const defaultDatabaseUrl = Deno.env.get("DATABASE_URL") ?? -// "postgres://postgres:password@localhost:5432/postgres"; - -// // Create dirs -// const tempDir = await Deno.makeTempDir(); -// const dbDir = resolve(module.path, "db"); -// const generatedClientDir = resolve( -// module.path, -// "_gen", -// "prisma", -// ); - -// // Copy db -// await copy(dbDir, tempDir, { overwrite: true }); - -// // Generate migrations & client -// console.log("Generating migrations"); -// const status = await new Deno.Command("deno", { -// args: [ -// "run", -// "-A", -// "npm:prisma@5.9.1", -// "migrate", -// "dev", -// ...(opts.createOnly ? ["--create-only"] : []), -// ], -// cwd: tempDir, -// stdin: "inherit", -// stdout: "inherit", -// stderr: "inherit", -// env: { -// DATABASE_URL: databaseUrl, -// PRISMA_CLIENT_FORCE_WASM: "true", -// }, -// }).output(); -// if (!status.success) { -// throw new Error("Failed to generate migrations"); -// } - -// if (!opts.createOnly) { -// // Specify the path to the library & binary types -// await (async () => { -// for ( -// const filename of [ -// "index.d.ts", -// "default.d.ts", -// "wasm.d.ts", -// "edge.d.ts", -// ] -// ) { -// const filePath = resolve(generatedClientDir, filename); -// let content = await Deno.readTextFile(filePath); -// const replaceLineA = `import * as runtime from './runtime/library.js'`; -// const replaceLineB = `import * as runtime from './runtime/binary.js'`; -// content = content -// .replace( -// replaceLineA, -// `// @deno-types="./runtime/library.d.ts"\n${replaceLineA}`, -// ) -// .replace( -// replaceLineB, -// `// @deno-types="./runtime/binary.d.ts"\n${replaceLineB}`, -// ) -// .replace(/from '.\/default'/g, `from './default.d.ts'`); -// await Deno.writeTextFile(filePath, content); -// } -// })(); - -// // Compile the ESM library -// console.log("Compiling ESM library"); -// buildPrismaPackage( -// generatedClientDir, -// resolve(generatedClientDir, "esm.js"), -// ); -// } - -// // Copy back migrations dir -// console.log("Copying migrations back"); -// const tempMigrationsDir = resolve(tempDir, "migrations"); -// const migrationsDir = resolve(module.path, "db", "migrations"); -// if (await exists(tempMigrationsDir)) { -// await copy(tempMigrationsDir, migrationsDir, { overwrite: true }); -// } -// } diff --git a/src/migrate/generate.ts b/src/migrate/generate.ts new file mode 100644 index 00000000..9269b473 --- /dev/null +++ b/src/migrate/generate.ts @@ -0,0 +1,72 @@ +// Generates Prisma client libraries. +// +// Wrapper around `prisma generate` + +import { resolve } from "../deps.ts"; +import { buildPrismaPackage } from "./build_prisma_esm.ts"; +import { Module, Project } from "../project/mod.ts"; +import { forEachPrismaSchema } from "./mod.ts"; + +export async function generateClient( + project: Project, + modules: Module[], +) { + await forEachPrismaSchema( + project, + modules, + async ({ databaseUrl, tempDir, generatedClientDir }) => { + // Generate migrations & client + const status = await new Deno.Command("deno", { + args: [ + "run", + "-A", + "npm:prisma@5.9.1", + "generate", + ], + cwd: tempDir, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + env: { + DATABASE_URL: databaseUrl, + PRISMA_CLIENT_FORCE_WASM: "true", + }, + }).output(); + if (!status.success) { + throw new Error("Failed to generate migrations"); + } + + // Specify the path to the library & binary types + for ( + const filename of [ + "index.d.ts", + "default.d.ts", + "wasm.d.ts", + "edge.d.ts", + ] + ) { + const filePath = resolve(generatedClientDir, filename); + let content = await Deno.readTextFile(filePath); + const replaceLineA = `import * as runtime from './runtime/library.js'`; + const replaceLineB = `import * as runtime from './runtime/binary.js'`; + content = content + .replace( + replaceLineA, + `// @deno-types="./runtime/library.d.ts"\n${replaceLineA}`, + ) + .replace( + replaceLineB, + `// @deno-types="./runtime/binary.d.ts"\n${replaceLineB}`, + ) + .replace(/from '.\/default'/g, `from './default.d.ts'`); + await Deno.writeTextFile(filePath, content); + } + + // Compile the ESM library + await buildPrismaPackage( + generatedClientDir, + resolve(generatedClientDir, "esm.js"), + ); + }, + ); +} diff --git a/src/project/module.ts b/src/project/module.ts index f7d8884a..2995465e 100644 --- a/src/project/module.ts +++ b/src/project/module.ts @@ -9,7 +9,21 @@ import { validateIdentifier } from "../types/identifiers/mod.ts"; import { IdentType } from "../types/identifiers/defs.ts"; export interface Module { + /** + * The path to the module in the project's _gen directory. + * + * This path can be modified and will be discarded on the next codegen. + */ path: string; + + /** + * The path to the module's source code. + * + * This path almost never be modified (including _gen), except for + * exclusions where auto-generating code (e.g. prisma migrate dev). + */ + sourcePath: string; + name: string; config: ModuleConfig; registry: Registry, @@ -23,6 +37,7 @@ export interface ModuleDatabase { export async function loadModule( modulePath: string, + sourcePath: string, name: string, registry: Registry, ): Promise { @@ -95,6 +110,7 @@ export async function loadModule( return { path: modulePath, + sourcePath, name, config, registry, diff --git a/src/project/project.ts b/src/project/project.ts index 911941df..275f26c5 100644 --- a/src/project/project.ts +++ b/src/project/project.ts @@ -62,13 +62,13 @@ export async function loadProject(opts: LoadProjectOpts): Promise { // Load modules const modules = new Map(); for (const projectModuleName in projectConfig.modules) { - const { path, registry } = await fetchAndResolveModule( + const { genPath, sourcePath, registry } = await fetchAndResolveModule( projectRoot, projectConfig, registries, projectModuleName, ); - const module = await loadModule(path, projectModuleName, registry); + const module = await loadModule(genPath, sourcePath, projectModuleName, registry); modules.set(projectModuleName, module); } @@ -117,6 +117,23 @@ export async function loadProject(opts: LoadProjectOpts): Promise { return { path: projectRoot, config: projectConfig, registries, modules }; } +interface FetchAndResolveModuleOutput { + /** + * Path the module was copied to in _gen. + */ + genPath: string; + + /** + * Path to the original module source code. + */ + sourcePath: string; + + /** + * Registry the module was fetched from. + */ + registry: Registry; +} + /** * Clones a registry to a local machine and resovles the path to the module. */ @@ -125,7 +142,7 @@ async function fetchAndResolveModule( projectConfig: ProjectConfig, registries: Map, moduleName: string, -): Promise<{ path: string; registry: Registry }> { +): Promise { const moduleNameIssue = validateIdentifier(moduleName, IdentType.ModuleScripts); if (moduleNameIssue) { throw new Error(moduleNameIssue.toString("module")); @@ -168,13 +185,12 @@ async function fetchAndResolveModule( projectRoot, "_gen", "modules", - registryName, moduleName, ); await Deno.mkdir(dirname(dstPath), { recursive: true }); await copy(modulePath, dstPath, { overwrite: true }); - return { path: dstPath, registry }; + return { genPath: dstPath, sourcePath: modulePath, registry }; } function registryNameForModule(module: ProjectModuleConfig): string { @@ -224,3 +240,7 @@ export async function listSourceFiles( } return files; } + +export async function cleanProject(project: Project) { + await Deno.remove(resolve(project.path, "_gen"), { recursive: true }); +} \ No newline at end of file diff --git a/tests/test_project/modules/foo/db/migrations/20240306224507_init/migration.sql b/tests/test_project/modules/foo/db/migrations/20240306224507_init/migration.sql new file mode 100644 index 00000000..00a1c2a3 --- /dev/null +++ b/tests/test_project/modules/foo/db/migrations/20240306224507_init/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - You are about to drop the `Identity` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `IdentityGuest` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Identity" DROP CONSTRAINT "Identity_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "IdentityGuest" DROP CONSTRAINT "IdentityGuest_identityId_fkey"; + +-- DropTable +DROP TABLE "Identity"; + +-- DropTable +DROP TABLE "IdentityGuest"; + +-- DropTable +DROP TABLE "User"; + +-- CreateTable +CREATE TABLE "DbEntry" ( + "id" UUID NOT NULL, + + CONSTRAINT "DbEntry_pkey" PRIMARY KEY ("id") +);