Skip to content

Commit a41b729

Browse files
committed
Merge branch 'next'
2 parents 51532ec + 29bf77e commit a41b729

230 files changed

Lines changed: 15235 additions & 3456 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

generate/applyGenericsToSchema.ts

Lines changed: 428 additions & 0 deletions
Large diffs are not rendered by default.

generate/extractInfoFromSchema.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as JsonRefParser from '@apidevtools/json-schema-ref-parser';
22
import fetch from 'cross-fetch';
33
import { compile as hyperschemaToTypings } from 'hyperschema-to-ts';
4+
import { applyGenerics } from './applyGenericsToSchema';
45
import simplifySchema from './generateSimplifiedSchema';
56
import toSafeName from './toSafeName';
67

@@ -26,6 +27,9 @@ export type EndpointInfo = {
2627
isEntityId: boolean;
2728
relType: string;
2829
};
30+
returnsItem?: boolean;
31+
requestBodyRequiresItem?: boolean;
32+
offersNestedItemsOptionInQueryParams?: boolean;
2933
simpleMethodAvailable: boolean;
3034
requestBodyType?: string;
3135
optionalRequestBody: boolean;
@@ -166,9 +170,15 @@ function findDataObjects(schema: JsonRefParser.JSONSchema) {
166170
function find(
167171
maybeDataSchema: JsonRefParser.JSONSchema,
168172
): JsonRefParser.JSONSchema[] {
173+
if (Array.isArray(maybeDataSchema)) {
174+
return maybeDataSchema.flatMap((schema) =>
175+
find(schema as JsonRefParser.JSONSchema),
176+
);
177+
}
178+
169179
if (maybeDataSchema.type === 'array') {
170180
if (typeof maybeDataSchema.items !== 'object') {
171-
throw new Error('No items?');
181+
return [];
172182
}
173183
return find(maybeDataSchema.items);
174184
}
@@ -338,10 +348,30 @@ function generateResourceInfo(
338348
}
339349
: undefined;
340350

351+
const requestBodyType = link.schema
352+
? `${toSafeName(baseTypeName, true)}${toSafeName(link.rel, true)}Schema`
353+
: undefined;
354+
355+
const returnSchema = link.jobSchema || link.targetSchema;
356+
const returnsItem = Boolean(
357+
returnSchema ? findTypeInDataProperty(returnSchema) === 'item' : false,
358+
);
359+
360+
const requestBodyRequiresItem = link.schema
361+
? findTypeInDataProperty(link.schema) === 'item'
362+
: false;
363+
341364
const endpointInfo: EndpointInfo = {
342365
returnsCollection: ['query', 'instances'].some((x) =>
343366
link.rel.includes(x),
344367
),
368+
returnsItem,
369+
requestBodyRequiresItem,
370+
offersNestedItemsOptionInQueryParams:
371+
returnsItem &&
372+
Boolean(
373+
link.hrefSchema?.properties && 'nested' in link.hrefSchema.properties,
374+
),
345375
docUrl: isCma
346376
? `https://www.datocms.com/docs/content-management-api/resources/${jsonApiType.replace(
347377
'_',
@@ -357,9 +387,7 @@ function generateResourceInfo(
357387
urlPlaceholders,
358388
entityIdPlaceholder:
359389
urlPlaceholders.find((p) => p.isEntityId) || undefined,
360-
requestBodyType: link.schema
361-
? `${toSafeName(baseTypeName, true)}${toSafeName(link.rel, true)}Schema`
362-
: undefined,
390+
requestBodyType,
363391
optionalRequestBody: Boolean(link.schema?.type?.includes('null')),
364392
simpleMethodAvailable: true,
365393
requestStructure: link.schema
@@ -425,20 +453,29 @@ function generateResourceInfo(
425453
}
426454

427455
async function schemaToTs(schema: any) {
428-
const result = await hyperschemaToTypings(schema, 'SiteApiSchema', {});
456+
const result = await hyperschemaToTypings(schema, 'SiteApiTypes', {});
429457
return result.replace(/export interface ([^ ]+) {/g, 'export type $1 = {');
430458
}
431459

460+
function clone<T>(obj: T): T {
461+
return JSON.parse(JSON.stringify(obj)) as T;
462+
}
463+
432464
export default async function extractInfoFromSchema(
433465
hyperschemaUrl: string,
434466
isCma: boolean,
435467
): Promise<SchemaInfo> {
436468
const rawSchema = await downloadHyperschema(hyperschemaUrl);
437469

438-
const simplifiedRawSchema = await downloadHyperschema(hyperschemaUrl);
439-
simplifySchema(simplifiedRawSchema);
470+
const rawSchemaToGenerateTypings = clone(rawSchema);
471+
const typings = applyGenerics(await schemaToTs(rawSchemaToGenerateTypings));
472+
473+
const rawSchemaToGenerateSimpleTypings = clone(rawSchema);
474+
simplifySchema(rawSchemaToGenerateSimpleTypings);
475+
const simpleTypings = applyGenerics(
476+
await schemaToTs(rawSchemaToGenerateSimpleTypings),
477+
);
440478

441-
const typings = await schemaToTs(rawSchema);
442479
const schema = await JsonRefParser.dereference(rawSchema);
443480

444481
if (!schema.properties) {
@@ -452,7 +489,7 @@ export default async function extractInfoFromSchema(
452489
generateResourceInfo(isCma, resource, schema),
453490
)
454491
.filter((resourceInfo) => resourceInfo.endpoints.length > 0),
455-
simpleTypings: await schemaToTs(simplifiedRawSchema),
456-
typings: typings,
492+
simpleTypings,
493+
typings,
457494
};
458495
}

generate/index.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,23 @@ async function generate(prefix: string, hyperschemaUrl: string) {
7474
);
7575

7676
await writeTemplate(
77-
'SchemaTypes.ts',
78-
{ typings: schemaInfo.typings },
79-
`./packages/${prefix}-client/src/generated/SchemaTypes.ts`,
77+
'ApiTypes.ts',
78+
{
79+
typings: schemaInfo.typings,
80+
simpleVariant: false,
81+
isCma: prefix === 'cma',
82+
},
83+
`./packages/${prefix}-client/src/generated/RawApiTypes.ts`,
8084
);
8185

8286
await writeTemplate(
83-
'SchemaTypes.ts',
84-
{ typings: schemaInfo.simpleTypings },
85-
`./packages/${prefix}-client/src/generated/SimpleSchemaTypes.ts`,
87+
'ApiTypes.ts',
88+
{
89+
typings: schemaInfo.simpleTypings,
90+
simpleVariant: true,
91+
isCma: prefix === 'cma',
92+
},
93+
`./packages/${prefix}-client/src/generated/ApiTypes.ts`,
8694
);
8795

8896
const resources = schemaInfo.resources.map((resource) => ({
@@ -103,9 +111,17 @@ async function generate(prefix: string, hyperschemaUrl: string) {
103111
);
104112

105113
for (const resourceInfo of schemaInfo.resources) {
106-
await writeTemplate<ResourceInfo & { isCma: boolean }>(
114+
await writeTemplate<
115+
ResourceInfo & { isCma: boolean; someEndpointReturnsItem: boolean }
116+
>(
107117
'resources/ResourceClass.ts',
108-
{ ...resourceInfo, isCma },
118+
{
119+
...resourceInfo,
120+
isCma,
121+
someEndpointReturnsItem: resourceInfo.endpoints.some(
122+
(e) => e.returnsItem,
123+
),
124+
},
109125
`./packages/${prefix}-client/src/generated/resources/${toSafeName(
110126
resourceInfo.jsonApiType,
111127
true,

generate/reExportEverything.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env node --stack_size=800 -r ts-node/register
2+
3+
import { readFileSync, writeFileSync } from 'node:fs';
4+
import path from 'node:path';
5+
import { Project, type SourceFile } from 'ts-morph';
6+
7+
// Get target package from command line argument
8+
const targetPackage = process.argv[2];
9+
if (!targetPackage) {
10+
console.error('Usage: ./generate/reExportEverything.ts <package-name>');
11+
console.error('Example: ./generate/reExportEverything.ts cma-client-node');
12+
process.exit(1);
13+
}
14+
15+
const SRC_FILE = path.resolve(
16+
__dirname,
17+
`../packages/${targetPackage}/src/index.ts`,
18+
);
19+
const CMA_CLIENT_PATH = path.resolve(
20+
__dirname,
21+
'../packages/cma-client/dist/types/index.d.ts',
22+
);
23+
24+
const START_MARKER = '// <AUTO-GENERATED-EXPORTS>';
25+
const END_MARKER = '// </AUTO-GENERATED-EXPORTS>';
26+
27+
const blacklist = new Set([
28+
'buildClient',
29+
'Client',
30+
'GenericClient',
31+
'Resources',
32+
'default',
33+
]);
34+
35+
const project = new Project({
36+
compilerOptions: {
37+
moduleResolution: 2, // Node
38+
target: 99, // ESNext
39+
},
40+
});
41+
42+
// Track collected exports
43+
const collected = {
44+
value: new Set<string>(),
45+
type: new Set<string>(),
46+
};
47+
48+
// Avoid infinite recursion
49+
const visitedFiles = new Set<string>();
50+
51+
function collectExports(file: SourceFile) {
52+
if (visitedFiles.has(file.getFilePath())) return;
53+
visitedFiles.add(file.getFilePath());
54+
55+
for (const stmt of file.getExportDeclarations()) {
56+
const moduleSpecifier = stmt.getModuleSpecifierSourceFile();
57+
58+
// Handle named exports
59+
const namedExports = stmt.getNamedExports();
60+
for (const ne of namedExports) {
61+
// Use alias if present, otherwise use original name
62+
const name = ne.getAliasNode()?.getText() || ne.getName();
63+
if (blacklist.has(name)) continue;
64+
65+
if (stmt.isTypeOnly()) collected.type.add(name);
66+
else collected.value.add(name);
67+
}
68+
69+
// Handle export * (wildcards) - recursively collect from the target file
70+
// Note: export * statements have 0 named exports and no namespace export identifier
71+
const isWildcard =
72+
stmt.getNamedExports().length === 0 && !stmt.getNamespaceExport();
73+
74+
if (isWildcard && moduleSpecifier) {
75+
// Case 1: moduleSpecifier is resolved by ts-morph
76+
collectExports(moduleSpecifier);
77+
} else if (isWildcard && stmt.getModuleSpecifier()) {
78+
// Case 2: relative imports that ts-morph didn't resolve
79+
const moduleString = stmt.getModuleSpecifier()?.getLiteralValue();
80+
if (moduleString) {
81+
// Try to resolve the module path relative to current file
82+
const currentDir = path.dirname(file.getFilePath());
83+
const resolvedPath = path.resolve(currentDir, `${moduleString}.d.ts`);
84+
85+
try {
86+
const targetFile = project.addSourceFileAtPath(resolvedPath);
87+
collectExports(targetFile);
88+
} catch (e) {
89+
console.warn(
90+
`Could not resolve wildcard export: ${moduleString} from ${file.getFilePath()}`,
91+
);
92+
}
93+
}
94+
}
95+
96+
// Handle "export * as NS" → collect namespace as a value export
97+
const nsExport = stmt.getNamespaceExport();
98+
if (nsExport) {
99+
const nsName = nsExport.getName();
100+
if (nsName && !blacklist.has(nsName)) {
101+
collected.value.add(nsName);
102+
}
103+
}
104+
}
105+
106+
// Collect direct exports from functions, classes, variables
107+
for (const fn of file.getFunctions()) {
108+
if (fn.isExported() && !blacklist.has(fn.getName() || '')) {
109+
collected.value.add(fn.getName() || '');
110+
}
111+
}
112+
113+
for (const cls of file.getClasses()) {
114+
if (cls.isExported() && !blacklist.has(cls.getName() || '')) {
115+
collected.value.add(cls.getName() || '');
116+
}
117+
}
118+
119+
for (const vs of file.getVariableStatements()) {
120+
if (vs.isExported()) {
121+
for (const decl of vs.getDeclarations()) {
122+
const name = decl.getName();
123+
if (!blacklist.has(name)) {
124+
collected.value.add(name);
125+
}
126+
}
127+
}
128+
}
129+
130+
// Also collect type-only exports directly declared in this file
131+
for (const ta of file.getTypeAliases()) {
132+
if (ta.isExported() && !blacklist.has(ta.getName())) {
133+
collected.type.add(ta.getName());
134+
}
135+
}
136+
for (const i of file.getInterfaces()) {
137+
if (i.isExported() && !blacklist.has(i.getName())) {
138+
collected.type.add(i.getName());
139+
}
140+
}
141+
for (const e of file.getEnums()) {
142+
if (e.isExported() && !blacklist.has(e.getName())) {
143+
collected.value.add(e.getName());
144+
}
145+
}
146+
}
147+
148+
const cmaFile = project.addSourceFileAtPath(CMA_CLIENT_PATH);
149+
collectExports(cmaFile);
150+
151+
const valueExports = Array.from(collected.value).sort();
152+
const typeExports = Array.from(collected.type).sort();
153+
154+
const newExportBlock = `
155+
${START_MARKER}
156+
export {
157+
${valueExports.join(',\n ')},
158+
} from '@datocms/cma-client';
159+
160+
export type {
161+
${typeExports.join(',\n ')},
162+
} from '@datocms/cma-client';
163+
${END_MARKER}
164+
`.trim();
165+
166+
// Read existing file
167+
let content = '';
168+
try {
169+
content = readFileSync(SRC_FILE, 'utf-8');
170+
} catch {
171+
console.warn(`File ${SRC_FILE} not found. Creating a new one.`);
172+
}
173+
174+
// Replace old export block if present, otherwise prepend
175+
if (content.includes(START_MARKER) && content.includes(END_MARKER)) {
176+
const regex = new RegExp(`${START_MARKER}[\\s\\S]*?${END_MARKER}`, 'm');
177+
content = content.replace(regex, newExportBlock);
178+
} else {
179+
content = `${newExportBlock}\n\n${content}`;
180+
}
181+
182+
// Write updated file
183+
writeFileSync(SRC_FILE, content);
184+
console.log(`Updated exports (including types) in ${SRC_FILE}`);

0 commit comments

Comments
 (0)