@stratal/inertia 0.0.27 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +379 -0
  2. package/README.md +158 -14
  3. package/dist/build-seo-tags-DBsHKxX9.mjs.map +1 -1
  4. package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
  5. package/dist/generator/type-generator.worker.d.mts +1 -1
  6. package/dist/generator/type-generator.worker.mjs +1 -1
  7. package/dist/index.d.mts +214 -92
  8. package/dist/index.d.mts.map +1 -1
  9. package/dist/index.mjs +390 -130
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/quarry.d.mts +6 -8
  12. package/dist/quarry.d.mts.map +1 -1
  13. package/dist/quarry.mjs +127 -12
  14. package/dist/quarry.mjs.map +1 -1
  15. package/dist/react/access.d.mts +95 -0
  16. package/dist/react/access.d.mts.map +1 -0
  17. package/dist/react/access.mjs +133 -0
  18. package/dist/react/access.mjs.map +1 -0
  19. package/dist/react-dom-server-legacy-stub.d.mts +20 -0
  20. package/dist/react-dom-server-legacy-stub.d.mts.map +1 -0
  21. package/dist/react-dom-server-legacy-stub.mjs +25 -0
  22. package/dist/react-dom-server-legacy-stub.mjs.map +1 -0
  23. package/dist/react.d.mts +4 -6
  24. package/dist/react.d.mts.map +1 -1
  25. package/dist/react.mjs +1 -1
  26. package/dist/react.mjs.map +1 -1
  27. package/dist/seo-runtime.d.mts +1 -1
  28. package/dist/seo-runtime.mjs +8 -6
  29. package/dist/seo-runtime.mjs.map +1 -1
  30. package/dist/services/ssr-exclusion.d.mts +38 -0
  31. package/dist/services/ssr-exclusion.d.mts.map +1 -0
  32. package/dist/services/ssr-exclusion.mjs +0 -0
  33. package/dist/services/ssr-exclusion.mjs.map +1 -0
  34. package/dist/ssr.d.mts +37 -8
  35. package/dist/ssr.d.mts.map +1 -1
  36. package/dist/ssr.mjs +7 -4
  37. package/dist/ssr.mjs.map +1 -1
  38. package/dist/testing.d.mts +3 -2
  39. package/dist/testing.d.mts.map +1 -1
  40. package/dist/testing.mjs +20 -6
  41. package/dist/testing.mjs.map +1 -1
  42. package/dist/{type-generator-DFpha_Fp.mjs → type-generator-BVw8mj1y.mjs} +373 -64
  43. package/dist/type-generator-BVw8mj1y.mjs.map +1 -0
  44. package/dist/types-BltKoOR7.d.mts +193 -0
  45. package/dist/types-BltKoOR7.d.mts.map +1 -0
  46. package/dist/types-D-j_Ee_h.d.mts +52 -0
  47. package/dist/types-D-j_Ee_h.d.mts.map +1 -0
  48. package/dist/types-DzE1pdZs.d.mts.map +1 -1
  49. package/dist/vite.d.mts +19 -6
  50. package/dist/vite.d.mts.map +1 -1
  51. package/dist/vite.mjs +67 -5
  52. package/dist/vite.mjs.map +1 -1
  53. package/package.json +38 -27
  54. package/dist/type-generator-DFpha_Fp.mjs.map +0 -1
  55. package/dist/types-BhgXhWx6.d.mts +0 -82
  56. package/dist/types-BhgXhWx6.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"testing.mjs","names":[],"sources":["../src/augment/test-response.ts","../src/testing.ts"],"sourcesContent":["import type { Page } from '@inertiajs/core'\nimport { getValueAtPath, hasValueAtPath, TestResponse } from '@stratal/testing'\nimport { expect } from 'vitest'\n\ndeclare module '@stratal/testing' {\n interface TestResponse {\n /** Assert the response is an Inertia response. Optionally run a callback with the page object for custom assertions. */\n assertInertia(callback?: (page: Page) => void): Promise<this>\n /** Assert the Inertia page component matches the expected name. */\n assertInertiaComponent(component: string): Promise<this>\n /** Assert the Inertia page prop at the given dot-path equals the expected value. */\n assertInertiaProp(path: string, expected: unknown): Promise<this>\n /** Assert the Inertia page prop at the given dot-path exists. */\n assertInertiaPropExists(path: string): Promise<this>\n /** Assert the Inertia page prop at the given dot-path does not exist. */\n assertInertiaPropMissing(path: string): Promise<this>\n /** Assert the Inertia page URL matches the expected value. */\n assertInertiaUrl(url: string): Promise<this>\n /** Assert the Inertia page version matches the expected value. */\n assertInertiaVersion(version: string | null): Promise<this>\n /** Assert the Inertia page flash data contains the given key with the expected value. */\n assertInertiaFlash(key: string, value: unknown): Promise<this>\n /** Assert a prop is listed as deferred in the given group. */\n assertInertiaDeferredProp(prop: string, group: string): Promise<this>\n /** Assert a prop is listed as a merge prop. */\n assertInertiaMergeProp(prop: string): Promise<this>\n /** Assert a prop is listed as a shared prop. */\n assertInertiaSharedProp(prop: string): Promise<this>\n /** Assert the response is a successful precognition response (204 with precognition headers). */\n assertSuccessfulPrecognition(): this\n /** Assert the response is a precognition validation error (422 with precognition headers). Optionally assert specific errors. */\n assertPrecognitionValidationErrors(errors?: Record<string, string>): Promise<this>\n }\n}\n\nexport function augmentTestResponse(): void {\n TestResponse.macro('assertInertia', async function (this: TestResponse, callback?: (page: Page) => void) {\n this.assertHeader('x-inertia', 'true')\n this.assertOk()\n\n if (callback) {\n const page = await this.json<Page>()\n callback(page)\n }\n\n return this\n })\n\n TestResponse.macro('assertInertiaComponent', async function (this: TestResponse, component: string) {\n const page = await this.json<Page>()\n\n expect(\n page.component,\n `Expected Inertia component \"${component}\", got \"${page.component}\"`,\n ).toBe(component)\n\n return this\n })\n\n TestResponse.macro('assertInertiaProp', async function (this: TestResponse, path: string, expected: unknown) {\n const page = await this.json<Page>()\n const actual = getValueAtPath(page.props, path)\n\n expect(\n actual,\n `Expected Inertia prop \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected)\n\n return this\n })\n\n TestResponse.macro('assertInertiaPropExists', async function (this: TestResponse, path: string) {\n const page = await this.json<Page>()\n const exists = hasValueAtPath(page.props, path)\n\n expect(\n exists,\n `Expected Inertia prop \"${path}\" to exist`,\n ).toBe(true)\n\n return this\n })\n\n TestResponse.macro('assertInertiaPropMissing', async function (this: TestResponse, path: string) {\n const page = await this.json<Page>()\n const exists = hasValueAtPath(page.props, path)\n\n expect(\n exists,\n `Expected Inertia prop \"${path}\" to not exist`,\n ).toBe(false)\n\n return this\n })\n\n TestResponse.macro('assertInertiaUrl', async function (this: TestResponse, url: string) {\n const page = await this.json<Page>()\n\n expect(\n page.url,\n `Expected Inertia URL \"${url}\", got \"${page.url}\"`,\n ).toBe(url)\n\n return this\n })\n\n TestResponse.macro('assertInertiaVersion', async function (this: TestResponse, version: string | null) {\n const page = await this.json<Page>()\n\n expect(\n page.version,\n `Expected Inertia version \"${version}\", got \"${page.version}\"`,\n ).toBe(version)\n\n return this\n })\n\n TestResponse.macro('assertInertiaFlash', async function (this: TestResponse, key: string, value: unknown) {\n const page = await this.json<Page>()\n const actual = page.flash?.[key]\n\n expect(\n actual,\n `Expected Inertia flash \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(value)\n\n return this\n })\n\n TestResponse.macro('assertInertiaDeferredProp', async function (this: TestResponse, prop: string, group: string) {\n const page = await this.json<Page>()\n\n expect(\n page.deferredProps?.[group],\n `Expected Inertia deferred group \"${group}\" to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertInertiaMergeProp', async function (this: TestResponse, prop: string) {\n const page = await this.json<Page>()\n\n expect(\n page.mergeProps,\n `Expected Inertia mergeProps to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertInertiaSharedProp', async function (this: TestResponse, prop: string) {\n const page = await this.json<Page>()\n\n expect(\n page.sharedProps,\n `Expected Inertia sharedProps to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertSuccessfulPrecognition', function (this: TestResponse) {\n this.assertNoContent()\n this.assertHeader('Precognition', 'true')\n this.assertHeader('Precognition-Success', 'true')\n\n return this\n })\n\n TestResponse.macro('assertPrecognitionValidationErrors', async function (this: TestResponse, errors?: Record<string, string>) {\n this.assertUnprocessable()\n this.assertHeader('Precognition', 'true')\n\n if (errors) {\n const body = await this.json<{ errors: Record<string, string> }>()\n\n expect(\n body.errors,\n `Expected precognition errors to match ${JSON.stringify(errors)}, got ${JSON.stringify(body.errors)}`,\n ).toStrictEqual(errors)\n }\n\n return this\n })\n}\n","import { augmentTestResponse } from './augment/test-response'\n\n// Augmentation (side-effect import: augments TestResponse types)\nimport './augment/test-response'\n\n// Patch TestResponse.prototype with Inertia assertion methods\naugmentTestResponse()\n\n// Re-export useful types for test authors\nexport type { Page as InertiaPage } from '@inertiajs/core'\n"],"mappings":"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"}
1
+ {"version":3,"file":"testing.mjs","names":[],"sources":["../src/augment/test-response.ts","../src/testing.ts"],"sourcesContent":["import type { Page, ScrollProp } from '@inertiajs/core'\nimport { getValueAtPath, hasValueAtPath, TestResponse } from '@stratal/testing'\nimport { expect } from 'vitest'\n\ndeclare module '@stratal/testing' {\n interface TestResponse {\n /** Assert the response is an Inertia response. Optionally run a callback with the page object for custom assertions. */\n assertInertia(callback?: (page: Page) => void): Promise<this>\n /** Assert the Inertia page component matches the expected name. */\n assertInertiaComponent(component: string): Promise<this>\n /** Assert the Inertia page prop at the given dot-path equals the expected value. */\n assertInertiaProp(path: string, expected: unknown): Promise<this>\n /** Assert the Inertia page prop at the given dot-path exists. */\n assertInertiaPropExists(path: string): Promise<this>\n /** Assert the Inertia page prop at the given dot-path does not exist. */\n assertInertiaPropMissing(path: string): Promise<this>\n /** Assert the Inertia page URL matches the expected value. */\n assertInertiaUrl(url: string): Promise<this>\n /** Assert the Inertia page version matches the expected value. */\n assertInertiaVersion(version: string | null): Promise<this>\n /** Assert the Inertia page flash data contains the given key with the expected value. */\n assertInertiaFlash(key: string, value: unknown): Promise<this>\n /** Assert a prop is listed as deferred in the given group. */\n assertInertiaDeferredProp(prop: string, group: string): Promise<this>\n /** Assert a prop is listed as a merge prop. */\n assertInertiaMergeProp(prop: string): Promise<this>\n /** Assert a prop carries infinite-scroll metadata, optionally matching some of its fields. */\n assertInertiaScrollProp(prop: string, expected?: Partial<ScrollProp>): Promise<this>\n /** Assert a prop is listed as a shared prop. */\n assertInertiaSharedProp(prop: string): Promise<this>\n /** Assert the response is a successful precognition response (204 with precognition headers). */\n assertSuccessfulPrecognition(): this\n /** Assert the response is a precognition validation error (422 with precognition headers). Optionally assert specific errors. */\n assertPrecognitionValidationErrors(errors?: Record<string, string>): Promise<this>\n }\n}\n\nexport function augmentTestResponse(): void {\n TestResponse.macro('assertInertia', async function (this: TestResponse, callback?: (page: Page) => void) {\n this.assertHeader('x-inertia', 'true')\n this.assertOk()\n\n if (callback) {\n const page = await this.json<Page>()\n callback(page)\n }\n\n return this\n })\n\n TestResponse.macro('assertInertiaComponent', async function (this: TestResponse, component: string) {\n const page = await this.json<Page>()\n\n expect(\n page.component,\n `Expected Inertia component \"${component}\", got \"${page.component}\"`,\n ).toBe(component)\n\n return this\n })\n\n TestResponse.macro('assertInertiaProp', async function (this: TestResponse, path: string, expected: unknown) {\n const page = await this.json<Page>()\n const actual = getValueAtPath(page.props, path)\n\n expect(\n actual,\n `Expected Inertia prop \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected)\n\n return this\n })\n\n TestResponse.macro('assertInertiaPropExists', async function (this: TestResponse, path: string) {\n const page = await this.json<Page>()\n const exists = hasValueAtPath(page.props, path)\n\n expect(\n exists,\n `Expected Inertia prop \"${path}\" to exist`,\n ).toBe(true)\n\n return this\n })\n\n TestResponse.macro('assertInertiaPropMissing', async function (this: TestResponse, path: string) {\n const page = await this.json<Page>()\n const exists = hasValueAtPath(page.props, path)\n\n expect(\n exists,\n `Expected Inertia prop \"${path}\" to not exist`,\n ).toBe(false)\n\n return this\n })\n\n TestResponse.macro('assertInertiaUrl', async function (this: TestResponse, url: string) {\n const page = await this.json<Page>()\n\n expect(\n page.url,\n `Expected Inertia URL \"${url}\", got \"${page.url}\"`,\n ).toBe(url)\n\n return this\n })\n\n TestResponse.macro('assertInertiaVersion', async function (this: TestResponse, version: string | null) {\n const page = await this.json<Page>()\n\n expect(\n page.version,\n `Expected Inertia version \"${version}\", got \"${page.version}\"`,\n ).toBe(version)\n\n return this\n })\n\n TestResponse.macro('assertInertiaFlash', async function (this: TestResponse, key: string, value: unknown) {\n const page = await this.json<Page>()\n const actual = page.flash?.[key]\n\n expect(\n actual,\n `Expected Inertia flash \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(value)\n\n return this\n })\n\n TestResponse.macro('assertInertiaDeferredProp', async function (this: TestResponse, prop: string, group: string) {\n const page = await this.json<Page>()\n\n expect(\n page.deferredProps?.[group],\n `Expected Inertia deferred group \"${group}\" to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertInertiaMergeProp', async function (this: TestResponse, prop: string) {\n const page = await this.json<Page>()\n\n expect(\n page.mergeProps,\n `Expected Inertia mergeProps to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertInertiaScrollProp', async function (this: TestResponse, prop: string, expected?: Partial<ScrollProp>) {\n const page = await this.json<Page>()\n const actual = page.scrollProps?.[prop]\n\n expect(\n actual,\n `Expected Inertia scrollProps to contain \"${prop}\"`,\n ).toBeDefined()\n\n if (expected) {\n expect(\n actual,\n `Expected Inertia scroll prop \"${prop}\" to match ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toMatchObject(expected)\n }\n\n return this\n })\n\n TestResponse.macro('assertInertiaSharedProp', async function (this: TestResponse, prop: string) {\n const page = await this.json<Page>()\n\n expect(\n page.sharedProps,\n `Expected Inertia sharedProps to contain \"${prop}\"`,\n ).toContain(prop)\n\n return this\n })\n\n TestResponse.macro('assertSuccessfulPrecognition', function (this: TestResponse) {\n this.assertNoContent()\n this.assertHeader('Precognition', 'true')\n this.assertHeader('Precognition-Success', 'true')\n\n return this\n })\n\n TestResponse.macro('assertPrecognitionValidationErrors', async function (this: TestResponse, errors?: Record<string, string>) {\n this.assertUnprocessable()\n this.assertHeader('Precognition', 'true')\n\n if (errors) {\n const body = await this.json<{ errors: Record<string, string> }>()\n\n expect(\n body.errors,\n `Expected precognition errors to match ${JSON.stringify(errors)}, got ${JSON.stringify(body.errors)}`,\n ).toStrictEqual(errors)\n }\n\n return this\n })\n}\n","import { augmentTestResponse } from './augment/test-response'\n\n// Augmentation (side-effect import: augments TestResponse types)\nimport './augment/test-response'\n\n// Patch TestResponse.prototype with Inertia assertion methods\naugmentTestResponse()\n\n// Re-export useful types for test authors\nexport type { Page as InertiaPage } from '@inertiajs/core'\n"],"mappings":"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"}
@@ -22,15 +22,18 @@ async function createProject(tsConfigPath) {
22
22
  ts
23
23
  };
24
24
  }
25
+ const INERTIA_CALL_NAMES = ["inertia", "modal"];
25
26
  const WRAPPER_TYPE_NAMES = [
26
27
  "InertiaDeferredProp",
27
28
  "InertiaMergeProp",
29
+ "InertiaScrollProp",
28
30
  "InertiaOptionalProp",
29
31
  "InertiaOnceProp",
30
32
  "InertiaAlwaysProp"
31
33
  ];
32
34
  function extractControllerPageTypes(project, SK, tsObj, srcDir, pagesDir) {
33
35
  project.addSourceFilesAtPaths(join(srcDir, "**/*.ts"));
36
+ primeTranslationKeys(project);
34
37
  const pages = /* @__PURE__ */ new Map();
35
38
  for (const sourceFile of project.getSourceFiles()) {
36
39
  const filePath = sourceFile.getFilePath();
@@ -40,7 +43,7 @@ function extractControllerPageTypes(project, SK, tsObj, srcDir, pagesDir) {
40
43
  for (const call of callExpressions) {
41
44
  const expr = call.getExpression();
42
45
  if (!expr.isKind(SK.PropertyAccessExpression)) continue;
43
- if (expr.getName() !== "inertia") continue;
46
+ if (!INERTIA_CALL_NAMES.includes(expr.getName())) continue;
44
47
  const args = call.getArguments();
45
48
  if (args.length === 0) continue;
46
49
  const firstArg = args[0];
@@ -78,6 +81,8 @@ function extractControllerPageTypes(project, SK, tsObj, srcDir, pagesDir) {
78
81
  }).sort((a, b) => a.componentName.localeCompare(b.componentName));
79
82
  }
80
83
  function unwrapWrapperType(type, tsObj, fallbackLocation) {
84
+ const keyMatch = matchTranslationKeyReference(type, fallbackLocation);
85
+ if (keyMatch) return emitTranslationKeyMatch(keyMatch, false);
81
86
  if (type.isUnion()) {
82
87
  const unwrapped = type.getUnionTypes().filter((t) => {
83
88
  const text = t.getText(void 0, tsObj.TypeFormatFlags.NoTruncation);
@@ -111,7 +116,21 @@ function stripReadonly(type, tsObj, fallbackLocation) {
111
116
  }
112
117
  return typeToString(type, tsObj, fallbackLocation);
113
118
  }
119
+ /**
120
+ * Types whose `.share()` contributes a shared page prop. Matched on the
121
+ * receiver's resolved type rather than its source text, so `ctx.share()`,
122
+ * `this.inertia.share()`, and any aliasing are all recognised while an
123
+ * unrelated `.share()` that merely happens to sit on a variable named
124
+ * something inertia-ish is not.
125
+ */
126
+ const SHARE_RECEIVER_TYPE_NAMES = /* @__PURE__ */ new Set(["RouterContext", "InertiaService"]);
127
+ function isShareReceiver(node) {
128
+ const type = node.getType().getNonNullableType();
129
+ const name = type.getSymbol()?.getName() ?? type.getAliasSymbol()?.getName();
130
+ return name !== void 0 && SHARE_RECEIVER_TYPE_NAMES.has(name);
131
+ }
114
132
  function extractShareCallTypes(project, SK, tsObj, srcDir) {
133
+ primeTranslationKeys(project);
115
134
  const shareTypes = /* @__PURE__ */ new Map();
116
135
  for (const sourceFile of project.getSourceFiles()) {
117
136
  const filePath = sourceFile.getFilePath();
@@ -122,14 +141,14 @@ function extractShareCallTypes(project, SK, tsObj, srcDir) {
122
141
  const expr = call.getExpression();
123
142
  if (!expr.isKind(SK.PropertyAccessExpression)) continue;
124
143
  if (expr.getName() !== "share") continue;
125
- if (!expr.getExpression().getText().includes("inertia")) continue;
144
+ if (!isShareReceiver(expr.getExpression())) continue;
126
145
  const args = call.getArguments();
127
146
  if (args.length < 2) continue;
128
147
  const keyArg = args[0];
129
148
  if (!keyArg.isKind(SK.StringLiteral)) continue;
130
149
  const key = keyArg.getLiteralValue();
131
150
  if (shareTypes.has(key)) continue;
132
- const valueType = widenLiteralType(args[1].getType(), tsObj);
151
+ const valueType = unwrapWrapperType(args[1].getType(), tsObj, args[1]);
133
152
  shareTypes.set(key, valueType);
134
153
  }
135
154
  }
@@ -162,7 +181,7 @@ function resolveModuleOptionsLiteral(optionsArg, SK) {
162
181
  const unwrapped = unwrapAs(body, SK);
163
182
  if (unwrapped?.isKind(SK.ObjectLiteralExpression)) return unwrapped;
164
183
  if (body.isKind(SK.Block)) {
165
- const returnStatements = body.getDescendantsOfKind(SK.ReturnStatement);
184
+ const returnStatements = ownReturnStatements(initializer, body, SK);
166
185
  for (let i = returnStatements.length - 1; i >= 0; i--) {
167
186
  const expr = returnStatements[i].getExpression();
168
187
  if (!expr) continue;
@@ -262,6 +281,19 @@ function resolveToVariableDeclaration(identifier, SK) {
262
281
  }
263
282
  return null;
264
283
  }
284
+ /**
285
+ * The `return` statements the given function owns, skipping any that belong to a
286
+ * function nested inside it.
287
+ *
288
+ * A config factory's body is full of nested resolvers, and several of them
289
+ * return object literals of their own. Reading every descendant `return` picks
290
+ * one of those over the factory's, and the options literal is then whatever the
291
+ * last resolver happened to answer with.
292
+ */
293
+ function ownReturnStatements(fn, body, SK) {
294
+ const isFunctionLike = (node) => node.isKind(SK.ArrowFunction) || node.isKind(SK.FunctionExpression) || node.isKind(SK.FunctionDeclaration) || node.isKind(SK.MethodDeclaration);
295
+ return body.getDescendantsOfKind(SK.ReturnStatement).filter((ret) => ret.getFirstAncestor(isFunctionLike) === fn);
296
+ }
265
297
  function extractLiteralFromRegisterAs(varDecl, SK) {
266
298
  if (!varDecl.isKind(SK.VariableDeclaration)) return null;
267
299
  const init = varDecl.getInitializer();
@@ -278,7 +310,7 @@ function extractLiteralFromRegisterAs(varDecl, SK) {
278
310
  const unwrapped = unwrapAs(body, SK);
279
311
  if (unwrapped?.isKind(SK.ObjectLiteralExpression)) return unwrapped;
280
312
  if (body.isKind(SK.Block)) {
281
- const returnStatements = body.getDescendantsOfKind(SK.ReturnStatement);
313
+ const returnStatements = ownReturnStatements(factory, body, SK);
282
314
  for (let i = returnStatements.length - 1; i >= 0; i--) {
283
315
  const retExpr = returnStatements[i].getExpression();
284
316
  if (!retExpr) continue;
@@ -375,40 +407,104 @@ function extractFlashTypes(project, SK, tsObj, srcDir) {
375
407
  type
376
408
  })) };
377
409
  }
378
- function extractSharedDataType(project, SK, tsObj, moduleFilePath) {
379
- const callExpressions = (project.getSourceFile(moduleFilePath) ?? project.addSourceFileAtPath(moduleFilePath)).getDescendantsOfKind(SK.CallExpression);
380
- for (const call of callExpressions) {
381
- const expr = call.getExpression();
382
- if (!expr.isKind(SK.PropertyAccessExpression)) continue;
383
- const propName = expr.getName();
384
- if (propName !== "forRoot" && propName !== "forRootAsync") continue;
385
- const objExpr = expr.getExpression();
386
- if (!objExpr.isKind(SK.Identifier) || objExpr.getText() !== "InertiaModule") continue;
387
- const args = call.getArguments();
388
- if (args.length === 0) continue;
389
- const optionsLiteral = resolveModuleOptionsLiteral(args[0], SK);
390
- if (!optionsLiteral || !optionsLiteral.isKind(SK.ObjectLiteralExpression)) continue;
391
- const sharedDataProp = optionsLiteral.getProperty("sharedData");
392
- if (!sharedDataProp) continue;
393
- if (!sharedDataProp.isKind(SK.PropertyAssignment)) continue;
394
- const initializer = sharedDataProp.getInitializer();
395
- if (!initializer?.isKind(SK.ObjectLiteralExpression)) continue;
396
- const members = [];
397
- for (const prop of initializer.getProperties()) {
398
- if (!prop.isKind(SK.PropertyAssignment)) continue;
399
- const name = prop.getName();
400
- const value = prop.getInitializer();
401
- if (!value) continue;
402
- let valueType;
403
- if (value.isKind(SK.ArrowFunction) || value.isKind(SK.FunctionExpression)) valueType = typeToString(value.getReturnType(), tsObj);
404
- else valueType = typeToString(value.getType(), tsObj);
405
- members.push({
406
- name,
407
- type: valueType,
408
- optional: false
409
- });
410
+ /**
411
+ * Resolves the app's access control definition into permission strings and role
412
+ * names, for the generated `AccessControlRegistry`.
413
+ *
414
+ * Reads the `accessControl:` property of the `AuthModule.forRootAsync(...)` call
415
+ * in the app module. That is single-valued by construction — no ambiguity about
416
+ * which of several `createAccessControl` calls is in effect — and it targets the
417
+ * definition that is actually wired up.
418
+ *
419
+ * Everything goes through the type checker rather than static evaluation of
420
+ * object literals, so an inline definition, a separate-file const, a workspace
421
+ * package, and an `extendRole()`-composed role all resolve identically.
422
+ *
423
+ * @throws when `accessControl` is present but its resources don't resolve to
424
+ * string literals — emitting a `string`-shaped union would type-check every
425
+ * permission and quietly defeat the point.
426
+ */
427
+ function extractAccessControlType(project, SK, srcDir) {
428
+ const normalizedSrcDir = srcDir.replace(/\\/g, "/");
429
+ for (const sourceFile of project.getSourceFiles()) {
430
+ const filePath = sourceFile.getFilePath();
431
+ if (!filePath.startsWith(normalizedSrcDir)) continue;
432
+ if (filePath.includes("__tests__") || filePath.includes(".spec.") || filePath.includes(".test.")) continue;
433
+ if (!sourceFile.getFullText().includes("accessControl")) continue;
434
+ for (const call of sourceFile.getDescendantsOfKind(SK.CallExpression)) {
435
+ const expr = call.getExpression();
436
+ if (!expr.isKind(SK.PropertyAccessExpression)) continue;
437
+ if (expr.getName() !== "forRootAsync") continue;
438
+ const optionsArg = call.getArguments()[0];
439
+ if (!optionsArg) continue;
440
+ const optionsLiteral = optionsArg.isKind(SK.ObjectLiteralExpression) ? optionsArg : resolveConfigLiteralFromAsProvider(optionsArg, SK);
441
+ if (!optionsLiteral?.isKind(SK.ObjectLiteralExpression)) continue;
442
+ const property = optionsLiteral.getProperty("accessControl");
443
+ if (!property?.isKind(SK.PropertyAssignment)) continue;
444
+ const initializer = property.getInitializer();
445
+ if (!initializer) continue;
446
+ const acType = initializer.getType();
447
+ const statementsType = acType.getProperty("ac")?.getTypeAtLocation(initializer).getProperty("statements")?.getTypeAtLocation(initializer);
448
+ if (!statementsType) continue;
449
+ const permissions = [];
450
+ for (const resourceSymbol of statementsType.getProperties()) {
451
+ const resource = resourceSymbol.getName();
452
+ const actions = resourceSymbol.getTypeAtLocation(initializer).getTupleElements().map((element) => element.getLiteralValue()).filter((value) => typeof value === "string");
453
+ if (actions.length === 0) throw new Error(`@stratal/inertia: the \`accessControl\` resource "${resource}" in ${filePath} has an action list that could not be resolved to string literals. Declare its actions as a literal array of strings (e.g. \`${resource}: ['read', 'update']\`) so the generated AccessControlRegistry can check permission strings.`);
454
+ permissions.push(resource, `${resource}:*`, ...actions.map((action) => `${resource}:${action}`));
455
+ }
456
+ if (permissions.length === 0) throw new Error(`@stratal/inertia: the \`accessControl\` option in ${filePath} has no resources that could be resolved to literal names. Declare \`resources\` as an object literal (e.g. \`resources: { posts: ['read'] }\`) so the generated AccessControlRegistry can check permission strings.`);
457
+ const roles = acType.getProperty("roles")?.getTypeAtLocation(initializer).getProperties().map((symbol) => symbol.getName()) ?? [];
458
+ if (roles.length === 0) throw new Error(`@stratal/inertia: the \`accessControl\` option in ${filePath} has no roles that could be resolved to literal names. Declare \`roles\` as an object literal (e.g. \`roles: { admin: {...} }\`) so the generated AccessControlRegistry can check role names.`);
459
+ return {
460
+ permissions: permissions.sort((a, b) => a.localeCompare(b)),
461
+ roles: roles.sort((a, b) => a.localeCompare(b))
462
+ };
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+ function extractSharedDataType(project, SK, tsObj, srcDir) {
468
+ primeTranslationKeys(project);
469
+ const normalizedSrcDir = srcDir.replace(/\\/g, "/");
470
+ for (const sourceFile of project.getSourceFiles()) {
471
+ const filePath = sourceFile.getFilePath();
472
+ if (!filePath.startsWith(normalizedSrcDir)) continue;
473
+ if (filePath.includes("__tests__") || filePath.includes(".spec.") || filePath.includes(".test.")) continue;
474
+ if (!sourceFile.getFullText().includes("InertiaModule")) continue;
475
+ for (const call of sourceFile.getDescendantsOfKind(SK.CallExpression)) {
476
+ const expr = call.getExpression();
477
+ if (!expr.isKind(SK.PropertyAccessExpression)) continue;
478
+ const propName = expr.getName();
479
+ if (propName !== "forRoot" && propName !== "forRootAsync") continue;
480
+ const objExpr = expr.getExpression();
481
+ if (!objExpr.isKind(SK.Identifier) || objExpr.getText() !== "InertiaModule") continue;
482
+ const args = call.getArguments();
483
+ if (args.length === 0) continue;
484
+ const optionsLiteral = resolveModuleOptionsLiteral(args[0], SK) ?? resolveConfigLiteralFromAsProvider(args[0], SK);
485
+ if (!optionsLiteral || !optionsLiteral.isKind(SK.ObjectLiteralExpression)) continue;
486
+ const sharedDataProp = optionsLiteral.getProperty("sharedData");
487
+ if (!sharedDataProp) continue;
488
+ if (!sharedDataProp.isKind(SK.PropertyAssignment)) continue;
489
+ const initializer = sharedDataProp.getInitializer();
490
+ if (!initializer?.isKind(SK.ObjectLiteralExpression)) continue;
491
+ const members = [];
492
+ for (const prop of initializer.getProperties()) {
493
+ if (!prop.isKind(SK.PropertyAssignment)) continue;
494
+ const name = prop.getName();
495
+ const value = prop.getInitializer();
496
+ if (!value) continue;
497
+ let valueType;
498
+ if (value.isKind(SK.ArrowFunction) || value.isKind(SK.FunctionExpression)) valueType = unwrapPromise(value.getReturnType(), tsObj, value);
499
+ else valueType = typeToString(value.getType(), tsObj);
500
+ members.push({
501
+ name,
502
+ type: valueType,
503
+ optional: false
504
+ });
505
+ }
506
+ if (members.length > 0) return { members };
410
507
  }
411
- if (members.length > 0) return { members };
412
508
  }
413
509
  return null;
414
510
  }
@@ -434,8 +530,38 @@ function resolvePagePropsTypeNames(pages) {
434
530
  }
435
531
  return result;
436
532
  }
533
+ /**
534
+ * The `InertiaI18nConfig` narrowing this run emits — shared by the emitter and the
535
+ * in-program seed so both are byte-for-byte the same shape.
536
+ */
537
+ function inertiaI18nAugmentationLines(only) {
538
+ return [
539
+ " interface InertiaI18nConfig {",
540
+ ` translationKeys: import('stratal/i18n').FilterByPrefix<import('stratal/i18n').MessageKeys, ${only.map((p) => `'${p}'`).join(" | ")}>`,
541
+ " }"
542
+ ];
543
+ }
544
+ /**
545
+ * Register the `InertiaI18nConfig` narrowing this run is about to emit into the
546
+ * ts-morph program, so `InertiaTranslationKeys` resolves to the prefix-filtered
547
+ * union while page props are serialized — deterministically, whether or not a
548
+ * prior inertia.d.ts exists on disk. Written at `outputPath`, overwriting any stale
549
+ * augmentation the source glob loaded, so a clean regen and a seeded regen resolve
550
+ * identically. When i18n is off or no prefixes are configured, the file is emptied
551
+ * so a stale narrowing can't linger.
552
+ */
553
+ function seedInertiaI18nAugmentation(project, outputPath, i18n) {
554
+ const body = i18n.enabled && i18n.only.length > 0 ? [
555
+ "declare module '@stratal/inertia' {",
556
+ ...inertiaI18nAugmentationLines(i18n.only),
557
+ "}",
558
+ "export {}",
559
+ ""
560
+ ].join("\n") : "export {}\n";
561
+ project.createSourceFile(outputPath, body, { overwrite: true });
562
+ }
437
563
  function generateInertiaTypes(input) {
438
- const { pages, sharedData, shareCallTypes, i18n, flashTypes } = input;
564
+ const { pages, sharedData, shareCallTypes, i18n, flashTypes, accessControl } = input;
439
565
  const typeNames = resolvePagePropsTypeNames(pages);
440
566
  const lines = ["// Auto-generated by @stratal/inertia. Do not edit."];
441
567
  if (pages.length > 0) {
@@ -454,10 +580,13 @@ function generateInertiaTypes(input) {
454
580
  lines.push(` '${page.componentName}': ${typeName}`);
455
581
  }
456
582
  lines.push(" }");
457
- if (i18n.enabled && i18n.only.length > 0) {
458
- const prefixUnion = i18n.only.map((p) => `'${p}'`).join(" | ");
459
- lines.push(" interface InertiaI18nConfig {");
460
- lines.push(` translationKeys: import('stratal/i18n').FilterByPrefix<import('stratal/i18n').MessageKeys, ${prefixUnion}>`);
583
+ if (i18n.enabled && i18n.only.length > 0) lines.push(...inertiaI18nAugmentationLines(i18n.only));
584
+ if (accessControl) {
585
+ const permissionsUnion = accessControl.permissions.length > 0 ? accessControl.permissions.map((p) => `'${p}'`).join(" | ") : "never";
586
+ const rolesUnion = accessControl.roles.length > 0 ? accessControl.roles.map((r) => `'${r}'`).join(" | ") : "never";
587
+ lines.push(" interface AccessControlRegistry {");
588
+ lines.push(` permissions: ${permissionsUnion}`);
589
+ lines.push(` roles: ${rolesUnion}`);
461
590
  lines.push(" }");
462
591
  }
463
592
  lines.push("}");
@@ -467,15 +596,19 @@ function generateInertiaTypes(input) {
467
596
  configMembers.push(` flashDataType: { ${flashProps} }`);
468
597
  }
469
598
  const sharedMembers = [];
470
- if (sharedData) for (const member of sharedData.members) sharedMembers.push(` ${member.name}${member.optional ? "?" : ""}: ${member.type}`);
599
+ const declared = /* @__PURE__ */ new Set();
600
+ const declare = (name, member) => {
601
+ if (declared.has(name)) return;
602
+ declared.add(name);
603
+ sharedMembers.push(member);
604
+ };
605
+ if (sharedData) for (const member of sharedData.members) declare(member.name, ` ${member.name}${member.optional ? "?" : ""}: ${member.type}`);
471
606
  if (i18n.enabled) {
472
- sharedMembers.push(" locale: string");
473
- sharedMembers.push(" translations: Record<string, string>");
474
- }
475
- for (const [key, type] of shareCallTypes) {
476
- if (sharedData?.members.some((m) => m.name === key)) continue;
477
- sharedMembers.push(` ${key}?: ${type}`);
607
+ declare("locale", " locale: string");
608
+ declare("translations", " translations: Record<string, string>");
478
609
  }
610
+ if (accessControl) declare("access", " access: import('@stratal/inertia').SharedAccess");
611
+ for (const [key, type] of shareCallTypes) declare(key, ` ${key}?: ${type}`);
479
612
  if (sharedMembers.length > 0) configMembers.push(` sharedPageProps: {\n${sharedMembers.join("\n")}\n }`);
480
613
  if (configMembers.length > 0) {
481
614
  lines.push("");
@@ -494,14 +627,192 @@ function widenLiteralType(type, tsObj, fallbackLocation) {
494
627
  if (type.isBooleanLiteral()) return "boolean";
495
628
  return typeToString(type, tsObj, fallbackLocation);
496
629
  }
630
+ const TRANSLATION_KEYS_REFERENCE = "import('@stratal/inertia').InertiaTranslationKeys";
631
+ const MESSAGE_KEYS_REFERENCE_MODULE = "import('stratal/i18n')";
632
+ const MESSAGE_KEYS_REFERENCE = `${MESSAGE_KEYS_REFERENCE_MODULE}.MessageKeys`;
633
+ /**
634
+ * Per-project cache of the resolved key sets, populated by
635
+ * {@link primeTranslationKeys} before a walk so {@link matchTranslationKeyReference}
636
+ * can match structurally.
637
+ */
638
+ const translationKeysByProject = /* @__PURE__ */ new WeakMap();
639
+ function stringLiteralUnionMembers(type) {
640
+ const parts = type.isUnion() ? type.getUnionTypes() : [type];
641
+ const members = /* @__PURE__ */ new Set();
642
+ for (const part of parts) {
643
+ if (!part.isStringLiteral()) return null;
644
+ members.add(part.getLiteralValue());
645
+ }
646
+ return members.size > 0 ? members : null;
647
+ }
648
+ /**
649
+ * Resolve the project's key sets once, before walking any prop types. Resolving
650
+ * throwaway probe aliases forces each union to concrete string literals, so they
651
+ * match what prop types resolve to during the walk. Done up front — never mid-walk
652
+ * — so the probe file's add/remove can't invalidate the `Type` objects the
653
+ * recursion is holding.
654
+ *
655
+ * The probe file MUST live inside the source tree (`probeDir`): app message
656
+ * namespaces are registered via `declare module 'stratal/i18n'` augmentations in
657
+ * app source, which are only in scope for files under that tree. A probe at the
658
+ * project root sees only the framework's base `MessageKeys` (a few dozen keys), so
659
+ * every app key fails to match. Placed under `srcDir`, `MessageKeys` resolves to
660
+ * the app's full key set.
661
+ *
662
+ * The filtered probe is built from the detected `only` prefixes
663
+ * (`FilterByPrefix<MessageKeys, ...>`) rather than `InertiaTranslationKeys`: it does
664
+ * not depend on the consumer's `InertiaI18nConfig` augmentation being applied inside
665
+ * the probe, and matches what prefix-filtered props (the common case) resolve to
666
+ * during the walk. When no prefixes are configured the filtered set equals the full
667
+ * set, so fall back to `InertiaTranslationKeys` (which resolves to `MessageKeys`).
668
+ */
669
+ function primeTranslationKeys(project, only, probeDir = "") {
670
+ if (translationKeysByProject.has(project)) return;
671
+ const filteredRef = only && only.length > 0 ? `${MESSAGE_KEYS_REFERENCE_MODULE}.FilterByPrefix<${MESSAGE_KEYS_REFERENCE}, ${only.map((p) => `'${p}'`).join(" | ")}>` : TRANSLATION_KEYS_REFERENCE;
672
+ const probe = project.createSourceFile(join(probeDir, "__stratal_translation_keys_probe__.ts"), `export type __ProbeFiltered = ${filteredRef}\nexport type __ProbeFull = ${MESSAGE_KEYS_REFERENCE}\n`, { overwrite: true });
673
+ try {
674
+ const filtered = stringLiteralUnionMembers(probe.getTypeAliasOrThrow("__ProbeFiltered").getType());
675
+ const full = stringLiteralUnionMembers(probe.getTypeAliasOrThrow("__ProbeFull").getType());
676
+ translationKeysByProject.set(project, {
677
+ filtered,
678
+ full
679
+ });
680
+ } finally {
681
+ project.removeSourceFile(probe);
682
+ }
683
+ }
684
+ function setsEqual(a, b) {
685
+ if (!b || a.size !== b.size) return false;
686
+ for (const value of a) if (!b.has(value)) return false;
687
+ return true;
688
+ }
689
+ function isSubset(a, b) {
690
+ if (!b) return false;
691
+ for (const value of a) if (!b.has(value)) return false;
692
+ return true;
693
+ }
694
+ /**
695
+ * A string-literal union is treated as the translation-key type resolved with an
696
+ * incomplete set of message-namespace augmentations in scope (see
697
+ * {@link matchTranslationKeyReference}) rather than a hand-picked subset only when
698
+ * it is BOTH:
699
+ * - a large fraction ({@link TRANSLATION_KEY_COVERAGE}) of the full key set, and
700
+ * - large in absolute terms ({@link TRANSLATION_KEY_MIN_MEMBERS}).
701
+ *
702
+ * The fraction alone is not enough: in a project with only a handful of keys, a
703
+ * deliberate two-value enum can already be half the set. The absolute floor keeps
704
+ * such narrow enums inlined regardless of how small the key set is — an
705
+ * under-resolved translation-key union spans dozens to thousands of keys, while a
706
+ * hand-picked enum spans a few.
707
+ */
708
+ const TRANSLATION_KEY_COVERAGE = .5;
709
+ const TRANSLATION_KEY_MIN_MEMBERS = 24;
710
+ /**
711
+ * Detect whether `type` is (a nullable wrapper around) one of the message-key
712
+ * unions, and if so which reference to emit instead of inlining it. Inlining leaks
713
+ * the entire key union (hundreds of literals) into every prop that uses it.
714
+ *
715
+ * The alias survives on a directly-referenced type, but is dropped once the union
716
+ * is reached through nested object/array expansion — ts-morph then yields a bare
717
+ * string-literal union (optionally joined with `null`/`undefined`). So match the
718
+ * alias name when present (never gate on `isUnion()` — the `InertiaTranslationKeys`
719
+ * conditional alias can report `isUnion() === false`), and otherwise fall back to
720
+ * structural identity against the project's resolved key sets, ignoring `null` /
721
+ * `undefined` members (which the caller re-attaches). The filtered set is checked
722
+ * first so a prefix-filtered union stays `InertiaTranslationKeys`; a union equal to
723
+ * the full set references `MessageKeys` (widening it to the filtered alias would
724
+ * silently drop the unshared namespaces).
725
+ *
726
+ * Exact identity is not always reachable: app message namespaces are registered via
727
+ * `declare module 'stratal/i18n'` augmentations, and a prop declared in a file that
728
+ * doesn't transitively import every namespace resolves `InertiaTranslationKeys` to a
729
+ * strict *subset* of the primed set — with no alias to recover (the value flows in
730
+ * through an inferred property access). So collapse any union that is a subset of a
731
+ * key set and clears the {@link TRANSLATION_KEY_COVERAGE} /
732
+ * {@link TRANSLATION_KEY_MIN_MEMBERS} thresholds *measured against that same set* —
733
+ * the filtered set for an `InertiaTranslationKeys` union, the full set for a
734
+ * `MessageKeys` union — to the type the source actually declares. Measuring each
735
+ * against its own set keeps a narrow prefix filter from shrinking the fraction below
736
+ * the threshold, and stops a non-frontend subset from being widened through the
737
+ * small filtered denominator. Small hand-picked key enums fall below the thresholds
738
+ * and stay inlined.
739
+ */
740
+ function matchTranslationKeyReference(type, fallbackLocation) {
741
+ const alias = type.getAliasSymbol?.()?.getName();
742
+ if (alias === "InertiaTranslationKeys") return {
743
+ ref: TRANSLATION_KEYS_REFERENCE,
744
+ hasNull: false,
745
+ hasUndefined: false
746
+ };
747
+ if (alias === "MessageKeys") return {
748
+ ref: MESSAGE_KEYS_REFERENCE,
749
+ hasNull: false,
750
+ hasUndefined: false
751
+ };
752
+ if (!fallbackLocation) return null;
753
+ const sets = translationKeysByProject.get(fallbackLocation.getProject());
754
+ if (!sets) return null;
755
+ const parts = type.isUnion() ? type.getUnionTypes() : [type];
756
+ let hasNull = false;
757
+ let hasUndefined = false;
758
+ const members = /* @__PURE__ */ new Set();
759
+ for (const part of parts) {
760
+ if (part.isNull()) {
761
+ hasNull = true;
762
+ continue;
763
+ }
764
+ if (part.isUndefined()) {
765
+ hasUndefined = true;
766
+ continue;
767
+ }
768
+ if (!part.isStringLiteral()) return null;
769
+ members.add(part.getLiteralValue());
770
+ }
771
+ if (members.size === 0) return null;
772
+ if (setsEqual(members, sets.filtered)) return {
773
+ ref: TRANSLATION_KEYS_REFERENCE,
774
+ hasNull,
775
+ hasUndefined
776
+ };
777
+ if (setsEqual(members, sets.full)) return {
778
+ ref: MESSAGE_KEYS_REFERENCE,
779
+ hasNull,
780
+ hasUndefined
781
+ };
782
+ if (sets.filtered && isSubset(members, sets.filtered) && members.size >= TRANSLATION_KEY_MIN_MEMBERS && members.size >= TRANSLATION_KEY_COVERAGE * sets.filtered.size) return {
783
+ ref: TRANSLATION_KEYS_REFERENCE,
784
+ hasNull,
785
+ hasUndefined
786
+ };
787
+ if (sets.full && isSubset(members, sets.full) && members.size >= TRANSLATION_KEY_MIN_MEMBERS && members.size >= TRANSLATION_KEY_COVERAGE * sets.full.size) return {
788
+ ref: MESSAGE_KEYS_REFERENCE,
789
+ hasNull,
790
+ hasUndefined
791
+ };
792
+ return null;
793
+ }
794
+ /**
795
+ * Render a key-union match, re-attaching `null` (and `undefined`, unless the `?`
796
+ * property marker already implies it) that {@link matchTranslationKeyReference}
797
+ * stripped for comparison.
798
+ */
799
+ function emitTranslationKeyMatch(match, undefinedImplied) {
800
+ const prefixes = [];
801
+ if (match.hasNull) prefixes.push("null");
802
+ if (match.hasUndefined && !undefinedImplied) prefixes.push("undefined");
803
+ return prefixes.length > 0 ? `${prefixes.join(" | ")} | ${match.ref}` : match.ref;
804
+ }
497
805
  function typeToString(type, tsObj, fallbackLocation) {
498
- if (type.isUnion() && type.getAliasSymbol?.()?.getName() === "MessageKeys") return "import('@stratal/inertia').InertiaTranslationKeys";
806
+ const keyMatch = matchTranslationKeyReference(type, fallbackLocation);
807
+ if (keyMatch) return emitTranslationKeyMatch(keyMatch, false);
499
808
  if (type.isObject() || type.isUnion() || type.isIntersection()) return expandTypeToInline(type, tsObj, fallbackLocation);
500
809
  const text = type.getText(void 0, tsObj.TypeFormatFlags.NoTruncation | tsObj.TypeFormatFlags.UseFullyQualifiedType);
501
810
  if (text.includes("import(")) return expandTypeToInline(type, tsObj, fallbackLocation);
502
811
  return text;
503
812
  }
504
813
  function expandPropertyType(type, tsObj, fallbackLocation, visiting, isOptional) {
814
+ const keyMatch = matchTranslationKeyReference(type, fallbackLocation);
815
+ if (keyMatch) return emitTranslationKeyMatch(keyMatch, isOptional);
505
816
  if (isOptional && type.isUnion()) {
506
817
  const parts = type.getUnionTypes().filter((t) => !t.isUndefined());
507
818
  if (parts.length === 0) return "undefined";
@@ -515,6 +826,8 @@ function expandTypeToInline(type, tsObj, fallbackLocation, visiting = /* @__PURE
515
826
  if (type.isBoolean()) return "boolean";
516
827
  visiting.add(type);
517
828
  try {
829
+ const keyMatch = matchTranslationKeyReference(type, fallbackLocation);
830
+ if (keyMatch) return emitTranslationKeyMatch(keyMatch, false);
518
831
  if (type.isObject() && !type.isArray() && !type.isReadonlyArray()) {
519
832
  const symbolName = type.getSymbol()?.getName();
520
833
  const text = type.getText(void 0, tsObj.TypeFormatFlags.NoTruncation | tsObj.TypeFormatFlags.UseFullyQualifiedType);
@@ -540,10 +853,7 @@ function expandTypeToInline(type, tsObj, fallbackLocation, visiting = /* @__PURE
540
853
  return type.isReadonlyArray() ? `ReadonlyArray<${inner}>` : `Array<${inner}>`;
541
854
  }
542
855
  }
543
- if (type.isUnion()) {
544
- if (type.getAliasSymbol?.()?.getName() === "MessageKeys") return "import('@stratal/inertia').InertiaTranslationKeys";
545
- return type.getUnionTypes().map((t) => expandTypeToInline(t, tsObj, fallbackLocation, visiting)).join(" | ");
546
- }
856
+ if (type.isUnion()) return type.getUnionTypes().map((t) => expandTypeToInline(t, tsObj, fallbackLocation, visiting)).join(" | ");
547
857
  if (type.isIntersection()) return type.getIntersectionTypes().map((t) => expandTypeToInline(t, tsObj, fallbackLocation, visiting)).join(" & ");
548
858
  const text = type.getText(void 0, tsObj.TypeFormatFlags.NoTruncation);
549
859
  if (text.includes("import(")) return "unknown";
@@ -562,9 +872,6 @@ function writeInertiaTypes(outputPath, content) {
562
872
  renameSync(tmpPath, outputPath);
563
873
  return true;
564
874
  }
565
- function findAppModulePath(cwd) {
566
- return [join(cwd, "src", "app.module.ts"), join(cwd, "src", "app.module.tsx")].find(existsSync);
567
- }
568
875
  function findPagesDir(cwd) {
569
876
  return join(cwd, "src", "inertia", "pages");
570
877
  }
@@ -579,17 +886,19 @@ async function runTypeGeneration(cwd) {
579
886
  const pagesDir = findPagesDir(cwd);
580
887
  const srcDir = join(cwd, "src");
581
888
  const outputPath = findOutputPath(cwd);
582
- const moduleFilePath = findAppModulePath(cwd);
583
889
  const { project, SyntaxKind, ts } = await createProject(findTsConfigPath(cwd));
584
- const pages = extractControllerPageTypes(project, SyntaxKind, ts, srcDir, pagesDir);
585
- const sharedData = moduleFilePath ? extractSharedDataType(project, SyntaxKind, ts, moduleFilePath) : null;
890
+ project.addSourceFilesAtPaths(join(srcDir, "**/*.ts"));
586
891
  const i18n = detectI18nConfig(project, SyntaxKind, srcDir);
892
+ seedInertiaI18nAugmentation(project, outputPath, i18n);
893
+ primeTranslationKeys(project, i18n.only, srcDir);
894
+ const pages = extractControllerPageTypes(project, SyntaxKind, ts, srcDir, pagesDir);
587
895
  writeInertiaTypes(outputPath, generateInertiaTypes({
588
896
  pages,
589
- sharedData,
897
+ sharedData: extractSharedDataType(project, SyntaxKind, ts, srcDir),
590
898
  shareCallTypes: extractShareCallTypes(project, SyntaxKind, ts, srcDir),
591
899
  i18n,
592
- flashTypes: extractFlashTypes(project, SyntaxKind, ts, srcDir)
900
+ flashTypes: extractFlashTypes(project, SyntaxKind, ts, srcDir),
901
+ accessControl: extractAccessControlType(project, SyntaxKind, srcDir)
593
902
  }));
594
903
  return {
595
904
  outputPath,
@@ -599,4 +908,4 @@ async function runTypeGeneration(cwd) {
599
908
  //#endregion
600
909
  export { runTypeGeneration as n, findPagesDir as t };
601
910
 
602
- //# sourceMappingURL=type-generator-DFpha_Fp.mjs.map
911
+ //# sourceMappingURL=type-generator-BVw8mj1y.mjs.map