Skip to content

Commit 1510808

Browse files
authored
Merge pull request #121 from IderAghbal/main
feat: respect the references in tsconfig files
2 parents 8c3e2cd + 5c43a53 commit 1510808

5 files changed

Lines changed: 263 additions & 9 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
},
6666
"dependencies": {
6767
"commander": "^14.0.0",
68+
"jsonc-parser": "^3.3.1",
6869
"ts-morph": "^26.0.0"
6970
},
7071
"devDependencies": {

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cli/config.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import * as url from "node:url";
44
import { Project } from "ts-morph";
55
import { tsImport } from "tsx/esm/api";
66
import type { DrizzleToZeroSchema } from "../relations";
7+
import {
8+
parse as JsoncParse,
9+
type ParseError as JsoncParseError,
10+
} from "jsonc-parser";
711

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

@@ -94,3 +98,101 @@ export async function getZeroSchemaDefsFromConfig({
9498
.join(", ")}`,
9599
);
96100
}
101+
102+
export async function discoverAllTsConfigs(
103+
initialTsConfigPath: string,
104+
): Promise<Set<string>> {
105+
const processedPaths = new Set<string>();
106+
const toProcess = [path.resolve(initialTsConfigPath)];
107+
108+
const processTsConfig = async (tsConfigPath: string) => {
109+
if (processedPaths.has(tsConfigPath)) {
110+
return [];
111+
}
112+
processedPaths.add(tsConfigPath);
113+
114+
try {
115+
const tsConfigContent = await fs.readFile(tsConfigPath, "utf-8");
116+
const errors: JsoncParseError[] = [];
117+
const tsConfig = JsoncParse(tsConfigContent, errors) as {
118+
references?: { path: string }[];
119+
};
120+
121+
if (errors.length > 0) {
122+
console.warn(
123+
`⚠️ drizzle-zero: Found syntax errors in ${path.relative(process.cwd(), tsConfigPath)}. The resolver will attempt to continue.`,
124+
);
125+
}
126+
127+
if (!tsConfig?.references) {
128+
return [];
129+
}
130+
131+
const tsConfigDir = path.dirname(tsConfigPath);
132+
const newPathsToProcess: string[] = [];
133+
134+
for (const ref of tsConfig.references) {
135+
const referencedTsConfigPath = await resolveReferencePath(
136+
ref.path,
137+
tsConfigDir,
138+
);
139+
140+
if (
141+
referencedTsConfigPath &&
142+
!processedPaths.has(referencedTsConfigPath)
143+
) {
144+
newPathsToProcess.push(referencedTsConfigPath);
145+
}
146+
}
147+
return newPathsToProcess;
148+
} catch (error) {
149+
if (
150+
error instanceof Error &&
151+
"code" in error &&
152+
error.code === "ENOENT"
153+
) {
154+
console.warn(
155+
`⚠️ drizzle-zero: Could not find tsconfig file: ${tsConfigPath}`,
156+
);
157+
} else {
158+
throw new Error(
159+
`❌ drizzle-zero: Error processing tsconfig file: ${tsConfigPath}: ${error}`,
160+
);
161+
}
162+
return [];
163+
}
164+
};
165+
166+
while (toProcess.length > 0) {
167+
const newPaths = (
168+
await Promise.all(toProcess.splice(0).map(processTsConfig))
169+
).flat();
170+
if (newPaths.length > 0) {
171+
toProcess.push(...newPaths);
172+
}
173+
}
174+
175+
return processedPaths;
176+
}
177+
178+
async function resolveReferencePath(
179+
refPath: string,
180+
tsConfigDir: string,
181+
): Promise<string | undefined> {
182+
const resolvedPath = path.resolve(tsConfigDir, refPath);
183+
184+
// The reference can be a directory or a file.
185+
// We assume that if it's a directory, then it contains a 'tsconfig.json' file.
186+
try {
187+
const stats = await fs.stat(resolvedPath);
188+
if (stats.isDirectory()) {
189+
return path.join(resolvedPath, "tsconfig.json");
190+
}
191+
return resolvedPath;
192+
} catch {
193+
console.warn(
194+
`⚠️ drizzle-zero: Could not resolve reference path: ${refPath}`,
195+
);
196+
return;
197+
}
198+
}

src/cli/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import * as fs from "node:fs/promises";
33
import * as path from "node:path";
44
import { pathToFileURL } from "node:url";
55
import { Project } from "ts-morph";
6-
import { getConfigFromFile, getDefaultConfigFilePath } from "./config";
6+
import {
7+
getConfigFromFile,
8+
getDefaultConfigFilePath,
9+
discoverAllTsConfigs,
10+
} from "./config";
711
import { getDefaultConfig } from "./drizzle-kit";
812
import { getGeneratedSchema } from "./shared";
913

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

7883
const tsProject = new Project({
7984
tsConfigFilePath: resolvedTsConfigPath,
85+
skipAddingFilesFromTsConfig: true,
8086
});
87+
for (const tsConfigPath of allTsConfigPaths) {
88+
tsProject.addSourceFilesFromTsConfig(tsConfigPath);
89+
}
8190

8291
const result = configFilePath
8392
? await getConfigFromFile({

tests/cli.test.ts

Lines changed: 142 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
22
import { Project } from "ts-morph";
33
import * as path from "node:path";
44
import * as fs from "node:fs/promises";
5-
import { getZeroSchemaDefsFromConfig } from "../src/cli/config";
5+
import {
6+
getZeroSchemaDefsFromConfig,
7+
discoverAllTsConfigs,
8+
} from "../src/cli/config";
69
import * as oneToOneSchema from "./schemas/one-to-one.zero";
710
import { getGeneratedSchema } from "../src/cli/shared";
811
import type { DrizzleToZeroSchema } from "../src/relations";
@@ -443,15 +446,15 @@ describe("getGeneratedSchema", () => {
443446
});
444447

445448
// Verify the import statement includes .js extension
446-
expect(generatedSchema).toContain('from "./tests/schemas/one-to-one.zero.js";');
447-
449+
expect(generatedSchema).toContain(
450+
'from "./tests/schemas/one-to-one.zero.js";',
451+
);
452+
448453
// Verify the rest of the schema is still generated correctly
449454
expect(generatedSchema).toContain("export const schema = {");
450455
expect(generatedSchema).toContain('"users": {');
451456
});
452457

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

499502
// Verify the import statement includes .js extension for drizzle schema
500503
expect(generatedSchema).toContain('from "./mock-drizzle-schema.js";');
501-
504+
502505
// Verify the rest of the schema is still generated correctly
503506
expect(generatedSchema).toContain("export const schema = {");
504507
expect(generatedSchema).toContain('"users": {');
@@ -629,12 +632,12 @@ describe("drizzle-kit functions", () => {
629632
// Create a temporary schema file that exports a valid drizzle schema
630633
const tempSchemaContent = `
631634
import { pgTable, serial, text } from 'drizzle-orm/pg-core';
632-
635+
633636
export const users = pgTable('users', {
634637
id: serial('id').primaryKey(),
635638
name: text('name').notNull()
636639
});
637-
640+
638641
export default { users };
639642
`;
640643

@@ -708,3 +711,134 @@ describe("drizzle-kit functions", () => {
708711
});
709712
});
710713
});
714+
715+
describe("discoverAllTsConfigs", () => {
716+
const tempDir = path.resolve(__dirname, "temp_tsconfigs");
717+
718+
beforeEach(async () => {
719+
await fs.mkdir(tempDir, { recursive: true });
720+
});
721+
722+
afterEach(async () => {
723+
await fs.rm(tempDir, { recursive: true, force: true });
724+
});
725+
726+
it("should find a single tsconfig with no references", async () => {
727+
const rootPath = path.join(tempDir, "tsconfig.json");
728+
await fs.writeFile(rootPath, JSON.stringify({ compilerOptions: {} }));
729+
730+
const result = await discoverAllTsConfigs(rootPath);
731+
expect(result).toEqual(new Set([rootPath]));
732+
});
733+
734+
it("should find one level of project references", async () => {
735+
const rootPath = path.join(tempDir, "tsconfig.json");
736+
const libADir = path.join(tempDir, "libs", "lib-a");
737+
const libBDir = path.join(tempDir, "libs", "lib-b");
738+
const libAPath = path.join(libADir, "tsconfig.json");
739+
const libBPath = path.join(libBDir, "tsconfig.json");
740+
741+
await fs.mkdir(libADir, { recursive: true });
742+
await fs.mkdir(libBDir, { recursive: true });
743+
744+
await fs.writeFile(
745+
rootPath,
746+
JSON.stringify({
747+
references: [{ path: "./libs/lib-a" }, { path: "./libs/lib-b" }],
748+
}),
749+
);
750+
await fs.writeFile(libAPath, JSON.stringify({ compilerOptions: {} }));
751+
await fs.writeFile(libBPath, JSON.stringify({ compilerOptions: {} }));
752+
753+
const result = await discoverAllTsConfigs(rootPath);
754+
expect(result).toEqual(new Set([rootPath, libAPath, libBPath]));
755+
});
756+
757+
it("should handle multi-level nested project references", async () => {
758+
const rootPath = path.join(tempDir, "tsconfig.json");
759+
const appDir = path.join(tempDir, "apps", "my-app");
760+
const sharedUiDir = path.join(tempDir, "libs", "shared-ui");
761+
const utilsDir = path.join(tempDir, "libs", "utils");
762+
763+
const appPath = path.join(appDir, "tsconfig.json");
764+
const sharedUiPath = path.join(sharedUiDir, "tsconfig.json");
765+
const utilsPath = path.join(utilsDir, "tsconfig.json");
766+
767+
await fs.mkdir(appDir, { recursive: true });
768+
await fs.mkdir(sharedUiDir, { recursive: true });
769+
await fs.mkdir(utilsDir, { recursive: true });
770+
771+
await fs.writeFile(
772+
rootPath,
773+
JSON.stringify({ references: [{ path: "./apps/my-app" }] }),
774+
);
775+
await fs.writeFile(
776+
appPath,
777+
JSON.stringify({ references: [{ path: "../../libs/shared-ui" }] }),
778+
);
779+
await fs.writeFile(
780+
sharedUiPath,
781+
JSON.stringify({ references: [{ path: "../utils" }] }),
782+
);
783+
await fs.writeFile(utilsPath, JSON.stringify({ compilerOptions: {} }));
784+
785+
const result = await discoverAllTsConfigs(rootPath);
786+
expect(result).toEqual(
787+
new Set([rootPath, appPath, sharedUiPath, utilsPath]),
788+
);
789+
});
790+
791+
it("should correctly handle circular references", async () => {
792+
const rootPath = path.join(tempDir, "tsconfig.json");
793+
const libADir = path.join(tempDir, "libs", "lib-a");
794+
const libBDir = path.join(tempDir, "libs", "lib-b");
795+
const libAPath = path.join(libADir, "tsconfig.json");
796+
const libBPath = path.join(libBDir, "tsconfig.json");
797+
798+
await fs.mkdir(libADir, { recursive: true });
799+
await fs.mkdir(libBDir, { recursive: true });
800+
801+
await fs.writeFile(
802+
rootPath,
803+
JSON.stringify({ references: [{ path: "./libs/lib-a" }] }),
804+
);
805+
await fs.writeFile(
806+
libAPath,
807+
JSON.stringify({ references: [{ path: "../lib-b" }] }),
808+
);
809+
await fs.writeFile(
810+
libBPath,
811+
JSON.stringify({ references: [{ path: "../lib-a" }] }),
812+
);
813+
814+
const result = await discoverAllTsConfigs(rootPath);
815+
expect(result).toEqual(new Set([rootPath, libAPath, libBPath]));
816+
});
817+
818+
it("should continue gracefully if a referenced tsconfig is not found", async () => {
819+
const rootPath = path.join(tempDir, "tsconfig.json");
820+
const libGoodDir = path.join(tempDir, "libs", "good");
821+
const libGoodPath = path.join(libGoodDir, "tsconfig.json");
822+
823+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
824+
825+
await fs.mkdir(libGoodDir, { recursive: true });
826+
827+
await fs.writeFile(
828+
rootPath,
829+
JSON.stringify({
830+
references: [{ path: "./libs/good" }, { path: "./libs/bad" }],
831+
}),
832+
);
833+
await fs.writeFile(libGoodPath, JSON.stringify({ compilerOptions: {} }));
834+
835+
const result = await discoverAllTsConfigs(rootPath);
836+
expect(result).toEqual(new Set([rootPath, libGoodPath]));
837+
838+
expect(warnSpy).toHaveBeenCalledWith(
839+
expect.stringContaining("Could not resolve reference path: ./libs/bad"),
840+
);
841+
842+
warnSpy.mockRestore();
843+
});
844+
});

0 commit comments

Comments
 (0)