From 4460e13a64039d4a255e8930b2622ff9a85c8460 Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Thu, 5 May 2022 18:40:27 -0400 Subject: [PATCH 1/4] Fix jestTransformer for moved cache + config firs Partially addresses #605 Sets a new cwd context in cases where the config file does not reside in the jest root directory. This is necessary because the config file can then give the cache location, but it will be relative to its own location (not that of the jest config) --- src/jestTransformer.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/jestTransformer.ts b/src/jestTransformer.ts index 04e486f9..e9649c01 100644 --- a/src/jestTransformer.ts +++ b/src/jestTransformer.ts @@ -1,11 +1,11 @@ import { SyncTransformer } from '@jest/transform'; import { ProjectConfig } from '@jest/types/build/Config'; import { readFileSync } from 'fs'; -import { relative } from 'path'; import { loadConfigSync } from './lib/config'; import { createExecContextSync } from './lib/exec-context'; import { createHash } from './lib/hash'; import { createPaths } from './lib/paths'; +import { dirName, relative, resolve } from 'path'; export type JestTransformerOptions = { configFile?: string; @@ -38,8 +38,11 @@ const jestTransformer: SyncTransformer = { const [__compatJestConfig] = rest; const jestConfig = __compatJestConfig?.config ?? __compatJestConfig; // jest v26 vs v27 changes to support both formats: end - const { rootDir: cwd } = jestConfig; + let { rootDir: cwd } = jestConfig; const { configFile, subsequentTransformer } = getOption(jestConfig); + // If the congFile is in another directory from jest's rootDir, reset the working path since + // since the cache and transforms will operate relative to that config's location + const cwd = resolve(cwd, dirname(configFile)); const [config, configHash] = loadConfigSync(cwd, configFile); const { execContext } = createExecContextSync(cwd, config, configHash); From a65b96fafd9b56bb48a13f0f1a6c2aaf6a9f5550 Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Fri, 6 May 2022 00:34:25 -0400 Subject: [PATCH 2/4] Add test --- README.md | 2 +- .../subdir/.graphql-let.yml | 7 +++ .../subdir/babel.config.json | 7 +++ .../subdir/pages/viewer.graphql | 7 +++ .../subdir/schema/type-defs.graphqls | 9 +++ .../jestTransformer.test.ts.snap | 61 +++++++++++++++++++ src/jestTransformer.test.ts | 43 +++++++++++-- src/jestTransformer.ts | 4 +- 8 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 src/__fixtures/jestTransformerCustomPaths/subdir/.graphql-let.yml create mode 100644 src/__fixtures/jestTransformerCustomPaths/subdir/babel.config.json create mode 100644 src/__fixtures/jestTransformerCustomPaths/subdir/pages/viewer.graphql create mode 100644 src/__fixtures/jestTransformerCustomPaths/subdir/schema/type-defs.graphqls diff --git a/README.md b/README.md index e1407082..0be4c38d 100644 --- a/README.md +++ b/README.md @@ -717,7 +717,7 @@ You're seeing the `*2`. It's used to skip `*1` and `*3`, and recodnized as gener - **[Create an issue](https://github.com/piglovesyou/graphql-let/issues/new)** if you have ideas, find a bug, or anything. - **Creating a PR** is always welcome! - - Running `npm run prepublishOnly` locally will get your local development + - Running `yarn run prepack` locally will get your local development ready. - Adding tests is preferable, but not necessary. Maybe someone else will fill it. diff --git a/src/__fixtures/jestTransformerCustomPaths/subdir/.graphql-let.yml b/src/__fixtures/jestTransformerCustomPaths/subdir/.graphql-let.yml new file mode 100644 index 00000000..6d76f329 --- /dev/null +++ b/src/__fixtures/jestTransformerCustomPaths/subdir/.graphql-let.yml @@ -0,0 +1,7 @@ +schema: ./schema/type-defs.graphqls +documents: "**/*.graphql" +plugins: + - typescript-operations + - typescript-react-apollo +respectGitIgnore: true +cacheDir: ../.cache diff --git a/src/__fixtures/jestTransformerCustomPaths/subdir/babel.config.json b/src/__fixtures/jestTransformerCustomPaths/subdir/babel.config.json new file mode 100644 index 00000000..dd380960 --- /dev/null +++ b/src/__fixtures/jestTransformerCustomPaths/subdir/babel.config.json @@ -0,0 +1,7 @@ +{ + presets: [ + '@babel/preset-env', + '@babel/preset-typescript', + '@babel/preset-react', + ], +} diff --git a/src/__fixtures/jestTransformerCustomPaths/subdir/pages/viewer.graphql b/src/__fixtures/jestTransformerCustomPaths/subdir/pages/viewer.graphql new file mode 100644 index 00000000..7f2a5b4f --- /dev/null +++ b/src/__fixtures/jestTransformerCustomPaths/subdir/pages/viewer.graphql @@ -0,0 +1,7 @@ +query Viewer { + viewer { + id + name + status + } +} diff --git a/src/__fixtures/jestTransformerCustomPaths/subdir/schema/type-defs.graphqls b/src/__fixtures/jestTransformerCustomPaths/subdir/schema/type-defs.graphqls new file mode 100644 index 00000000..aed22561 --- /dev/null +++ b/src/__fixtures/jestTransformerCustomPaths/subdir/schema/type-defs.graphqls @@ -0,0 +1,9 @@ +type User { + id: ID! + name: String! + status: String! +} + +type Query { + viewer: User +} diff --git a/src/__snapshots__/jestTransformer.test.ts.snap b/src/__snapshots__/jestTransformer.test.ts.snap index e23c143a..541ff145 100644 --- a/src/__snapshots__/jestTransformer.test.ts.snap +++ b/src/__snapshots__/jestTransformer.test.ts.snap @@ -60,3 +60,64 @@ function useViewerLazyQuery(baseOptions) { return Apollo.useLazyQuery(ViewerDocument, options); }" `; + +exports[`graphql-let/jestTransformer transforms .graphql when custom configFile and cache dirs set 1`] = ` +"\\"use strict\\"; + +function _typeof(obj) { \\"@babel/helpers - typeof\\"; if (typeof Symbol === \\"function\\" && typeof Symbol.iterator === \\"symbol\\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \\"function\\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \\"symbol\\" : typeof obj; }; } return _typeof(obj); } + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.useViewerQuery = useViewerQuery; +exports.useViewerLazyQuery = useViewerLazyQuery; +exports.ViewerDocument = void 0; + +var Apollo = _interopRequireWildcard(require(\\"@apollo/client\\")); + +var _templateObject; + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== \\"function\\") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== \\"object\\" && typeof obj !== \\"function\\") { return { \\"default\\": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== \\"default\\" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj[\\"default\\"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } + +var defaultOptions = {}; +var ViewerDocument = (0, Apollo.gql)(_templateObject || (_templateObject = _taggedTemplateLiteral([\\"\\\\n query Viewer {\\\\n viewer {\\\\n id\\\\n name\\\\n status\\\\n }\\\\n}\\\\n \\"]))); +/** + * __useViewerQuery__ + * + * To run a query within a React component, call \`useViewerQuery\` and pass it any options that fit your needs. + * When your component renders, \`useViewerQuery\` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useViewerQuery({ + * variables: { + * }, + * }); + */ + +exports.ViewerDocument = ViewerDocument; + +function useViewerQuery(baseOptions) { + var options = _objectSpread(_objectSpread({}, defaultOptions), baseOptions); + + return Apollo.useQuery(ViewerDocument, options); +} + +function useViewerLazyQuery(baseOptions) { + var options = _objectSpread(_objectSpread({}, defaultOptions), baseOptions); + + return Apollo.useLazyQuery(ViewerDocument, options); +}" +`; diff --git a/src/jestTransformer.test.ts b/src/jestTransformer.test.ts index 8f99fea9..dbc6f23b 100644 --- a/src/jestTransformer.test.ts +++ b/src/jestTransformer.test.ts @@ -9,12 +9,9 @@ let cwd: string; let config: Config.ProjectConfig; describe('graphql-let/jestTransformer', () => { - beforeAll(async () => { + test('transforms .graphql', async () => { [cwd] = await prepareFixtures(__dirname, '__fixtures/jestTransformer'); config = { rootDir: cwd } as Config.ProjectConfig; - }); - - test('transforms .graphql', async () => { const fileName = 'pages/viewer.graphql'; const stats = await compiler(cwd, [fileName], 'node'); const { 0: fileData } = stats @@ -30,6 +27,44 @@ describe('graphql-let/jestTransformer', () => { ) as { code: string }; expect(removeSourcemapReference(transformedContent)).toMatchSnapshot(); }); + + test('transforms .graphql when custom configFile and cache dirs set', async () => { + [cwd] = await prepareFixtures( + __dirname, + '__fixtures/jestTransformerCustomPaths', + ); + + // Compile the file + const fileName = 'pages/viewer.graphql'; + const stats = await compiler(cwd + '/subdir', [fileName], 'node', { + configFile: './.graphql-let.yml', + }); + const { 0: fileData } = stats + .toJson() + .modules!.map((m) => m.source) + .filter(Boolean); + + // Run the config + config = { + rootDir: cwd, + transform: [ + [ + 'filePattern', + 'graphql-let/jestTransformer.js', + { + configFile: './subdir/.graphql-let.yml', + }, + ], + ], + } as any; + const fullPath = join(cwd + '/subdir', fileName); + const { code: transformedContent } = jestTransformer.process( + fileData!, + fullPath, + { config } as TransformOptions, + ) as { code: string }; + expect(removeSourcemapReference(transformedContent)).toMatchSnapshot(); + }); }); function removeSourcemapReference(code: string) { diff --git a/src/jestTransformer.ts b/src/jestTransformer.ts index e9649c01..64aeec2b 100644 --- a/src/jestTransformer.ts +++ b/src/jestTransformer.ts @@ -1,11 +1,11 @@ import { SyncTransformer } from '@jest/transform'; import { ProjectConfig } from '@jest/types/build/Config'; import { readFileSync } from 'fs'; +import { dirname, relative, resolve } from 'path'; import { loadConfigSync } from './lib/config'; import { createExecContextSync } from './lib/exec-context'; import { createHash } from './lib/hash'; import { createPaths } from './lib/paths'; -import { dirName, relative, resolve } from 'path'; export type JestTransformerOptions = { configFile?: string; @@ -42,8 +42,8 @@ const jestTransformer: SyncTransformer = { const { configFile, subsequentTransformer } = getOption(jestConfig); // If the congFile is in another directory from jest's rootDir, reset the working path since // since the cache and transforms will operate relative to that config's location - const cwd = resolve(cwd, dirname(configFile)); const [config, configHash] = loadConfigSync(cwd, configFile); + cwd = configFile ? resolve(cwd, dirname(configFile)) : cwd; const { execContext } = createExecContextSync(cwd, config, configHash); const { tsxFullPath } = createPaths(execContext, relative(cwd, sourcePath)); From b871894a4f1ac9b6deedf1eb84ecc8d694529d7e Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Fri, 6 May 2022 13:04:35 -0400 Subject: [PATCH 3/4] retrigger checks From 62dd08006f879e6d129f2ff7dfc3e972997e456c Mon Sep 17 00:00:00 2001 From: Charlie Croom Date: Wed, 14 Jun 2023 17:31:40 -0400 Subject: [PATCH 4/4] Refactor to allow user specified cwd --- README.md | 130 ++++++++++-------- .../loader/monorepo/config/.graphql-let.yml | 9 +- src/jestTransformer.ts | 8 +- src/lib/config.test.ts | 12 +- src/lib/config.ts | 31 +++-- src/lib/exec-context.ts | 4 +- src/lib/paths.ts | 7 +- src/loader.test.ts | 4 +- src/loader.ts | 14 +- 9 files changed, 127 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 0be4c38d..fe65bc49 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,17 @@ integrating graphql-let. [A blog post](https://the-guild.dev/blog/graphql-let) ## Table of Contents -- [Why this exists](#why-this-exists) -- [Entrypoints and features](#entrypoints-and-features) -- [Getting started with webpack loader](#getting-started-with-webpack-loader) -- [Getting started with babel-plugin-macros](#getting-started-with-babel-plugin-macros) -- [Getting started with Babel Plugin](#getting-started-with-babel-plugin) -- [Difference between .graphql-let.yml and codegen.yml](#difference-between-graphql-letyml-and-codegenyml) -- [Jest Transformer](#jest-transformer) -- [Experimental feature: Resolver Types](#experimental-feature-resolver-types) -- [FAQ](#faq) -- [Contribution](#contribution) -- [License](#license) +- [Why this exists](#why-this-exists) +- [Entrypoints and features](#entrypoints-and-features) +- [Getting started with webpack loader](#getting-started-with-webpack-loader) +- [Getting started with babel-plugin-macros](#getting-started-with-babel-plugin-macros) +- [Getting started with Babel Plugin](#getting-started-with-babel-plugin) +- [Difference between .graphql-let.yml and codegen.yml](#difference-between-graphql-letyml-and-codegenyml) +- [Jest Transformer](#jest-transformer) +- [Experimental feature: Resolver Types](#experimental-feature-resolver-types) +- [FAQ](#faq) +- [Contribution](#contribution) +- [License](#license) ## Why this exists @@ -55,10 +55,10 @@ const News: React.FC = () => { Summary of characteristics of each entrypoint. -- **CLI** for efficient code generation before your type checking -- **webpack loader** to get HMR even on modifying GraphQL documents -- **babel-plugin-macros** for the minimum configuration -- **Babel plugin** if you don't want babel-plugin-macros +- **CLI** for efficient code generation before your type checking +- **webpack loader** to get HMR even on modifying GraphQL documents +- **babel-plugin-macros** for the minimum configuration +- **Babel plugin** if you don't want babel-plugin-macros All of them mostly do the same behind the scene. @@ -125,14 +125,14 @@ Note there are a few differences between the entrypoints. There are things to make graphql-let light and stable. -- Sharing the processes. Generating files is expensive, so it runs less time - to run GraphQL code generator and TypeScript API. -- Caching. Embedding hashes, as your source states, reduces the number of - unnecessary processing. -- Sharing the promises. The webpack compilation in typical SSR applications as - Next.js runs [targets](https://webpack.js.org/concepts/targets/) of `"node"` - and `"web"` simultaneously. If sources are the same, the compilation should - be once. +- Sharing the processes. Generating files is expensive, so it runs less time + to run GraphQL code generator and TypeScript API. +- Caching. Embedding hashes, as your source states, reduces the number of + unnecessary processing. +- Sharing the promises. The webpack compilation in typical SSR applications as + Next.js runs [targets](https://webpack.js.org/concepts/targets/) of `"node"` + and `"web"` simultaneously. If sources are the same, the compilation should + be once. @@ -214,6 +214,7 @@ Please note that files in `cacheDir` are only intermediate cache, possibly havin + "excludes": [".cache"] } ``` + Also, remember you have to `.gitignore` the `.cache` directory in the next section. ### 3. Add lines to .gitignore @@ -460,6 +461,15 @@ respectGitIgnore: true cacheDir: node_modules/.cache/graphql-let cacheDir: .cache +# "cwd", optional. Directory of the config file itself by default. +# When evaluating `documents`, `cacheDir`, `schema`, what should be the "root" dir. +# Because the cache is stored as a literal subdirectory matching the documents, +# the search paths must share a common ancestor defined here. +# Practically, this allows the config file and other folders +# to be stored outside the process directory +# Example: +cwd: ../../ + # "TSConfigFile", optional. `tsconfig.json` by default. # You can specify a custom config for generating `.d.ts`s. # Examples: @@ -480,33 +490,33 @@ silent: false Simple example: ```yaml -schema: "schema/**/*.graphqls" +schema: 'schema/**/*.graphqls' documents: - - "**/*.graphql" - - "!shouldBeIgnored1" + - '**/*.graphql' + - '!shouldBeIgnored1' plugins: - - typescript-operations - - typescript-react-apollo + - typescript-operations + - typescript-react-apollo ``` Example with a bit more complicated options: ```yaml schema: - - https://api.github.com/graphql: - headers: - Authorization: YOUR-TOKEN-HERE + - https://api.github.com/graphql: + headers: + Authorization: YOUR-TOKEN-HERE documents: - - "**/*.graphql" - - "!shouldBeIgnored1" + - '**/*.graphql' + - '!shouldBeIgnored1' plugins: - - typescript-operations - - typescript-react-apollo + - typescript-operations + - typescript-react-apollo respectGitIgnore: true config: - reactApolloVersion: 3 - apolloReactComponentsImportFrom: "@apollo/client/react/components" - useIndexSignature: true + reactApolloVersion: 3 + apolloReactComponentsImportFrom: '@apollo/client/react/components' + useIndexSignature: true cacheDir: .cache TSConfigFile: tsconfig.compile.json typeInjectEntrypoint: typings/graphql-let.d.ts @@ -514,11 +524,11 @@ typeInjectEntrypoint: typings/graphql-let.d.ts ### Limitations of `graphql-let/babel` -- **Sadly**, type injection can't be done with TaggedTemplateExpression such - as `` gql`query {}` ``. This is the limitation of TypeScript. - [Please answer me if you have any ideas.](https://stackoverflow.com/questions/61917066/can-taggedtempalte-have-overload-signatures-with-a-certain-string-literal-argume) -- Fragments are still not available. Please watch - [the issue.](https://github.com/piglovesyou/graphql-let/issues/65) +- **Sadly**, type injection can't be done with TaggedTemplateExpression such + as `` gql`query {}` ``. This is the limitation of TypeScript. + [Please answer me if you have any ideas.](https://stackoverflow.com/questions/61917066/can-taggedtempalte-have-overload-signatures-with-a-certain-string-literal-argume) +- Fragments are still not available. Please watch + [the issue.](https://github.com/piglovesyou/graphql-let/issues/65) ## Jest Transformer @@ -582,10 +592,10 @@ schema. Just use what you need; it's most likely to be `jest-transform-graphql`. If you meet the following conditions, graphql-let generates Resolver Types. -- You have file paths including glob patterns in `schema` -- You have - [`@graphql-codegen/typescript-resolvers`](https://graphql-code-generator.com/docs/plugins/typescript-resolvers) - installed +- You have file paths including glob patterns in `schema` +- You have + [`@graphql-codegen/typescript-resolvers`](https://graphql-code-generator.com/docs/plugins/typescript-resolvers) + installed Run: @@ -686,8 +696,8 @@ Define your fragment named as `partial.graphql` ```graphql fragment Partial on User { - id - name + id + name } ``` @@ -696,9 +706,9 @@ and import it. ```graphql # import Partial from './partial.graphql' query Viewer { - viewer { - ...Partial - } + viewer { + ...Partial + } } ``` @@ -714,14 +724,14 @@ You're seeing the `*2`. It's used to skip `*1` and `*3`, and recodnized as gener ## Contribution -- **[Create an issue](https://github.com/piglovesyou/graphql-let/issues/new)** - if you have ideas, find a bug, or anything. -- **Creating a PR** is always welcome! - - Running `yarn run prepack` locally will get your local development - ready. - - Adding tests is preferable, but not necessary. Maybe someone else will - fill it. -- [We have a chronic accumulation of dependabot PRs](https://github.com/piglovesyou/graphql-let/pulls/app%2Fdependabot). Please help us fix these version conflicts by cloning the dependabot branches. +- **[Create an issue](https://github.com/piglovesyou/graphql-let/issues/new)** + if you have ideas, find a bug, or anything. +- **Creating a PR** is always welcome! + - Running `yarn run prepack` locally will get your local development + ready. + - Adding tests is preferable, but not necessary. Maybe someone else will + fill it. +- [We have a chronic accumulation of dependabot PRs](https://github.com/piglovesyou/graphql-let/pulls/app%2Fdependabot). Please help us fix these version conflicts by cloning the dependabot branches. ## License diff --git a/src/__fixtures/loader/monorepo/config/.graphql-let.yml b/src/__fixtures/loader/monorepo/config/.graphql-let.yml index 567f7a6d..f5cd1982 100644 --- a/src/__fixtures/loader/monorepo/config/.graphql-let.yml +++ b/src/__fixtures/loader/monorepo/config/.graphql-let.yml @@ -1,7 +1,8 @@ -schema: ../../packages/schema/type-defs.graphqls -documents: "**/*.graphql" +schema: ../schema/type-defs.graphqls +documents: '**/*.graphql' plugins: - - typescript-operations - - typescript-react-apollo + - typescript-operations + - typescript-react-apollo respectGitIgnore: true +cwd: ../packages/app cacheDir: .cache diff --git a/src/jestTransformer.ts b/src/jestTransformer.ts index 64aeec2b..3548da16 100644 --- a/src/jestTransformer.ts +++ b/src/jestTransformer.ts @@ -1,7 +1,7 @@ import { SyncTransformer } from '@jest/transform'; import { ProjectConfig } from '@jest/types/build/Config'; import { readFileSync } from 'fs'; -import { dirname, relative, resolve } from 'path'; +import { relative } from 'path'; import { loadConfigSync } from './lib/config'; import { createExecContextSync } from './lib/exec-context'; import { createHash } from './lib/hash'; @@ -38,12 +38,12 @@ const jestTransformer: SyncTransformer = { const [__compatJestConfig] = rest; const jestConfig = __compatJestConfig?.config ?? __compatJestConfig; // jest v26 vs v27 changes to support both formats: end - let { rootDir: cwd } = jestConfig; + const { rootDir } = jestConfig; const { configFile, subsequentTransformer } = getOption(jestConfig); // If the congFile is in another directory from jest's rootDir, reset the working path since // since the cache and transforms will operate relative to that config's location - const [config, configHash] = loadConfigSync(cwd, configFile); - cwd = configFile ? resolve(cwd, dirname(configFile)) : cwd; + const [config, configHash] = loadConfigSync(rootDir, configFile); + const cwd = config.cwd; const { execContext } = createExecContextSync(cwd, config, configHash); const { tsxFullPath } = createPaths(execContext, relative(cwd, sourcePath)); diff --git a/src/lib/config.test.ts b/src/lib/config.test.ts index 58198e97..9d708698 100644 --- a/src/lib/config.test.ts +++ b/src/lib/config.test.ts @@ -1,8 +1,12 @@ -import loadConfig, { loadConfigSync } from './config'; +import loadConfig, { ConfigTypes, loadConfigSync } from './config'; import { prepareFixtures } from './__tools/file'; let cwd: string; +// Some returned config values are absolute file paths that can change +// depending on the environment. This function makes them consistent by removing them. +const cleanConfig = ({ cwd, ...other }: ConfigTypes) => other; + describe('config.ts', () => { beforeAll(async () => { [cwd] = await prepareFixtures(__dirname, '__fixtures/config'); @@ -16,8 +20,8 @@ describe('config.ts', () => { }); test('loads config with default values', async () => { - const actual = await loadConfig(cwd, '.graphql-let-simple.yml'); - expect(actual).toMatchSnapshot(); + const [actual, hash] = await loadConfig(cwd, '.graphql-let-simple.yml'); + expect([cleanConfig(actual), hash]).toMatchSnapshot(); }); test('interpolates environment variables', () => { @@ -32,6 +36,6 @@ describe('config.ts', () => { test('overwrite default values', async () => { const [actual] = await loadConfig(cwd, '.graphql-let-overwrite.yml'); - expect(actual).toMatchSnapshot(); + expect(cleanConfig(actual)).toMatchSnapshot(); }); }); diff --git a/src/lib/config.ts b/src/lib/config.ts index 3eee7a87..2de6213a 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,5 +1,5 @@ import { Types } from '@graphql-codegen/plugin-helpers'; -import { resolve } from 'path'; +import { dirname, resolve } from 'path'; import { env } from 'string-env-interpolation'; import { parse as parseYaml } from 'yaml'; import { DEFAULT_CONFIG_FILENAME } from './consts'; @@ -8,12 +8,13 @@ import { createHash } from './hash'; import { SCHEMA_TYPES_BASENAME } from './paths'; import { printError, printWarning } from './print'; -export type PartialGraphqlCodegenOptions = Omit; +type PartialGraphqlCodegenOptions = Omit; -export type GraphQLLetAdditionalOptions = { +type GraphQLLetAdditionalOptions = { plugins: Array>; respectGitIgnore?: boolean; cacheDir?: string; + cwd?: string; TSConfigFile?: string; // gqlDtsEntrypoint?: string; typeInjectEntrypoint?: string; @@ -27,7 +28,7 @@ export type ConfigTypes = PartialGraphqlCodegenOptions & { documents: Types.OperationDocumentGlobPath[]; } & Required; -export function buildConfig(raw: UserConfigTypes): ConfigTypes { +function buildConfig(raw: UserConfigTypes, configDir: string): ConfigTypes { if (typeof raw !== 'object') printError(new Error('A config file must shape an object')); @@ -70,10 +71,19 @@ You can still have it, but it's redundant and can be problem if the types are ma new Error(`config.documents should be an array or a string`), ) as never); + if (documents.some((doc) => doc.indexOf('../') === 0)) + printError( + new Error( + `"documents" should not escape the config directory and must have a common ancestor. + Consider using cwd to set the common parent folder and define documents relative from that.`, + ), + ); + return { ...raw, // Normalized codegen options documents, + cwd: raw.cwd ? resolve(configDir, raw.cwd) : configDir, // Set graphql-let default values respectGitIgnore: raw.respectGitIgnore !== undefined ? raw.respectGitIgnore : true, @@ -86,12 +96,15 @@ You can still have it, but it's redundant and can be problem if the types are ma }; } -export const getConfigPath = (cwd: string, configFilePath?: string) => +const getConfigPath = (cwd: string, configFilePath?: string) => resolve(cwd, configFilePath || DEFAULT_CONFIG_FILENAME); -const getConfigFromContent = (content: string): [ConfigTypes, string] => { +const getConfigFromContent = ( + content: string, + configDir: string, +): [ConfigTypes, string] => { content = env(content); - return [buildConfig(parseYaml(content)), createHash(content)]; + return [buildConfig(parseYaml(content), configDir), createHash(content)]; }; // Refactor with gensync @@ -101,7 +114,7 @@ export default async function loadConfig( ): Promise<[ConfigTypes, string]> { const configPath = getConfigPath(cwd, configFilePath); const content = await readFile(configPath, 'utf-8'); - return getConfigFromContent(content); + return getConfigFromContent(content, dirname(configPath)); } export function loadConfigSync( @@ -110,5 +123,5 @@ export function loadConfigSync( ): [ConfigTypes, string] { const configPath = getConfigPath(cwd, configFilePath); const content = readFileSync(configPath, 'utf-8'); - return getConfigFromContent(content); + return getConfigFromContent(content, dirname(configPath)); } diff --git a/src/lib/exec-context.ts b/src/lib/exec-context.ts index b902eae6..5ddd0910 100644 --- a/src/lib/exec-context.ts +++ b/src/lib/exec-context.ts @@ -85,9 +85,9 @@ const createExecContextGenerator = gensync(function* ( config: ConfigTypes, configHash: string, ) { - const cacheFullDir = getCacheFullDir(cwd, config.cacheDir); + const cacheFullDir = getCacheFullDir(config.cwd, config.cacheDir); const execContext = { - cwd, + cwd: config.cwd, // TODO: Remove and rely on config.cwd; left for now to clarify diff config, configHash, cacheFullDir, diff --git a/src/lib/paths.ts b/src/lib/paths.ts index ab499fd0..d2096f16 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -34,8 +34,11 @@ export function createPaths( export const SCHEMA_TYPES_BASENAME = '__types__'; export function createSchemaPaths(execContext: ExecContext) { - const { cwd, config, cacheFullDir } = execContext; - const typeInjectFullDir = join(cwd, dirname(config.typeInjectEntrypoint)); + const { config, cacheFullDir } = execContext; + const typeInjectFullDir = join( + config.cwd, + dirname(config.typeInjectEntrypoint), + ); const tsxRelPath = `${SCHEMA_TYPES_BASENAME}.tsx`; const tsxFullPath = join(cacheFullDir, tsxRelPath); diff --git a/src/loader.test.ts b/src/loader.test.ts index 47fb9d5e..cd1efbc1 100644 --- a/src/loader.test.ts +++ b/src/loader.test.ts @@ -180,7 +180,9 @@ describe('graphql-let/loader', () => { ); await acceptsConfigPathInOptionsConfigFile( fixtureDir, - require.resolve('./__fixtures/loader/monorepo/config/.graphql-let.yml'), + require.resolve( + './.__fixtures/loader/monorepo-fullpath/config/.graphql-let.yml', + ), ); }); }); diff --git a/src/loader.ts b/src/loader.ts index c38d0d4a..07cfc82f 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -47,10 +47,11 @@ const processLoaderForSources = memoize( sourceFullPath: string, sourceContent: string | Buffer, addDependency: (path: string) => void, - cwd: string, - options: GraphQLLetLoaderOptions, + loaderCwd: string, + { configFile }: GraphQLLetLoaderOptions, ): Promise => { - const [config, configHash] = await loadConfig(cwd, options.configFile); + const [config, configHash] = await loadConfig(loaderCwd, configFile); + const cwd = config.cwd; const { silent } = config; const sourceRelPath = relative(cwd, sourceFullPath); if (!silent) updateLog(`Processing ${sourceRelPath}...`); @@ -124,10 +125,11 @@ const processLoaderForDocuments = memoize( gqlFullPath: string, gqlContent: string | Buffer, addDependency: (path: string) => void, - cwd: string, - options: GraphQLLetLoaderOptions, + loaderCwd: string, + { configFile }: GraphQLLetLoaderOptions, ): Promise => { - const [config, configHash] = await loadConfig(cwd, options.configFile); + const [config, configHash] = await loadConfig(loaderCwd, configFile); + const cwd = config.cwd; const { silent } = config; const graphqlRelPath = relative(cwd, gqlFullPath); if (!silent) updateLog(`Processing ${graphqlRelPath}...`);