Skip to content

Commit 256a09b

Browse files
authored
preserve imports from stale files (#90)
This PR adds support for automatically preserving `import` statements in `functions.ts` and `api.ts` files ## Why This was required because if a user had a setup in which they were using functions from files other than `api.ts` and had `@save` annotation on those functions, re-running the codegen (introspect with overwrite set to `true`) would preserve the function, but would lose its import statement. This meant that the user would have to re-write their import statements again, which a really bad experience. ## Note `import` statements are always preserved and organized. They don't need a `@save` annotation. ## Review Since this is a relatively large PR, it'll be easier to review it per commit
1 parent 11f9cd6 commit 256a09b

File tree

15 files changed

+203
-11
lines changed

15 files changed

+203
-11
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

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

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

docs/documentation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ When re-introspecting the connector, user changes in `functions.ts` or `api.ts`
7676

7777
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
7878

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

8183
```javascript

src/app/parser/typescript/cleanup.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,12 @@ export function fixImports(generatedCodeList: types.GeneratedCode[]) {
1919
project.createSourceFile(
2020
generatedCode.filePath,
2121
generatedCode.fileContent,
22-
{
23-
overwrite: true,
24-
},
22+
{ overwrite: true },
2523
);
2624
}
2725

2826
try {
2927
for (const sourceFile of project.getSourceFiles()) {
30-
sourceFile.fixMissingImports().organizeImports();
31-
3228
// find the source file in the generated code file list
3329
let curSourceFile =
3430
generatedCodeList.filter(
@@ -41,12 +37,18 @@ export function fixImports(generatedCodeList: types.GeneratedCode[]) {
4137
path.basename(sourceFile.getFilePath()),
4238
)[0];
4339

44-
if (curSourceFile) {
40+
if (curSourceFile) { // if the current file is either functions.ts or api.ts, fix imports and organize them
41+
sourceFile.fixMissingImports().organizeImports();
4542
curSourceFile!.fileContent = sourceFile.getFullText();
4643
} else {
47-
logger.error(
48-
`Error while fixing imports: Unable to find the source file for ${sourceFile.getFilePath()}\n\nSkipping import fixing and cleanup for this file`,
49-
);
44+
if (
45+
path.basename(sourceFile.getFilePath()) === "functions.ts" ||
46+
path.basename(sourceFile.getFilePath()) === "api.ts"
47+
) { // we only want to show this error if we're unable to find functions.ts or api.ts
48+
logger.error(
49+
`Error while fixing imports: Unable to find the source file for ${sourceFile.getFilePath()}\n\nSkipping import fixing and cleanup for this file`,
50+
);
51+
}
5052
}
5153
}
5254
} catch (error) {

src/app/parser/typescript/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@ export function preserveUserChanges(
2626
morph.preserveSavedVariables(staleSourceFile, freshSourceFile);
2727
morph.preserveSavedFunctions(staleSourceFile, freshSourceFile);
2828

29+
// imports don't need @save annotation, they are always preserved and organized
30+
morph.preserveImportDeclarations(staleSourceFile, freshSourceFile);
31+
2932
return freshSourceFile.getFullText();
3033
}

src/app/parser/typescript/morph.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,40 @@ describe("morph::user-defined-types", async () => {
137137
}
138138
});
139139

140+
const importDeclarationsTests: TestCase[] = [
141+
{
142+
name: "Preserve Imports",
143+
directory: "./test-data/morph-tests/imports/",
144+
}
145+
];
146+
147+
describe("morph::import-declarations", async () => {
148+
for (const testCase of importDeclarationsTests) {
149+
before(function () {
150+
setupTest(testCase);
151+
});
152+
153+
it(testCase.name, async () => {
154+
morph.preserveImportDeclarations(
155+
testCase.staleSourceFile!,
156+
testCase.freshSourceFile!,
157+
);
158+
159+
const gotStr = await prettier.format(
160+
testCase.freshSourceFile?.getFullText()!,
161+
{
162+
parser: "typescript",
163+
},
164+
);
165+
166+
assert.equal(testCase.mergedFileContents, gotStr);
167+
168+
// uncomment to update merged golden file
169+
// fs.writeFileSync(path.resolve(testCase.directory, "merged.ts"), gotStr);
170+
});
171+
}
172+
});
173+
140174
function setupTest(testCase: TestCase) {
141175
testCase.directory = path.resolve(__dirname, testCase.directory);
142176
const staleProject = new ts.Project();

src/app/parser/typescript/morph.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ export function preserveSavedClasses(
6565
preserveNode(staleSourceClasses, freshSourceClasses, freshTsSourceFile);
6666
}
6767

68+
export function preserveImportDeclarations(
69+
staleTsSourceFile: tsMorph.SourceFile,
70+
freshTsSourceFile: tsMorph.SourceFile,
71+
) {
72+
const staleSourceImports = walk.getAllImportDeclarationsMap(staleTsSourceFile);
73+
const freshSourceImports = walk.getAllImportDeclarationsMap(freshTsSourceFile);
74+
75+
staleSourceImports.forEach((staleNode, staleNodeName) => {
76+
const freshNode = freshSourceImports.get(staleNodeName);
77+
78+
if (freshNode) {
79+
// this import statement already exists in the fresh source file
80+
// replace it with the stale import statement
81+
freshNode.replaceWithText(staleNode.getText());
82+
} else {
83+
// this import statement does not exist in the fresh source file
84+
// add it to the fresh source
85+
freshTsSourceFile.insertStatements(0, staleNode.getText());
86+
}
87+
})
88+
}
89+
90+
6891
/**
6992
* this function preservers nodes that are marked with `@save` annotation in the stale source file
7093
* if a saved node is missing in the fresh source file, it will be copied to it
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function helloWorld() {
2+
return "hello world";
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as a from "./a";
2+
import * as tsMorph from "ts-morph";
3+
import { exit } from "process";
4+
import { SemVer } from "semver";
5+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import { helloWorld } from "./a";
4+
import {
5+
SourceFile,
6+
SourceFileEmitOptions,
7+
NamespaceImport,
8+
NamedImports,
9+
NamedExports,
10+
ModuleDeclaration,
11+
} from "ts-morph";
12+
import * as process from "process";
13+
import { SemVer } from "semver";
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { helloWorld } from "./a";
2+
import {
3+
SourceFile,
4+
SourceFileEmitOptions,
5+
NamespaceImport,
6+
NamedImports,
7+
NamedExports,
8+
ModuleDeclaration,
9+
} from "ts-morph";
10+
import * as process from "process";
11+
import * as path from "path";
12+
import * as fs from "fs";
13+
14+
export function hello() {}

0 commit comments

Comments
 (0)