diff --git a/README.md b/README.md index a96d2a30..056913e3 100644 --- a/README.md +++ b/README.md @@ -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") @@ -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 ``` diff --git a/package.json b/package.json index 9864fb11..9b40e005 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/contentful-typescript-codegen.ts b/src/contentful-typescript-codegen.ts index f97e1ede..7032aeb3 100644 --- a/src/contentful-typescript-codegen.ts +++ b/src/contentful-typescript-codegen.ts @@ -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 @@ -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 diff --git a/src/loadEnvironment.ts b/src/loadEnvironment.ts new file mode 100644 index 00000000..75c59c2f --- /dev/null +++ b/src/loadEnvironment.ts @@ -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> + getLocales(): Promise> +} + +export type EnvironmentGetter = () => Promise + +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) +} diff --git a/test/loadEnvironment.test.ts b/test/loadEnvironment.test.ts new file mode 100644 index 00000000..edff61c1 --- /dev/null +++ b/test/loadEnvironment.test.ts @@ -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") + }) +}) diff --git a/yarn.lock b/yarn.lock index 89de48b8..40852963 100644 --- a/yarn.lock +++ b/yarn.lock @@ -339,6 +339,13 @@ resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-13.4.0.tgz#a59c311ebd1b801ee00edbc08663c8d78da26171" integrity sha512-YPdYqGmWiGAood7ri2BUXfPQKNthkQYV1rmQdaq4UAxWK5NLB5NXckaUYLbohqhHKtrq4tnfzVn+ePWID7Dzbg== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@iarna/cli@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641" @@ -535,6 +542,24 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@nodelib/fs.scandir@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" @@ -759,6 +784,26 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" @@ -960,6 +1005,11 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^7.1.0, acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" @@ -970,6 +1020,11 @@ acorn@^8.0.5: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== +acorn@^8.4.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + agent-base@4, agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -1135,6 +1190,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2052,6 +2112,11 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -5073,7 +5138,7 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" -make-error@1.x: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -7732,6 +7797,25 @@ ts-jest@^26.0.0: semver "7.x" yargs-parser "20.x" +ts-node@^10.6.0: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tslib@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" @@ -8040,6 +8124,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-to-istanbul@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.1.tgz#04bfd1026ba4577de5472df4f5e89af49de5edda" @@ -8426,3 +8515,8 @@ yargs@^8.0.2: which-module "^2.0.0" y18n "^3.2.1" yargs-parser "^7.0.0" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==