Skip to content

Commit 2b20741

Browse files
jordanhunt22Convex, Inc.
authored andcommitted
[Components] Add flag for direct imports from components (#42600)
Adds a flag in `convex.json` called `codegen.useApiImports` that enables importing the component types directly from the component instead of blowing out the types. This works by adding grabbing the import specifier we use to import the `convex.config` and using it to import from `/_generated/component.js` instead. This is the same for both sibling and non-sibling components. I also added a sample project to exercise this functionality and confirm that it works as expected. We can add an NPM package once they get updated with the new entry point. GitOrigin-RevId: a9a45f4c616ee1a1032acf984855007324036cbd
1 parent 82429fe commit 2b20741

File tree

6 files changed

+100
-23
lines changed

6 files changed

+100
-23
lines changed

src/cli/codegen_templates/component_api.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { Context } from "../../bundler/context.js";
33
import { entryPoints } from "../../bundler/index.js";
44
import {
55
ComponentDirectory,
6+
toAbsolutePath,
67
toComponentDefinitionPath,
8+
ComponentDefinitionPath,
79
} from "../lib/components/definition/directoryStructure.js";
810
import { StartPushResponse } from "../lib/deployApi/startPush.js";
911
import { importPath, moduleIdentifier } from "./api.js";
@@ -64,7 +66,8 @@ export async function componentApiDTS(
6466
startPush: StartPushResponse,
6567
rootComponent: ComponentDirectory,
6668
componentDirectory: ComponentDirectory,
67-
opts: { staticApi: boolean },
69+
componentsMap: Map<string, ComponentDirectory>,
70+
opts: { staticApi: boolean; useComponentApiImports: boolean },
6871
) {
6972
const definitionPath = toComponentDefinitionPath(
7073
rootComponent,
@@ -103,12 +106,43 @@ export async function componentApiDTS(
103106
printedMessage: `No analysis found for child component ${childComponent.path}`,
104107
});
105108
}
106-
for await (const line of codegenExports(
107-
ctx,
108-
childComponent.name,
109-
childComponentAnalysis,
110-
)) {
111-
lines.push(line);
109+
if (opts.useComponentApiImports) {
110+
const absolutePath = toAbsolutePath(
111+
rootComponent,
112+
childComponent.path as ComponentDefinitionPath,
113+
);
114+
115+
let childComponentWithRelativePath = componentsMap?.get(absolutePath);
116+
if (!childComponentWithRelativePath) {
117+
return await ctx.crash({
118+
exitCode: 1,
119+
errorType: "fatal",
120+
printedMessage: `Invalid child component directory: ${childComponent.path}`,
121+
});
122+
}
123+
124+
let importPath;
125+
126+
// If the user uses a different import specifier than the absolute path of the child component, use the import specifier.
127+
if (
128+
childComponentWithRelativePath.importSpecifier &&
129+
childComponentWithRelativePath.importSpecifier !== childComponent.path
130+
) {
131+
importPath = childComponentWithRelativePath.importSpecifier;
132+
} else {
133+
importPath = `../${childComponent.path}`;
134+
}
135+
lines.push(
136+
` "${childComponent.name}": import("${importPath}/_generated/component.js").ComponentApi<"${childComponent.name}">,`,
137+
);
138+
} else {
139+
for await (const line of codegenExports(
140+
ctx,
141+
childComponent.name,
142+
childComponentAnalysis,
143+
)) {
144+
lines.push(line);
145+
}
112146
}
113147
}
114148

src/cli/lib/codegen.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,7 @@ export async function doInitialComponentCodegen(
167167
) {
168168
const { projectConfig } = await readProjectConfig(ctx);
169169

170-
// This component defined in a dist directory; it is probably in a node_module
171-
// directory, installed from a package. It is stuck with the files it has.
172-
// Heuristics for this:
173-
// - component definition has a dist/ directory as an ancestor
174-
// - component definition is a .js file
175-
// - presence of .js.map files
176-
// We may improve this heuristic.
177-
const isPublishedPackage =
178-
componentDirectory.definitionPath.endsWith(".js") &&
179-
!componentDirectory.isRoot;
180-
if (isPublishedPackage) {
170+
if (isPublishedPackage(componentDirectory)) {
181171
if (opts?.verbose) {
182172
logMessage(
183173
`skipping initial codegen for installed package ${componentDirectory.path}`,
@@ -239,12 +229,28 @@ export async function doInitialComponentCodegen(
239229
}
240230
}
241231

232+
/* This component defined in a dist directory; it is probably in a node_module
233+
* directory, installed from a package. It is stuck with the files it has.
234+
* Heuristics for this:
235+
* - component definition has a dist/ directory as an ancestor
236+
* - component definition is a .js file
237+
* - presence of .js.map files
238+
* We may improve this heuristic.
239+
*/
240+
export function isPublishedPackage(componentDirectory: ComponentDirectory) {
241+
return (
242+
componentDirectory.definitionPath.endsWith(".js") &&
243+
!componentDirectory.isRoot
244+
);
245+
}
246+
242247
export async function doFinalComponentCodegen(
243248
ctx: Context,
244249
tmpDir: TempDir,
245250
rootComponent: ComponentDirectory,
246251
componentDirectory: ComponentDirectory,
247252
startPushResponse: StartPushResponse,
253+
componentsMap: Map<string, ComponentDirectory>,
248254
opts?: {
249255
dryRun?: boolean;
250256
debug?: boolean;
@@ -308,7 +314,11 @@ export async function doFinalComponentCodegen(
308314
startPushResponse,
309315
rootComponent,
310316
componentDirectory,
311-
{ staticApi: projectConfig.codegen.staticApi },
317+
componentsMap,
318+
{
319+
staticApi: projectConfig.codegen.staticApi,
320+
useComponentApiImports: projectConfig.codegen.useComponentApiImports,
321+
},
312322
);
313323
await writeFormattedFile(
314324
ctx,

src/cli/lib/components.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ async function startComponentsPushAndCodegen(
352352
rootComponent,
353353
rootComponent,
354354
startPushResponse,
355+
components,
355356
options,
356357
);
357358
for (const directory of components.values()) {
@@ -361,6 +362,7 @@ async function startComponentsPushAndCodegen(
361362
rootComponent,
362363
directory,
363364
startPushResponse,
365+
components,
364366
options,
365367
);
366368
}

src/cli/lib/components/definition/bundle.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,15 +356,28 @@ async function findComponentDependencies(
356356
const componentImports = imports.filter((imp) =>
357357
imp.path.includes(".config."),
358358
);
359-
for (const importPath of componentImports.map((dep) => dep.path)) {
360-
const imported = componentsByAbsPath.get(path.resolve(importPath));
359+
for (const imp of componentImports) {
360+
const imported = componentsByAbsPath.get(path.resolve(imp.path));
361361
if (!imported) {
362362
return await ctx.crash({
363363
exitCode: 1,
364364
errorType: "invalid filesystem data",
365-
printedMessage: `Didn't find ${path.resolve(importPath)} in ${[...componentsByAbsPath.keys()].toString()}`,
365+
printedMessage: `Didn't find ${path.resolve(imp.path)} in ${[...componentsByAbsPath.keys()].toString()}`,
366366
});
367367
}
368+
369+
// Grab the import specifier from the metafile (e.g. `@convex-dev/workpool/convex.config`) so
370+
// we can use it to import component APIs
371+
if (imp.original) {
372+
const importSpecifier = imp.original;
373+
const relativeSpecifier = importSpecifier.replace(
374+
/\/convex\.config.*$/,
375+
"",
376+
);
377+
378+
imported.importSpecifier = relativeSpecifier;
379+
}
380+
368381
dependencyGraph.push([importer, imported]);
369382
}
370383
}

src/cli/lib/components/definition/directoryStructure.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ export type ComponentDirectory = {
3737
* Is this component a root without a config file?
3838
*/
3939
isRootWithoutConfig: boolean;
40+
41+
/**
42+
* The import specifier used to import this component, with `/convex.config.*` stripped.
43+
* For example, if imported as `@convex-dev/workpool/convex.config`, this would be `@convex-dev/workpool`.
44+
* For relative imports like `../examples/foo/convex.config.js`, this would be `../examples/foo`.
45+
* This is undefined for components discovered through the filesystem (not through imports).
46+
*/
47+
importSpecifier?: string;
4048
};
4149

4250
/**

src/cli/lib/config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface ProjectConfig {
7676
codegen: {
7777
staticApi: boolean;
7878
staticDataModel: boolean;
79+
useComponentApiImports: boolean;
7980
};
8081
}
8182

@@ -207,9 +208,13 @@ export async function parseProjectConfig(
207208
if (typeof obj.codegen.staticDataModel === "undefined") {
208209
obj.codegen.staticDataModel = false;
209210
}
211+
if (typeof obj.codegen.useComponentApiImports === "undefined") {
212+
obj.codegen.useComponentApiImports = false;
213+
}
210214
if (
211215
typeof obj.codegen.staticApi !== "boolean" ||
212-
typeof obj.codegen.staticDataModel !== "boolean"
216+
typeof obj.codegen.staticDataModel !== "boolean" ||
217+
typeof obj.codegen.useComponentApiImports !== "boolean"
213218
) {
214219
return await ctx.crash({
215220
exitCode: 1,
@@ -319,6 +324,7 @@ export async function readProjectConfig(ctx: Context): Promise<{
319324
codegen: {
320325
staticApi: false,
321326
staticDataModel: false,
327+
useComponentApiImports: false,
322328
},
323329
},
324330
configPath: configName(),
@@ -634,6 +640,9 @@ function stripDefaults(projectConfig: ProjectConfig): any {
634640
if (stripped.codegen.staticDataModel === false) {
635641
delete stripped.codegen.staticDataModel;
636642
}
643+
if (stripped.codegen.useComponentApiImports === false) {
644+
delete stripped.codegen.useComponentApiImports;
645+
}
637646
if (Object.keys(stripped.codegen).length === 0) {
638647
delete stripped.codegen;
639648
}
@@ -697,6 +706,7 @@ export async function pullConfig(
697706
codegen: {
698707
staticApi: false,
699708
staticDataModel: false,
709+
useComponentApiImports: false,
700710
},
701711
project,
702712
team,

0 commit comments

Comments
 (0)