Skip to content

Commit

Permalink
feat: support typescript config (#110)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
G-Rath and GabrielAnca authored Dec 15, 2022
1 parent 319ce84 commit 90c2a17
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 11 deletions.
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@ Then, add the following to your `package.json`:

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

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

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

```js
const contentfulManagement = require("contentful-management")
Expand All @@ -51,6 +50,36 @@ module.exports = function() {
}
```

And the same example in TypeScript:

```ts
import { strict as assert } from "assert"
import contentfulManagement from "contentful-management"
import { EnvironmentGetter } from "contentful-typescript-codegen"

const { CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN, CONTENTFUL_SPACE_ID, CONTENTFUL_ENVIRONMENT } = process.env

assert(CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN)
assert(CONTENTFUL_SPACE_ID)
assert(CONTENTFUL_ENVIRONMENT)

const getContentfulEnvironment: EnvironmentGetter = () => {
const contentfulClient = contentfulManagement.createClient({
accessToken: CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN,
})

return contentfulClient
.getSpace(CONTENTFUL_SPACE_ID)
.then(space => space.getEnvironment(CONTENTFUL_ENVIRONMENT))
}

module.exports = getContentfulEnvironment
```

> **Note**
>
> `ts-node` must be installed to use a TypeScript config
### Command line options

```
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@
"meow": "^9.0.0"
},
"peerDependencies": {
"prettier": ">= 1"
"prettier": ">= 1",
"ts-node": ">= 9.0.0"
},
"peerDependenciesMeta": {
"ts-node": {
"optional": true
}
},
"devDependencies": {
"@contentful/rich-text-types": "^13.4.0",
Expand All @@ -59,6 +65,7 @@
"rollup-plugin-typescript2": "^0.22.1",
"semantic-release": "^17.4.1",
"ts-jest": "^26.0.0",
"ts-node": "^10.6.0",
"tslint": "^5.18.0",
"tslint-config-prettier": "^1.18.0",
"tslint-config-standard": "^8.0.1",
Expand Down
9 changes: 4 additions & 5 deletions src/contentful-typescript-codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import render from "./renderers/render"
import renderFieldsOnly from "./renderers/renderFieldsOnly"
import path from "path"
import { outputFileSync } from "fs-extra"
import { loadEnvironment } from "./loadEnvironment"

const meow = require("meow")

export { ContentfulEnvironment, EnvironmentGetter } from "./loadEnvironment"

const cli = meow(
`
Usage
Expand Down Expand Up @@ -60,11 +63,7 @@ const cli = meow(
)

async function runCodegen(outputFile: string) {
const getEnvironmentPath = path.resolve(process.cwd(), "./getContentfulEnvironment.js")
const getEnvironment = require(getEnvironmentPath)
const environment = await getEnvironment()
const contentTypes = await environment.getContentTypes({ limit: 1000 })
const locales = await environment.getLocales()
const { contentTypes, locales } = await loadEnvironment()
const outputPath = path.resolve(process.cwd(), outputFile)

let output
Expand Down
78 changes: 78 additions & 0 deletions src/loadEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as path from "path"
import * as fs from "fs"
import { ContentfulCollection, ContentTypeCollection, LocaleCollection } from "contentful"

// todo: switch to contentful-management interfaces here
export interface ContentfulEnvironment {
getContentTypes(options: { limit: number }): Promise<ContentfulCollection<unknown>>
getLocales(): Promise<ContentfulCollection<unknown>>
}

export type EnvironmentGetter = () => Promise<ContentfulEnvironment>

export async function loadEnvironment() {
try {
const getEnvironment = getEnvironmentGetter()
const environment = await getEnvironment()

return {
contentTypes: (await environment.getContentTypes({ limit: 1000 })) as ContentTypeCollection,
locales: (await environment.getLocales()) as LocaleCollection,
}
} finally {
if (registerer) {
registerer.enabled(false)
}
}
}

/* istanbul ignore next */
const interopRequireDefault = (obj: any): { default: any } =>
obj && obj.__esModule ? obj : { default: obj }

type Registerer = { enabled(value: boolean): void }

let registerer: Registerer | null = null

function enableTSNodeRegisterer() {
if (registerer) {
registerer.enabled(true)

return
}

try {
registerer = require("ts-node").register() as Registerer
registerer.enabled(true)
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
throw new Error(
`'ts-node' is required for TypeScript configuration files. Make sure it is installed\nError: ${e.message}`,
)
}

throw e
}
}

function determineEnvironmentPath() {
const pathWithoutExtension = path.resolve(process.cwd(), "./getContentfulEnvironment")

if (fs.existsSync(`${pathWithoutExtension}.ts`)) {
return `${pathWithoutExtension}.ts`
}

return `${pathWithoutExtension}.js`
}

function getEnvironmentGetter(): EnvironmentGetter {
const getEnvironmentPath = determineEnvironmentPath()

if (getEnvironmentPath.endsWith(".ts")) {
enableTSNodeRegisterer()

return interopRequireDefault(require(getEnvironmentPath)).default
}

return require(getEnvironmentPath)
}
109 changes: 109 additions & 0 deletions test/loadEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as fs from "fs"
import { loadEnvironment } from "../src/loadEnvironment"

const contentfulEnvironment = () => ({
getContentTypes: () => [],
getLocales: () => [],
})

const getContentfulEnvironmentFileFactory = jest.fn((_type: string) => contentfulEnvironment)

jest.mock(
require("path").resolve(process.cwd(), "./getContentfulEnvironment.js"),
() => getContentfulEnvironmentFileFactory("js"),
{ virtual: true },
)

jest.mock(
require("path").resolve(process.cwd(), "./getContentfulEnvironment.ts"),
() => getContentfulEnvironmentFileFactory("ts"),
{ virtual: true },
)

const tsNodeRegistererEnabled = jest.fn()
const tsNodeRegister = jest.fn()

jest.mock("ts-node", () => ({ register: tsNodeRegister }))

describe("loadEnvironment", () => {
beforeEach(() => {
jest.resetAllMocks()
jest.restoreAllMocks()
jest.resetModules()

getContentfulEnvironmentFileFactory.mockReturnValue(contentfulEnvironment)
tsNodeRegister.mockReturnValue({ enabled: tsNodeRegistererEnabled })
})

describe("when getContentfulEnvironment.ts exists", () => {
beforeEach(() => {
jest.spyOn(fs, "existsSync").mockReturnValue(true)
})

describe("when ts-node is not found", () => {
beforeEach(() => {
// technically this is throwing after the `require` call,
// but it still tests the same code path so is fine
tsNodeRegister.mockImplementation(() => {
throw new (class extends Error {
public code: string

constructor(message?: string) {
super(message)
this.code = "MODULE_NOT_FOUND"
}
})()
})
})

it("throws a nice error", async () => {
await expect(loadEnvironment()).rejects.toThrow(
"'ts-node' is required for TypeScript configuration files",
)
})
})

describe("when there is another error", () => {
beforeEach(() => {
tsNodeRegister.mockImplementation(() => {
throw new Error("something else went wrong!")
})
})

it("re-throws", async () => {
await expect(loadEnvironment()).rejects.toThrow("something else went wrong!")
})
})

describe("when called multiple times", () => {
it("re-uses the registerer", async () => {
await loadEnvironment()
await loadEnvironment()

expect(tsNodeRegister).toHaveBeenCalledTimes(1)
})
})

it("requires the typescript config", async () => {
await loadEnvironment()

expect(getContentfulEnvironmentFileFactory).toHaveBeenCalledWith("ts")
expect(getContentfulEnvironmentFileFactory).not.toHaveBeenCalledWith("js")
})

it("disables the registerer afterwards", async () => {
await loadEnvironment()

expect(tsNodeRegistererEnabled).toHaveBeenCalledWith(false)
})
})

it("requires the javascript config", async () => {
jest.spyOn(fs, "existsSync").mockReturnValue(false)

await loadEnvironment()

expect(getContentfulEnvironmentFileFactory).toHaveBeenCalledWith("js")
expect(getContentfulEnvironmentFileFactory).not.toHaveBeenCalledWith("ts")
})
})
Loading

0 comments on commit 90c2a17

Please sign in to comment.