Skip to content

Commit 60eab52

Browse files
dmealingclaude
andcommitted
fix(codegen-ts-tanstack): import <Entity>.meta from the UI file's own target
The hooks and grid-hook generators write the DB-free <Entity>.meta.ts into their own target, beside the file that imports it. The import path was built by appending `.meta` to the entity module's specifier, which on a multi-target project is another package's importBase (`@acme/db/generated/shop/commerce/Product.meta`), a file that does not exist there. An adopter's web target compiled only because of a tsconfig alias mapping that path back onto its own generated files. The descriptor import is now a same-target sibling specifier. Same-target projects are unchanged (the goldens still read `./Product.meta`). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1mRRB1K2WdYy82kntMQdx
1 parent 1e47c63 commit 60eab52

4 files changed

Lines changed: 55 additions & 19 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ here.**
1010

1111
## [Unreleased]
1212

13+
### Fixed
14+
15+
- **TanStack hooks and grid hooks import `<Entity>.meta` from beside themselves.** The UI
16+
generators write the DB-free `<Entity>.meta.ts` into their own target, but the generated
17+
`import { Program } from …` pointed at the entity module's path plus `.meta`. On a
18+
multi-target project that is another package's `importBase`
19+
(`@acme/db/generated/…/Program.meta`), where no such file exists, so the web target only
20+
compiled with a hand-written `tsconfig` alias. It is now a sibling import
21+
(`./Program.meta`). **Upgrading:** run `meta gen`; an alias added to work around it can be
22+
removed.
23+
1324
## [1.0.10] — 2026-09-29
1425

1526
_npm `1.0.10` (all 14 `@metaobjectsdev/*` packages in lockstep), PyPI `1.0.10`, NuGet `1.0.10` and

‎server/typescript/packages/codegen-ts-tanstack/src/templates/grid-hook-file.ts‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ import type { MetaObject, MetaLayout } from "@metaobjectsdev/metadata";
1313
import { LAYOUT_SUBTYPE_DATA_GRID } from "@metaobjectsdev/metadata";
1414
import type { RenderContext } from "@metaobjectsdev/codegen-ts";
1515

16-
/** The DB-free descriptor sibling of an entity module: `./Author` → `./Author.meta`,
17-
* `./Author.js` → `./Author.meta.js`. The UI files take the `<Entity>` descriptor
18-
* from there so a browser bundle never pulls the Drizzle table in — it is the ONLY
19-
* value they import from the entity module; everything else is `import type`. */
20-
function metaModuleOf(entityModule: string): string {
21-
return entityModule.endsWith(".js")
22-
? `${entityModule.slice(0, -3)}.meta.js`
23-
: `${entityModule}.meta`;
16+
/** The DB-free `<Entity>.meta` descriptor. The UI generator writes it into ITS OWN target,
17+
* beside this file, so it is always a sibling import — never the entity module's path,
18+
* which on a multi-target project is another package's importBase. The UI files take the
19+
* `<Entity>` descriptor from there so a browser bundle never pulls the Drizzle table in —
20+
* it is the ONLY value they import from the entity module; everything else is `import type`. */
21+
function metaModuleOf(entity: MetaObject, ctx: RenderContext): string {
22+
return siblingSpecifier(ctx.selfTarget, effectivePackage(entity), `${entity.name}.meta`, ctx.extStyle);
2423
}
2524

2625
import { GENERATED_HEADER, GENERATED_EDIT_NOTE, entityModuleSpecifier, siblingSpecifier,
@@ -84,7 +83,7 @@ export function renderGridHookFile(entity: MetaObject, ctx: RenderContext): stri
8483
const buildFilterQsSym = imp("buildFilterQs@@metaobjectsdev/runtime-web");
8584

8685
const entityImports: Code = code`
87-
import { ${entityName} } from ${JSON.stringify(metaModuleOf(entityModule))};
86+
import { ${entityName} } from ${JSON.stringify(metaModuleOf(entity, ctx))};
8887
import type { ${entityName} as ${entityName}Row } from ${JSON.stringify(entityModule)};
8988
`;
9089

‎server/typescript/packages/codegen-ts-tanstack/src/templates/hooks-file.ts‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import { code, imp, joinCode, Import, type Code } from "ts-poet";
22
import type { MetaObject } from "@metaobjectsdev/metadata";
33
import type { RenderContext, RelationEntry } from "@metaobjectsdev/codegen-ts";
44

5-
/** The DB-free descriptor sibling of an entity module: `./Author` → `./Author.meta`,
6-
* `./Author.js` → `./Author.meta.js`. The UI files take the `<Entity>` descriptor
7-
* from there so a browser bundle never pulls the Drizzle table in — it is the ONLY
8-
* value they import from the entity module; everything else is `import type`. */
9-
function metaModuleOf(entityModule: string): string {
10-
return entityModule.endsWith(".js")
11-
? `${entityModule.slice(0, -3)}.meta.js`
12-
: `${entityModule}.meta`;
5+
/** The DB-free `<Entity>.meta` descriptor. The UI generator writes it into ITS OWN target,
6+
* beside this file, so it is always a sibling import — never the entity module's path,
7+
* which on a multi-target project is another package's importBase. The UI files take the
8+
* `<Entity>` descriptor from there so a browser bundle never pulls the Drizzle table in —
9+
* it is the ONLY value they import from the entity module; everything else is `import type`. */
10+
function metaModuleOf(entity: MetaObject, ctx: RenderContext): string {
11+
return siblingSpecifier(ctx.selfTarget, effectivePackage(entity), `${entity.name}.meta`, ctx.extStyle);
1312
}
1413

1514
import {
@@ -22,6 +21,7 @@ import {
2221
tphPlan,
2322
getPkInfo,
2423
effectivePackage,
24+
siblingSpecifier,
2525
} from "@metaobjectsdev/codegen-ts";
2626

2727
/**
@@ -183,7 +183,7 @@ function renderReadOnlyHooksFile(entity: MetaObject, entityModule: string, ctx:
183183
const buildFilterQsSym = imp("buildFilterQs@@metaobjectsdev/runtime-web");
184184

185185
const entityImports: Code = code`
186-
import { ${entityName} } from ${JSON.stringify(metaModuleOf(entityModule))};
186+
import { ${entityName} } from ${JSON.stringify(metaModuleOf(entity, ctx))};
187187
import {
188188
type ${entityName} as ${entityName}Row,
189189
type ${entityName}Filter,
@@ -262,7 +262,7 @@ function renderFullHooksFile(entity: MetaObject, entityModule: string, ctx: Rend
262262
const buildFilterQsSym = imp("buildFilterQs@@metaobjectsdev/runtime-web");
263263

264264
const entityImports: Code = code`
265-
import { ${entityName} } from ${JSON.stringify(metaModuleOf(entityModule))};
265+
import { ${entityName} } from ${JSON.stringify(metaModuleOf(entity, ctx))};
266266
import {
267267
type ${entityName} as ${entityName}Row,
268268
type ${entityName}Insert,

‎server/typescript/packages/codegen-ts-tanstack/test/cross-target.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
33
import { makeRenderContext, buildPkMap, buildRelationMap, type ResolvedTarget } from "@metaobjectsdev/codegen-ts";
44
import { renderHooksFile } from "../src/templates/hooks-file.js";
55
import { renderColumnsFile } from "../src/templates/columns-file.js";
6+
import { renderGridHookFile } from "../src/templates/grid-hook-file.js";
67
import { MetaDataLoader } from "@metaobjectsdev/metadata";
78
import { FileSource } from "@metaobjectsdev/metadata/core";
89

@@ -46,3 +47,28 @@ describe("columns-file — cross target", () => {
4647
expect(out).not.toContain(`from "./Product"`);
4748
});
4849
});
50+
51+
// The `<Entity>.meta.ts` descriptor is written by the UI generator into ITS OWN target,
52+
// beside the hooks/grid file — not into the entity's target. Deriving its import from the
53+
// entity module's cross-target path pointed at `@acme/db/generated/.../Product.meta`, a
54+
// file that does not exist in the db package; an adopter had to add a tsconfig alias
55+
// mapping that path back onto the web target to make it compile.
56+
describe("the .meta descriptor is imported from the UI file's own target", () => {
57+
it("hooks: cross target imports ./Product.meta, not the db package path", async () => {
58+
const { entity, ctx } = await ctxFor(web, model);
59+
const out = renderHooksFile(entity, ctx);
60+
expect(out).toContain(`import { Product } from "./Product.meta"`);
61+
expect(out).not.toContain(`@acme/db/generated/shop/commerce/Product.meta`);
62+
});
63+
it("grid hook: cross target imports ./Product.meta, not the db package path", async () => {
64+
const { entity, ctx } = await ctxFor(web, model);
65+
const out = renderGridHookFile(entity, ctx);
66+
expect(out).toContain(`import { Product } from "./Product.meta"`);
67+
expect(out).not.toContain(`@acme/db/generated/shop/commerce/Product.meta`);
68+
});
69+
it("same target is unchanged", async () => {
70+
const { entity, ctx } = await ctxFor(model, model);
71+
expect(renderHooksFile(entity, ctx)).toContain(`import { Product } from "./Product.meta"`);
72+
expect(renderGridHookFile(entity, ctx)).toContain(`import { Product } from "./Product.meta"`);
73+
});
74+
});

0 commit comments

Comments
 (0)