Skip to content
Open
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
6,748 changes: 3,374 additions & 3,374 deletions integration/zero-schema.gen.ts

Large diffs are not rendered by default.

6,956 changes: 3,478 additions & 3,478 deletions no-config-integration/zero-schema.gen.ts

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions src/canonicalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Orders the parts of a Zero schema that carry no meaning so that generating
* from an unchanged Drizzle schema always produces the same output.
*
* Nothing downstream reads these key orders: `normalizeClientSchema` sorts
* tables and columns before hashing the client schema, and every other
* consumer looks entries up by name. Without this pass the orders are simply
* whatever order the schema happened to be written in, so moving a table
* between two exports or a column between two lines rewrote the generated
* file for no reason.
*
* Ordering that *does* carry meaning is left alone: a table's `primaryKey`,
* the hops of a relationship, and the parallel `sourceField`/`destField`
* arrays inside a hop, which pair up by position.
*/

/** Code-unit order, so the result never depends on the host locale. */
export const compareKeys = (a: string, b: string): number =>
a < b ? -1 : a > b ? 1 : 0;

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value);

const sortKeys = <T>(value: Record<string, T>): Record<string, T> =>
Object.fromEntries(
Object.entries(value).sort(([a], [b]) => compareKeys(a, b)),
);

const canonicalizeTable = (table: unknown): unknown => {
if (!isRecord(table) || !isRecord(table.columns)) {
return table;
}

return {...table, columns: sortKeys(table.columns)};
};

/**
* Sorts a schema's tables, columns, relationship owners and relationship
* names. Idempotent, so it is safe to apply again on the way into codegen.
*/
export function canonicalizeZeroSchema<TSchema>(schema: TSchema): TSchema {
if (!isRecord(schema)) {
return schema;
}

const canonical: Record<string, unknown> = {...schema};

if (isRecord(schema.tables)) {
canonical.tables = sortKeys(
Object.fromEntries(
Object.entries(schema.tables).map(([name, table]) => [
name,
canonicalizeTable(table),
]),
),
);
}

if (isRecord(schema.relationships)) {
canonical.relationships = sortKeys(
Object.fromEntries(
Object.entries(schema.relationships).map(([name, relationships]) => [
name,
isRecord(relationships) ? sortKeys(relationships) : relationships,
]),
),
);
}

return canonical as TSchema;
}
9 changes: 6 additions & 3 deletions src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,16 @@ export function getZeroSchemaDefsFromConfig({
configPath: string;
exportName: string;
}) {
const fileName = configPath.slice(configPath.lastIndexOf('/') + 1);
// Look the file up by absolute path. A bare file name makes ts-morph fall
// back to a "path ends with" search, which silently picks the first match by
// directory depth when a project holds more than one config of that name.
const fullConfigPath = path.resolve(process.cwd(), configPath);

const sourceFile = tsProject.getSourceFile(fileName);
const sourceFile = tsProject.getSourceFile(fullConfigPath);

if (!sourceFile) {
throw new Error(
`❌ drizzle-zero: Failed to find type definitions for ${fileName}`,
`❌ drizzle-zero: Failed to find type definitions for ${fullConfigPath}`,
);
}

Expand Down
51 changes: 51 additions & 0 deletions src/cli/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {pathToFileURL} from 'node:url';

class PrettierNotFoundError extends Error {
constructor() {
super(
'⚠️ drizzle-zero: prettier could not be found. Install it locally with\n npm i -D prettier',
);
this.name = 'PrettierNotFoundError';
}
}

export async function loadPrettier() {
try {
return await import('prettier');
} catch (_) {}

try {
const path = require.resolve('prettier', {paths: [process.cwd()]});
return await import(pathToFileURL(path).href);
} catch {
throw new PrettierNotFoundError();
}
}

export async function formatSchema(
schema: string,
filePath: string,
): Promise<string> {
let prettier: Awaited<ReturnType<typeof loadPrettier>>;

try {
prettier = await loadPrettier();
} catch (error) {
if (!(error instanceof PrettierNotFoundError)) {
throw error;
}

console.warn('⚠️ drizzle-zero: prettier not found, skipping formatting');
return schema;
}

// Anything past this point is prettier failing on input it was given, not
// prettier being absent. Reporting it as "not found" would hide a broken
// prettier config behind output that silently differs from every other run.
const prettierOptions = await prettier.resolveConfig(filePath);

return prettier.format(schema, {
...prettierOptions,
parser: 'typescript',
});
}
35 changes: 1 addition & 34 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Command} from 'commander';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import {pathToFileURL} from 'node:url';
import {Project} from 'ts-morph';
import {getConfigFromFile, getDefaultConfigFilePath} from './config';
import {getDefaultConfig} from './drizzle-kit';
import {formatSchema} from './format';
import {getGeneratedSchema} from './shared';
import {checkSignature, signContent} from './signature';
import {discoverAllTsConfigs} from './tsconfig';
Expand All @@ -18,39 +18,6 @@ const defaultOutputFile = './zero-schema.gen.ts';
const defaultTsConfigFile = './tsconfig.json';
const defaultDrizzleKitConfigPath = './drizzle.config.ts';

export async function loadPrettier() {
try {
return await import('prettier');
} catch (_) {}

try {
const path = require.resolve('prettier', {paths: [process.cwd()]});
return await import(pathToFileURL(path).href);
} catch {
throw new Error(
'⚠️ drizzle-zero: prettier could not be found. Install it locally with\n npm i -D prettier',
);
}
}

export async function formatSchema(
schema: string,
filePath: string,
): Promise<string> {
try {
const prettier = await loadPrettier();
const prettierOptions = await prettier.resolveConfig(filePath);

return prettier.format(schema, {
...prettierOptions,
parser: 'typescript',
});
} catch {
console.warn('⚠️ drizzle-zero: prettier not found, skipping formatting');
return schema;
}
}

export interface GeneratorOptions {
config?: string;
tsConfigPath?: string;
Expand Down
Loading