Skip to content
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
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Add support for `@save` in `api.ts` ([#89](https://github.com/hasura/ndc-open-api-lambda/pull/89))
- Add support for preserving imports from stale files ([#90](https://github.com/hasura/ndc-open-api-lambda/pull/90))

## [[1.5.2](https://github.com/hasura/ndc-open-api-lambda/releases/tag/v1.5.2)] 2025-03-25

Expand Down
2 changes: 2 additions & 0 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ When re-introspecting the connector, user changes in `functions.ts` or `api.ts`

This will ensure that the statements marked with `@save` are not overwritten and the saved statements will be added if missing in the newly generated `functions.ts` or the `apt.ts` file

> **NOTE:** `import` statements are always preserved and orgranized for both files, they don't need the `@save` annotation

Example

```javascript
Expand Down
20 changes: 11 additions & 9 deletions src/app/parser/typescript/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,12 @@ export function fixImports(generatedCodeList: types.GeneratedCode[]) {
project.createSourceFile(
generatedCode.filePath,
generatedCode.fileContent,
{
overwrite: true,
},
{ overwrite: true },
);
}

try {
for (const sourceFile of project.getSourceFiles()) {
sourceFile.fixMissingImports().organizeImports();

// find the source file in the generated code file list
let curSourceFile =
generatedCodeList.filter(
Expand All @@ -41,12 +37,18 @@ export function fixImports(generatedCodeList: types.GeneratedCode[]) {
path.basename(sourceFile.getFilePath()),
)[0];

if (curSourceFile) {
if (curSourceFile) { // if the current file is either functions.ts or api.ts, fix imports and organize them
sourceFile.fixMissingImports().organizeImports();
curSourceFile!.fileContent = sourceFile.getFullText();
} else {
logger.error(
`Error while fixing imports: Unable to find the source file for ${sourceFile.getFilePath()}\n\nSkipping import fixing and cleanup for this file`,
);
if (
path.basename(sourceFile.getFilePath()) === "functions.ts" ||
path.basename(sourceFile.getFilePath()) === "api.ts"
) { // we only want to show this error if we're unable to find functions.ts or api.ts
logger.error(
`Error while fixing imports: Unable to find the source file for ${sourceFile.getFilePath()}\n\nSkipping import fixing and cleanup for this file`,
);
}
}
}
} catch (error) {
Expand Down
3 changes: 3 additions & 0 deletions src/app/parser/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ export function preserveUserChanges(
morph.preserveSavedVariables(staleSourceFile, freshSourceFile);
morph.preserveSavedFunctions(staleSourceFile, freshSourceFile);

// imports don't need @save annotation, they are always preserved and organized
morph.preserveImportDeclarations(staleSourceFile, freshSourceFile);

return freshSourceFile.getFullText();
}
34 changes: 34 additions & 0 deletions src/app/parser/typescript/morph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,40 @@ describe("morph::user-defined-types", async () => {
}
});

const importDeclarationsTests: TestCase[] = [
{
name: "Preserve Imports",
directory: "./test-data/morph-tests/imports/",
}
];

describe("morph::import-declarations", async () => {
for (const testCase of importDeclarationsTests) {
before(function () {
setupTest(testCase);
});

it(testCase.name, async () => {
morph.preserveImportDeclarations(
testCase.staleSourceFile!,
testCase.freshSourceFile!,
);

const gotStr = await prettier.format(
testCase.freshSourceFile?.getFullText()!,
{
parser: "typescript",
},
);

assert.equal(testCase.mergedFileContents, gotStr);

// uncomment to update merged golden file
// fs.writeFileSync(path.resolve(testCase.directory, "merged.ts"), gotStr);
});
}
});

function setupTest(testCase: TestCase) {
testCase.directory = path.resolve(__dirname, testCase.directory);
const staleProject = new ts.Project();
Expand Down
23 changes: 23 additions & 0 deletions src/app/parser/typescript/morph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ export function preserveSavedClasses(
preserveNode(staleSourceClasses, freshSourceClasses, freshTsSourceFile);
}

export function preserveImportDeclarations(
staleTsSourceFile: tsMorph.SourceFile,
freshTsSourceFile: tsMorph.SourceFile,
) {
const staleSourceImports = walk.getAllImportDeclarationsMap(staleTsSourceFile);
const freshSourceImports = walk.getAllImportDeclarationsMap(freshTsSourceFile);

staleSourceImports.forEach((staleNode, staleNodeName) => {
const freshNode = freshSourceImports.get(staleNodeName);

if (freshNode) {
// this import statement already exists in the fresh source file
// replace it with the stale import statement
freshNode.replaceWithText(staleNode.getText());
} else {
// this import statement does not exist in the fresh source file
// add it to the fresh source
freshTsSourceFile.insertStatements(0, staleNode.getText());
}
})
}


/**
* this function preservers nodes that are marked with `@save` annotation in the stale source file
* if a saved node is missing in the fresh source file, it will be copied to it
Expand Down
3 changes: 3 additions & 0 deletions src/app/parser/typescript/test-data/morph-tests/imports/a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function helloWorld() {
return "hello world";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as a from "./a";
import * as tsMorph from "ts-morph";
import { exit } from "process";
import { SemVer } from "semver";

13 changes: 13 additions & 0 deletions src/app/parser/typescript/test-data/morph-tests/imports/merged.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as fs from "fs";
import * as path from "path";
import { helloWorld } from "./a";
import {
SourceFile,
SourceFileEmitOptions,
NamespaceImport,
NamedImports,
NamedExports,
ModuleDeclaration,
} from "ts-morph";
import * as process from "process";
import { SemVer } from "semver";
14 changes: 14 additions & 0 deletions src/app/parser/typescript/test-data/morph-tests/imports/stale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { helloWorld } from "./a";
import {
SourceFile,
SourceFileEmitOptions,
NamespaceImport,
NamedImports,
NamedExports,
ModuleDeclaration,
} from "ts-morph";
import * as process from "process";
import * as path from "path";
import * as fs from "fs";

export function hello() {}
8 changes: 8 additions & 0 deletions src/app/parser/typescript/test-data/walk-tests/imports/got
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as a from "./another-file";
import * as b from "./another-dir/another-file";
import * as tsMorph from "ts-morph";
import { exit } from "process";
import { SemVer } from "semver";
import * as path from "path";
import * as fs from "fs";
import * as ts from "typescript"
10 changes: 10 additions & 0 deletions src/app/parser/typescript/test-data/walk-tests/imports/want.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"\"./another-file\"": "import * as a from \"./another-file\";",
"\"./another-dir/another-file\"": "import * as b from \"./another-dir/another-file\";",
"\"ts-morph\"": "import * as tsMorph from \"ts-morph\";",
"\"process\"": "import { exit } from \"process\";",
"\"semver\"": "import { SemVer } from \"semver\";",
"\"path\"": "import * as path from \"path\";",
"\"fs\"": "import * as fs from \"fs\";",
"\"typescript\"": "import * as ts from \"typescript\""
}
30 changes: 30 additions & 0 deletions src/app/parser/typescript/walk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,36 @@ describe("walk::user-defined-types-tests", async () => {
}
});

const importDeclarationsTests: TestCase[] = [{
name: "getImportDeclarationTests",
testFile: "./test-data/walk-tests/imports/got",
goldenFile: "./test-data/walk-tests/imports/want.json",
}];

describe("walk::import-declarations-tests", async () => {
for (const testCase of importDeclarationsTests) {
before(function () {
setupTestCase(testCase);
});

it(`${testCase.name}`, function () {
const project = new ts.Project();
const tsSourceFile = project.addSourceFileAtPath(testCase.testFile);

const importsMap = tsWalk.getAllImportDeclarationsMap(tsSourceFile);
const gotImports: Record<string, string> = {};

importsMap.forEach((importDeclaration, importName) => {
gotImports[importName] = importDeclaration.getText();
});

assert.deepEqual(testCase.goldenFileContents, gotImports);

// writeFileSync(testCase.goldenFile, JSON.stringify(gotImports, null, 2));
});
}
});

function setupTestCase(testCase: TestCase) {
testCase.testFile = path.resolve(__dirname, testCase.testFile);
testCase.goldenFile = path.resolve(__dirname, testCase.goldenFile);
Expand Down
44 changes: 44 additions & 0 deletions src/app/parser/typescript/walk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,47 @@ export function getClassDeclarations(tsSourceFile: ts.SourceFile): ts.ClassDecla
});
return allClassDeclarations;
}

export function getAllImportDecalarations(tsSourceFile: ts.SourceFile): ts.ImportDeclaration[] {
const allImportDeclarations: ts.ImportDeclaration[] = [];
if (!tsSourceFile) {
return allImportDeclarations;
}
tsSourceFile.forEachChild((node) => {
if (ts.Node.isImportDeclaration(node)) {
allImportDeclarations.push(node as ts.ImportDeclaration);
}
});

return allImportDeclarations;
}

/**
* get all import declarations as a map of imported library to ts.ImportDeclaration
*/
export function getAllImportDeclarationsMap(tsSourceFile: ts.SourceFile): Map<string, ts.ImportDeclaration> {
const allImportDeclarations = getAllImportDecalarations(tsSourceFile);
const allImportDeclarationsMap: Map<string, ts.ImportDeclaration> = new Map();
allImportDeclarations.forEach((importDeclaration) => {
const importLibrary = getLibraryFromImportDeclaration(importDeclaration)?.getText();
if (!importLibrary) {
logger.error("unable to get import library from import declaration: ", importDeclaration.getText());
return;
}
allImportDeclarationsMap.set(importLibrary, importDeclaration);
});
return allImportDeclarationsMap;
}

function getLibraryFromImportDeclaration(importDeclaration: ts.ImportDeclaration): ts.StringLiteral | undefined {
for (let i = importDeclaration.getChildCount() - 1; i >= 0; i--) {
// import declarations are of the format: import * as tsMorph from "ts-morph";
// the library name is "ts-morph", which is a string literal
// the library name is typically the last string literal in the import declaration
const child = importDeclaration.getChildAtIndexIfKind(i, ts.SyntaxKind.StringLiteral);
if (child) {
return child
}
}
return undefined;
}
4 changes: 2 additions & 2 deletions src/app/writer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export async function writeToFileSystem(codeToWrite: types.GeneratedCode[]) {
(element) => element.fileType === "functions-ts",
)[0]!;

await apiWriter.writeToFileSystem(apiTsCode);
await functionsWriter.writeToFileSystem(functionsTsCode, apiTsCode);
await packageJsonWriter.writeToFileSystem();
tsConfigWriter.writeToFileSystem();
await apiWriter.writeToFileSystem(apiTsCode);
await functionsWriter.writeToFileSystem(functionsTsCode, apiTsCode);

logger.info("running npm install :: installing dependencies");
if (process.env.HASURA_PLUGIN_CONNECTOR_CONTEXT_PATH) {
Expand Down