@happyvertical/smrt-mobile-contract 0.37.7
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/AGENTS.md +53 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +99 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.js +711 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/smrt-knowledge.json +38 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +0 -0
- package/package.json +49 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @happyvertical/smrt-mobile-contract
|
|
2
|
+
|
|
3
|
+
TypeScript codegen bridging the SMRT manifest to the KMP mobile foundation
|
|
4
|
+
(ADR 0001, epic #1745). Generalizes amaru's `amaru-mobile-contract`
|
|
5
|
+
(manifest → allowlist → contract → Kotlin) and reporter's Swift emission.
|
|
6
|
+
Zero runtime dependencies by design — it reads `manifest.json` files
|
|
7
|
+
directly, so it stays out of the smrt package DAG.
|
|
8
|
+
|
|
9
|
+
## Two generation surfaces
|
|
10
|
+
|
|
11
|
+
1. **Framework contract** (`frameworkKotlinFiles()` / `frameworkSwiftFiles()`)
|
|
12
|
+
— the stable files checked into `@happyvertical/smrt-mobile` under
|
|
13
|
+
`src/commonMain/kotlin/.../contract/` (support types, pack localization,
|
|
14
|
+
`/api/mobile/*` auth/session/device DTOs). This package is their single
|
|
15
|
+
source of truth: `pnpm generate:framework` rewrites them; `pnpm build`
|
|
16
|
+
verifies freshness (`scripts/generate-framework.mjs`). (The script is
|
|
17
|
+
deliberately NOT named `generate` — turbo's `build` task auto-runs a
|
|
18
|
+
`generate` script, which would rewrite the files on every build and
|
|
19
|
+
neutralize the freshness gate.) The Swift mirror
|
|
20
|
+
(`MobileContract.swift`) is emitted on demand for SwiftUI apps and is not
|
|
21
|
+
checked into smrt-mobile (iOS wiring is Phase 6).
|
|
22
|
+
2. **Per-app domain DTOs** (`buildMobileContract()` +
|
|
23
|
+
`generateKotlinDtoFiles()` / `generateSwiftDtoFile()`) — consumer apps
|
|
24
|
+
pass their SMRT `manifest.json`(s) plus an object allowlist and a Kotlin
|
|
25
|
+
package; they get `@Serializable` Kotlin DTOs (defaults on every field for
|
|
26
|
+
safe partial deserialization) and plain Swift structs. Type mapping is
|
|
27
|
+
driven by manifest field `type` metadata (`text`/`integer`/`decimal`/
|
|
28
|
+
`boolean`/`datetime`/`foreignKey`/`crossPackageRef`/`json`), with amaru's
|
|
29
|
+
name heuristics only as a fallback for typeless fields. Relationship
|
|
30
|
+
declarations (`oneToMany`/`manyToMany`/`hasMany`) are excluded — they are
|
|
31
|
+
not scalar columns. Field defaults come from `default` (current manifests)
|
|
32
|
+
with `defaultValue` (amaru-era) as fallback. Decimals emit as
|
|
33
|
+
`DecimalString` (string-encoded — no float precision loss); field names
|
|
34
|
+
stay manifest-faithful (wire-format names, including `created_at`).
|
|
35
|
+
|
|
36
|
+
## Conventions
|
|
37
|
+
|
|
38
|
+
- `DecimalString`/`Measurement` and the auth contract types are imported from
|
|
39
|
+
`com.happyvertical.smrt.mobile.contract`, never re-emitted per app.
|
|
40
|
+
- Allowlist entries may be short names or qualified names
|
|
41
|
+
(`@scope/pkg:Object`); ambiguous short names across manifests are an error.
|
|
42
|
+
- `sourceHash` (sha256 over allowlist + projected objects) is stamped into
|
|
43
|
+
generated output so drift is detectable.
|
|
44
|
+
- `turbo.json` here widens the build inputs to smrt-mobile's checked-in
|
|
45
|
+
contract dir so the freshness verify can't replay stale from cache.
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm build # vite build + verify smrt-mobile framework contract freshness
|
|
51
|
+
pnpm generate:framework # vite build + REWRITE the smrt-mobile framework contract
|
|
52
|
+
pnpm test # vitest
|
|
53
|
+
```
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @happyvertical/smrt-mobile-contract
|
|
2
|
+
|
|
3
|
+
Codegen for the SMRT mobile foundation: projects a SMRT `manifest.json`
|
|
4
|
+
through a mobile allowlist into typed Kotlin (`@Serializable` DTOs for the
|
|
5
|
+
KMP module) and Swift (plain structs) — plus the generated framework
|
|
6
|
+
contract files checked into
|
|
7
|
+
[`@happyvertical/smrt-mobile`](../smrt-mobile/README.md).
|
|
8
|
+
|
|
9
|
+
Design: [ADR 0001](../../docs/content/adr/0001-kmp-mobile-foundation.md);
|
|
10
|
+
epic happyvertical/smrt#1745. Agent/developer guidance:
|
|
11
|
+
[AGENTS.md](./AGENTS.md). Zero runtime dependencies by design — it reads
|
|
12
|
+
`manifest.json` files directly and stays out of the smrt package DAG.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add -D @happyvertical/smrt-mobile-contract
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage — per-app domain DTOs
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
buildMobileContract,
|
|
25
|
+
generateKotlinDtoFiles,
|
|
26
|
+
generateSwiftDtoFile,
|
|
27
|
+
writeFileSet,
|
|
28
|
+
} from '@happyvertical/smrt-mobile-contract';
|
|
29
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
30
|
+
|
|
31
|
+
const manifest = JSON.parse(
|
|
32
|
+
readFileSync('packages/my-objects/src/manifest/manifest.json', 'utf8'),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const contract = buildMobileContract({
|
|
36
|
+
manifests: [manifest], // one or more SMRT manifests
|
|
37
|
+
allowlist: ['FieldReport', 'Attachment'], // short or qualified names
|
|
38
|
+
kotlinPackage: 'com.example.mobile.generated',
|
|
39
|
+
sourceLabel: 'packages/my-objects/src/manifest/manifest.json',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
writeFileSet(
|
|
43
|
+
'apps/mobile/shared/src/commonMain/kotlin/generated',
|
|
44
|
+
generateKotlinDtoFiles(contract),
|
|
45
|
+
);
|
|
46
|
+
writeFileSync(
|
|
47
|
+
'apps/mobile/iosApp/App/GeneratedMobileDtos.swift',
|
|
48
|
+
generateSwiftDtoFile(contract),
|
|
49
|
+
);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Allowlist rules
|
|
53
|
+
|
|
54
|
+
- Entries may be short names (`FieldReport`) or qualified names
|
|
55
|
+
(`@scope/pkg:FieldReport`). A short name that matches objects in more than
|
|
56
|
+
one manifest is an error — use the qualified name.
|
|
57
|
+
- Two allowlisted objects may not share a short name: generated `<Name>Dto`
|
|
58
|
+
files would overwrite each other, so the build throws instead.
|
|
59
|
+
- Relationship declarations (`oneToMany`/`manyToMany`/`hasMany`) are excluded
|
|
60
|
+
from DTO projection — they are not scalar columns.
|
|
61
|
+
|
|
62
|
+
### Type mapping
|
|
63
|
+
|
|
64
|
+
| Manifest `type` | Kotlin | Swift |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `text` / `status` / `foreignKey` / `crossPackageRef` | `String` | `String` |
|
|
67
|
+
| `integer` | `Long` (64-bit — byte sizes/epochs overflow `Int`) | `Int64` |
|
|
68
|
+
| `decimal` | `DecimalString?` (string-encoded, no float precision loss) | `String?` |
|
|
69
|
+
| `boolean` | `Boolean` | `Bool` |
|
|
70
|
+
| `datetime` | `Instant?` | `String?` |
|
|
71
|
+
| `json` | `JsonObject` | `String` (raw JSON) |
|
|
72
|
+
|
|
73
|
+
Field defaults come from `default` (current manifests) with `defaultValue`
|
|
74
|
+
(amaru-era) as a fallback; every generated Kotlin field default-initializes
|
|
75
|
+
for safe partial deserialization. Field names stay manifest-faithful
|
|
76
|
+
(wire-format names, including `created_at`); keyword collisions are
|
|
77
|
+
backtick-escaped per language. `sourceHash` (sha256 over allowlist +
|
|
78
|
+
projected objects) is stamped into all output so drift is detectable.
|
|
79
|
+
|
|
80
|
+
## Usage — the framework contract
|
|
81
|
+
|
|
82
|
+
The stable framework contract (support types, pack localization, and the
|
|
83
|
+
`/api/mobile` auth/session/device DTOs) is emitted by this package and
|
|
84
|
+
checked into `smrt-mobile`'s `contract/` directory. This package's `build`
|
|
85
|
+
byte-verifies those files; regenerate after editing `src/emit-framework.ts`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pnpm --filter @happyvertical/smrt-mobile-contract generate:framework
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The Swift mirror (`frameworkSwiftFiles()`) is emitted on demand for SwiftUI
|
|
92
|
+
apps; a parity test keeps it field-for-field in sync with the Kotlin side.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pnpm build # vite build + framework contract freshness verify
|
|
98
|
+
pnpm test # vitest (goldens, mapping rules, parity, file-set round-trip)
|
|
99
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export declare function buildMobileContract(options: MobileContractOptions): MobileContract;
|
|
2
|
+
|
|
3
|
+
/** Kotlin framework contract files, keyed by file name. */
|
|
4
|
+
export declare function frameworkKotlinFiles(): Map<string, string>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Swift mirror of the framework contract for SwiftUI apps (reporter
|
|
8
|
+
* precedent). Not checked into smrt-mobile — iOS wiring lands with
|
|
9
|
+
* smrt-ios (Phase 6); apps can emit it via this API meanwhile.
|
|
10
|
+
*/
|
|
11
|
+
export declare function frameworkSwiftFiles(): Map<string, string>;
|
|
12
|
+
|
|
13
|
+
export declare const GENERATED_HEADER: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Emits one Kotlin DTO file per allowlisted object, plus a
|
|
17
|
+
* `MobileContractManifest.kt` recording the contract identity, all in the
|
|
18
|
+
* consumer's Kotlin package. Framework support types (DecimalString, …) are
|
|
19
|
+
* imported from `com.happyvertical.smrt.mobile.contract`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateKotlinDtoFiles(contract: MobileContract): Map<string, string>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Emits all allowlisted domain DTOs into a single Swift file. Structs are
|
|
25
|
+
* plain (no Codable) per the reporter seed — apps decode JSON as needed.
|
|
26
|
+
* Kotlin `JsonObject` fields arrive as raw JSON strings on the Swift side.
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateSwiftDtoFile(contract: MobileContract): string;
|
|
29
|
+
|
|
30
|
+
export declare interface MobileContract {
|
|
31
|
+
schemaVersion: number;
|
|
32
|
+
contractVersion: string;
|
|
33
|
+
sourceLabel: string;
|
|
34
|
+
sourceHash: string;
|
|
35
|
+
objectAllowlist: string[];
|
|
36
|
+
objectCount: number;
|
|
37
|
+
kotlin: {
|
|
38
|
+
packageName: string;
|
|
39
|
+
};
|
|
40
|
+
typeMappings: Record<string, string>;
|
|
41
|
+
objects: MobileContractObject[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export declare interface MobileContractField {
|
|
45
|
+
/** Manifest-faithful wire name (also the generated property name). */
|
|
46
|
+
name: string;
|
|
47
|
+
kotlinType: string;
|
|
48
|
+
swiftType: string;
|
|
49
|
+
nullable: boolean;
|
|
50
|
+
/** Kotlin default-value expression, or null for required (id). */
|
|
51
|
+
kotlinDefault: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export declare interface MobileContractObject {
|
|
55
|
+
name: string;
|
|
56
|
+
qualifiedName: string;
|
|
57
|
+
dtoName: string;
|
|
58
|
+
tableName: string | null;
|
|
59
|
+
hasTenantId: boolean;
|
|
60
|
+
fields: MobileContractField[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export declare interface MobileContractOptions {
|
|
64
|
+
manifests: SmrtManifest[];
|
|
65
|
+
/** Object names (or qualified names) exposed to mobile. */
|
|
66
|
+
allowlist: string[];
|
|
67
|
+
/** Kotlin package the domain DTOs are emitted into. */
|
|
68
|
+
kotlinPackage: string;
|
|
69
|
+
/** Provenance label recorded in the contract (e.g. the manifest path). */
|
|
70
|
+
sourceLabel?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Schema version of the mobile-contract.json envelope. */
|
|
74
|
+
export declare const SMRT_MOBILE_CONTRACT_SCHEMA_VERSION = 1;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Version stamp of the framework contract surface (auth/session/device/pack
|
|
78
|
+
* localization types). Bump when the framework contract shape changes.
|
|
79
|
+
*/
|
|
80
|
+
export declare const SMRT_MOBILE_CONTRACT_VERSION = "2026-07-01.v1";
|
|
81
|
+
|
|
82
|
+
/** The subset of a SMRT `manifest.json` the codegen consumes. */
|
|
83
|
+
export declare interface SmrtManifest {
|
|
84
|
+
version?: number | string;
|
|
85
|
+
packageName?: string;
|
|
86
|
+
objects: Record<string, SmrtManifestObject> | SmrtManifestObject[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Shapes for the SMRT mobile contract pipeline:
|
|
91
|
+
* manifest.json (+ allowlist) → MobileContract → generated Kotlin/Swift DTOs.
|
|
92
|
+
*/
|
|
93
|
+
/** One field entry in a SMRT manifest's `objects[*].fields` map. */
|
|
94
|
+
export declare interface SmrtManifestField {
|
|
95
|
+
type?: string;
|
|
96
|
+
required?: boolean;
|
|
97
|
+
related?: string;
|
|
98
|
+
/** Current SMRT manifests emit `default`; amaru-era manifests used `defaultValue`. */
|
|
99
|
+
default?: unknown;
|
|
100
|
+
defaultValue?: unknown;
|
|
101
|
+
_meta?: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One object entry in a SMRT manifest (model or collection). */
|
|
105
|
+
export declare interface SmrtManifestObject {
|
|
106
|
+
name: string;
|
|
107
|
+
className?: string;
|
|
108
|
+
qualifiedName?: string;
|
|
109
|
+
collection?: string | null;
|
|
110
|
+
packageName?: string;
|
|
111
|
+
fields?: Record<string, SmrtManifestField>;
|
|
112
|
+
schema?: {
|
|
113
|
+
tableName?: string;
|
|
114
|
+
} | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Compares every entry against disk. Returns the list of missing/stale
|
|
119
|
+
* relative paths (empty = fresh).
|
|
120
|
+
*/
|
|
121
|
+
export declare function verifyFileSet(rootDir: string, files: Map<string, string>): string[];
|
|
122
|
+
|
|
123
|
+
/** Writes every `relPath → content` entry under `rootDir`. */
|
|
124
|
+
export declare function writeFileSet(rootDir: string, files: Map<string, string>): void;
|
|
125
|
+
|
|
126
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
//#region src/contract-version.ts
|
|
5
|
+
var SMRT_MOBILE_CONTRACT_SCHEMA_VERSION = 1;
|
|
6
|
+
var SMRT_MOBILE_CONTRACT_VERSION = "2026-07-01.v1";
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/identifiers.ts
|
|
9
|
+
var KOTLIN_HARD_KEYWORDS = /* @__PURE__ */ new Set([
|
|
10
|
+
"as",
|
|
11
|
+
"break",
|
|
12
|
+
"class",
|
|
13
|
+
"continue",
|
|
14
|
+
"do",
|
|
15
|
+
"else",
|
|
16
|
+
"false",
|
|
17
|
+
"for",
|
|
18
|
+
"fun",
|
|
19
|
+
"if",
|
|
20
|
+
"in",
|
|
21
|
+
"interface",
|
|
22
|
+
"is",
|
|
23
|
+
"null",
|
|
24
|
+
"object",
|
|
25
|
+
"package",
|
|
26
|
+
"return",
|
|
27
|
+
"super",
|
|
28
|
+
"this",
|
|
29
|
+
"throw",
|
|
30
|
+
"true",
|
|
31
|
+
"try",
|
|
32
|
+
"typealias",
|
|
33
|
+
"typeof",
|
|
34
|
+
"val",
|
|
35
|
+
"var",
|
|
36
|
+
"when",
|
|
37
|
+
"while"
|
|
38
|
+
]);
|
|
39
|
+
var SWIFT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
40
|
+
"associatedtype",
|
|
41
|
+
"as",
|
|
42
|
+
"break",
|
|
43
|
+
"case",
|
|
44
|
+
"catch",
|
|
45
|
+
"class",
|
|
46
|
+
"continue",
|
|
47
|
+
"default",
|
|
48
|
+
"defer",
|
|
49
|
+
"deinit",
|
|
50
|
+
"do",
|
|
51
|
+
"else",
|
|
52
|
+
"enum",
|
|
53
|
+
"extension",
|
|
54
|
+
"fallthrough",
|
|
55
|
+
"false",
|
|
56
|
+
"fileprivate",
|
|
57
|
+
"for",
|
|
58
|
+
"func",
|
|
59
|
+
"guard",
|
|
60
|
+
"if",
|
|
61
|
+
"import",
|
|
62
|
+
"in",
|
|
63
|
+
"init",
|
|
64
|
+
"inout",
|
|
65
|
+
"internal",
|
|
66
|
+
"is",
|
|
67
|
+
"let",
|
|
68
|
+
"nil",
|
|
69
|
+
"open",
|
|
70
|
+
"operator",
|
|
71
|
+
"private",
|
|
72
|
+
"protocol",
|
|
73
|
+
"public",
|
|
74
|
+
"repeat",
|
|
75
|
+
"rethrows",
|
|
76
|
+
"return",
|
|
77
|
+
"self",
|
|
78
|
+
"static",
|
|
79
|
+
"struct",
|
|
80
|
+
"subscript",
|
|
81
|
+
"super",
|
|
82
|
+
"switch",
|
|
83
|
+
"throw",
|
|
84
|
+
"throws",
|
|
85
|
+
"true",
|
|
86
|
+
"try",
|
|
87
|
+
"typealias",
|
|
88
|
+
"var",
|
|
89
|
+
"where",
|
|
90
|
+
"while"
|
|
91
|
+
]);
|
|
92
|
+
var PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
93
|
+
function kotlinIdentifier(name) {
|
|
94
|
+
return KOTLIN_HARD_KEYWORDS.has(name) || !PLAIN_IDENTIFIER.test(name) ? `\`${name}\`` : name;
|
|
95
|
+
}
|
|
96
|
+
function swiftIdentifier(name) {
|
|
97
|
+
return SWIFT_KEYWORDS.has(name) || !PLAIN_IDENTIFIER.test(name) ? `\`${name}\`` : name;
|
|
98
|
+
}
|
|
99
|
+
function kotlinStringLiteral(value) {
|
|
100
|
+
let out = "\"";
|
|
101
|
+
for (const char of value) switch (char) {
|
|
102
|
+
case "\\":
|
|
103
|
+
out += "\\\\";
|
|
104
|
+
break;
|
|
105
|
+
case "\"":
|
|
106
|
+
out += "\\\"";
|
|
107
|
+
break;
|
|
108
|
+
case "$":
|
|
109
|
+
out += "\\$";
|
|
110
|
+
break;
|
|
111
|
+
case "\n":
|
|
112
|
+
out += "\\n";
|
|
113
|
+
break;
|
|
114
|
+
case "\r":
|
|
115
|
+
out += "\\r";
|
|
116
|
+
break;
|
|
117
|
+
case " ":
|
|
118
|
+
out += "\\t";
|
|
119
|
+
break;
|
|
120
|
+
default: {
|
|
121
|
+
const code = char.codePointAt(0) ?? 0;
|
|
122
|
+
out += code < 32 || code >= 55296 && code <= 57343 ? `\\u${code.toString(16).padStart(4, "0")}` : char;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return `${out}"`;
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/build-contract.ts
|
|
129
|
+
var KOTLIN_TYPE_BY_MANIFEST_TYPE = {
|
|
130
|
+
id: "String",
|
|
131
|
+
text: "String",
|
|
132
|
+
status: "String",
|
|
133
|
+
foreignKey: "String",
|
|
134
|
+
crossPackageRef: "String",
|
|
135
|
+
integer: "Long",
|
|
136
|
+
decimal: "DecimalString?",
|
|
137
|
+
boolean: "Boolean",
|
|
138
|
+
datetime: "Instant?",
|
|
139
|
+
json: "JsonObject"
|
|
140
|
+
};
|
|
141
|
+
var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
142
|
+
"oneToMany",
|
|
143
|
+
"manyToMany",
|
|
144
|
+
"hasMany"
|
|
145
|
+
]);
|
|
146
|
+
var SWIFT_TYPE_BY_KOTLIN_TYPE = {
|
|
147
|
+
String: "String",
|
|
148
|
+
Long: "Int64",
|
|
149
|
+
Boolean: "Bool",
|
|
150
|
+
"Instant?": "String?",
|
|
151
|
+
"DecimalString?": "String?",
|
|
152
|
+
JsonObject: "String",
|
|
153
|
+
"List<String>": "[String]"
|
|
154
|
+
};
|
|
155
|
+
var BOOLEAN_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
156
|
+
"required",
|
|
157
|
+
"safetyCritical",
|
|
158
|
+
"hasSafetyCriticalFallback"
|
|
159
|
+
]);
|
|
160
|
+
var INTEGER_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
161
|
+
"version",
|
|
162
|
+
"sortOrder",
|
|
163
|
+
"precision",
|
|
164
|
+
"pageStart",
|
|
165
|
+
"pageEnd",
|
|
166
|
+
"replayOrder",
|
|
167
|
+
"clientRevision",
|
|
168
|
+
"serverRevision"
|
|
169
|
+
]);
|
|
170
|
+
var STRING_LIST_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
171
|
+
"ruleIds",
|
|
172
|
+
"systemIds",
|
|
173
|
+
"supportedLocales",
|
|
174
|
+
"targetLocales",
|
|
175
|
+
"requestedLocales",
|
|
176
|
+
"includedLocales"
|
|
177
|
+
]);
|
|
178
|
+
function buildMobileContract(options) {
|
|
179
|
+
const { manifests, allowlist, kotlinPackage, sourceLabel = "" } = options;
|
|
180
|
+
const candidates = manifests.flatMap(listObjects);
|
|
181
|
+
const objects = [];
|
|
182
|
+
const seenQualifiedNames = /* @__PURE__ */ new Set();
|
|
183
|
+
for (const entry of allowlist) {
|
|
184
|
+
const matches = candidates.filter((object) => object.name === entry || object.qualifiedName === entry);
|
|
185
|
+
if (matches.length === 0) throw new Error(`mobile allowlist references missing object: ${entry}`);
|
|
186
|
+
if (matches.length > 1) {
|
|
187
|
+
const found = matches.map((match) => match.qualifiedName ?? match.name).join(", ");
|
|
188
|
+
throw new Error(`mobile allowlist entry "${entry}" is ambiguous (${found}) \u2014 use the qualified name`);
|
|
189
|
+
}
|
|
190
|
+
const qualifiedName = matches[0].qualifiedName ?? matches[0].name;
|
|
191
|
+
if (seenQualifiedNames.has(qualifiedName)) continue;
|
|
192
|
+
seenQualifiedNames.add(qualifiedName);
|
|
193
|
+
objects.push(toMobileObject(matches[0]));
|
|
194
|
+
}
|
|
195
|
+
const dtoNameOwners = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const object of objects) {
|
|
197
|
+
const prior = dtoNameOwners.get(object.dtoName);
|
|
198
|
+
if (prior) throw new Error(`mobile allowlist produces duplicate DTO name "${object.dtoName}" (${prior} and ${object.qualifiedName}) \u2014 generated files would overwrite each other; allowlisted objects need unique short names`);
|
|
199
|
+
dtoNameOwners.set(object.dtoName, object.qualifiedName);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
schemaVersion: 1,
|
|
203
|
+
contractVersion: SMRT_MOBILE_CONTRACT_VERSION,
|
|
204
|
+
sourceLabel,
|
|
205
|
+
sourceHash: createHash("sha256").update(JSON.stringify({
|
|
206
|
+
allowlist,
|
|
207
|
+
objects
|
|
208
|
+
})).digest("hex"),
|
|
209
|
+
objectAllowlist: [...allowlist],
|
|
210
|
+
objectCount: objects.length,
|
|
211
|
+
kotlin: { packageName: kotlinPackage },
|
|
212
|
+
typeMappings: { ...KOTLIN_TYPE_BY_MANIFEST_TYPE },
|
|
213
|
+
objects
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function listObjects(manifest) {
|
|
217
|
+
return Array.isArray(manifest.objects) ? manifest.objects : Object.values(manifest.objects);
|
|
218
|
+
}
|
|
219
|
+
function toMobileObject(object) {
|
|
220
|
+
const fieldEntries = Object.entries(object.fields ?? {});
|
|
221
|
+
const fields = [];
|
|
222
|
+
if (!fieldEntries.some(([name]) => name === "id")) fields.push({
|
|
223
|
+
name: "id",
|
|
224
|
+
kotlinType: "String",
|
|
225
|
+
swiftType: "String",
|
|
226
|
+
nullable: false,
|
|
227
|
+
kotlinDefault: null
|
|
228
|
+
});
|
|
229
|
+
for (const [name, meta] of fieldEntries) {
|
|
230
|
+
if (meta.type && RELATION_FIELD_TYPES.has(meta.type)) continue;
|
|
231
|
+
fields.push(mapField(name, meta));
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
name: object.name,
|
|
235
|
+
qualifiedName: object.qualifiedName ?? object.name,
|
|
236
|
+
dtoName: `${object.name}Dto`,
|
|
237
|
+
tableName: object.schema?.tableName ?? null,
|
|
238
|
+
hasTenantId: fieldEntries.some(([name]) => name === "tenantId"),
|
|
239
|
+
fields
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function mapField(name, meta) {
|
|
243
|
+
const kotlinType = kotlinTypeFor(name, meta);
|
|
244
|
+
return {
|
|
245
|
+
name,
|
|
246
|
+
kotlinType,
|
|
247
|
+
swiftType: SWIFT_TYPE_BY_KOTLIN_TYPE[kotlinType] ?? "String",
|
|
248
|
+
nullable: kotlinType.endsWith("?"),
|
|
249
|
+
kotlinDefault: name === "id" ? null : kotlinDefaultFor(kotlinType, meta)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function kotlinTypeFor(name, meta) {
|
|
253
|
+
if (name === "id") return "String";
|
|
254
|
+
if (meta.type && KOTLIN_TYPE_BY_MANIFEST_TYPE[meta.type]) return KOTLIN_TYPE_BY_MANIFEST_TYPE[meta.type];
|
|
255
|
+
if (STRING_LIST_FIELD_NAMES.has(name)) return "List<String>";
|
|
256
|
+
if (name.endsWith("Json")) return "JsonObject";
|
|
257
|
+
if (BOOLEAN_FIELD_NAMES.has(name)) return "Boolean";
|
|
258
|
+
if (INTEGER_FIELD_NAMES.has(name)) return "Long";
|
|
259
|
+
if (name.endsWith("At") || name.endsWith("On") || name.endsWith("_at")) return "Instant?";
|
|
260
|
+
return "String";
|
|
261
|
+
}
|
|
262
|
+
function kotlinDefaultFor(kotlinType, meta) {
|
|
263
|
+
const declared = meta.default !== void 0 ? meta.default : meta.defaultValue;
|
|
264
|
+
switch (kotlinType) {
|
|
265
|
+
case "JsonObject": return "JsonObject(emptyMap())";
|
|
266
|
+
case "List<String>": return "emptyList()";
|
|
267
|
+
case "Boolean": return declared === true || declared === "true" ? "true" : "false";
|
|
268
|
+
case "Long": return integerDefault(declared);
|
|
269
|
+
case "Instant?":
|
|
270
|
+
case "DecimalString?": return "null";
|
|
271
|
+
default: return stringDefault(declared);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function integerDefault(defaultValue) {
|
|
275
|
+
const parsed = Number(String(defaultValue ?? "").replace(/['"]/g, ""));
|
|
276
|
+
return Number.isFinite(parsed) && String(defaultValue ?? "").trim() !== "" ? String(Math.trunc(parsed)) : "0";
|
|
277
|
+
}
|
|
278
|
+
function stringDefault(defaultValue) {
|
|
279
|
+
if (defaultValue === void 0 || defaultValue === null || defaultValue === "null") return "\"\"";
|
|
280
|
+
const text = String(defaultValue).trim();
|
|
281
|
+
if (text === "") return "\"\"";
|
|
282
|
+
if (text.length > 1 && text.startsWith("\"") && text.endsWith("\"")) try {
|
|
283
|
+
const parsed = JSON.parse(text);
|
|
284
|
+
return typeof parsed === "string" ? kotlinStringLiteral(parsed) : "\"\"";
|
|
285
|
+
} catch {
|
|
286
|
+
return "\"\"";
|
|
287
|
+
}
|
|
288
|
+
const singleQuoted = text.match(/^'([^']*)'$/);
|
|
289
|
+
if (singleQuoted) return kotlinStringLiteral(singleQuoted[1]);
|
|
290
|
+
return kotlinStringLiteral(text);
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/emit-framework.ts
|
|
294
|
+
var REGENERATE_COMMAND = "pnpm --filter @happyvertical/smrt-mobile-contract generate:framework";
|
|
295
|
+
var HEADER = [
|
|
296
|
+
"// Generated by @happyvertical/smrt-mobile-contract (framework contract).",
|
|
297
|
+
`// Do not edit by hand \u2014 regenerate with \`${REGENERATE_COMMAND}\`.`,
|
|
298
|
+
""
|
|
299
|
+
].join("\n");
|
|
300
|
+
var SUPPORT_TYPES_KT = `${HEADER}
|
|
301
|
+
package com.happyvertical.smrt.mobile.contract
|
|
302
|
+
|
|
303
|
+
import kotlinx.serialization.Serializable
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Decimal values cross the wire as strings so no precision is lost between
|
|
307
|
+
* the server's DECIMAL columns and platform floating-point types.
|
|
308
|
+
*/
|
|
309
|
+
typealias DecimalString = String
|
|
310
|
+
|
|
311
|
+
@Serializable
|
|
312
|
+
data class Measurement(
|
|
313
|
+
val value: DecimalString,
|
|
314
|
+
val unit: String,
|
|
315
|
+
val source: String? = null,
|
|
316
|
+
)
|
|
317
|
+
`;
|
|
318
|
+
var PACK_LOCALIZATION_KT = `${HEADER}
|
|
319
|
+
package com.happyvertical.smrt.mobile.contract
|
|
320
|
+
|
|
321
|
+
import kotlinx.serialization.Serializable
|
|
322
|
+
import kotlinx.serialization.json.JsonObject
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Locale policy for an offline pack: which locale was requested, which are
|
|
326
|
+
* included, and where lookups fall back when a translation is missing.
|
|
327
|
+
*/
|
|
328
|
+
@Serializable
|
|
329
|
+
data class PackLanguage(
|
|
330
|
+
val defaultLocale: String = "en-US",
|
|
331
|
+
val requestedLocales: List<String> = emptyList(),
|
|
332
|
+
val includedLocales: List<String> = emptyList(),
|
|
333
|
+
val fallbackLocale: String = "en-US",
|
|
334
|
+
val policy: String = "",
|
|
335
|
+
val reviewState: String = "",
|
|
336
|
+
val hasSafetyCriticalFallback: Boolean = false,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Provenance of one localized string: where the text came from and whether the
|
|
341
|
+
* translation is reviewed, safety-critical, or a fallback.
|
|
342
|
+
*/
|
|
343
|
+
@Serializable
|
|
344
|
+
data class PackTextSourceRef(
|
|
345
|
+
val sourceLocale: String = "",
|
|
346
|
+
val sourceDocumentId: String = "",
|
|
347
|
+
val sourceSectionId: String = "",
|
|
348
|
+
val sourceFactId: String = "",
|
|
349
|
+
val reviewState: String = "",
|
|
350
|
+
val safetyCritical: Boolean = false,
|
|
351
|
+
val fallbackReason: String = "",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* One locale's bundle of localized strings for a pack, keyed by text key,
|
|
356
|
+
* with per-key source provenance.
|
|
357
|
+
*/
|
|
358
|
+
@Serializable
|
|
359
|
+
data class PackLanguageBundle(
|
|
360
|
+
val locale: String = "",
|
|
361
|
+
val reviewState: String = "",
|
|
362
|
+
val textHash: String = "",
|
|
363
|
+
val strings: JsonObject = JsonObject(emptyMap()),
|
|
364
|
+
val sourceRefs: Map<String, PackTextSourceRef> = emptyMap(),
|
|
365
|
+
val hasSafetyCriticalFallback: Boolean = false,
|
|
366
|
+
val policy: String = "",
|
|
367
|
+
)
|
|
368
|
+
`;
|
|
369
|
+
var MOBILE_AUTH_CONTRACT_KT = `${HEADER}
|
|
370
|
+
package com.happyvertical.smrt.mobile.contract
|
|
371
|
+
|
|
372
|
+
import kotlinx.serialization.Serializable
|
|
373
|
+
import kotlinx.serialization.json.JsonObject
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* \`/api/mobile\` auth + session contract (server-brokered PKCE).
|
|
377
|
+
*
|
|
378
|
+
* The server owns the OIDC/PKCE exchange: \`auth/start\` returns the
|
|
379
|
+
* authorization URL plus the \`state\`/\`codeVerifier\` the client must persist
|
|
380
|
+
* and echo back on \`auth/complete\`. Reference implementation: anytown
|
|
381
|
+
* dashboard; SMRT-shipped handlers are tracked in issue #1748.
|
|
382
|
+
*/
|
|
383
|
+
|
|
384
|
+
@Serializable
|
|
385
|
+
data class MobileUserSummary(
|
|
386
|
+
val id: String,
|
|
387
|
+
val email: String = "",
|
|
388
|
+
val label: String = "",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
@Serializable
|
|
392
|
+
data class MobileTenantSummary(
|
|
393
|
+
val id: String,
|
|
394
|
+
val name: String = "",
|
|
395
|
+
val slug: String = "",
|
|
396
|
+
val planName: String = "",
|
|
397
|
+
val subscriptionStatus: String = "",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
@Serializable
|
|
401
|
+
data class MobileTenantOption(
|
|
402
|
+
val id: String,
|
|
403
|
+
val name: String = "",
|
|
404
|
+
val slug: String = "",
|
|
405
|
+
val roleSlug: String = "",
|
|
406
|
+
val roleLabel: String = "",
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
@Serializable
|
|
410
|
+
data class MobileAuthProviderSummary(
|
|
411
|
+
val id: String,
|
|
412
|
+
val label: String = "",
|
|
413
|
+
val type: String = "",
|
|
414
|
+
val supportsPkce: Boolean = false,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@Serializable
|
|
418
|
+
data class MobileAuthStartRequest(
|
|
419
|
+
val providerId: String? = null,
|
|
420
|
+
val redirectUri: String,
|
|
421
|
+
val scopes: List<String> = emptyList(),
|
|
422
|
+
val state: String? = null,
|
|
423
|
+
val loginHint: String? = null,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
@Serializable
|
|
427
|
+
data class MobileAuthStartResponse(
|
|
428
|
+
val providerId: String,
|
|
429
|
+
val authorizationUrl: String,
|
|
430
|
+
val state: String,
|
|
431
|
+
val codeVerifier: String? = null,
|
|
432
|
+
val nonce: String? = null,
|
|
433
|
+
val redirectUri: String,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
@Serializable
|
|
437
|
+
data class MobileAuthCompleteRequest(
|
|
438
|
+
val providerId: String? = null,
|
|
439
|
+
val code: String,
|
|
440
|
+
val state: String? = null,
|
|
441
|
+
val codeVerifier: String? = null,
|
|
442
|
+
val redirectUri: String,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
@Serializable
|
|
446
|
+
data class MobileAuthSession(
|
|
447
|
+
val accessToken: String,
|
|
448
|
+
val tokenType: String = "Bearer",
|
|
449
|
+
val expiresAt: String = "",
|
|
450
|
+
val user: MobileUserSummary,
|
|
451
|
+
val activeTenant: MobileTenantOption? = null,
|
|
452
|
+
val tenants: List<MobileTenantOption> = emptyList(),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
@Serializable
|
|
456
|
+
data class MobileSessionBootstrap(
|
|
457
|
+
val user: MobileUserSummary,
|
|
458
|
+
val activeTenant: MobileTenantOption? = null,
|
|
459
|
+
val tenants: List<MobileTenantOption> = emptyList(),
|
|
460
|
+
val extras: JsonObject? = null,
|
|
461
|
+
)
|
|
462
|
+
`;
|
|
463
|
+
var MOBILE_DEVICE_CONTRACT_KT = `${HEADER}
|
|
464
|
+
package com.happyvertical.smrt.mobile.contract
|
|
465
|
+
|
|
466
|
+
import kotlinx.serialization.Serializable
|
|
467
|
+
|
|
468
|
+
@Serializable
|
|
469
|
+
data class MobileDevicePermissionState(
|
|
470
|
+
val status: String,
|
|
471
|
+
val canRequest: Boolean = false,
|
|
472
|
+
val reason: String? = null,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
@Serializable
|
|
476
|
+
data class MobileDeviceCapability(
|
|
477
|
+
val surface: String,
|
|
478
|
+
val label: String = "",
|
|
479
|
+
val supported: Boolean = false,
|
|
480
|
+
val permission: MobileDevicePermissionState,
|
|
481
|
+
val preferredInput: String = "",
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
@Serializable
|
|
485
|
+
data class MobileDeviceCapabilities(
|
|
486
|
+
val camera: MobileDeviceCapability,
|
|
487
|
+
val microphone: MobileDeviceCapability,
|
|
488
|
+
val checkedAtEpochMillis: Long? = null,
|
|
489
|
+
)
|
|
490
|
+
`;
|
|
491
|
+
var MOBILE_CONTRACT_INFO_KT = `${HEADER}
|
|
492
|
+
package com.happyvertical.smrt.mobile.contract
|
|
493
|
+
|
|
494
|
+
const val SMRT_MOBILE_CONTRACT_SCHEMA_VERSION: Int = 1
|
|
495
|
+
const val SMRT_MOBILE_CONTRACT_VERSION: String = "${SMRT_MOBILE_CONTRACT_VERSION}"
|
|
496
|
+
`;
|
|
497
|
+
function frameworkKotlinFiles() {
|
|
498
|
+
return /* @__PURE__ */ new Map([
|
|
499
|
+
["SupportTypes.kt", SUPPORT_TYPES_KT],
|
|
500
|
+
["PackLocalization.kt", PACK_LOCALIZATION_KT],
|
|
501
|
+
["MobileAuthContract.kt", MOBILE_AUTH_CONTRACT_KT],
|
|
502
|
+
["MobileDeviceContract.kt", MOBILE_DEVICE_CONTRACT_KT],
|
|
503
|
+
["MobileContractInfo.kt", MOBILE_CONTRACT_INFO_KT]
|
|
504
|
+
]);
|
|
505
|
+
}
|
|
506
|
+
var MOBILE_CONTRACT_SWIFT = `// Generated by @happyvertical/smrt-mobile-contract (framework contract).
|
|
507
|
+
// Do not edit by hand \u2014 regenerate with \`${REGENERATE_COMMAND}\`.
|
|
508
|
+
import Foundation
|
|
509
|
+
|
|
510
|
+
let SmrtMobileContractSchemaVersion = 1
|
|
511
|
+
let SmrtMobileContractVersion = "${SMRT_MOBILE_CONTRACT_VERSION}"
|
|
512
|
+
|
|
513
|
+
/// Decimal values cross the wire as strings (no precision loss).
|
|
514
|
+
typealias DecimalString = String
|
|
515
|
+
|
|
516
|
+
struct Measurement {
|
|
517
|
+
let value: DecimalString
|
|
518
|
+
let unit: String
|
|
519
|
+
let source: String?
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
struct MobileUserSummary {
|
|
523
|
+
let id: String
|
|
524
|
+
let email: String
|
|
525
|
+
let label: String
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
struct MobileTenantSummary {
|
|
529
|
+
let id: String
|
|
530
|
+
let name: String
|
|
531
|
+
let slug: String
|
|
532
|
+
let planName: String
|
|
533
|
+
let subscriptionStatus: String
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
struct MobileTenantOption {
|
|
537
|
+
let id: String
|
|
538
|
+
let name: String
|
|
539
|
+
let slug: String
|
|
540
|
+
let roleSlug: String
|
|
541
|
+
let roleLabel: String
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
struct MobileAuthProviderSummary {
|
|
545
|
+
let id: String
|
|
546
|
+
let label: String
|
|
547
|
+
let type: String
|
|
548
|
+
let supportsPkce: Bool
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
struct MobileAuthStartRequest {
|
|
552
|
+
let providerId: String?
|
|
553
|
+
let redirectUri: String
|
|
554
|
+
let scopes: [String]
|
|
555
|
+
let state: String?
|
|
556
|
+
let loginHint: String?
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
struct MobileAuthStartResponse {
|
|
560
|
+
let providerId: String
|
|
561
|
+
let authorizationUrl: String
|
|
562
|
+
let state: String
|
|
563
|
+
let codeVerifier: String?
|
|
564
|
+
let nonce: String?
|
|
565
|
+
let redirectUri: String
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
struct MobileAuthCompleteRequest {
|
|
569
|
+
let providerId: String?
|
|
570
|
+
let code: String
|
|
571
|
+
let state: String?
|
|
572
|
+
let codeVerifier: String?
|
|
573
|
+
let redirectUri: String
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
struct MobileAuthSession {
|
|
577
|
+
let accessToken: String
|
|
578
|
+
let tokenType: String
|
|
579
|
+
let expiresAt: String
|
|
580
|
+
let user: MobileUserSummary
|
|
581
|
+
let activeTenant: MobileTenantOption?
|
|
582
|
+
let tenants: [MobileTenantOption]
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
struct MobileSessionBootstrap {
|
|
586
|
+
let user: MobileUserSummary
|
|
587
|
+
let activeTenant: MobileTenantOption?
|
|
588
|
+
let tenants: [MobileTenantOption]
|
|
589
|
+
let extras: String?
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
struct MobileDevicePermissionState {
|
|
593
|
+
let status: String
|
|
594
|
+
let canRequest: Bool
|
|
595
|
+
let reason: String?
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
struct MobileDeviceCapability {
|
|
599
|
+
let surface: String
|
|
600
|
+
let label: String
|
|
601
|
+
let supported: Bool
|
|
602
|
+
let permission: MobileDevicePermissionState
|
|
603
|
+
let preferredInput: String
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
struct MobileDeviceCapabilities {
|
|
607
|
+
let camera: MobileDeviceCapability
|
|
608
|
+
let microphone: MobileDeviceCapability
|
|
609
|
+
let checkedAtEpochMillis: Int64?
|
|
610
|
+
}
|
|
611
|
+
`;
|
|
612
|
+
function frameworkSwiftFiles() {
|
|
613
|
+
return /* @__PURE__ */ new Map([["MobileContract.swift", MOBILE_CONTRACT_SWIFT]]);
|
|
614
|
+
}
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/emit-kotlin.ts
|
|
617
|
+
var GENERATED_HEADER = [
|
|
618
|
+
"// Generated by @happyvertical/smrt-mobile-contract.",
|
|
619
|
+
"// Do not edit by hand.",
|
|
620
|
+
""
|
|
621
|
+
].join("\n");
|
|
622
|
+
var FRAMEWORK_CONTRACT_PACKAGE = "com.happyvertical.smrt.mobile.contract";
|
|
623
|
+
function generateKotlinDtoFiles(contract) {
|
|
624
|
+
const files = /* @__PURE__ */ new Map();
|
|
625
|
+
files.set("MobileContractManifest.kt", manifestFile(contract));
|
|
626
|
+
for (const object of contract.objects) files.set(`${object.dtoName}.kt`, dtoFile(contract, object));
|
|
627
|
+
return files;
|
|
628
|
+
}
|
|
629
|
+
function manifestFile(contract) {
|
|
630
|
+
return [
|
|
631
|
+
GENERATED_HEADER,
|
|
632
|
+
`package ${contract.kotlin.packageName}`,
|
|
633
|
+
"",
|
|
634
|
+
`const val MOBILE_CONTRACT_SCHEMA_VERSION: Int = ${contract.schemaVersion}`,
|
|
635
|
+
`const val MOBILE_CONTRACT_VERSION: String = "${contract.contractVersion}"`,
|
|
636
|
+
`const val MOBILE_CONTRACT_SOURCE_HASH: String = "${contract.sourceHash}"`,
|
|
637
|
+
"",
|
|
638
|
+
"val MOBILE_CONTRACT_OBJECTS: List<String> = listOf(",
|
|
639
|
+
...contract.objects.map((object) => ` "${object.name}",`),
|
|
640
|
+
")",
|
|
641
|
+
""
|
|
642
|
+
].join("\n");
|
|
643
|
+
}
|
|
644
|
+
function dtoFile(contract, object) {
|
|
645
|
+
const imports = /* @__PURE__ */ new Set(["kotlinx.serialization.Serializable"]);
|
|
646
|
+
for (const field of object.fields) {
|
|
647
|
+
if (field.kotlinType.includes("Instant")) imports.add("kotlinx.datetime.Instant");
|
|
648
|
+
if (field.kotlinType.includes("JsonObject")) imports.add("kotlinx.serialization.json.JsonObject");
|
|
649
|
+
if (field.kotlinType.includes("DecimalString")) imports.add(`${FRAMEWORK_CONTRACT_PACKAGE}.DecimalString`);
|
|
650
|
+
}
|
|
651
|
+
return [
|
|
652
|
+
GENERATED_HEADER,
|
|
653
|
+
`package ${contract.kotlin.packageName}`,
|
|
654
|
+
"",
|
|
655
|
+
...[...imports].sort().map((importPath) => `import ${importPath}`),
|
|
656
|
+
"",
|
|
657
|
+
"@Serializable",
|
|
658
|
+
`data class ${object.dtoName}(`,
|
|
659
|
+
...object.fields.map((field) => kotlinProperty(field)),
|
|
660
|
+
")",
|
|
661
|
+
""
|
|
662
|
+
].join("\n");
|
|
663
|
+
}
|
|
664
|
+
function kotlinProperty(field) {
|
|
665
|
+
const defaultValue = field.kotlinDefault === null ? "" : ` = ${field.kotlinDefault}`;
|
|
666
|
+
return ` val ${kotlinIdentifier(field.name)}: ${field.kotlinType}${defaultValue},`;
|
|
667
|
+
}
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/emit-swift.ts
|
|
670
|
+
function generateSwiftDtoFile(contract) {
|
|
671
|
+
const lines = [
|
|
672
|
+
"// Generated by @happyvertical/smrt-mobile-contract.",
|
|
673
|
+
"// Do not edit by hand.",
|
|
674
|
+
"import Foundation",
|
|
675
|
+
"",
|
|
676
|
+
`let MobileContractSchemaVersion = ${contract.schemaVersion}`,
|
|
677
|
+
`let MobileContractVersion = "${contract.contractVersion}"`,
|
|
678
|
+
`let MobileContractSourceHash = "${contract.sourceHash}"`,
|
|
679
|
+
""
|
|
680
|
+
];
|
|
681
|
+
for (const object of contract.objects) {
|
|
682
|
+
lines.push(`struct ${object.dtoName} {`);
|
|
683
|
+
for (const field of object.fields) lines.push(` let ${swiftIdentifier(field.name)}: ${swiftType(field)}`);
|
|
684
|
+
lines.push("}", "");
|
|
685
|
+
}
|
|
686
|
+
return lines.join("\n");
|
|
687
|
+
}
|
|
688
|
+
function swiftType(field) {
|
|
689
|
+
return field.swiftType;
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/file-set.ts
|
|
693
|
+
function writeFileSet(rootDir, files) {
|
|
694
|
+
for (const [relPath, content] of files) {
|
|
695
|
+
const absPath = join(rootDir, relPath);
|
|
696
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
697
|
+
writeFileSync(absPath, content);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function verifyFileSet(rootDir, files) {
|
|
701
|
+
const stale = [];
|
|
702
|
+
for (const [relPath, content] of files) {
|
|
703
|
+
const absPath = join(rootDir, relPath);
|
|
704
|
+
if (!existsSync(absPath) || readFileSync(absPath, "utf8") !== content) stale.push(relPath);
|
|
705
|
+
}
|
|
706
|
+
return stale;
|
|
707
|
+
}
|
|
708
|
+
//#endregion
|
|
709
|
+
export { GENERATED_HEADER, SMRT_MOBILE_CONTRACT_SCHEMA_VERSION, SMRT_MOBILE_CONTRACT_VERSION, buildMobileContract, frameworkKotlinFiles, frameworkSwiftFiles, generateKotlinDtoFiles, generateSwiftDtoFile, verifyFileSet, writeFileSet };
|
|
710
|
+
|
|
711
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/contract-version.ts","../src/identifiers.ts","../src/build-contract.ts","../src/emit-framework.ts","../src/emit-kotlin.ts","../src/emit-swift.ts","../src/file-set.ts"],"sourcesContent":["/** Schema version of the mobile-contract.json envelope. */\nexport const SMRT_MOBILE_CONTRACT_SCHEMA_VERSION = 1;\n\n/**\n * Version stamp of the framework contract surface (auth/session/device/pack\n * localization types). Bump when the framework contract shape changes.\n */\nexport const SMRT_MOBILE_CONTRACT_VERSION = '2026-07-01.v1';\n","/**\n * Identifier + literal escaping for generated Kotlin/Swift source. Manifest\n * field names are valid JS identifiers, but may collide with Kotlin/Swift\n * keywords; string defaults may contain characters that Kotlin string\n * templates or escapes treat specially.\n */\n\nconst KOTLIN_HARD_KEYWORDS = new Set([\n 'as',\n 'break',\n 'class',\n 'continue',\n 'do',\n 'else',\n 'false',\n 'for',\n 'fun',\n 'if',\n 'in',\n 'interface',\n 'is',\n 'null',\n 'object',\n 'package',\n 'return',\n 'super',\n 'this',\n 'throw',\n 'true',\n 'try',\n 'typealias',\n 'typeof',\n 'val',\n 'var',\n 'when',\n 'while',\n]);\n\nconst SWIFT_KEYWORDS = new Set([\n 'associatedtype',\n 'as',\n 'break',\n 'case',\n 'catch',\n 'class',\n 'continue',\n 'default',\n 'defer',\n 'deinit',\n 'do',\n 'else',\n 'enum',\n 'extension',\n 'fallthrough',\n 'false',\n 'fileprivate',\n 'for',\n 'func',\n 'guard',\n 'if',\n 'import',\n 'in',\n 'init',\n 'inout',\n 'internal',\n 'is',\n 'let',\n 'nil',\n 'open',\n 'operator',\n 'private',\n 'protocol',\n 'public',\n 'repeat',\n 'rethrows',\n 'return',\n 'self',\n 'static',\n 'struct',\n 'subscript',\n 'super',\n 'switch',\n 'throw',\n 'throws',\n 'true',\n 'try',\n 'typealias',\n 'var',\n 'where',\n 'while',\n]);\n\nconst PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** Backtick-escapes Kotlin hard keywords (and non-plain identifiers). */\nexport function kotlinIdentifier(name: string): string {\n return KOTLIN_HARD_KEYWORDS.has(name) || !PLAIN_IDENTIFIER.test(name)\n ? `\\`${name}\\``\n : name;\n}\n\n/** Backtick-escapes Swift keywords (and non-plain identifiers). */\nexport function swiftIdentifier(name: string): string {\n return SWIFT_KEYWORDS.has(name) || !PLAIN_IDENTIFIER.test(name)\n ? `\\`${name}\\``\n : name;\n}\n\n/**\n * Emits a Kotlin double-quoted string literal. Escapes `$` (string-template\n * interpolation), quotes, backslashes, and control characters — JSON escaping\n * is NOT Kotlin-safe (`$` stays raw and `\\f` is not a Kotlin escape).\n */\nexport function kotlinStringLiteral(value: string): string {\n let out = '\"';\n for (const char of value) {\n switch (char) {\n case '\\\\':\n out += '\\\\\\\\';\n break;\n case '\"':\n out += '\\\\\"';\n break;\n case '$':\n out += '\\\\$';\n break;\n case '\\n':\n out += '\\\\n';\n break;\n case '\\r':\n out += '\\\\r';\n break;\n case '\\t':\n out += '\\\\t';\n break;\n default: {\n const code = char.codePointAt(0) ?? 0;\n // Control chars and lone surrogates must be \\u-escaped: a raw lone\n // surrogate would be written to disk as U+FFFD, silently corrupting\n // the literal.\n out +=\n code < 0x20 || (code >= 0xd800 && code <= 0xdfff)\n ? `\\\\u${code.toString(16).padStart(4, '0')}`\n : char;\n }\n }\n }\n return `${out}\"`;\n}\n","import { createHash } from 'node:crypto';\nimport {\n SMRT_MOBILE_CONTRACT_SCHEMA_VERSION,\n SMRT_MOBILE_CONTRACT_VERSION,\n} from './contract-version.js';\nimport { kotlinStringLiteral } from './identifiers.js';\nimport type {\n MobileContract,\n MobileContractField,\n MobileContractObject,\n MobileContractOptions,\n SmrtManifestField,\n SmrtManifestObject,\n} from './types.js';\n\n/**\n * SMRT manifest field type → Kotlin type. Decimals cross the wire as\n * `DecimalString` (string-encoded, no float precision loss); timestamps as\n * nullable `Instant`; JSON payloads as `JsonObject`.\n */\nconst KOTLIN_TYPE_BY_MANIFEST_TYPE: Record<string, string> = {\n id: 'String',\n text: 'String',\n status: 'String',\n foreignKey: 'String',\n crossPackageRef: 'String',\n // 64-bit: SMRT INTEGER columns hold values like byte sizes and epoch\n // millis that overflow a 32-bit Int at decode time.\n integer: 'Long',\n decimal: 'DecimalString?',\n boolean: 'Boolean',\n datetime: 'Instant?',\n json: 'JsonObject',\n};\n\n/**\n * Relationship declarations, not scalar columns — an object row's wire\n * payload does not carry them, so they are excluded from DTO projection.\n */\nconst RELATION_FIELD_TYPES = new Set(['oneToMany', 'manyToMany', 'hasMany']);\n\nconst SWIFT_TYPE_BY_KOTLIN_TYPE: Record<string, string> = {\n String: 'String',\n // Explicit Int64: Swift Int is 32-bit on watchOS arm64_32/armv7k, and the\n // whole point of Long is byte-size/epoch values that overflow 32 bits.\n Long: 'Int64',\n Boolean: 'Bool',\n 'Instant?': 'String?',\n 'DecimalString?': 'String?',\n // Raw JSON string on the Swift side — Swift DTOs are plain structs (no\n // Codable) per the reporter seed; apps parse as needed.\n JsonObject: 'String',\n 'List<String>': '[String]',\n};\n\n/** Name-based fallbacks for manifest fields that carry no `type`. */\nconst BOOLEAN_FIELD_NAMES = new Set([\n 'required',\n 'safetyCritical',\n 'hasSafetyCriticalFallback',\n]);\nconst INTEGER_FIELD_NAMES = new Set([\n 'version',\n 'sortOrder',\n 'precision',\n 'pageStart',\n 'pageEnd',\n 'replayOrder',\n 'clientRevision',\n 'serverRevision',\n]);\nconst STRING_LIST_FIELD_NAMES = new Set([\n 'ruleIds',\n 'systemIds',\n 'supportedLocales',\n 'targetLocales',\n 'requestedLocales',\n 'includedLocales',\n]);\n\nexport function buildMobileContract(\n options: MobileContractOptions,\n): MobileContract {\n const { manifests, allowlist, kotlinPackage, sourceLabel = '' } = options;\n const candidates = manifests.flatMap(listObjects);\n\n const objects: MobileContractObject[] = [];\n const seenQualifiedNames = new Set<string>();\n for (const entry of allowlist) {\n const matches = candidates.filter(\n (object) => object.name === entry || object.qualifiedName === entry,\n );\n if (matches.length === 0) {\n throw new Error(`mobile allowlist references missing object: ${entry}`);\n }\n if (matches.length > 1) {\n const found = matches\n .map((match) => match.qualifiedName ?? match.name)\n .join(', ');\n throw new Error(\n `mobile allowlist entry \"${entry}\" is ambiguous (${found}) — use the qualified name`,\n );\n }\n const qualifiedName = matches[0].qualifiedName ?? matches[0].name;\n if (seenQualifiedNames.has(qualifiedName)) {\n // The same object listed under two spellings (short + qualified) is\n // redundant, not a collision — project it once.\n continue;\n }\n seenQualifiedNames.add(qualifiedName);\n objects.push(toMobileObject(matches[0]));\n }\n\n const dtoNameOwners = new Map<string, string>();\n for (const object of objects) {\n const prior = dtoNameOwners.get(object.dtoName);\n if (prior) {\n throw new Error(\n `mobile allowlist produces duplicate DTO name \"${object.dtoName}\" ` +\n `(${prior} and ${object.qualifiedName}) — generated files would ` +\n 'overwrite each other; allowlisted objects need unique short names',\n );\n }\n dtoNameOwners.set(object.dtoName, object.qualifiedName);\n }\n\n const sourceHash = createHash('sha256')\n .update(JSON.stringify({ allowlist, objects }))\n .digest('hex');\n\n return {\n schemaVersion: SMRT_MOBILE_CONTRACT_SCHEMA_VERSION,\n contractVersion: SMRT_MOBILE_CONTRACT_VERSION,\n sourceLabel,\n sourceHash,\n objectAllowlist: [...allowlist],\n objectCount: objects.length,\n kotlin: { packageName: kotlinPackage },\n typeMappings: { ...KOTLIN_TYPE_BY_MANIFEST_TYPE },\n objects,\n };\n}\n\nfunction listObjects(manifest: {\n objects: Record<string, SmrtManifestObject> | SmrtManifestObject[];\n}): SmrtManifestObject[] {\n return Array.isArray(manifest.objects)\n ? manifest.objects\n : Object.values(manifest.objects);\n}\n\nfunction toMobileObject(object: SmrtManifestObject): MobileContractObject {\n const fieldEntries = Object.entries(object.fields ?? {});\n const fields: MobileContractField[] = [];\n\n if (!fieldEntries.some(([name]) => name === 'id')) {\n fields.push({\n name: 'id',\n kotlinType: 'String',\n swiftType: 'String',\n nullable: false,\n kotlinDefault: null,\n });\n }\n\n for (const [name, meta] of fieldEntries) {\n if (meta.type && RELATION_FIELD_TYPES.has(meta.type)) {\n continue;\n }\n fields.push(mapField(name, meta));\n }\n\n return {\n name: object.name,\n qualifiedName: object.qualifiedName ?? object.name,\n dtoName: `${object.name}Dto`,\n tableName: object.schema?.tableName ?? null,\n hasTenantId: fieldEntries.some(([name]) => name === 'tenantId'),\n fields,\n };\n}\n\nfunction mapField(name: string, meta: SmrtManifestField): MobileContractField {\n const kotlinType = kotlinTypeFor(name, meta);\n return {\n name,\n kotlinType,\n swiftType: SWIFT_TYPE_BY_KOTLIN_TYPE[kotlinType] ?? 'String',\n nullable: kotlinType.endsWith('?'),\n kotlinDefault: name === 'id' ? null : kotlinDefaultFor(kotlinType, meta),\n };\n}\n\nfunction kotlinTypeFor(name: string, meta: SmrtManifestField): string {\n if (name === 'id') return 'String';\n if (meta.type && KOTLIN_TYPE_BY_MANIFEST_TYPE[meta.type]) {\n return KOTLIN_TYPE_BY_MANIFEST_TYPE[meta.type];\n }\n // Typeless manifest entries: fall back to the name heuristics the amaru\n // seed codegen used.\n if (STRING_LIST_FIELD_NAMES.has(name)) return 'List<String>';\n if (name.endsWith('Json')) return 'JsonObject';\n if (BOOLEAN_FIELD_NAMES.has(name)) return 'Boolean';\n if (INTEGER_FIELD_NAMES.has(name)) return 'Long';\n if (name.endsWith('At') || name.endsWith('On') || name.endsWith('_at'))\n return 'Instant?';\n return 'String';\n}\n\nfunction kotlinDefaultFor(kotlinType: string, meta: SmrtManifestField): string {\n // Current SMRT manifests carry the typed value in `default`; amaru-era\n // manifests carried a (possibly quoted) string in `defaultValue`.\n const declared =\n meta.default !== undefined ? meta.default : meta.defaultValue;\n switch (kotlinType) {\n case 'JsonObject':\n return 'JsonObject(emptyMap())';\n case 'List<String>':\n return 'emptyList()';\n case 'Boolean':\n return declared === true || declared === 'true' ? 'true' : 'false';\n case 'Long':\n return integerDefault(declared);\n case 'Instant?':\n case 'DecimalString?':\n return 'null';\n default:\n return stringDefault(declared);\n }\n}\n\nfunction integerDefault(defaultValue: unknown): string {\n const parsed = Number(String(defaultValue ?? '').replace(/['\"]/g, ''));\n return Number.isFinite(parsed) && String(defaultValue ?? '').trim() !== ''\n ? String(Math.trunc(parsed))\n : '0';\n}\n\nfunction stringDefault(defaultValue: unknown): string {\n if (\n defaultValue === undefined ||\n defaultValue === null ||\n defaultValue === 'null'\n ) {\n return '\"\"';\n }\n\n const text = String(defaultValue).trim();\n if (text === '') {\n return '\"\"';\n }\n if (text.length > 1 && text.startsWith('\"') && text.endsWith('\"')) {\n try {\n const parsed: unknown = JSON.parse(text);\n return typeof parsed === 'string' ? kotlinStringLiteral(parsed) : '\"\"';\n } catch {\n return '\"\"';\n }\n }\n\n const singleQuoted = text.match(/^'([^']*)'$/);\n if (singleQuoted) {\n return kotlinStringLiteral(singleQuoted[1]);\n }\n\n // A raw (unquoted) string value — the shape current SMRT manifests emit.\n return kotlinStringLiteral(text);\n}\n","import {\n SMRT_MOBILE_CONTRACT_SCHEMA_VERSION,\n SMRT_MOBILE_CONTRACT_VERSION,\n} from './contract-version.js';\n\n/**\n * The generated *framework* contract: the stable files checked into\n * `@happyvertical/smrt-mobile` under\n * `src/commonMain/kotlin/com/happyvertical/smrt/mobile/contract/`.\n * This module is their single source of truth — `pnpm generate:framework`\n * rewrites them; the package build verifies freshness.\n */\n\n// One source for the regenerate command in every emitted header — the script\n// is deliberately named generate:framework (a plain `generate` script would\n// be auto-run by turbo's build task and neutralize the freshness gate).\nconst REGENERATE_COMMAND =\n 'pnpm --filter @happyvertical/smrt-mobile-contract generate:framework';\n\nconst HEADER = [\n '// Generated by @happyvertical/smrt-mobile-contract (framework contract).',\n `// Do not edit by hand — regenerate with \\`${REGENERATE_COMMAND}\\`.`,\n '',\n].join('\\n');\n\nconst SUPPORT_TYPES_KT = `${HEADER}\npackage com.happyvertical.smrt.mobile.contract\n\nimport kotlinx.serialization.Serializable\n\n/**\n * Decimal values cross the wire as strings so no precision is lost between\n * the server's DECIMAL columns and platform floating-point types.\n */\ntypealias DecimalString = String\n\n@Serializable\ndata class Measurement(\n val value: DecimalString,\n val unit: String,\n val source: String? = null,\n)\n`;\n\nconst PACK_LOCALIZATION_KT = `${HEADER}\npackage com.happyvertical.smrt.mobile.contract\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonObject\n\n/**\n * Locale policy for an offline pack: which locale was requested, which are\n * included, and where lookups fall back when a translation is missing.\n */\n@Serializable\ndata class PackLanguage(\n val defaultLocale: String = \"en-US\",\n val requestedLocales: List<String> = emptyList(),\n val includedLocales: List<String> = emptyList(),\n val fallbackLocale: String = \"en-US\",\n val policy: String = \"\",\n val reviewState: String = \"\",\n val hasSafetyCriticalFallback: Boolean = false,\n)\n\n/**\n * Provenance of one localized string: where the text came from and whether the\n * translation is reviewed, safety-critical, or a fallback.\n */\n@Serializable\ndata class PackTextSourceRef(\n val sourceLocale: String = \"\",\n val sourceDocumentId: String = \"\",\n val sourceSectionId: String = \"\",\n val sourceFactId: String = \"\",\n val reviewState: String = \"\",\n val safetyCritical: Boolean = false,\n val fallbackReason: String = \"\",\n)\n\n/**\n * One locale's bundle of localized strings for a pack, keyed by text key,\n * with per-key source provenance.\n */\n@Serializable\ndata class PackLanguageBundle(\n val locale: String = \"\",\n val reviewState: String = \"\",\n val textHash: String = \"\",\n val strings: JsonObject = JsonObject(emptyMap()),\n val sourceRefs: Map<String, PackTextSourceRef> = emptyMap(),\n val hasSafetyCriticalFallback: Boolean = false,\n val policy: String = \"\",\n)\n`;\n\nconst MOBILE_AUTH_CONTRACT_KT = `${HEADER}\npackage com.happyvertical.smrt.mobile.contract\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonObject\n\n/**\n * \\`/api/mobile\\` auth + session contract (server-brokered PKCE).\n *\n * The server owns the OIDC/PKCE exchange: \\`auth/start\\` returns the\n * authorization URL plus the \\`state\\`/\\`codeVerifier\\` the client must persist\n * and echo back on \\`auth/complete\\`. Reference implementation: anytown\n * dashboard; SMRT-shipped handlers are tracked in issue #1748.\n */\n\n@Serializable\ndata class MobileUserSummary(\n val id: String,\n val email: String = \"\",\n val label: String = \"\",\n)\n\n@Serializable\ndata class MobileTenantSummary(\n val id: String,\n val name: String = \"\",\n val slug: String = \"\",\n val planName: String = \"\",\n val subscriptionStatus: String = \"\",\n)\n\n@Serializable\ndata class MobileTenantOption(\n val id: String,\n val name: String = \"\",\n val slug: String = \"\",\n val roleSlug: String = \"\",\n val roleLabel: String = \"\",\n)\n\n@Serializable\ndata class MobileAuthProviderSummary(\n val id: String,\n val label: String = \"\",\n val type: String = \"\",\n val supportsPkce: Boolean = false,\n)\n\n@Serializable\ndata class MobileAuthStartRequest(\n val providerId: String? = null,\n val redirectUri: String,\n val scopes: List<String> = emptyList(),\n val state: String? = null,\n val loginHint: String? = null,\n)\n\n@Serializable\ndata class MobileAuthStartResponse(\n val providerId: String,\n val authorizationUrl: String,\n val state: String,\n val codeVerifier: String? = null,\n val nonce: String? = null,\n val redirectUri: String,\n)\n\n@Serializable\ndata class MobileAuthCompleteRequest(\n val providerId: String? = null,\n val code: String,\n val state: String? = null,\n val codeVerifier: String? = null,\n val redirectUri: String,\n)\n\n@Serializable\ndata class MobileAuthSession(\n val accessToken: String,\n val tokenType: String = \"Bearer\",\n val expiresAt: String = \"\",\n val user: MobileUserSummary,\n val activeTenant: MobileTenantOption? = null,\n val tenants: List<MobileTenantOption> = emptyList(),\n)\n\n@Serializable\ndata class MobileSessionBootstrap(\n val user: MobileUserSummary,\n val activeTenant: MobileTenantOption? = null,\n val tenants: List<MobileTenantOption> = emptyList(),\n val extras: JsonObject? = null,\n)\n`;\n\nconst MOBILE_DEVICE_CONTRACT_KT = `${HEADER}\npackage com.happyvertical.smrt.mobile.contract\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MobileDevicePermissionState(\n val status: String,\n val canRequest: Boolean = false,\n val reason: String? = null,\n)\n\n@Serializable\ndata class MobileDeviceCapability(\n val surface: String,\n val label: String = \"\",\n val supported: Boolean = false,\n val permission: MobileDevicePermissionState,\n val preferredInput: String = \"\",\n)\n\n@Serializable\ndata class MobileDeviceCapabilities(\n val camera: MobileDeviceCapability,\n val microphone: MobileDeviceCapability,\n val checkedAtEpochMillis: Long? = null,\n)\n`;\n\nconst MOBILE_CONTRACT_INFO_KT = `${HEADER}\npackage com.happyvertical.smrt.mobile.contract\n\nconst val SMRT_MOBILE_CONTRACT_SCHEMA_VERSION: Int = ${SMRT_MOBILE_CONTRACT_SCHEMA_VERSION}\nconst val SMRT_MOBILE_CONTRACT_VERSION: String = \"${SMRT_MOBILE_CONTRACT_VERSION}\"\n`;\n\n/** Kotlin framework contract files, keyed by file name. */\nexport function frameworkKotlinFiles(): Map<string, string> {\n return new Map([\n ['SupportTypes.kt', SUPPORT_TYPES_KT],\n ['PackLocalization.kt', PACK_LOCALIZATION_KT],\n ['MobileAuthContract.kt', MOBILE_AUTH_CONTRACT_KT],\n ['MobileDeviceContract.kt', MOBILE_DEVICE_CONTRACT_KT],\n ['MobileContractInfo.kt', MOBILE_CONTRACT_INFO_KT],\n ]);\n}\n\nconst MOBILE_CONTRACT_SWIFT = `// Generated by @happyvertical/smrt-mobile-contract (framework contract).\n// Do not edit by hand — regenerate with \\`${REGENERATE_COMMAND}\\`.\nimport Foundation\n\nlet SmrtMobileContractSchemaVersion = ${SMRT_MOBILE_CONTRACT_SCHEMA_VERSION}\nlet SmrtMobileContractVersion = \"${SMRT_MOBILE_CONTRACT_VERSION}\"\n\n/// Decimal values cross the wire as strings (no precision loss).\ntypealias DecimalString = String\n\nstruct Measurement {\n let value: DecimalString\n let unit: String\n let source: String?\n}\n\nstruct MobileUserSummary {\n let id: String\n let email: String\n let label: String\n}\n\nstruct MobileTenantSummary {\n let id: String\n let name: String\n let slug: String\n let planName: String\n let subscriptionStatus: String\n}\n\nstruct MobileTenantOption {\n let id: String\n let name: String\n let slug: String\n let roleSlug: String\n let roleLabel: String\n}\n\nstruct MobileAuthProviderSummary {\n let id: String\n let label: String\n let type: String\n let supportsPkce: Bool\n}\n\nstruct MobileAuthStartRequest {\n let providerId: String?\n let redirectUri: String\n let scopes: [String]\n let state: String?\n let loginHint: String?\n}\n\nstruct MobileAuthStartResponse {\n let providerId: String\n let authorizationUrl: String\n let state: String\n let codeVerifier: String?\n let nonce: String?\n let redirectUri: String\n}\n\nstruct MobileAuthCompleteRequest {\n let providerId: String?\n let code: String\n let state: String?\n let codeVerifier: String?\n let redirectUri: String\n}\n\nstruct MobileAuthSession {\n let accessToken: String\n let tokenType: String\n let expiresAt: String\n let user: MobileUserSummary\n let activeTenant: MobileTenantOption?\n let tenants: [MobileTenantOption]\n}\n\nstruct MobileSessionBootstrap {\n let user: MobileUserSummary\n let activeTenant: MobileTenantOption?\n let tenants: [MobileTenantOption]\n let extras: String?\n}\n\nstruct MobileDevicePermissionState {\n let status: String\n let canRequest: Bool\n let reason: String?\n}\n\nstruct MobileDeviceCapability {\n let surface: String\n let label: String\n let supported: Bool\n let permission: MobileDevicePermissionState\n let preferredInput: String\n}\n\nstruct MobileDeviceCapabilities {\n let camera: MobileDeviceCapability\n let microphone: MobileDeviceCapability\n let checkedAtEpochMillis: Int64?\n}\n`;\n\n/**\n * Swift mirror of the framework contract for SwiftUI apps (reporter\n * precedent). Not checked into smrt-mobile — iOS wiring lands with\n * smrt-ios (Phase 6); apps can emit it via this API meanwhile.\n */\nexport function frameworkSwiftFiles(): Map<string, string> {\n return new Map([['MobileContract.swift', MOBILE_CONTRACT_SWIFT]]);\n}\n","import { kotlinIdentifier } from './identifiers.js';\nimport type {\n MobileContract,\n MobileContractField,\n MobileContractObject,\n} from './types.js';\n\nexport const GENERATED_HEADER = [\n '// Generated by @happyvertical/smrt-mobile-contract.',\n '// Do not edit by hand.',\n '',\n].join('\\n');\n\nconst FRAMEWORK_CONTRACT_PACKAGE = 'com.happyvertical.smrt.mobile.contract';\n\n/**\n * Emits one Kotlin DTO file per allowlisted object, plus a\n * `MobileContractManifest.kt` recording the contract identity, all in the\n * consumer's Kotlin package. Framework support types (DecimalString, …) are\n * imported from `com.happyvertical.smrt.mobile.contract`.\n */\nexport function generateKotlinDtoFiles(\n contract: MobileContract,\n): Map<string, string> {\n const files = new Map<string, string>();\n\n files.set('MobileContractManifest.kt', manifestFile(contract));\n for (const object of contract.objects) {\n files.set(`${object.dtoName}.kt`, dtoFile(contract, object));\n }\n\n return files;\n}\n\nfunction manifestFile(contract: MobileContract): string {\n return [\n GENERATED_HEADER,\n `package ${contract.kotlin.packageName}`,\n '',\n `const val MOBILE_CONTRACT_SCHEMA_VERSION: Int = ${contract.schemaVersion}`,\n `const val MOBILE_CONTRACT_VERSION: String = \"${contract.contractVersion}\"`,\n `const val MOBILE_CONTRACT_SOURCE_HASH: String = \"${contract.sourceHash}\"`,\n '',\n 'val MOBILE_CONTRACT_OBJECTS: List<String> = listOf(',\n ...contract.objects.map((object) => ` \"${object.name}\",`),\n ')',\n '',\n ].join('\\n');\n}\n\nfunction dtoFile(\n contract: MobileContract,\n object: MobileContractObject,\n): string {\n const imports = new Set(['kotlinx.serialization.Serializable']);\n for (const field of object.fields) {\n if (field.kotlinType.includes('Instant')) {\n imports.add('kotlinx.datetime.Instant');\n }\n if (field.kotlinType.includes('JsonObject')) {\n imports.add('kotlinx.serialization.json.JsonObject');\n }\n if (field.kotlinType.includes('DecimalString')) {\n imports.add(`${FRAMEWORK_CONTRACT_PACKAGE}.DecimalString`);\n }\n }\n\n return [\n GENERATED_HEADER,\n `package ${contract.kotlin.packageName}`,\n '',\n ...[...imports].sort().map((importPath) => `import ${importPath}`),\n '',\n '@Serializable',\n `data class ${object.dtoName}(`,\n ...object.fields.map((field) => kotlinProperty(field)),\n ')',\n '',\n ].join('\\n');\n}\n\nfunction kotlinProperty(field: MobileContractField): string {\n const defaultValue =\n field.kotlinDefault === null ? '' : ` = ${field.kotlinDefault}`;\n return ` val ${kotlinIdentifier(field.name)}: ${field.kotlinType}${defaultValue},`;\n}\n","import { swiftIdentifier } from './identifiers.js';\nimport type { MobileContract, MobileContractField } from './types.js';\n\n/**\n * Emits all allowlisted domain DTOs into a single Swift file. Structs are\n * plain (no Codable) per the reporter seed — apps decode JSON as needed.\n * Kotlin `JsonObject` fields arrive as raw JSON strings on the Swift side.\n */\nexport function generateSwiftDtoFile(contract: MobileContract): string {\n const lines: string[] = [\n '// Generated by @happyvertical/smrt-mobile-contract.',\n '// Do not edit by hand.',\n 'import Foundation',\n '',\n `let MobileContractSchemaVersion = ${contract.schemaVersion}`,\n `let MobileContractVersion = \"${contract.contractVersion}\"`,\n `let MobileContractSourceHash = \"${contract.sourceHash}\"`,\n '',\n ];\n\n for (const object of contract.objects) {\n lines.push(`struct ${object.dtoName} {`);\n for (const field of object.fields) {\n lines.push(` let ${swiftIdentifier(field.name)}: ${swiftType(field)}`);\n }\n lines.push('}', '');\n }\n\n return lines.join('\\n');\n}\n\nfunction swiftType(field: MobileContractField): string {\n return field.swiftType;\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\n/** Writes every `relPath → content` entry under `rootDir`. */\nexport function writeFileSet(\n rootDir: string,\n files: Map<string, string>,\n): void {\n for (const [relPath, content] of files) {\n const absPath = join(rootDir, relPath);\n mkdirSync(dirname(absPath), { recursive: true });\n writeFileSync(absPath, content);\n }\n}\n\n/**\n * Compares every entry against disk. Returns the list of missing/stale\n * relative paths (empty = fresh).\n */\nexport function verifyFileSet(\n rootDir: string,\n files: Map<string, string>,\n): string[] {\n const stale: string[] = [];\n for (const [relPath, content] of files) {\n const absPath = join(rootDir, relPath);\n if (!existsSync(absPath) || readFileSync(absPath, 'utf8') !== content) {\n stale.push(relPath);\n }\n }\n return stale;\n}\n"],"mappings":";;;;AACO,IAAM,sCAAsC;AAM5C,IAAM,+BAA+B;;;ACA5C,IAAM,uCAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,IAAM,iCAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,IAAM,mBAAmB;AAGlB,SAAS,iBAAiB,MAAsB;CACrD,OAAO,qBAAqB,IAAI,IAAI,KAAK,CAAC,iBAAiB,KAAK,IAAI,IAChE,KAAK,KAAI,MACT;AACN;AAGO,SAAS,gBAAgB,MAAsB;CACpD,OAAO,eAAe,IAAI,IAAI,KAAK,CAAC,iBAAiB,KAAK,IAAI,IAC1D,KAAK,KAAI,MACT;AACN;AAOO,SAAS,oBAAoB,OAAuB;CACzD,IAAI,MAAM;CACV,KAAA,MAAW,QAAQ,OACjB,QAAQ,MAAR;EACE,KAAK;GACH,OAAO;GACP;EACF,KAAK;GACH,OAAO;GACP;EACF,KAAK;GACH,OAAO;GACP;EACF,KAAK;GACH,OAAO;GACP;EACF,KAAK;GACH,OAAO;GACP;EACF,KAAK;GACH,OAAO;GACP;EACF,SAAS;GACP,MAAM,OAAO,KAAK,YAAY,CAAC,KAAK;GAIpC,OACE,OAAO,MAAS,QAAQ,SAAU,QAAQ,QACtC,MAAM,KAAK,SAAS,EAAE,CAAA,CAAE,SAAS,GAAG,GAAG,MACvC;EACR;CACF;CAEF,OAAO,GAAG,IAAG;AACf;;;AChIA,IAAM,+BAAuD;CAC3D,IAAI;CACJ,MAAM;CACN,QAAQ;CACR,YAAY;CACZ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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"generatedAt": "2026-07-03T03:19:17.875Z",
|
|
4
|
+
"packageName": "@happyvertical/smrt-mobile-contract",
|
|
5
|
+
"packageVersion": "0.37.7",
|
|
6
|
+
"sourceManifestPath": "dist/manifest.json",
|
|
7
|
+
"agentDocPath": "AGENTS.md",
|
|
8
|
+
"sourceHashes": {
|
|
9
|
+
"manifest": "6fff72b6fd058fb7db3dc7a8aa088a2239ca6ff3dd835c4c2c9282aa0282d77b",
|
|
10
|
+
"packageJson": "51583b8d58802f0df9e869e226db3e6857a368683ea0087aac3e19068b4141e9",
|
|
11
|
+
"agents": "3f6b1c91d0a7809ce9ea67201d7b1d1659e3f03977eac39ca10e0ed8e3fac32a"
|
|
12
|
+
},
|
|
13
|
+
"exports": [
|
|
14
|
+
"."
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@types/node": "24.13.2",
|
|
18
|
+
"typescript": "^5.9.3",
|
|
19
|
+
"vite": "8.1.2",
|
|
20
|
+
"vitest": "^4.1.9"
|
|
21
|
+
},
|
|
22
|
+
"smrtDependencies": [],
|
|
23
|
+
"sdkDependencies": [],
|
|
24
|
+
"tags": [],
|
|
25
|
+
"risks": [],
|
|
26
|
+
"objects": [],
|
|
27
|
+
"surfaces": [],
|
|
28
|
+
"prompts": [],
|
|
29
|
+
"relationshipsV2": {
|
|
30
|
+
"foreignKeyFields": 0,
|
|
31
|
+
"crossPackageRefFields": 0,
|
|
32
|
+
"junctionCollections": 0,
|
|
33
|
+
"hierarchicalObjects": 0,
|
|
34
|
+
"polymorphicAssociations": 0,
|
|
35
|
+
"uuidColumns": 0
|
|
36
|
+
},
|
|
37
|
+
"agentDoc": "# @happyvertical/smrt-mobile-contract\n\nTypeScript codegen bridging the SMRT manifest to the KMP mobile foundation\n(ADR 0001, epic #1745). Generalizes amaru's `amaru-mobile-contract`\n(manifest → allowlist → contract → Kotlin) and reporter's Swift emission.\nZero runtime dependencies by design — it reads `manifest.json` files\ndirectly, so it stays out of the smrt package DAG.\n\n## Two generation surfaces\n\n1. **Framework contract** (`frameworkKotlinFiles()` / `frameworkSwiftFiles()`)\n — the stable files checked into `@happyvertical/smrt-mobile` under\n `src/commonMain/kotlin/.../contract/` (support types, pack localization,\n `/api/mobile/*` auth/session/device DTOs). This package is their single\n source of truth: `pnpm generate:framework` rewrites them; `pnpm build`\n verifies freshness (`scripts/generate-framework.mjs`). (The script is\n deliberately NOT named `generate` — turbo's `build` task auto-runs a\n `generate` script, which would rewrite the files on every build and\n neutralize the freshness gate.) The Swift mirror\n (`MobileContract.swift`) is emitted on demand for SwiftUI apps and is not\n checked into smrt-mobile (iOS wiring is Phase 6).\n2. **Per-app domain DTOs** (`buildMobileContract()` +\n `generateKotlinDtoFiles()` / `generateSwiftDtoFile()`) — consumer apps\n pass their SMRT `manifest.json`(s) plus an object allowlist and a Kotlin\n package; they get `@Serializable` Kotlin DTOs (defaults on every field for\n safe partial deserialization) and plain Swift structs. Type mapping is\n driven by manifest field `type` metadata (`text`/`integer`/`decimal`/\n `boolean`/`datetime`/`foreignKey`/`crossPackageRef`/`json`), with amaru's\n name heuristics only as a fallback for typeless fields. Relationship\n declarations (`oneToMany`/`manyToMany`/`hasMany`) are excluded — they are\n not scalar columns. Field defaults come from `default` (current manifests)\n with `defaultValue` (amaru-era) as fallback. Decimals emit as\n `DecimalString` (string-encoded — no float precision loss); field names\n stay manifest-faithful (wire-format names, including `created_at`).\n\n## Conventions\n\n- `DecimalString`/`Measurement` and the auth contract types are imported from\n `com.happyvertical.smrt.mobile.contract`, never re-emitted per app.\n- Allowlist entries may be short names or qualified names\n (`@scope/pkg:Object`); ambiguous short names across manifests are an error.\n- `sourceHash` (sha256 over allowlist + projected objects) is stamped into\n generated output so drift is detectable.\n- `turbo.json` here widens the build inputs to smrt-mobile's checked-in\n contract dir so the freshness verify can't replay stale from cache.\n\n## Commands\n\n```bash\npnpm build # vite build + verify smrt-mobile framework contract freshness\npnpm generate:framework # vite build + REWRITE the smrt-mobile framework contract\npnpm test # vitest\n```\n"
|
|
38
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export declare interface MobileContract {
|
|
2
|
+
schemaVersion: number;
|
|
3
|
+
contractVersion: string;
|
|
4
|
+
sourceLabel: string;
|
|
5
|
+
sourceHash: string;
|
|
6
|
+
objectAllowlist: string[];
|
|
7
|
+
objectCount: number;
|
|
8
|
+
kotlin: {
|
|
9
|
+
packageName: string;
|
|
10
|
+
};
|
|
11
|
+
typeMappings: Record<string, string>;
|
|
12
|
+
objects: MobileContractObject[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export declare interface MobileContractField {
|
|
16
|
+
/** Manifest-faithful wire name (also the generated property name). */
|
|
17
|
+
name: string;
|
|
18
|
+
kotlinType: string;
|
|
19
|
+
swiftType: string;
|
|
20
|
+
nullable: boolean;
|
|
21
|
+
/** Kotlin default-value expression, or null for required (id). */
|
|
22
|
+
kotlinDefault: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare interface MobileContractObject {
|
|
26
|
+
name: string;
|
|
27
|
+
qualifiedName: string;
|
|
28
|
+
dtoName: string;
|
|
29
|
+
tableName: string | null;
|
|
30
|
+
hasTenantId: boolean;
|
|
31
|
+
fields: MobileContractField[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare interface MobileContractOptions {
|
|
35
|
+
manifests: SmrtManifest[];
|
|
36
|
+
/** Object names (or qualified names) exposed to mobile. */
|
|
37
|
+
allowlist: string[];
|
|
38
|
+
/** Kotlin package the domain DTOs are emitted into. */
|
|
39
|
+
kotlinPackage: string;
|
|
40
|
+
/** Provenance label recorded in the contract (e.g. the manifest path). */
|
|
41
|
+
sourceLabel?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The subset of a SMRT `manifest.json` the codegen consumes. */
|
|
45
|
+
export declare interface SmrtManifest {
|
|
46
|
+
version?: number | string;
|
|
47
|
+
packageName?: string;
|
|
48
|
+
objects: Record<string, SmrtManifestObject> | SmrtManifestObject[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Shapes for the SMRT mobile contract pipeline:
|
|
53
|
+
* manifest.json (+ allowlist) → MobileContract → generated Kotlin/Swift DTOs.
|
|
54
|
+
*/
|
|
55
|
+
/** One field entry in a SMRT manifest's `objects[*].fields` map. */
|
|
56
|
+
export declare interface SmrtManifestField {
|
|
57
|
+
type?: string;
|
|
58
|
+
required?: boolean;
|
|
59
|
+
related?: string;
|
|
60
|
+
/** Current SMRT manifests emit `default`; amaru-era manifests used `defaultValue`. */
|
|
61
|
+
default?: unknown;
|
|
62
|
+
defaultValue?: unknown;
|
|
63
|
+
_meta?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One object entry in a SMRT manifest (model or collection). */
|
|
67
|
+
export declare interface SmrtManifestObject {
|
|
68
|
+
name: string;
|
|
69
|
+
className?: string;
|
|
70
|
+
qualifiedName?: string;
|
|
71
|
+
collection?: string | null;
|
|
72
|
+
packageName?: string;
|
|
73
|
+
fields?: Record<string, SmrtManifestField>;
|
|
74
|
+
schema?: {
|
|
75
|
+
tableName?: string;
|
|
76
|
+
} | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { }
|
package/dist/types.js
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@happyvertical/smrt-mobile-contract",
|
|
3
|
+
"version": "0.37.7",
|
|
4
|
+
"description": "SMRT mobile contract codegen: manifest + allowlist → mobile-contract.json → Kotlin/Swift DTOs, plus the generated framework contract for @happyvertical/smrt-mobile",
|
|
5
|
+
"author": "HappyVertical",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"CLAUDE.md",
|
|
12
|
+
"dist",
|
|
13
|
+
"AGENTS.md"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "24.13.2",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vite": "8.1.2",
|
|
25
|
+
"vitest": "^4.1.9"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"registry": "https://registry.npmjs.org",
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/happyvertical/smrt.git",
|
|
34
|
+
"directory": "packages/smrt-mobile-contract"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "vite build && node scripts/generate-framework.mjs --verify",
|
|
38
|
+
"build:watch": "vite build --watch",
|
|
39
|
+
"dev": "npm run build:watch & npm run test:watch",
|
|
40
|
+
"generate:framework": "vite build && node scripts/generate-framework.mjs --write",
|
|
41
|
+
"verify:generated": "node scripts/generate-framework.mjs --verify",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
45
|
+
"clean": "rm -rf dist",
|
|
46
|
+
"verify:pack": "node ../../scripts/verify-package-types-exports.js .",
|
|
47
|
+
"build:fresh": "npm run clean && npm run build && npm run verify:pack"
|
|
48
|
+
}
|
|
49
|
+
}
|