Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(api): nestling generated schemas into a new schemas/ directory #756

Merged
merged 2 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 63 additions & 23 deletions packages/api/src/codegen/languages/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ interface OperationTypeHousing {
};
}

/**
* This is the conversion prefix that we add to all `$ref` pointers we find in generated JSON
* Schema.
*
* Because the pointer name is a string we want to have it reference the schema constant we're
* adding into the codegen'd schema file. As there's no way, not even using `eval()` in this case,
* to convert a string to a constant we're prefixing them with this so we can later remove it and
* rewrite the value to a literal. eg. `'Pet'` becomes `Pet`.
*
* And because our TypeScript type name generator properly ignores `:`, this is safe to prepend to
* all generated type names.
*/
const REF_PLACEHOLDER_REGEX = /"::convert::([a-zA-Z_$\\d]*)"/g;

export default class TSGenerator extends CodeGenerator {
project: Project;

Expand Down Expand Up @@ -100,10 +114,6 @@ export default class TSGenerator extends CodeGenerator {
};

this.project = new Project({
manipulationSettings: {
indentationText: IndentationText.TwoSpaces,
quoteKind: QuoteKind.Single,
},
compilerOptions: {
// If we're exporting a TypeScript SDK then we don't need to pollute the codegen directory
// with unnecessary declaration `.d.ts` files.
Expand All @@ -120,6 +130,11 @@ export default class TSGenerator extends CodeGenerator {
// Basically without this option CJS code will fail.
...(options.compilerTarget === 'cjs' ? { esModuleInterop: true } : {}),
},
manipulationSettings: {
indentationText: IndentationText.TwoSpaces,
quoteKind: QuoteKind.Single,
},
useInMemoryFileSystem: true,
});

this.compilerTarget = options.compilerTarget;
Expand Down Expand Up @@ -269,9 +284,18 @@ export default class TSGenerator extends CodeGenerator {
}

return [
...this.project.getSourceFiles().map(sourceFile => ({
[sourceFile.getBaseName()]: sourceFile.getFullText(),
})),
...this.project.getSourceFiles().map(sourceFile => {
// `getFilePath` will always return a string that contains a preceeding directory separator
// however when we're creating these codegen'd files that may cause us to create that file
// in the root directory (because it's preceeded by a `/`). We don't want that to happen so
// we're slicing off that first character.
let filePath = sourceFile.getFilePath().toString();
filePath = filePath.substring(1);

return {
[filePath]: sourceFile.getFullText(),
};
}),

// Because we're returning the raw source files for TS generation we also need to separately
// emit out our declaration files so we can put those into a separate file in the installed
Expand Down Expand Up @@ -452,40 +476,56 @@ sdk.server('https://eu.api.example.com/v14');`),
*/
private createSchemasFile() {
const sourceFile = this.project.createSourceFile('schemas.ts', '');
const schemasDir = this.project.createDirectory('schemas');

const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());

Array.from(sortedSchemas).forEach(([schemaName, schema]) => {
sourceFile.addVariableStatement({
const schemaFile = schemasDir.createSourceFile(`${schemaName}.ts`);

// Because we're chunking our schemas into a `schemas/` directory we need to add imports
// for these schemas into our main `schemas.ts` file.`
sourceFile.addImportDeclaration({
defaultImport: schemaName,
moduleSpecifier: `./schemas/${schemaName}`,
});

let str = JSON.stringify(schema);
const referencedSchemas = str.match(REF_PLACEHOLDER_REGEX)?.map(s => s.replace(REF_PLACEHOLDER_REGEX, '$1'));
if (referencedSchemas) {
referencedSchemas.sort();
referencedSchemas.forEach(ref => {
// Because this schema is referenced from another file we need to create an `import`
// declaration for it.
schemaFile.addImportDeclaration({
defaultImport: ref,
moduleSpecifier: `./${ref}`,
});
});
}

// Load the schema into the schema file within the `schemas/` directory.
schemaFile.addVariableStatement({
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: schemaName,
initializer: writer => {
/**
* This is the conversion prefix that we add to all `$ref` pointers we find in
* generated JSON Schema.
*
* Because the pointer name is a string we want to have it reference the schema
* constant we're adding into the codegen'd schema file. As there's no way, not even
* using `eval()` in this case, to convert a string to a constant we're prefixing
* them with this so we can later remove it and rewrite the value to a literal.
* eg. `'Pet'` becomes `Pet`.
*
* And because our TypeScript type name generator properly ignores `:`, this is safe
* to prepend to all generated type names.
*/
let str = JSON.stringify(schema);
str = str.replace(/"::convert::([a-zA-Z_$\\d]*)"/g, '$1');
// We can't have `::convert::<schemaName>` variables within these schema files so we
// need to clean them up.
str = str.replace(REF_PLACEHOLDER_REGEX, '$1');

writer.writeLine(`${str} as const`);
return writer;
},
},
],
});

schemaFile.addStatements(`export default ${schemaName}`);
});

// Export all of our schemas from inside the main `schemas.ts` file.
sourceFile.addStatements(`export { ${Array.from(sortedSchemas.keys()).join(', ')} }`);

return sourceFile;
Expand Down
Loading