@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.
- package/CHANGELOG.md +379 -0
- package/README.md +158 -14
- package/dist/build-seo-tags-DBsHKxX9.mjs.map +1 -1
- package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
- package/dist/generator/type-generator.worker.d.mts +1 -1
- package/dist/generator/type-generator.worker.mjs +1 -1
- package/dist/index.d.mts +214 -92
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +390 -130
- package/dist/index.mjs.map +1 -1
- package/dist/quarry.d.mts +6 -8
- package/dist/quarry.d.mts.map +1 -1
- package/dist/quarry.mjs +127 -12
- package/dist/quarry.mjs.map +1 -1
- package/dist/react/access.d.mts +95 -0
- package/dist/react/access.d.mts.map +1 -0
- package/dist/react/access.mjs +133 -0
- package/dist/react/access.mjs.map +1 -0
- package/dist/react-dom-server-legacy-stub.d.mts +20 -0
- package/dist/react-dom-server-legacy-stub.d.mts.map +1 -0
- package/dist/react-dom-server-legacy-stub.mjs +25 -0
- package/dist/react-dom-server-legacy-stub.mjs.map +1 -0
- package/dist/react.d.mts +4 -6
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/seo-runtime.d.mts +1 -1
- package/dist/seo-runtime.mjs +8 -6
- package/dist/seo-runtime.mjs.map +1 -1
- package/dist/services/ssr-exclusion.d.mts +38 -0
- package/dist/services/ssr-exclusion.d.mts.map +1 -0
- package/dist/services/ssr-exclusion.mjs +0 -0
- package/dist/services/ssr-exclusion.mjs.map +1 -0
- package/dist/ssr.d.mts +37 -8
- package/dist/ssr.d.mts.map +1 -1
- package/dist/ssr.mjs +7 -4
- package/dist/ssr.mjs.map +1 -1
- package/dist/testing.d.mts +3 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +20 -6
- package/dist/testing.mjs.map +1 -1
- package/dist/{type-generator-DFpha_Fp.mjs → type-generator-BVw8mj1y.mjs} +373 -64
- package/dist/type-generator-BVw8mj1y.mjs.map +1 -0
- package/dist/types-BltKoOR7.d.mts +193 -0
- package/dist/types-BltKoOR7.d.mts.map +1 -0
- package/dist/types-D-j_Ee_h.d.mts +52 -0
- package/dist/types-D-j_Ee_h.d.mts.map +1 -0
- package/dist/types-DzE1pdZs.d.mts.map +1 -1
- package/dist/vite.d.mts +19 -6
- package/dist/vite.d.mts.map +1 -1
- package/dist/vite.mjs +67 -5
- package/dist/vite.mjs.map +1 -1
- package/package.json +38 -27
- package/dist/type-generator-DFpha_Fp.mjs.map +0 -1
- package/dist/types-BhgXhWx6.d.mts +0 -82
- package/dist/types-BhgXhWx6.d.mts.map +0 -1
package/dist/testing.mjs.map
CHANGED
|
@@ -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()
|
|
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()
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
911
|
+
//# sourceMappingURL=type-generator-BVw8mj1y.mjs.map
|