From 0858fde9906e968778ae24db6e454ee542ff7ddb Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 18 Dec 2024 18:28:12 -0500 Subject: [PATCH] feat(generator): ability to control import extension (#1284) --- src/generator/config/config.test.ts | 22 +++++++ src/generator/config/config.ts | 5 +- src/generator/config/configInit.ts | 74 ++++++++++++++++++------ src/generator/config/defaults.ts | 4 +- src/generator/generator/generate.test.ts | 28 ++++++++- src/generator/helpers/moduleGenerator.ts | 12 +++- 6 files changed, 123 insertions(+), 22 deletions(-) diff --git a/src/generator/config/config.test.ts b/src/generator/config/config.test.ts index dc9ed8c9b..a39e85b4d 100644 --- a/src/generator/config/config.test.ts +++ b/src/generator/config/config.test.ts @@ -1,6 +1,28 @@ import { expect } from 'vitest' +import { describe } from 'vitest' import { test } from '../../../tests/_/helpers.js' import { createConfig } from './config.js' +import type { ConfigInitSchemaSdl } from './configInit.js' + +const schema: ConfigInitSchemaSdl = { + type: `sdl`, + sdl: `type Query { ok: Boolean }`, +} + +describe(`import format`, () => { + test(`defaults to jsExtension`, async () => { + const config = await createConfig({ schema }) + expect(config.importFormat).toEqual(`jsExtension`) + }) + test(`noExtension`, async () => { + const customPathFile = `./tests/_/fixtures/custom.graphql` + const config = await createConfig({ + schema: { type: `sdlFile`, dirOrFilePath: customPathFile }, + importFormat: `noExtension`, + }) + expect(config.importFormat).toEqual(`noExtension`) + }) +}) test(`can load schema from custom path`, async () => { const customPathFile = `./tests/_/fixtures/custom.graphql` diff --git a/src/generator/config/config.ts b/src/generator/config/config.ts index 5c677c42b..260b94aec 100644 --- a/src/generator/config/config.ts +++ b/src/generator/config/config.ts @@ -11,11 +11,12 @@ import type { Extension } from '../extension/types.js' import { type ConfigInit, type ConfigInitLibraryPaths, + type InputImportFormat, type InputLint, type InputOutputCase, libraryPathKeys, } from './configInit.js' -import { defaultLibraryPaths, defaultNamespace, defaultOutputCase } from './defaults.js' +import { defaultImportFormat, defaultLibraryPaths, defaultNamespace, defaultOutputCase } from './defaults.js' import { defaultName } from './defaults.js' export interface Config { @@ -43,6 +44,7 @@ export interface Config { } formatter: Formatter extensions: Extension[] + importFormat: InputImportFormat paths: { project: { inputs: { @@ -201,6 +203,7 @@ To suppress this warning disable formatting in one of the following ways: return { fs, name, + importFormat: configInit.importFormat ?? defaultImportFormat, nameNamespace, extensions: configInit.extensions ?? [], outputCase, diff --git a/src/generator/config/configInit.ts b/src/generator/config/configInit.ts index def4dfe66..35cd6249c 100644 --- a/src/generator/config/configInit.ts +++ b/src/generator/config/configInit.ts @@ -20,6 +20,39 @@ export const OutputCase = { } as const export type InputOutputCase = keyof typeof OutputCase +export const ImportFormat = { + jsExtension: `jsExtension`, + tsExtension: `tsExtension`, + noExtension: `noExtension`, +} as const +export type InputImportFormat = keyof typeof ImportFormat + +export interface ConfigInitSchemaSdl { + type: `sdl` + sdl: string +} +export interface ConfigInitSchemaInstance { + type: `instance` + instance: Grafaid.Schema.Schema +} +export interface ConfigInitSchemaSdlFile { + type: `sdlFile` + /** + * Defaults to the source directory if set, otherwise the current working directory. + */ + dirOrFilePath?: string +} +export interface ConfigInitSchemaUrl { + type: `url` + url: URL + options?: InputIntrospectionOptions +} + +export type ConfigInitSchema = + | ConfigInitSchemaSdl + | ConfigInitSchemaInstance + | ConfigInitSchemaSdlFile + | ConfigInitSchemaUrl export interface ConfigInit { /** * File system API to use. @@ -66,25 +99,13 @@ export interface ConfigInit { */ currentWorkingDirectory?: string /** - * The schema to use for generation. Can be an existing SDL file on disk, a schema instance already in memory, or an endpoint that will be introspected. + * The schema to use for generation. Can be one of: + * + * 1. An existing SDL file on disk, + * 2. A schema instance already in memory, + * 3. An endpoint that will be introspected. */ - schema: { - type: 'sdl' - sdl: string - } | { - type: 'instance' - instance: Grafaid.Schema.Schema - } | { - type: 'sdlFile' - /** - * Defaults to the source directory if set, otherwise the current working directory. - */ - dirOrFilePath?: string - } | { - type: 'url' - url: URL - options?: InputIntrospectionOptions - } + schema: ConfigInitSchema /** * If the schema comes from a non-sdl-file source like a GraphQL endpoint URL, should a derived SDL file be written to disk? * @@ -136,6 +157,23 @@ export interface ConfigInit { * If not set, Graffle will look for a file called `scalars.ts` in the project directory. */ scalars?: string + /** + * How should import identifiers be generated? Can be one of: + * + * 1. `jsExtension` e.g. `import ... from './bar.js'` + * 2. `tsExtension` e.g. `import ... from './bar.ts'` + * 3. `noExtension` e.g. `import ... from './bar'` + * + * @defaultValue `jsExtension` + * + * @remarks + * + * A user request for this option can be found at https://github.com/graffle-js/graffle/issues/1282. + * + * There is a planned feature to have a default be dynamic according to the state of your project's tsconfig.json. + * See https://github.com/graffle-js/graffle/issues/1283. + */ + importFormat?: InputImportFormat /** * Override import paths to graffle package within the generated code. * Used by Graffle test suite to have generated clients point to source diff --git a/src/generator/config/defaults.ts b/src/generator/config/defaults.ts index 11223bd05..25af56e67 100644 --- a/src/generator/config/defaults.ts +++ b/src/generator/config/defaults.ts @@ -1,4 +1,4 @@ -import type { ConfigInitLibraryPaths, InputOutputCase } from './configInit.js' +import type { ConfigInitLibraryPaths, InputImportFormat, InputOutputCase } from './configInit.js' export const defaultNamespace = `Graffle` @@ -13,3 +13,5 @@ export const defaultLibraryPaths = { } satisfies ConfigInitLibraryPaths export const defaultOutputCase: InputOutputCase = `kebab` + +export const defaultImportFormat: InputImportFormat = `jsExtension` diff --git a/src/generator/generator/generate.test.ts b/src/generator/generator/generate.test.ts index b5779b49e..ee29d1cf8 100644 --- a/src/generator/generator/generate.test.ts +++ b/src/generator/generator/generate.test.ts @@ -2,9 +2,35 @@ import { globby } from 'globby' import * as Memfs from 'memfs' import { readFile } from 'node:fs/promises' import * as Path from 'node:path' -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' +import type { ConfigInitSchemaSdl } from '../_.js' import { generate } from './generate.js' +const schema: ConfigInitSchemaSdl = { + type: `sdl`, + sdl: `type Query { ok: Boolean }`, +} + +describe(`importFormat`, () => { + test(`default is jsExtension`, async () => { + await generate({ + fs: Memfs.fs.promises as any, + schema, + }) + const SchemaTs = Memfs.fs.readFileSync(`./graffle/modules/schema.ts`, `utf8`) + expect(SchemaTs).toMatch(/import.*".\/data.js"/) + }) + test(`noExtension`, async () => { + await generate({ + fs: Memfs.fs.promises as any, + schema, + importFormat: `noExtension`, + }) + const SchemaTs = Memfs.fs.readFileSync(`./graffle/modules/schema.ts`, `utf8`) + expect(SchemaTs).toMatch(/import.*".\/data"/) + }) +}) + test(`kitchen-sink generated modules`, async () => { const basePath = `./tests/_/schemas/kitchen-sink/graffle` const filePaths = await globby(`${basePath}/**/*.ts`) diff --git a/src/generator/helpers/moduleGenerator.ts b/src/generator/helpers/moduleGenerator.ts index fc8a1129f..86c38a7e1 100644 --- a/src/generator/helpers/moduleGenerator.ts +++ b/src/generator/helpers/moduleGenerator.ts @@ -1,4 +1,5 @@ import { camelCase, kebabCase, pascalCase, snakeCase } from 'es-toolkit' +import { casesExhausted } from '../../lib/prelude.js' import type { Config } from '../config/config.js' import { createCodeGenerator, @@ -61,7 +62,16 @@ export const getFileName = (config: Config, generator: ModuleGenerator | Generat export const getImportName = (config: Config, generator: ModuleGenerator | GeneratedModule) => { const name = getBaseName(config, generator) - return `${name}.js` + switch (config.importFormat) { + case `jsExtension`: + return `${name}.js` + case `tsExtension`: + return `${name}.ts` + case `noExtension`: + return name + default: + throw casesExhausted(config.importFormat) + } } export const caseFormatters = {