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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
},
"dependencies": {
"commander": "^14.0.0",
"jsonc-parser": "^3.3.1",
"ts-morph": "^26.0.0"
},
"devDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 102 additions & 0 deletions src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import * as url from "node:url";
import { Project } from "ts-morph";
import { tsImport } from "tsx/esm/api";
import type { DrizzleToZeroSchema } from "../relations";
import {
parse as JsoncParse,
type ParseError as JsoncParseError,
} from "jsonc-parser";

export const defaultConfigFilePath = "drizzle-zero.config.ts";

Expand Down Expand Up @@ -94,3 +98,101 @@ export async function getZeroSchemaDefsFromConfig({
.join(", ")}`,
);
}

export async function discoverAllTsConfigs(
initialTsConfigPath: string,
): Promise<Set<string>> {
const processedPaths = new Set<string>();
const toProcess = [path.resolve(initialTsConfigPath)];

const processTsConfig = async (tsConfigPath: string) => {
if (processedPaths.has(tsConfigPath)) {
return [];
}
processedPaths.add(tsConfigPath);

try {
const tsConfigContent = await fs.readFile(tsConfigPath, "utf-8");
const errors: JsoncParseError[] = [];
const tsConfig = JsoncParse(tsConfigContent, errors) as {
references?: { path: string }[];
};

if (errors.length > 0) {
console.warn(
`⚠️ drizzle-zero: Found syntax errors in ${path.relative(process.cwd(), tsConfigPath)}. The resolver will attempt to continue.`,
);
}

if (!tsConfig?.references) {
return [];
}

const tsConfigDir = path.dirname(tsConfigPath);
const newPathsToProcess: string[] = [];

for (const ref of tsConfig.references) {
const referencedTsConfigPath = await resolveReferencePath(
ref.path,
tsConfigDir,
);

if (
referencedTsConfigPath &&
!processedPaths.has(referencedTsConfigPath)
) {
newPathsToProcess.push(referencedTsConfigPath);
}
}
return newPathsToProcess;
} catch (error) {
if (
error instanceof Error &&
"code" in error &&
error.code === "ENOENT"
) {
console.warn(
`⚠️ drizzle-zero: Could not find tsconfig file: ${tsConfigPath}`,
);
} else {
throw new Error(
`❌ drizzle-zero: Error processing tsconfig file: ${tsConfigPath}: ${error}`,
);
}
return [];
}
};

while (toProcess.length > 0) {
const newPaths = (
await Promise.all(toProcess.splice(0).map(processTsConfig))
).flat();
if (newPaths.length > 0) {
toProcess.push(...newPaths);
}
}

return processedPaths;
}

async function resolveReferencePath(
refPath: string,
tsConfigDir: string,
): Promise<string | undefined> {
const resolvedPath = path.resolve(tsConfigDir, refPath);

// The reference can be a directory or a file.
// We assume that if it's a directory, then it contains a 'tsconfig.json' file.
try {
const stats = await fs.stat(resolvedPath);
if (stats.isDirectory()) {
return path.join(resolvedPath, "tsconfig.json");
}
return resolvedPath;
} catch {
console.warn(
`⚠️ drizzle-zero: Could not resolve reference path: ${refPath}`,
);
return;
}
}
11 changes: 10 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ 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 {
getConfigFromFile,
getDefaultConfigFilePath,
discoverAllTsConfigs,
} from "./config";
import { getDefaultConfig } from "./drizzle-kit";
import { getGeneratedSchema } from "./shared";

Expand Down Expand Up @@ -74,10 +78,15 @@ async function main(opts: GeneratorOptions = {}) {
"😶‍🌫️ drizzle-zero: Using all tables/columns from Drizzle schema",
);
}
const allTsConfigPaths = await discoverAllTsConfigs(resolvedTsConfigPath);

const tsProject = new Project({
tsConfigFilePath: resolvedTsConfigPath,
skipAddingFilesFromTsConfig: true,
});
for (const tsConfigPath of allTsConfigPaths) {
tsProject.addSourceFilesFromTsConfig(tsConfigPath);
}

const result = configFilePath
? await getConfigFromFile({
Expand Down
150 changes: 142 additions & 8 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { Project } from "ts-morph";
import * as path from "node:path";
import * as fs from "node:fs/promises";
import { getZeroSchemaDefsFromConfig } from "../src/cli/config";
import {
getZeroSchemaDefsFromConfig,
discoverAllTsConfigs,
} from "../src/cli/config";
import * as oneToOneSchema from "./schemas/one-to-one.zero";
import { getGeneratedSchema } from "../src/cli/shared";
import type { DrizzleToZeroSchema } from "../src/relations";
Expand Down Expand Up @@ -443,15 +446,15 @@ describe("getGeneratedSchema", () => {
});

// Verify the import statement includes .js extension
expect(generatedSchema).toContain('from "./tests/schemas/one-to-one.zero.js";');

expect(generatedSchema).toContain(
'from "./tests/schemas/one-to-one.zero.js";',
);

// Verify the rest of the schema is still generated correctly
expect(generatedSchema).toContain("export const schema = {");
expect(generatedSchema).toContain('"users": {');
});



it("should add .js file extensions to drizzle-kit imports when jsFileExtension is true", async () => {
// Mock the DrizzleToZeroSchema type
vi.mock("drizzle-zero", () => ({
Expand Down Expand Up @@ -498,7 +501,7 @@ describe("getGeneratedSchema", () => {

// Verify the import statement includes .js extension for drizzle schema
expect(generatedSchema).toContain('from "./mock-drizzle-schema.js";');

// Verify the rest of the schema is still generated correctly
expect(generatedSchema).toContain("export const schema = {");
expect(generatedSchema).toContain('"users": {');
Expand Down Expand Up @@ -629,12 +632,12 @@ describe("drizzle-kit functions", () => {
// Create a temporary schema file that exports a valid drizzle schema
const tempSchemaContent = `
import { pgTable, serial, text } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull()
});

export default { users };
`;

Expand Down Expand Up @@ -708,3 +711,134 @@ describe("drizzle-kit functions", () => {
});
});
});

describe("discoverAllTsConfigs", () => {
const tempDir = path.resolve(__dirname, "temp_tsconfigs");

beforeEach(async () => {
await fs.mkdir(tempDir, { recursive: true });
});

afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});

it("should find a single tsconfig with no references", async () => {
const rootPath = path.join(tempDir, "tsconfig.json");
await fs.writeFile(rootPath, JSON.stringify({ compilerOptions: {} }));

const result = await discoverAllTsConfigs(rootPath);
expect(result).toEqual(new Set([rootPath]));
});

it("should find one level of project references", async () => {
const rootPath = path.join(tempDir, "tsconfig.json");
const libADir = path.join(tempDir, "libs", "lib-a");
const libBDir = path.join(tempDir, "libs", "lib-b");
const libAPath = path.join(libADir, "tsconfig.json");
const libBPath = path.join(libBDir, "tsconfig.json");

await fs.mkdir(libADir, { recursive: true });
await fs.mkdir(libBDir, { recursive: true });

await fs.writeFile(
rootPath,
JSON.stringify({
references: [{ path: "./libs/lib-a" }, { path: "./libs/lib-b" }],
}),
);
await fs.writeFile(libAPath, JSON.stringify({ compilerOptions: {} }));
await fs.writeFile(libBPath, JSON.stringify({ compilerOptions: {} }));

const result = await discoverAllTsConfigs(rootPath);
expect(result).toEqual(new Set([rootPath, libAPath, libBPath]));
});

it("should handle multi-level nested project references", async () => {
const rootPath = path.join(tempDir, "tsconfig.json");
const appDir = path.join(tempDir, "apps", "my-app");
const sharedUiDir = path.join(tempDir, "libs", "shared-ui");
const utilsDir = path.join(tempDir, "libs", "utils");

const appPath = path.join(appDir, "tsconfig.json");
const sharedUiPath = path.join(sharedUiDir, "tsconfig.json");
const utilsPath = path.join(utilsDir, "tsconfig.json");

await fs.mkdir(appDir, { recursive: true });
await fs.mkdir(sharedUiDir, { recursive: true });
await fs.mkdir(utilsDir, { recursive: true });

await fs.writeFile(
rootPath,
JSON.stringify({ references: [{ path: "./apps/my-app" }] }),
);
await fs.writeFile(
appPath,
JSON.stringify({ references: [{ path: "../../libs/shared-ui" }] }),
);
await fs.writeFile(
sharedUiPath,
JSON.stringify({ references: [{ path: "../utils" }] }),
);
await fs.writeFile(utilsPath, JSON.stringify({ compilerOptions: {} }));

const result = await discoverAllTsConfigs(rootPath);
expect(result).toEqual(
new Set([rootPath, appPath, sharedUiPath, utilsPath]),
);
});

it("should correctly handle circular references", async () => {
const rootPath = path.join(tempDir, "tsconfig.json");
const libADir = path.join(tempDir, "libs", "lib-a");
const libBDir = path.join(tempDir, "libs", "lib-b");
const libAPath = path.join(libADir, "tsconfig.json");
const libBPath = path.join(libBDir, "tsconfig.json");

await fs.mkdir(libADir, { recursive: true });
await fs.mkdir(libBDir, { recursive: true });

await fs.writeFile(
rootPath,
JSON.stringify({ references: [{ path: "./libs/lib-a" }] }),
);
await fs.writeFile(
libAPath,
JSON.stringify({ references: [{ path: "../lib-b" }] }),
);
await fs.writeFile(
libBPath,
JSON.stringify({ references: [{ path: "../lib-a" }] }),
);

const result = await discoverAllTsConfigs(rootPath);
expect(result).toEqual(new Set([rootPath, libAPath, libBPath]));
});

it("should continue gracefully if a referenced tsconfig is not found", async () => {
const rootPath = path.join(tempDir, "tsconfig.json");
const libGoodDir = path.join(tempDir, "libs", "good");
const libGoodPath = path.join(libGoodDir, "tsconfig.json");

const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

await fs.mkdir(libGoodDir, { recursive: true });

await fs.writeFile(
rootPath,
JSON.stringify({
references: [{ path: "./libs/good" }, { path: "./libs/bad" }],
}),
);
await fs.writeFile(libGoodPath, JSON.stringify({ compilerOptions: {} }));

const result = await discoverAllTsConfigs(rootPath);
expect(result).toEqual(new Set([rootPath, libGoodPath]));

expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("Could not resolve reference path: ./libs/bad"),
);

warnSpy.mockRestore();
});
});
Loading