Skip to content

Commit 90c2a17

Browse files
G-RathGabrielAnca
andauthored
feat: support typescript config (#110)
* feat: support `getContentfulEnvironment.ts` * fix: loosen types of load environment * fix: add `ts-node` as an optional peer dependency * refactor: move exported types and functions to top of file * chore: add todo comment * chore: bump `ts-node` to v10.6.0 or higher * test: add extra case + deduplicate * docs: update readme with typescript config * Fix typo Co-authored-by: Gabriel Anca <[email protected]>
1 parent 319ce84 commit 90c2a17

6 files changed

+327
-11
lines changed

README.md

+33-4
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@ Then, add the following to your `package.json`:
3030

3131
Feel free to change the output path to whatever you like.
3232

33-
Next, the codegen will expect you to have created a file called `getContentfulEnvironment.js` in the
34-
root of your project directory, and it should export a promise that resolves with your Contentful
35-
environment.
33+
Next, the codegen will expect you to have created a file called either `getContentfulEnvironment.js` or `getContentfulEnvironment.ts`
34+
in the root of your project directory, which should export a promise that resolves with your Contentful environment.
3635

3736
The reason for this is that you can do whatever you like to set up your Contentful Management
38-
Client. Here's an example:
37+
Client. Here's an example of a JavaScript config:
3938

4039
```js
4140
const contentfulManagement = require("contentful-management")
@@ -51,6 +50,36 @@ module.exports = function() {
5150
}
5251
```
5352

53+
And the same example in TypeScript:
54+
55+
```ts
56+
import { strict as assert } from "assert"
57+
import contentfulManagement from "contentful-management"
58+
import { EnvironmentGetter } from "contentful-typescript-codegen"
59+
60+
const { CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN, CONTENTFUL_SPACE_ID, CONTENTFUL_ENVIRONMENT } = process.env
61+
62+
assert(CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN)
63+
assert(CONTENTFUL_SPACE_ID)
64+
assert(CONTENTFUL_ENVIRONMENT)
65+
66+
const getContentfulEnvironment: EnvironmentGetter = () => {
67+
const contentfulClient = contentfulManagement.createClient({
68+
accessToken: CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN,
69+
})
70+
71+
return contentfulClient
72+
.getSpace(CONTENTFUL_SPACE_ID)
73+
.then(space => space.getEnvironment(CONTENTFUL_ENVIRONMENT))
74+
}
75+
76+
module.exports = getContentfulEnvironment
77+
```
78+
79+
> **Note**
80+
>
81+
> `ts-node` must be installed to use a TypeScript config
82+
5483
### Command line options
5584

5685
```

package.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@
3434
"meow": "^9.0.0"
3535
},
3636
"peerDependencies": {
37-
"prettier": ">= 1"
37+
"prettier": ">= 1",
38+
"ts-node": ">= 9.0.0"
39+
},
40+
"peerDependenciesMeta": {
41+
"ts-node": {
42+
"optional": true
43+
}
3844
},
3945
"devDependencies": {
4046
"@contentful/rich-text-types": "^13.4.0",
@@ -59,6 +65,7 @@
5965
"rollup-plugin-typescript2": "^0.22.1",
6066
"semantic-release": "^17.4.1",
6167
"ts-jest": "^26.0.0",
68+
"ts-node": "^10.6.0",
6269
"tslint": "^5.18.0",
6370
"tslint-config-prettier": "^1.18.0",
6471
"tslint-config-standard": "^8.0.1",

src/contentful-typescript-codegen.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import render from "./renderers/render"
22
import renderFieldsOnly from "./renderers/renderFieldsOnly"
33
import path from "path"
44
import { outputFileSync } from "fs-extra"
5+
import { loadEnvironment } from "./loadEnvironment"
56

67
const meow = require("meow")
78

9+
export { ContentfulEnvironment, EnvironmentGetter } from "./loadEnvironment"
10+
811
const cli = meow(
912
`
1013
Usage
@@ -60,11 +63,7 @@ const cli = meow(
6063
)
6164

6265
async function runCodegen(outputFile: string) {
63-
const getEnvironmentPath = path.resolve(process.cwd(), "./getContentfulEnvironment.js")
64-
const getEnvironment = require(getEnvironmentPath)
65-
const environment = await getEnvironment()
66-
const contentTypes = await environment.getContentTypes({ limit: 1000 })
67-
const locales = await environment.getLocales()
66+
const { contentTypes, locales } = await loadEnvironment()
6867
const outputPath = path.resolve(process.cwd(), outputFile)
6968

7069
let output

src/loadEnvironment.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import * as path from "path"
2+
import * as fs from "fs"
3+
import { ContentfulCollection, ContentTypeCollection, LocaleCollection } from "contentful"
4+
5+
// todo: switch to contentful-management interfaces here
6+
export interface ContentfulEnvironment {
7+
getContentTypes(options: { limit: number }): Promise<ContentfulCollection<unknown>>
8+
getLocales(): Promise<ContentfulCollection<unknown>>
9+
}
10+
11+
export type EnvironmentGetter = () => Promise<ContentfulEnvironment>
12+
13+
export async function loadEnvironment() {
14+
try {
15+
const getEnvironment = getEnvironmentGetter()
16+
const environment = await getEnvironment()
17+
18+
return {
19+
contentTypes: (await environment.getContentTypes({ limit: 1000 })) as ContentTypeCollection,
20+
locales: (await environment.getLocales()) as LocaleCollection,
21+
}
22+
} finally {
23+
if (registerer) {
24+
registerer.enabled(false)
25+
}
26+
}
27+
}
28+
29+
/* istanbul ignore next */
30+
const interopRequireDefault = (obj: any): { default: any } =>
31+
obj && obj.__esModule ? obj : { default: obj }
32+
33+
type Registerer = { enabled(value: boolean): void }
34+
35+
let registerer: Registerer | null = null
36+
37+
function enableTSNodeRegisterer() {
38+
if (registerer) {
39+
registerer.enabled(true)
40+
41+
return
42+
}
43+
44+
try {
45+
registerer = require("ts-node").register() as Registerer
46+
registerer.enabled(true)
47+
} catch (e) {
48+
if (e.code === "MODULE_NOT_FOUND") {
49+
throw new Error(
50+
`'ts-node' is required for TypeScript configuration files. Make sure it is installed\nError: ${e.message}`,
51+
)
52+
}
53+
54+
throw e
55+
}
56+
}
57+
58+
function determineEnvironmentPath() {
59+
const pathWithoutExtension = path.resolve(process.cwd(), "./getContentfulEnvironment")
60+
61+
if (fs.existsSync(`${pathWithoutExtension}.ts`)) {
62+
return `${pathWithoutExtension}.ts`
63+
}
64+
65+
return `${pathWithoutExtension}.js`
66+
}
67+
68+
function getEnvironmentGetter(): EnvironmentGetter {
69+
const getEnvironmentPath = determineEnvironmentPath()
70+
71+
if (getEnvironmentPath.endsWith(".ts")) {
72+
enableTSNodeRegisterer()
73+
74+
return interopRequireDefault(require(getEnvironmentPath)).default
75+
}
76+
77+
return require(getEnvironmentPath)
78+
}

test/loadEnvironment.test.ts

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as fs from "fs"
2+
import { loadEnvironment } from "../src/loadEnvironment"
3+
4+
const contentfulEnvironment = () => ({
5+
getContentTypes: () => [],
6+
getLocales: () => [],
7+
})
8+
9+
const getContentfulEnvironmentFileFactory = jest.fn((_type: string) => contentfulEnvironment)
10+
11+
jest.mock(
12+
require("path").resolve(process.cwd(), "./getContentfulEnvironment.js"),
13+
() => getContentfulEnvironmentFileFactory("js"),
14+
{ virtual: true },
15+
)
16+
17+
jest.mock(
18+
require("path").resolve(process.cwd(), "./getContentfulEnvironment.ts"),
19+
() => getContentfulEnvironmentFileFactory("ts"),
20+
{ virtual: true },
21+
)
22+
23+
const tsNodeRegistererEnabled = jest.fn()
24+
const tsNodeRegister = jest.fn()
25+
26+
jest.mock("ts-node", () => ({ register: tsNodeRegister }))
27+
28+
describe("loadEnvironment", () => {
29+
beforeEach(() => {
30+
jest.resetAllMocks()
31+
jest.restoreAllMocks()
32+
jest.resetModules()
33+
34+
getContentfulEnvironmentFileFactory.mockReturnValue(contentfulEnvironment)
35+
tsNodeRegister.mockReturnValue({ enabled: tsNodeRegistererEnabled })
36+
})
37+
38+
describe("when getContentfulEnvironment.ts exists", () => {
39+
beforeEach(() => {
40+
jest.spyOn(fs, "existsSync").mockReturnValue(true)
41+
})
42+
43+
describe("when ts-node is not found", () => {
44+
beforeEach(() => {
45+
// technically this is throwing after the `require` call,
46+
// but it still tests the same code path so is fine
47+
tsNodeRegister.mockImplementation(() => {
48+
throw new (class extends Error {
49+
public code: string
50+
51+
constructor(message?: string) {
52+
super(message)
53+
this.code = "MODULE_NOT_FOUND"
54+
}
55+
})()
56+
})
57+
})
58+
59+
it("throws a nice error", async () => {
60+
await expect(loadEnvironment()).rejects.toThrow(
61+
"'ts-node' is required for TypeScript configuration files",
62+
)
63+
})
64+
})
65+
66+
describe("when there is another error", () => {
67+
beforeEach(() => {
68+
tsNodeRegister.mockImplementation(() => {
69+
throw new Error("something else went wrong!")
70+
})
71+
})
72+
73+
it("re-throws", async () => {
74+
await expect(loadEnvironment()).rejects.toThrow("something else went wrong!")
75+
})
76+
})
77+
78+
describe("when called multiple times", () => {
79+
it("re-uses the registerer", async () => {
80+
await loadEnvironment()
81+
await loadEnvironment()
82+
83+
expect(tsNodeRegister).toHaveBeenCalledTimes(1)
84+
})
85+
})
86+
87+
it("requires the typescript config", async () => {
88+
await loadEnvironment()
89+
90+
expect(getContentfulEnvironmentFileFactory).toHaveBeenCalledWith("ts")
91+
expect(getContentfulEnvironmentFileFactory).not.toHaveBeenCalledWith("js")
92+
})
93+
94+
it("disables the registerer afterwards", async () => {
95+
await loadEnvironment()
96+
97+
expect(tsNodeRegistererEnabled).toHaveBeenCalledWith(false)
98+
})
99+
})
100+
101+
it("requires the javascript config", async () => {
102+
jest.spyOn(fs, "existsSync").mockReturnValue(false)
103+
104+
await loadEnvironment()
105+
106+
expect(getContentfulEnvironmentFileFactory).toHaveBeenCalledWith("js")
107+
expect(getContentfulEnvironmentFileFactory).not.toHaveBeenCalledWith("ts")
108+
})
109+
})

0 commit comments

Comments
 (0)