@defold-typescript/types 0.19.3 → 0.20.0
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/api-availability.json +2755 -0
- package/api-migrations.json +31 -0
- package/api-signatures.json +2324 -0
- package/api-targets.json +493 -45
- package/generated/b2d.d.ts +6 -0
- package/generated/b2d_body.d.ts +329 -1
- package/generated/b2d_chain.d.ts +106 -0
- package/generated/b2d_fixture.d.ts +155 -0
- package/generated/b2d_joint.d.ts +823 -0
- package/generated/b2d_shape.d.ts +215 -0
- package/generated/b2d_world.d.ts +314 -0
- package/generated/builtin-messages.d.ts +18 -0
- package/generated/camera.d.ts +62 -5
- package/generated/compute.d.ts +306 -0
- package/generated/go.d.ts +29 -4
- package/generated/graphics.d.ts +95 -0
- package/generated/gui.d.ts +1 -3
- package/generated/json.d.ts +2 -2
- package/generated/kinds/gui-script.d.ts +7 -0
- package/generated/kinds/render-script.d.ts +7 -0
- package/generated/kinds/script.d.ts +7 -0
- package/generated/label.d.ts +1 -1
- package/generated/liveupdate.d.ts +22 -4
- package/generated/material.d.ts +444 -0
- package/generated/model.d.ts +46 -5
- package/generated/physics.d.ts +1 -3
- package/generated/profiler.d.ts +2 -3
- package/generated/render.d.ts +25 -9
- package/generated/resource.d.ts +2 -2
- package/generated/sprite.d.ts +15 -1
- package/generated/tilemap.d.ts +14 -0
- package/generated/versions/defold-1.12.4/b2d.d.ts +27 -0
- package/generated/versions/defold-1.12.4/b2d_body.d.ts +348 -0
- package/generated/versions/defold-1.12.4/buffer.d.ts +176 -0
- package/generated/versions/defold-1.12.4/camera.d.ts +245 -0
- package/generated/versions/defold-1.12.4/collectionfactory.d.ts +146 -0
- package/generated/versions/defold-1.12.4/collectionproxy.d.ts +71 -0
- package/generated/versions/defold-1.12.4/crash.d.ts +143 -0
- package/generated/versions/defold-1.12.4/factory.d.ts +123 -0
- package/generated/versions/defold-1.12.4/font.d.ts +81 -0
- package/generated/versions/defold-1.12.4/go.d.ts +1029 -0
- package/generated/versions/defold-1.12.4/graphics.d.ts +226 -0
- package/generated/versions/defold-1.12.4/gui.d.ts +2372 -0
- package/generated/versions/defold-1.12.4/html5.d.ts +53 -0
- package/generated/versions/defold-1.12.4/http.d.ts +84 -0
- package/generated/versions/defold-1.12.4/iac.d.ts +18 -0
- package/generated/versions/defold-1.12.4/iap.d.ts +60 -0
- package/generated/versions/defold-1.12.4/image.d.ts +139 -0
- package/generated/versions/defold-1.12.4/index.d.ts +41 -0
- package/generated/versions/defold-1.12.4/json.d.ts +76 -0
- package/generated/versions/defold-1.12.4/label.d.ts +94 -0
- package/generated/versions/defold-1.12.4/liveupdate.d.ts +122 -0
- package/generated/versions/defold-1.12.4/model.d.ts +205 -0
- package/generated/versions/defold-1.12.4/msg.d.ts +10 -0
- package/generated/versions/defold-1.12.4/particlefx.d.ts +166 -0
- package/generated/versions/defold-1.12.4/physics.d.ts +547 -0
- package/generated/versions/defold-1.12.4/profiler.d.ts +234 -0
- package/generated/versions/defold-1.12.4/push.d.ts +66 -0
- package/generated/versions/defold-1.12.4/render.d.ts +1149 -0
- package/generated/versions/defold-1.12.4/resource.d.ts +1518 -0
- package/generated/versions/defold-1.12.4/socket.d.ts +795 -0
- package/generated/versions/defold-1.12.4/sound.d.ts +291 -0
- package/generated/versions/defold-1.12.4/sprite.d.ts +143 -0
- package/generated/versions/defold-1.12.4/sys.d.ts +695 -0
- package/generated/versions/defold-1.12.4/tilemap.d.ts +181 -0
- package/generated/versions/defold-1.12.4/timer.d.ts +114 -0
- package/generated/versions/defold-1.12.4/types.d.ts +61 -0
- package/generated/versions/defold-1.12.4/vmath.d.ts +806 -0
- package/generated/versions/defold-1.12.4/webview.d.ts +68 -0
- package/generated/versions/defold-1.12.4/window.d.ts +198 -0
- package/generated/versions/defold-1.12.4/zlib.d.ts +40 -0
- package/index.d.ts +22 -0
- package/package.json +8 -1
- package/scripts/fidelity-audit.ts +69 -12
- package/scripts/fidelity-baseline.json +56 -0
- package/scripts/generate-api-availability.ts +139 -0
- package/scripts/generate-api-signatures.ts +66 -0
- package/scripts/import-defold-release.ts +480 -0
- package/scripts/regen.ts +53 -6
- package/scripts/sync-api-docs.ts +89 -41
- package/src/api-availability.ts +0 -0
- package/src/core-types.ts +9 -0
- package/src/emit-dts.ts +674 -17
- package/src/emit-messages.ts +72 -2
- package/src/index.ts +26 -1
- package/src/lifecycle.ts +43 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type ApiAvailability,
|
|
5
|
+
type ApiMigrationCatalog,
|
|
6
|
+
applyMigrationOverlay,
|
|
7
|
+
collectSymbolIdentities,
|
|
8
|
+
deriveAvailabilityMatrix,
|
|
9
|
+
symbolIdentityKey,
|
|
10
|
+
type VersionSurface,
|
|
11
|
+
validateAvailability,
|
|
12
|
+
} from "../src/api-availability";
|
|
13
|
+
import { type ApiModule, parseDefoldApiDoc } from "../src/api-doc";
|
|
14
|
+
import { type ApiTarget, loadApiTargets, loadTargetModules } from "./regen";
|
|
15
|
+
|
|
16
|
+
const PACKAGE_ROOT = resolve(import.meta.dir, "..");
|
|
17
|
+
const MIGRATIONS_PATH = resolve(PACKAGE_ROOT, "api-migrations.json");
|
|
18
|
+
const AVAILABILITY_PATH = resolve(PACKAGE_ROOT, "api-availability.json");
|
|
19
|
+
|
|
20
|
+
const VERSION_FROM_ID = /^defold-(\d+\.\d+\.\d+)$/;
|
|
21
|
+
|
|
22
|
+
export interface AvailabilityArtifact {
|
|
23
|
+
readonly versions: readonly string[];
|
|
24
|
+
readonly records: readonly ApiAvailability[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function versionOf(target: ApiTarget): string {
|
|
28
|
+
const match = VERSION_FROM_ID.exec(target.id);
|
|
29
|
+
if (!match?.[1]) throw new Error(`cannot derive a release version from target id "${target.id}"`);
|
|
30
|
+
return match[1];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function versionTuple(version: string): number[] {
|
|
34
|
+
return version.split(".").map((part) => Number.parseInt(part, 10));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function compareVersions(a: string, b: string): number {
|
|
38
|
+
const [ta, tb] = [versionTuple(a), versionTuple(b)];
|
|
39
|
+
for (let index = 0; index < Math.max(ta.length, tb.length); index += 1) {
|
|
40
|
+
const diff = (ta[index] ?? 0) - (tb[index] ?? 0);
|
|
41
|
+
if (diff !== 0) return diff;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Every complete committed surface, newest first. The committed targets are the
|
|
47
|
+
// `source == null` entries of `api-targets.json` (ref-doc-sourced historical
|
|
48
|
+
// targets are resolved on demand and never a committed-derivation input). This
|
|
49
|
+
// registry-derived, version-desc list is the single source for the availability
|
|
50
|
+
// version axis — it mirrors the CLI `DEFOLD_VERSIONS` order without inverting
|
|
51
|
+
// the package layering (`types` must not import `cli`).
|
|
52
|
+
export function selectCompleteVersionSurfaces(targets: readonly ApiTarget[]): ApiTarget[] {
|
|
53
|
+
const complete = targets
|
|
54
|
+
.filter((target) => (target.source ?? null) == null)
|
|
55
|
+
.sort((a, b) => compareVersions(versionOf(b), versionOf(a)));
|
|
56
|
+
if (complete.length === 0) {
|
|
57
|
+
throw new Error("api-targets.json has no committed (source == null) targets");
|
|
58
|
+
}
|
|
59
|
+
return complete;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function loadMigrationCatalog(path: string = MIGRATIONS_PATH): ApiMigrationCatalog {
|
|
63
|
+
const raw: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
64
|
+
if (
|
|
65
|
+
typeof raw !== "object" ||
|
|
66
|
+
raw === null ||
|
|
67
|
+
!Array.isArray((raw as { migrations?: unknown }).migrations)
|
|
68
|
+
) {
|
|
69
|
+
throw new Error(`${path}: expected an object with a "migrations" array`);
|
|
70
|
+
}
|
|
71
|
+
return raw as ApiMigrationCatalog;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BuildAvailabilityOptions {
|
|
75
|
+
readonly packageRoot?: string;
|
|
76
|
+
readonly registryPath?: string;
|
|
77
|
+
readonly catalog?: ApiMigrationCatalog;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildAvailabilityArtifact(
|
|
81
|
+
options: BuildAvailabilityOptions = {},
|
|
82
|
+
): AvailabilityArtifact {
|
|
83
|
+
const packageRoot = options.packageRoot ?? PACKAGE_ROOT;
|
|
84
|
+
const registryPath = options.registryPath ?? resolve(packageRoot, "api-targets.json");
|
|
85
|
+
const targets = loadApiTargets(registryPath);
|
|
86
|
+
const completeTargets = selectCompleteVersionSurfaces(targets);
|
|
87
|
+
const parse = (target: ApiTarget): ApiModule[] =>
|
|
88
|
+
loadTargetModules(target, packageRoot).map((entry) => parseDefoldApiDoc(entry.doc));
|
|
89
|
+
const surfaces: VersionSurface[] = completeTargets.map((target) => ({
|
|
90
|
+
version: versionOf(target),
|
|
91
|
+
modules: parse(target),
|
|
92
|
+
}));
|
|
93
|
+
const versions = surfaces.map((surface) => surface.version);
|
|
94
|
+
|
|
95
|
+
const derived = deriveAvailabilityMatrix({ surfaces });
|
|
96
|
+
const catalog =
|
|
97
|
+
options.catalog ?? loadMigrationCatalog(resolve(packageRoot, "api-migrations.json"));
|
|
98
|
+
const universe = collectSymbolIdentities(surfaces.flatMap((surface) => surface.modules));
|
|
99
|
+
const records = applyMigrationOverlay({ derived, catalog, universe, versions });
|
|
100
|
+
|
|
101
|
+
const newestSurface = surfaces[0] as VersionSurface;
|
|
102
|
+
const currentSurface = new Set(
|
|
103
|
+
collectSymbolIdentities(newestSurface.modules).map(symbolIdentityKey),
|
|
104
|
+
);
|
|
105
|
+
const knownIdentities = new Set(universe.map(symbolIdentityKey));
|
|
106
|
+
const errors = validateAvailability({ records, versions, currentSurface, knownIdentities });
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
throw new Error(`api-availability validation failed:\n ${errors.join("\n ")}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { versions, records };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function biomeFormatJson(raw: string): string {
|
|
115
|
+
const out = Bun.spawnSync(
|
|
116
|
+
["bunx", "biome", "format", "--stdin-file-path=api-availability.json"],
|
|
117
|
+
{
|
|
118
|
+
stdin: Buffer.from(raw),
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
if (out.exitCode !== 0) {
|
|
122
|
+
throw new Error(`biome format failed: ${out.stderr.toString()}`);
|
|
123
|
+
}
|
|
124
|
+
return out.stdout.toString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function serializeAvailabilityArtifact(artifact: AvailabilityArtifact): string {
|
|
128
|
+
return biomeFormatJson(JSON.stringify(artifact));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (import.meta.main) {
|
|
132
|
+
const artifact = buildAvailabilityArtifact();
|
|
133
|
+
if (process.argv.includes("--write")) {
|
|
134
|
+
Bun.write(AVAILABILITY_PATH, serializeAvailabilityArtifact(artifact));
|
|
135
|
+
console.log(`wrote ${AVAILABILITY_PATH} (${artifact.records.length} records)`);
|
|
136
|
+
} else {
|
|
137
|
+
process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { symbolIdentityKey } from "../src/api-availability";
|
|
3
|
+
import { selectCompleteVersionSurfaces, versionOf } from "./generate-api-availability";
|
|
4
|
+
import { generateModuleSignatures, loadApiTargets, loadTargetModules } from "./regen";
|
|
5
|
+
|
|
6
|
+
const PACKAGE_ROOT = resolve(import.meta.dir, "..");
|
|
7
|
+
const SIGNATURES_PATH = resolve(PACKAGE_ROOT, "api-signatures.json");
|
|
8
|
+
|
|
9
|
+
export interface SignaturesArtifact {
|
|
10
|
+
// version -> symbolIdentityKey -> authoritative TS signature text
|
|
11
|
+
readonly versions: Record<string, Record<string, string>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BuildSignaturesOptions {
|
|
15
|
+
readonly packageRoot?: string;
|
|
16
|
+
readonly registryPath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sortObjectKeys(record: Record<string, string>): Record<string, string> {
|
|
20
|
+
const sorted: Record<string, string> = {};
|
|
21
|
+
for (const key of Object.keys(record).sort()) sorted[key] = record[key] as string;
|
|
22
|
+
return sorted;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildSignaturesArtifact(options: BuildSignaturesOptions = {}): SignaturesArtifact {
|
|
26
|
+
const packageRoot = options.packageRoot ?? PACKAGE_ROOT;
|
|
27
|
+
const registryPath = options.registryPath ?? resolve(packageRoot, "api-targets.json");
|
|
28
|
+
const targets = selectCompleteVersionSurfaces(loadApiTargets(registryPath));
|
|
29
|
+
const versions: Record<string, Record<string, string>> = {};
|
|
30
|
+
for (const target of targets) {
|
|
31
|
+
const version = versionOf(target);
|
|
32
|
+
const perSymbol: Record<string, string> = {};
|
|
33
|
+
for (const entry of loadTargetModules(target, packageRoot)) {
|
|
34
|
+
for (const { identity, tsSignature } of generateModuleSignatures(entry)) {
|
|
35
|
+
perSymbol[symbolIdentityKey(identity)] = tsSignature;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
versions[version] = sortObjectKeys(perSymbol);
|
|
39
|
+
}
|
|
40
|
+
return { versions };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function biomeFormatJson(raw: string): string {
|
|
44
|
+
const out = Bun.spawnSync(["bunx", "biome", "format", "--stdin-file-path=api-signatures.json"], {
|
|
45
|
+
stdin: Buffer.from(raw),
|
|
46
|
+
});
|
|
47
|
+
if (out.exitCode !== 0) {
|
|
48
|
+
throw new Error(`biome format failed: ${out.stderr.toString()}`);
|
|
49
|
+
}
|
|
50
|
+
return out.stdout.toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function serializeSignaturesArtifact(artifact: SignaturesArtifact): string {
|
|
54
|
+
return biomeFormatJson(JSON.stringify(artifact));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (import.meta.main) {
|
|
58
|
+
const artifact = buildSignaturesArtifact();
|
|
59
|
+
if (process.argv.includes("--write")) {
|
|
60
|
+
Bun.write(SIGNATURES_PATH, serializeSignaturesArtifact(artifact));
|
|
61
|
+
const count = Object.values(artifact.versions).reduce((n, v) => n + Object.keys(v).length, 0);
|
|
62
|
+
console.log(`wrote ${SIGNATURES_PATH} (${count} signatures)`);
|
|
63
|
+
} else {
|
|
64
|
+
process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { isKnownDefoldTypeToken } from "../src/emit-dts";
|
|
4
|
+
import { refDocCacheDir, resolveRefDoc } from "./doc-source";
|
|
5
|
+
import {
|
|
6
|
+
apiElementIdentity,
|
|
7
|
+
IGNORED_UPSTREAM,
|
|
8
|
+
mergeApiDocs,
|
|
9
|
+
readZip,
|
|
10
|
+
SYNC_MANIFEST,
|
|
11
|
+
type ZipAccessor,
|
|
12
|
+
} from "./sync-api-docs";
|
|
13
|
+
|
|
14
|
+
const EXACT_VERSION = /^\d+\.\d+\.\d+$/;
|
|
15
|
+
const LUA_NAMESPACE = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/;
|
|
16
|
+
const PACKAGE_ROOT = resolve(import.meta.dir, "..");
|
|
17
|
+
|
|
18
|
+
export const DEFOLD_1_13_PROMOTED_NAMESPACES = [
|
|
19
|
+
"b2d.chain",
|
|
20
|
+
"b2d.fixture",
|
|
21
|
+
"b2d.joint",
|
|
22
|
+
"b2d.shape",
|
|
23
|
+
"b2d.world",
|
|
24
|
+
"compute",
|
|
25
|
+
"material",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
interface RawElement extends Record<string, unknown> {
|
|
29
|
+
type?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RawApiDoc extends Record<string, unknown> {
|
|
34
|
+
info?: { namespace?: string };
|
|
35
|
+
elements?: RawElement[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ReleaseBaselineModule {
|
|
39
|
+
readonly namespace: string;
|
|
40
|
+
readonly fixture: string;
|
|
41
|
+
readonly doc: unknown;
|
|
42
|
+
readonly sourceEntries?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ReleaseBaseline {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly modules: readonly ReleaseBaselineModule[];
|
|
48
|
+
readonly luaStdlib: readonly ReleaseBaselineModule[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ReleaseImportSnapshot {
|
|
52
|
+
readonly namespace: string;
|
|
53
|
+
readonly fixture: string;
|
|
54
|
+
readonly doc: unknown;
|
|
55
|
+
readonly sourceEntries: readonly string[];
|
|
56
|
+
readonly symbolSources: readonly SymbolSource[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SymbolSource {
|
|
60
|
+
readonly identity: string;
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly kind: string;
|
|
63
|
+
readonly sourceEntries: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ReleaseSource {
|
|
67
|
+
readonly namespace: string;
|
|
68
|
+
readonly entries: string[];
|
|
69
|
+
readonly functionCount: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface NamespaceDelta {
|
|
73
|
+
readonly added: string[];
|
|
74
|
+
readonly removed: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface SymbolDelta {
|
|
78
|
+
readonly namespace: string;
|
|
79
|
+
readonly added: string[];
|
|
80
|
+
readonly removed: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface MovedNamespace {
|
|
84
|
+
readonly namespace: string;
|
|
85
|
+
readonly from: string[];
|
|
86
|
+
readonly to: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface UnknownTypeBlocker {
|
|
90
|
+
readonly namespace: string;
|
|
91
|
+
readonly symbol: string;
|
|
92
|
+
readonly tokens: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface UnmappedNamespaceBlocker {
|
|
96
|
+
readonly namespace: string;
|
|
97
|
+
readonly entries: string[];
|
|
98
|
+
readonly symbols: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ReleaseImportManifest {
|
|
102
|
+
readonly version: string;
|
|
103
|
+
readonly baseline: string;
|
|
104
|
+
readonly ready: boolean;
|
|
105
|
+
readonly sources: ReleaseSource[];
|
|
106
|
+
readonly namespaces: NamespaceDelta;
|
|
107
|
+
readonly moved: MovedNamespace[];
|
|
108
|
+
readonly symbols: SymbolDelta[];
|
|
109
|
+
readonly blockers: {
|
|
110
|
+
readonly unknownTypes: UnknownTypeBlocker[];
|
|
111
|
+
readonly unmappedFunctionNamespaces: UnmappedNamespaceBlocker[];
|
|
112
|
+
};
|
|
113
|
+
readonly snapshots: Array<{
|
|
114
|
+
readonly namespace: string;
|
|
115
|
+
readonly fixture: string;
|
|
116
|
+
readonly sourceEntries: readonly string[];
|
|
117
|
+
readonly symbols: readonly SymbolSource[];
|
|
118
|
+
}>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ReleaseImportPlan = Omit<ReleaseImportManifest, "snapshots"> & {
|
|
122
|
+
readonly manifest: ReleaseImportManifest;
|
|
123
|
+
readonly snapshots: ReleaseImportSnapshot[];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
127
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parsedDoc(raw: string): RawApiDoc | null {
|
|
131
|
+
try {
|
|
132
|
+
const value: unknown = JSON.parse(raw);
|
|
133
|
+
return isRecord(value) ? (value as RawApiDoc) : null;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function namespaceOf(doc: RawApiDoc): string | null {
|
|
140
|
+
const namespace = isRecord(doc.info) ? doc.info.namespace : undefined;
|
|
141
|
+
return typeof namespace === "string" && LUA_NAMESPACE.test(namespace) ? namespace : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function elementsOf(doc: unknown): RawElement[] {
|
|
145
|
+
if (!isRecord(doc) || !Array.isArray(doc.elements)) return [];
|
|
146
|
+
return doc.elements.filter(isRecord) as RawElement[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function symbolNames(doc: unknown): string[] {
|
|
150
|
+
return [
|
|
151
|
+
...new Set(elementsOf(doc).flatMap((element) => (element.name ? [element.name] : []))),
|
|
152
|
+
].sort();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function expectedSourceEntries(module: ReleaseBaselineModule): string[] {
|
|
156
|
+
if (module.sourceEntries) return [...module.sourceEntries].sort();
|
|
157
|
+
const mapped = SYNC_MANIFEST.find((entry) => entry.namespace === module.namespace);
|
|
158
|
+
if (mapped) return [mapped.zipEntry, ...(mapped.mergeEntries ?? [])].sort();
|
|
159
|
+
return [`doc/${module.namespace.replace(/\./g, "_")}.json`];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function typeTokens(value: unknown, out: Set<string>): void {
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
for (const item of value) typeTokens(item, out);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!isRecord(value)) return;
|
|
168
|
+
if (Array.isArray(value.types)) {
|
|
169
|
+
for (const token of value.types) {
|
|
170
|
+
if (typeof token === "string" && !isKnownDefoldTypeToken(token)) out.add(token);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const nested of Object.values(value)) typeTokens(nested, out);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function unknownTypeBlockers(
|
|
177
|
+
namespace: string,
|
|
178
|
+
doc: unknown,
|
|
179
|
+
knownConstants: ReadonlySet<string>,
|
|
180
|
+
): UnknownTypeBlocker[] {
|
|
181
|
+
const out: UnknownTypeBlocker[] = [];
|
|
182
|
+
for (const element of elementsOf(doc)) {
|
|
183
|
+
const tokens = new Set<string>();
|
|
184
|
+
typeTokens(element, tokens);
|
|
185
|
+
for (const token of knownConstants) tokens.delete(token);
|
|
186
|
+
if (tokens.size === 0) continue;
|
|
187
|
+
out.push({ namespace, symbol: element.name ?? "<unnamed>", tokens: [...tokens].sort() });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function reportWithoutSnapshots(plan: Omit<ReleaseImportPlan, "manifest">): ReleaseImportManifest {
|
|
193
|
+
return {
|
|
194
|
+
version: plan.version,
|
|
195
|
+
baseline: plan.baseline,
|
|
196
|
+
ready: plan.ready,
|
|
197
|
+
sources: plan.sources,
|
|
198
|
+
namespaces: plan.namespaces,
|
|
199
|
+
moved: plan.moved,
|
|
200
|
+
symbols: plan.symbols,
|
|
201
|
+
blockers: plan.blockers,
|
|
202
|
+
snapshots: plan.snapshots.map(({ namespace, fixture, sourceEntries, symbolSources }) => ({
|
|
203
|
+
namespace,
|
|
204
|
+
fixture,
|
|
205
|
+
sourceEntries,
|
|
206
|
+
symbols: symbolSources,
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function buildReleaseImportPlan(input: {
|
|
212
|
+
version: string;
|
|
213
|
+
zip: ZipAccessor;
|
|
214
|
+
baseline: ReleaseBaseline;
|
|
215
|
+
}): ReleaseImportPlan {
|
|
216
|
+
if (!EXACT_VERSION.test(input.version)) {
|
|
217
|
+
throw new Error(`expected an exact Defold release version, received '${input.version}'`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const grouped = new Map<string, Array<{ entry: string; doc: RawApiDoc }>>();
|
|
221
|
+
for (const zipEntry of [...input.zip.entries()].sort()) {
|
|
222
|
+
const doc = parsedDoc(input.zip.read(zipEntry));
|
|
223
|
+
if (!doc) continue;
|
|
224
|
+
const namespace = namespaceOf(doc);
|
|
225
|
+
if (!namespace) continue;
|
|
226
|
+
const group = grouped.get(namespace) ?? [];
|
|
227
|
+
group.push({ entry: zipEntry, doc });
|
|
228
|
+
grouped.set(namespace, group);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const baselineModules = [...input.baseline.modules, ...input.baseline.luaStdlib];
|
|
232
|
+
const generatedNamespaces = new Set(input.baseline.modules.map((module) => module.namespace));
|
|
233
|
+
const baselineByNamespace = new Map(baselineModules.map((module) => [module.namespace, module]));
|
|
234
|
+
const incomingNamespaces = [...grouped.keys()].sort();
|
|
235
|
+
const baselineNamespaces = [...baselineByNamespace.keys()].sort();
|
|
236
|
+
const namespaces = {
|
|
237
|
+
added: incomingNamespaces.filter((namespace) => !baselineByNamespace.has(namespace)),
|
|
238
|
+
removed: baselineNamespaces.filter((namespace) => !grouped.has(namespace)),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const sources: ReleaseSource[] = incomingNamespaces.map((namespace) => {
|
|
242
|
+
const group = grouped.get(namespace) as Array<{ entry: string; doc: RawApiDoc }>;
|
|
243
|
+
return {
|
|
244
|
+
namespace,
|
|
245
|
+
entries: group.map(({ entry }) => entry).sort(),
|
|
246
|
+
functionCount: group.reduce(
|
|
247
|
+
(count, { doc }) =>
|
|
248
|
+
count + elementsOf(doc).filter((element) => element.type === "FUNCTION").length,
|
|
249
|
+
0,
|
|
250
|
+
),
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const knownConstants = new Set<string>();
|
|
255
|
+
for (const group of grouped.values()) {
|
|
256
|
+
for (const { doc } of group) {
|
|
257
|
+
for (const element of elementsOf(doc)) {
|
|
258
|
+
if (element.type === "CONSTANT" && typeof element.name === "string") {
|
|
259
|
+
knownConstants.add(element.name);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const snapshots: ReleaseImportSnapshot[] = [];
|
|
266
|
+
const moved: MovedNamespace[] = [];
|
|
267
|
+
const symbols: SymbolDelta[] = [];
|
|
268
|
+
const unknownTypes: UnknownTypeBlocker[] = [];
|
|
269
|
+
for (const module of baselineModules) {
|
|
270
|
+
const group = grouped.get(module.namespace);
|
|
271
|
+
if (!group) continue;
|
|
272
|
+
const sourceEntries = group.map(({ entry }) => entry).sort();
|
|
273
|
+
const doc = mergeApiDocs(group.map((item) => item.doc));
|
|
274
|
+
const symbolEntries = new Map<string, SymbolSource>();
|
|
275
|
+
for (const item of group) {
|
|
276
|
+
for (const element of elementsOf(item.doc)) {
|
|
277
|
+
const identity = apiElementIdentity(module.namespace, element);
|
|
278
|
+
if (!identity || typeof element.name !== "string" || typeof element.type !== "string")
|
|
279
|
+
continue;
|
|
280
|
+
const existing = symbolEntries.get(identity);
|
|
281
|
+
if (existing) existing.sourceEntries.push(item.entry);
|
|
282
|
+
else {
|
|
283
|
+
symbolEntries.set(identity, {
|
|
284
|
+
identity,
|
|
285
|
+
name: element.name,
|
|
286
|
+
kind: element.type,
|
|
287
|
+
sourceEntries: [item.entry],
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const symbolSources = [...symbolEntries.values()]
|
|
293
|
+
.map((symbol) => ({ ...symbol, sourceEntries: [...new Set(symbol.sourceEntries)].sort() }))
|
|
294
|
+
.sort((a, b) => a.identity.localeCompare(b.identity));
|
|
295
|
+
snapshots.push({
|
|
296
|
+
namespace: module.namespace,
|
|
297
|
+
fixture: module.fixture,
|
|
298
|
+
doc,
|
|
299
|
+
sourceEntries,
|
|
300
|
+
symbolSources,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const expected = expectedSourceEntries(module);
|
|
304
|
+
if (expected.some((entry) => !sourceEntries.includes(entry))) {
|
|
305
|
+
moved.push({ namespace: module.namespace, from: expected, to: sourceEntries });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const before = new Set(symbolNames(module.doc));
|
|
309
|
+
const after = new Set(symbolNames(doc));
|
|
310
|
+
const added = [...after].filter((name) => !before.has(name)).sort();
|
|
311
|
+
const removed = [...before].filter((name) => !after.has(name)).sort();
|
|
312
|
+
if (added.length > 0 || removed.length > 0)
|
|
313
|
+
symbols.push({ namespace: module.namespace, added, removed });
|
|
314
|
+
if (generatedNamespaces.has(module.namespace)) {
|
|
315
|
+
unknownTypes.push(...unknownTypeBlockers(module.namespace, doc, knownConstants));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const unmappedFunctionNamespaces = sources
|
|
320
|
+
.filter(
|
|
321
|
+
(source) =>
|
|
322
|
+
source.functionCount > 0 &&
|
|
323
|
+
!baselineByNamespace.has(source.namespace) &&
|
|
324
|
+
!IGNORED_UPSTREAM.has(source.namespace),
|
|
325
|
+
)
|
|
326
|
+
.map((source) => ({
|
|
327
|
+
namespace: source.namespace,
|
|
328
|
+
entries: source.entries,
|
|
329
|
+
symbols: symbolNames(
|
|
330
|
+
mergeApiDocs((grouped.get(source.namespace) ?? []).map((item) => item.doc)),
|
|
331
|
+
).filter((name) =>
|
|
332
|
+
elementsOf(
|
|
333
|
+
mergeApiDocs((grouped.get(source.namespace) ?? []).map((item) => item.doc)),
|
|
334
|
+
).some((element) => element.type === "FUNCTION" && element.name === name),
|
|
335
|
+
),
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
snapshots.sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
339
|
+
moved.sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
340
|
+
symbols.sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
341
|
+
unknownTypes.sort(
|
|
342
|
+
(a, b) => a.namespace.localeCompare(b.namespace) || a.symbol.localeCompare(b.symbol),
|
|
343
|
+
);
|
|
344
|
+
const ready = unknownTypes.length === 0 && unmappedFunctionNamespaces.length === 0;
|
|
345
|
+
const planBase = {
|
|
346
|
+
version: input.version,
|
|
347
|
+
baseline: input.baseline.id,
|
|
348
|
+
ready,
|
|
349
|
+
sources,
|
|
350
|
+
namespaces,
|
|
351
|
+
moved,
|
|
352
|
+
symbols,
|
|
353
|
+
blockers: { unknownTypes, unmappedFunctionNamespaces },
|
|
354
|
+
snapshots,
|
|
355
|
+
};
|
|
356
|
+
const manifest = reportWithoutSnapshots(planBase);
|
|
357
|
+
return { ...planBase, manifest };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function releaseImportReportJson(plan: ReleaseImportPlan): string {
|
|
361
|
+
return `${JSON.stringify(plan.manifest, null, 2)}\n`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function applyReleaseImport(plan: ReleaseImportPlan, packageRoot = PACKAGE_ROOT): string[] {
|
|
365
|
+
if (!plan.ready)
|
|
366
|
+
throw new Error("release import is blocked; resolve every reported blocker first");
|
|
367
|
+
const relativeRoot = `fixtures/defold-${plan.version}`;
|
|
368
|
+
const written: string[] = [];
|
|
369
|
+
for (const snapshot of plan.snapshots) {
|
|
370
|
+
const relative = `${relativeRoot}/${snapshot.fixture}`;
|
|
371
|
+
const path = resolve(packageRoot, relative);
|
|
372
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
373
|
+
writeFileSync(path, `${JSON.stringify(snapshot.doc, null, 2)}\n`);
|
|
374
|
+
written.push(relative);
|
|
375
|
+
}
|
|
376
|
+
const manifestPath = `${relativeRoot}/import-manifest.json`;
|
|
377
|
+
writeFileSync(resolve(packageRoot, manifestPath), releaseImportReportJson(plan));
|
|
378
|
+
written.push(manifestPath);
|
|
379
|
+
return written.sort();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export interface ReleaseImportArgs {
|
|
383
|
+
readonly version: string;
|
|
384
|
+
readonly check: boolean;
|
|
385
|
+
readonly json: boolean;
|
|
386
|
+
readonly zipPath?: string;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function parseReleaseImportArgs(args: readonly string[]): ReleaseImportArgs {
|
|
390
|
+
const version = args[0];
|
|
391
|
+
if (!version || !EXACT_VERSION.test(version)) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
"import-defold-release requires an exact Defold release version (for example 1.13.0)",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
let check = false;
|
|
397
|
+
let json = false;
|
|
398
|
+
let zipPath: string | undefined;
|
|
399
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
400
|
+
const arg = args[index];
|
|
401
|
+
if (arg === "--check") check = true;
|
|
402
|
+
else if (arg === "--json") json = true;
|
|
403
|
+
else if (arg === "--zip") {
|
|
404
|
+
zipPath = args[index + 1];
|
|
405
|
+
if (!zipPath || zipPath.startsWith("--")) throw new Error("--zip requires a path");
|
|
406
|
+
index += 1;
|
|
407
|
+
} else {
|
|
408
|
+
throw new Error(`unknown argument '${arg}'`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return { version, check, json, ...(zipPath ? { zipPath } : {}) };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function loadBaseline(packageRoot = PACKAGE_ROOT): ReleaseBaseline {
|
|
415
|
+
const registry = JSON.parse(readFileSync(resolve(packageRoot, "api-targets.json"), "utf8")) as {
|
|
416
|
+
targets: Array<{
|
|
417
|
+
id: string;
|
|
418
|
+
default?: boolean;
|
|
419
|
+
fixturesDir: string;
|
|
420
|
+
modules: Array<{ namespace: string; fixture: string }>;
|
|
421
|
+
luaStdlib?: Array<{ namespace: string; fixture: string }>;
|
|
422
|
+
}>;
|
|
423
|
+
};
|
|
424
|
+
const target = registry.targets.find((candidate) => candidate.default === true);
|
|
425
|
+
if (!target) throw new Error("api-targets.json has no default baseline target");
|
|
426
|
+
const load = (module: { namespace: string; fixture: string }): ReleaseBaselineModule => ({
|
|
427
|
+
...module,
|
|
428
|
+
doc: JSON.parse(readFileSync(resolve(packageRoot, target.fixturesDir, module.fixture), "utf8")),
|
|
429
|
+
});
|
|
430
|
+
const modules = target.modules
|
|
431
|
+
.filter((module) => SYNC_MANIFEST.some((source) => source.namespace === module.namespace))
|
|
432
|
+
.map(load);
|
|
433
|
+
for (const namespace of DEFOLD_1_13_PROMOTED_NAMESPACES) {
|
|
434
|
+
if (modules.some((module) => module.namespace === namespace)) continue;
|
|
435
|
+
modules.push({
|
|
436
|
+
namespace,
|
|
437
|
+
fixture: `${namespace.replace(/\./g, "_")}_doc.json`,
|
|
438
|
+
doc: { info: { namespace }, elements: [] },
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
id: target.id,
|
|
443
|
+
modules,
|
|
444
|
+
luaStdlib: (target.luaStdlib ?? []).map(load),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (import.meta.main) {
|
|
449
|
+
try {
|
|
450
|
+
const args = parseReleaseImportArgs(process.argv.slice(2));
|
|
451
|
+
const resolved = args.zipPath
|
|
452
|
+
? { zip: readZip(args.zipPath), provenance: "local" as const }
|
|
453
|
+
: await resolveRefDoc({ version: args.version, cacheDir: refDocCacheDir() });
|
|
454
|
+
const plan = buildReleaseImportPlan({
|
|
455
|
+
version: args.version,
|
|
456
|
+
zip: resolved.zip,
|
|
457
|
+
baseline: loadBaseline(),
|
|
458
|
+
});
|
|
459
|
+
if (args.json) process.stdout.write(releaseImportReportJson(plan));
|
|
460
|
+
else {
|
|
461
|
+
console.log(`Defold ${args.version} release import: ${plan.ready ? "ready" : "blocked"}`);
|
|
462
|
+
console.log(` source: ${resolved.provenance}`);
|
|
463
|
+
console.log(
|
|
464
|
+
` namespaces: +${plan.namespaces.added.length} -${plan.namespaces.removed.length}`,
|
|
465
|
+
);
|
|
466
|
+
console.log(` moved: ${plan.moved.length}; symbol deltas: ${plan.symbols.length}`);
|
|
467
|
+
console.log(
|
|
468
|
+
` blockers: ${plan.blockers.unknownTypes.length} unknown type symbol(s), ${plan.blockers.unmappedFunctionNamespaces.length} unmapped namespace(s)`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (!args.check && plan.ready) {
|
|
472
|
+
const written = applyReleaseImport(plan);
|
|
473
|
+
if (!args.json) console.log(` wrote: ${written.join(", ")}`);
|
|
474
|
+
}
|
|
475
|
+
if (!plan.ready) process.exitCode = 1;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
478
|
+
process.exitCode = 1;
|
|
479
|
+
}
|
|
480
|
+
}
|