Skip to content

Commit

Permalink
Using global declaration with namespace instead of using module (#47)
Browse files Browse the repository at this point in the history
Changed generated types to the global declaration as default, so it can be used without manual imports.

And use a namespace(default is `GatsbyTypes`) to avoid type conflicts with other global declarations. It doesn't allow undefined or empty string, after the upgrade, every typenames in user codebase will be changed by the namespace.

```tsx
import type { SEOQuery } from '~/src/__generated__/gatsby-types';

function SEO() {
  const data = useStaticQuery<SEOQuery>(
    graphql`
      query SEO { ... }
    `
  );
  ...
```

will be changed into:

```tsx
// no need to import the type
function SEO() {
  const data = useStaticQuery<GatsbyTypes.SEOQuery>(
    graphql`
      query SEO { ... }
    `
  );
  ...
```

Since the Flow doesn't have namespace syntax, It uses `$` as the fallback behavior.

Closes #24
Closes #16
  • Loading branch information
cometkim committed Mar 8, 2020
1 parent 4456ae6 commit 00ec525
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 30 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -209,6 +209,10 @@ Or [gatsby-plugin-graphql-codegen](https://github.com/d4rekanguok/gatsby-typescr

## Changelog

### v2.0.0

- **[BREAKING CHANGE]** Generated types are now using global declaration with a namespace (default is `GatsbyTypes`).

### v1.1.2

- Export inline fragment subtypes. ([#45](https://github.com/cometkim/gatsby-plugin-typegen/issues/45))
Expand Down
12 changes: 9 additions & 3 deletions src/gatsby-node.ts
Expand Up @@ -80,6 +80,7 @@ export const onPostBootstrap: GatsbyNode['onPostBootstrap'] = async ({
}) => {
const {
language,
namespace,
outputPath,
includeResolvers,
emitSchema,
Expand Down Expand Up @@ -130,6 +131,7 @@ export const onPostBootstrap: GatsbyNode['onPostBootstrap'] = async ({
const codegenWorker = setupCodegenWorker({
schemaAst: schema,
language,
namespace,
outputPath,
includeResolvers,
reporter,
Expand All @@ -146,7 +148,11 @@ export const onPostBootstrap: GatsbyNode['onPostBootstrap'] = async ({
if (process.env.NODE_ENV === 'development') {
reporter.verbose('[typegen][dev] Watching query changes and re-run workers');

const insertTypeWorker = autoFix && setupInsertTypeWorker({ reporter });
const insertTypeWorker = autoFix && setupInsertTypeWorker({
language,
namespace,
reporter,
});
const pushInsertTypeTask = (task: InsertTypeTask) => {
if (!insertTypeWorker) {
return;
Expand Down Expand Up @@ -178,12 +184,12 @@ export const onPostBootstrap: GatsbyNode['onPostBootstrap'] = async ({

pushCodegenTask();

if (pluginOptions.language === 'typescript' && /\.tsx?$/.test(componentPath)) {
if (language === 'typescript' && /\.tsx?$/.test(componentPath)) {
pushInsertTypeTask({ file: componentPath });
}

// Flow version is bit more slower because should check the `@flow` comment exist.
if (pluginOptions.language === 'flow' && /\.jsx?$/.test(componentPath)) {
if (language === 'flow' && /\.jsx?$/.test(componentPath)) {
const content = await readFile(componentPath);
const hasFlowComment = content.includes('@flow');
reporter.verbose(`[typegen] Check if the file has flow comment: ${hasFlowComment}`);
Expand Down
23 changes: 16 additions & 7 deletions src/plugin-utils.ts
Expand Up @@ -2,7 +2,11 @@ import path from 'path';
import { Required } from 'utility-types';
import { Store, Reporter } from 'gatsby';

import { PluginOptions, DeprecatedPluginOptions, SchemaOutputOptions } from './types';
import {
PluginOptions,
SchemaOutputOptions,
DeprecatedPluginOptions,
} from './types';
import { formatLanguage } from './common';

// No parsing by default, save introspection result file as json format.
Expand All @@ -11,7 +15,7 @@ const DEFAULT_SCHEMA_OUTPUT_OPTION = {
commentDescriptions: true,
} as const;

type MapTrueToDefault<T> = T extends { [key: string]: infer V }
type MapEmitSchemaOption<T> = T extends { [key: string]: infer V }
? V extends true
? { [key: string]: typeof DEFAULT_SCHEMA_OUTPUT_OPTION }
: { [key: string]: SchemaOutputOptions }
Expand All @@ -20,9 +24,12 @@ type MapTrueToDefault<T> = T extends { [key: string]: infer V }
export type RequiredPluginOptions = Required<
Omit<
PluginOptions,
keyof DeprecatedPluginOptions | 'emitSchema'
(
| keyof DeprecatedPluginOptions
| 'emitSchema'
)
> & {
emitSchema: MapTrueToDefault<PluginOptions['emitSchema']>
emitSchema: MapEmitSchemaOption<PluginOptions['emitSchema']>
}
>;

Expand Down Expand Up @@ -50,15 +57,16 @@ export const requirePluginOptions: RequirePluginOptionsFn = (

const {
language = 'typescript',
namespace = 'GatsbyTypes',
emitSchema: emitSchemaOptionMap = {},
includeResolvers = false,
autoFix = true,
emitSchema: emitSchemaOptionMap = {},
emitPluginDocuments = {},
schemaOutputPath,
typeDefsOutputPath,
} = pluginOptions;

const emitSchema: MapTrueToDefault<typeof emitSchemaOptionMap> = {};
const emitSchema: MapEmitSchemaOption<typeof emitSchemaOptionMap> = {};
for (const [key, options] of Object.entries(emitSchemaOptionMap)) {
if (options === true) {
emitSchema[key] = {
Expand Down Expand Up @@ -86,7 +94,7 @@ export const requirePluginOptions: RequirePluginOptionsFn = (
: path.resolve(basePath, 'src/__generated__/gatsby-types.js')
);

if ((language === 'typescript') !== (outputPath.endsWith('ts'))) {
if ((language === 'typescript') !== /\.tsx?$/.test(outputPath)) {
reporter.warn(
reporter.stripIndent(
`The language you specified is not match to file extension.
Expand All @@ -98,6 +106,7 @@ export const requirePluginOptions: RequirePluginOptionsFn = (

return {
language,
namespace,
outputPath,
includeResolvers,
autoFix,
Expand Down
10 changes: 10 additions & 0 deletions src/types.ts
Expand Up @@ -9,6 +9,16 @@ export type PluginOptions = {
*/
language?: 'typescript' | 'flow',

/**
* Namespace is required to avoid conflict on generated types and other global declarations.
*
* Flow will use $ prefix as fallback.
* e.g) `type GatsbyTypes$MySiteQuery = ...`
*
* @default 'GatsbyTypes'
*/
namespace?: string;

/**
* Path to save generated typeDefs file.
*
Expand Down
48 changes: 40 additions & 8 deletions src/workers/codegen.ts
Expand Up @@ -5,9 +5,13 @@ import { codegen } from '@graphql-codegen/core';
import { Source } from '@graphql-toolkit/common';

import { delay, writeFile, formatLanguage } from '../common';
import { RequiredPluginOptions } from '../plugin-utils';

const CARGO_DELAY = 1000 as const;

const TYPEDEF_EXPORT_NODE_REGEXP = /export type ((.*)(\{\|?|;)($|\s))/g;
const TYPEDEF_EXPORT_NODE_REPLACER = 'declare type $1';

// Preset configurations to ensure compatibility with Gatsby.
const DEFAULT_SHARED_CONFIG = {
namingConvention: {
Expand All @@ -23,6 +27,7 @@ const DEFAULT_TYPESCRIPT_CONFIG = {
avoidOptionals: true,
immutableTypes: true,
maybeValue: 'T | undefined',
noExport: true,
} as const;

const DEFAULT_FLOW_CONFIG = {
Expand All @@ -42,15 +47,17 @@ interface SetupCodegenWorkerFn {
(props: {
schemaAst: GraphQLSchema,
reporter: Reporter,
language: RequiredPluginOptions['language'],
namespace: string,
outputPath: string,
language: 'typescript' | 'flow',
includeResolvers: boolean,
}): CodegenWorker;
}
export const setupCodegenWorker: SetupCodegenWorkerFn = ({
schemaAst,
outputPath,
language,
namespace,
includeResolvers,
reporter,
}) => {
Expand All @@ -70,12 +77,17 @@ export const setupCodegenWorker: SetupCodegenWorkerFn = ({
};
if (language === 'typescript') {
codegenOptions.pluginMap['typescript'] = require('@graphql-codegen/typescript');
codegenOptions.plugins.push({ typescript: DEFAULT_TYPESCRIPT_CONFIG });
codegenOptions.plugins.push({
typescript: {
...DEFAULT_TYPESCRIPT_CONFIG,
},
});
codegenOptions.pluginMap['typescriptOperations'] = require('@graphql-codegen/typescript-operations');
codegenOptions.plugins.push({
typescriptOperations: {
...DEFAULT_TYPESCRIPT_CONFIG,
exportFragmentSpreadSubTypes: true
// See https://github.com/cometkim/gatsby-plugin-typegen/issues/45
exportFragmentSpreadSubTypes: true,
}
});
if (includeResolvers) {
Expand All @@ -88,26 +100,46 @@ export const setupCodegenWorker: SetupCodegenWorkerFn = ({
}
} else /* flow */ {
codegenOptions.pluginMap['flow'] = require('@graphql-codegen/flow');
codegenOptions.plugins.push({ flow: DEFAULT_FLOW_CONFIG });
codegenOptions.plugins.push({
flow: {
...DEFAULT_FLOW_CONFIG,
typesPrefix: `${namespace}$`,
},
});
codegenOptions.pluginMap['flowOperations'] = require('@graphql-codegen/flow-operations');
codegenOptions.plugins.push({
flowOperations: {
...DEFAULT_FLOW_CONFIG,
exportFragmentSpreadSubTypes: true
// See https://github.com/cometkim/gatsby-plugin-typegen/issues/45
exportFragmentSpreadSubTypes: true,
typesPrefix: `${namespace}$`,
}
});
if (includeResolvers) {
codegenOptions.pluginMap['flowResolvers'] = require('@graphql-codegen/flow-resolvers');
// Where is contextType option????? WHERE
codegenOptions.plugins.push({ flowResolvers: {} });
codegenOptions.plugins.push({
flowResolvers: {
typesPrefix: `${namespace}$`,
},
});
}
}

reporter.verbose(`[typegen] Generate type definitions to ${outputPath}. (language: ${formatLanguage(language)})`);

try {
const result = await codegen(codegenOptions);
await writeFile(outputPath, '/* eslint-disable */\n\n' + result);
let result = await codegen(codegenOptions);

if (language === 'typescript') {
result = `declare namespace ${namespace} {\n${result}\n}`;
} else /* flow */ {
result = result.replace(TYPEDEF_EXPORT_NODE_REGEXP, TYPEDEF_EXPORT_NODE_REPLACER)
}

result = '/* eslint-disable */\n\n' + result;

await writeFile(outputPath, result);
} catch (e) {
reporter.panicOnBuild('[typegen] An error on codegen', e);
}
Expand Down
28 changes: 16 additions & 12 deletions src/workers/insert-types.ts
Expand Up @@ -2,6 +2,7 @@ import { Reporter } from 'gatsby';
import { queue, AsyncQueue, asyncify } from 'async';

import { readFile, writeFile } from '../common';
import { RequiredPluginOptions } from '../plugin-utils';

const CONCURRENCY = 2;

Expand All @@ -21,11 +22,7 @@ const CONCURRENCY = 2;
* [^`]*?
* `)
*/
const STATIC_QUERY_HOOK_REGEXP = /(?<CallExpressionName>useStaticQuery(?<TypeTemplate><(?<TypeArgument>\S*)>)?)\([\s\S]*?graphql(?<TemplateLiteral>`\s*?(?<QueryDefinitionStart>query (?<QueryName>\S*)[^{]{)[^`]*?`)/g
const STATIC_QUERY_HOOK_REPLACER = (substring: string, ...args: any[]): string => {
const { length: l, [l - 1]: groups } = args;
return substring.replace(groups['CallExpressionName'], `useStaticQuery<${groups['QueryName']}Query>`);
}
const STATIC_QUERY_HOOK_REGEXP = /(?<CallExpressionName>useStaticQuery(?<TypeTemplate><(?<TypeArgument>\S*)>)?)\([\s\S]*?graphql(?<TemplateLiteral>`\s*?(?<QueryDefinitionStart>query (?<QueryName>\S*)[^{]{)[^`]*?`)/g;

/**
* (?<JsxTagOpening><StaticQuery
Expand All @@ -45,11 +42,7 @@ const STATIC_QUERY_HOOK_REPLACER = (substring: string, ...args: any[]): string =
* [^`]*?
* `)
*/
const STATIC_QUERY_COMPONENT_REGEXP = /(?<JsxTagOpening><StaticQuery(?<TagTypeTemplate><(?<TagTypeArgument>\S+)>)?)[\s\S]+?query={[\s\S]*?graphql(?<TemplateLiteral>`\s*?(?<QueryDefinitionStart>query (?<QueryName>\S*)[^{]?\{)[^`]*`)/g
const STATIC_QUERY_COMPONENT_REPLACER = (substring: string, ...args: any[]): string => {
const { length: l, [l - 1]: groups } = args;
return substring.replace(groups['JsxTagOpening'], `<StaticQuery<${groups['QueryName']}Query>`);
}
const STATIC_QUERY_COMPONENT_REGEXP = /(?<JsxTagOpening><StaticQuery(?<TagTypeTemplate><(?<TagTypeArgument>\S+)>)?)[\s\S]+?query={[\s\S]*?graphql(?<TemplateLiteral>`\s*?(?<QueryDefinitionStart>query (?<QueryName>\S*)[^{]?\{)[^`]*`)/g;

export type InsertTypeTask = {
file: string,
Expand All @@ -60,18 +53,29 @@ export type InsertTypeWorker = AsyncQueue<InsertTypeTask>;
interface SetupInsertTypeWorkerFn {
(props: {
reporter: Reporter,
language: RequiredPluginOptions['language'],
namespace: string,
}): InsertTypeWorker;
}
export const setupInsertTypeWorker: SetupInsertTypeWorkerFn = ({
reporter,
language,
namespace,
}) => {
const worker = queue<InsertTypeTask>(asyncify(async (task: InsertTypeTask) => {
const { file } = task;
const accessor = language === 'typescript' ? '.' : '$';

const content = await readFile(file);
const fixed = content
.replace(STATIC_QUERY_HOOK_REGEXP, STATIC_QUERY_HOOK_REPLACER)
.replace(STATIC_QUERY_COMPONENT_REGEXP, STATIC_QUERY_COMPONENT_REPLACER)
.replace(STATIC_QUERY_HOOK_REGEXP, (substring: string, ...args: any[]): string => {
const { length: l, [l - 1]: groups } = args;
return substring.replace(groups['CallExpressionName'], `useStaticQuery<${namespace}${accessor}${groups['QueryName']}Query>`);
})
.replace(STATIC_QUERY_COMPONENT_REGEXP, (substring: string, ...args: any[]): string => {
const { length: l, [l - 1]: groups } = args;
return substring.replace(groups['JsxTagOpening'], `<StaticQuery<${namespace}${accessor}${groups['QueryName']}Query>`);
})

if (content !== fixed) {
reporter.verbose(`[typegen] Insert type definitions into ${file}\nbecause documents were changed.`);
Expand Down

0 comments on commit 00ec525

Please sign in to comment.