Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/void/src/plugin-drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ export function createDb() {
`;
}

// void/db virtual module
// void/db virtual module. Each dialect keeps the SQL helper export first: schema files
// that db/schema.ts reaches from outside db/ import void/db while this module is still
// loading. Vite SSR runs imports in source order, so the helpers must be loaded before
// the schema import.
const schemaImport = schema
? `import * as schema from ${JSON.stringify(schema.path)};`
: '';
Expand All @@ -161,10 +164,10 @@ export function createDb() {
const mysqlDrizzleConfig =
(schema ? ', schema, mode: "default"' : '') + (casingOption ? `, ${casingOption}` : '');
return `
export { ${DRIZZLE_OPERATORS} } from ${JSON.stringify(drizzleOrmResolved)};
import { drizzle } from ${JSON.stringify(drizzleMysql2Resolved)};
import { getRuntimeBinding, getRuntimeRequestResource } from ${JSON.stringify(envModuleResolved)};
${schemaImport}
export { ${DRIZZLE_OPERATORS} } from ${JSON.stringify(drizzleOrmResolved)};

const _requestResourceKey = {};

Expand Down Expand Up @@ -243,10 +246,10 @@ export function createDb(connectionString) {
if (dialect === 'postgresql') {
// Like MySQL: drizzle builds the pool, so its bare pg import is the only one.
return `
export { ${DRIZZLE_OPERATORS} } from ${JSON.stringify(drizzleOrmResolved)};
import { drizzle } from ${JSON.stringify(drizzleNodePgResolved)};
import { getRuntimeBinding, getRuntimeRequestResource } from ${JSON.stringify(envModuleResolved)};
${schemaImport}
export { ${DRIZZLE_OPERATORS} } from ${JSON.stringify(drizzleOrmResolved)};

const _requestResourceKey = {};

Expand Down Expand Up @@ -295,10 +298,10 @@ export function createDb(connectionString) {

// SQLite (default)
return `
export { ${DRIZZLE_OPERATORS} } from ${JSON.stringify(drizzleOrmResolved)};
import { drizzle } from ${JSON.stringify(drizzleD1Resolved)};
import { getRuntimeRequestResource, requireRuntimeBinding } from ${JSON.stringify(envModuleResolved)};
${schemaImport}
export { ${DRIZZLE_OPERATORS} } from ${JSON.stringify(drizzleOrmResolved)};

const _requestResourceKey = {};

Expand Down
16 changes: 16 additions & 0 deletions packages/void/test/unit/plugin-drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,22 @@ export const requestPoolOptions = (env) =>
expect(code).not.toContain('requireRuntimeBinding');
});

it.each(['sqlite', 'postgresql', 'mysql'] as const)(
'exports the %s SQL helpers before void/db imports the schema',
(dialect) => {
// A table file that db/schema.ts re-exports from outside db/ imports this module while
// it loads the schema. Vite SSR runs imports in source order (#472).
const root = createProjectRoot();
const plugin = drizzlePlugin(root, false, dialect);
const load = getHookHandler(plugin.load) as (this: object, id: string) => string;

const code = load.call({}, '\0virtual:void-db').trim();

expect(code.startsWith('export { sql, eq, ')).toBe(true);
expect(code).toContain('import * as schema');
},
);

it('treats files in db/schema/ beside db/schema.ts as schema files', () => {
// `void gen model` writes db/schema/<name>.ts and re-exports it from db/schema.ts.
const root = createProjectRoot();
Expand Down
55 changes: 54 additions & 1 deletion packages/void/test/unit/plugin-vite-environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import module, { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { cloudflare } from '@cloudflare/vite-plugin';
import { createBuilder, createServer, normalizePath, resolveConfig } from 'vite';
import { afterEach, describe, expect, it, vi } from 'vite-plus/test';
Expand Down Expand Up @@ -945,6 +945,59 @@ describe('voidPlugin Vite environments', () => {
}
});

it('loads a table file outside db/ that imports sql from void/db after a route loads void/db', async () => {
// #472: db/schema.ts re-exports a table file outside db/, which imports void/db while
// void/db is still loading db/schema.ts.
const root = realpathSync.native(createProjectRoot());
mkdirSync(join(root, 'db'));
writeFileSync(
join(root, 'db/schema.ts'),
'export * from "../src/resources/projects/projects.schema";\n',
);
mkdirSync(join(root, 'src/resources/projects'), { recursive: true });
const sqliteCore = fileURLToPath(import.meta.resolve('drizzle-orm/sqlite-core'));
writeFileSync(
join(root, 'src/resources/projects/projects.schema.ts'),
[
"import { sql } from 'void/db';",
`import { integer, sqliteTable, text } from ${JSON.stringify(normalizePath(sqliteCore))};`,
"export const projects = sqliteTable('projects', {",
" id: integer('id').primaryKey(),",
" createdAt: text('created_at').default(sql`(datetime('now'))`),",
'});',
'',
].join('\n'),
);
mkdirSync(join(root, 'routes/api'), { recursive: true });
writeFileSync(
join(root, 'routes/api/projects.ts'),
[
"import { db } from 'void/db';",
"import { projects } from '@schema';",
'export const GET = () => Response.json({ db: typeof db, projects: typeof projects });',
'',
].join('\n'),
);
vi.spyOn(process, 'cwd').mockReturnValue(root);

const server = await createServer({
root,
configFile: false,
logLevel: 'silent',
plugins: [voidPlugin()],
optimizeDeps: { noDiscovery: true },
server: { middlewareMode: true, ws: false, preTransformRequests: false },
});
try {
const route = await server.ssrLoadModule('/routes/api/projects.ts');
const response = (route.GET as () => Response)();

await expect(response.json()).resolves.toEqual({ db: 'object', projects: 'object' });
} finally {
await server.close();
}
});

describe('Node target builds with an installed void', () => {
/** An API-only Node-target app whose route imports void and an adapter from node_modules. */
function createNodeApp(route: string): string {
Expand Down
Loading