Skip to content

Commit

Permalink
feat: better esm support, remove top-level node imports (graphql-nexu…
Browse files Browse the repository at this point in the history
…s#1112)

* test for esm/esbuild use of nexus

* refactor: add nodeImports, remove top-level references

* Don't minify the esbuild test script

* A bit more explicit of a check on the process being node
  • Loading branch information
tgriesser authored Jul 2, 2022
1 parent eec4b91 commit 4d8e371
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 79 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ examples/*/dist
website/static/playground-dist
yarn-error.log
coverage/*
tests/esm/out/
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"@types/prettier": "^1.18.3",
"@typescript-eslint/eslint-plugin": "2.7.0",
"dripip": "^0.10.0",
"esbuild": "^0.14.48",
"eslint": "^6.6.0",
"get-port": "^5.1.1",
"graphql": "^16.3.0",
Expand Down
8 changes: 8 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function nodeImports() {
const fs = require('fs') as typeof import('fs')
const path = require('path') as typeof import('path')
return {
fs,
path,
}
}
12 changes: 4 additions & 8 deletions src/typegenAutoConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { GraphQLNamedType, GraphQLSchema, isOutputType } from 'graphql'
import * as path from 'path'
import type { TypegenInfo } from './builder'
import type { TypingImport } from './definitions/_types'
import { TYPEGEN_HEADER } from './lang'
import { nodeImports } from './node'
import { getOwnPackage, log, objValues, relativePathTo, typeScriptFileExtension } from './utils'

/** Any common types / constants that would otherwise be circular-imported */
Expand Down Expand Up @@ -129,16 +129,12 @@ export function typegenAutoConfig(options: SourceTypesConfigOptions, contextType
}
})

const path = nodeImports().path
const typeSources = await Promise.all(
options.modules.map(async (source) => {
// Keeping all of this in here so if we don't have any sources
// e.g. in the Playground, it doesn't break things.

// Yeah, this doesn't exist in Node 6, but since this is a new
// lib and Node 6 is close to EOL so if you really need it, open a PR :)
const fs = require('fs') as typeof import('fs')
const util = require('util') as typeof import('util')
const readFile = util.promisify(fs.readFile)
const { module: pathOrModule, glob = true, onlyTypes, alias, typeMatch } = source
if (path.isAbsolute(pathOrModule) && path.extname(pathOrModule) !== '.ts') {
return console.warn(
Expand All @@ -154,7 +150,7 @@ export function typegenAutoConfig(options: SourceTypesConfigOptions, contextType
if (path.extname(resolvedPath) !== '.ts') {
resolvedPath = findTypingForFile(resolvedPath, pathOrModule)
}
fileContents = await readFile(resolvedPath, 'utf-8')
fileContents = String(await nodeImports().fs.promises.readFile(resolvedPath, 'utf-8'))
} catch (e) {
if (e instanceof Error && e.message.indexOf('Cannot find module') !== -1) {
console.error(`GraphQL Nexus: Unable to find file or module ${pathOrModule}, skipping`)
Expand Down Expand Up @@ -277,7 +273,7 @@ export function typegenAutoConfig(options: SourceTypesConfigOptions, contextType
function findTypingForFile(absolutePath: string, pathOrModule: string) {
// First try to find the "d.ts" adjacent to the file
try {
const typeDefPath = absolutePath.replace(path.extname(absolutePath), '.d.ts')
const typeDefPath = absolutePath.replace(nodeImports().path.extname(absolutePath), '.d.ts')
require.resolve(typeDefPath)
return typeDefPath
} catch (e) {
Expand Down
4 changes: 3 additions & 1 deletion src/typegenFormatPrettier.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from 'path'
import type * as Prettier from 'prettier'
import { nodeImports } from './node'

export type TypegenFormatFn = (content: string, type: 'types' | 'schema') => string | Promise<string>

Expand All @@ -22,6 +22,8 @@ export function typegenFormatPrettier(prettierConfig: string | object): TypegenF

let prettierConfigResolved: Prettier.Options

const path = nodeImports().path

if (typeof prettierConfig === 'string') {
/* istanbul ignore if */
if (!path.isAbsolute(prettierConfig)) {
Expand Down
26 changes: 11 additions & 15 deletions src/typegenMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { GraphQLSchema, lexicographicSortSchema } from 'graphql'
import * as path from 'path'
import type { BuilderConfigInput, TypegenInfo } from './builder'
import type { ConfiguredTypegen } from './core'
import type { NexusGraphQLSchema } from './definitions/_types'
import { SDL_HEADER, TYPEGEN_HEADER } from './lang'
import { nodeImports } from './node'
import { printSchemaWithDirectives } from './printSchemaWithDirectives'
import { typegenAutoConfig } from './typegenAutoConfig'
import { typegenFormatPrettier } from './typegenFormatPrettier'
Expand Down Expand Up @@ -75,30 +75,26 @@ export class TypegenMetadata {
}

async writeFile(type: 'schema' | 'types', output: string, filePath: string) {
if (typeof filePath !== 'string' || !path.isAbsolute(filePath)) {
if (typeof filePath !== 'string' || !nodeImports().path.isAbsolute(filePath)) {
return Promise.reject(
new Error(`Expected an absolute path to output the Nexus ${type}, saw ${filePath}`)
)
}
const fs = require('fs') as typeof import('fs')
const util = require('util') as typeof import('util')
const [readFile, writeFile, removeFile, mkdir] = [
util.promisify(fs.readFile),
util.promisify(fs.writeFile),
util.promisify(fs.unlink),
util.promisify(fs.mkdir),
]
const fs = nodeImports().fs
const formattedOutput =
typeof this.config.formatTypegen === 'function' ? await this.config.formatTypegen(output, type) : output
const content = this.config.prettierConfig
? await typegenFormatPrettier(this.config.prettierConfig)(formattedOutput, type)
: formattedOutput

const [toSave, existing] = await Promise.all([content, readFile(filePath, 'utf8').catch(() => '')])
const [toSave, existing] = await Promise.all([
content,
fs.promises.readFile(filePath, 'utf8').catch(() => ''),
])
if (toSave !== existing) {
const dirPath = path.dirname(filePath)
const dirPath = nodeImports().path.dirname(filePath)
try {
await mkdir(dirPath, { recursive: true })
await fs.promises.mkdir(dirPath, { recursive: true })
} catch (e) {
if (e.code !== 'EEXIST') {
throw e
Expand All @@ -108,14 +104,14 @@ export class TypegenMetadata {
// apparently. See issue motivating this logic here:
// https://github.com/graphql-nexus/schema/issues/247.
try {
await removeFile(filePath)
await fs.promises.unlink(filePath)
} catch (e) {
/* istanbul ignore next */
if (e.code !== 'ENOENT' && e.code !== 'ENOTDIR') {
throw e
}
}
return writeFile(filePath, toSave)
return fs.promises.writeFile(filePath, toSave)
}
}

Expand Down
94 changes: 52 additions & 42 deletions src/typegenUtils.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,75 @@
import * as path from 'path'
import type { BuilderConfigInput } from './builder'
import type { ConfiguredTypegen } from './core'
import { nodeImports } from './node'
import type { TypegenMetadataConfig } from './typegenMetadata'
import { assertAbsolutePath, getOwnPackage, isProductionStage } from './utils'

/** Normalizes the builder config into the config we need for typegen */
export function resolveTypegenConfig(config: BuilderConfigInput): TypegenMetadataConfig {
const {
outputs,
shouldGenerateArtifacts = Boolean(!process.env.NODE_ENV || process.env.NODE_ENV !== 'production'),
...rest
} = config
const { outputs, shouldGenerateArtifacts = defaultShouldGenerateArtifacts(), ...rest } = config

const defaultSDLFilePath = path.join(process.cwd(), 'schema.graphql')
function getOutputPaths() {
const defaultSDLFilePath = nodeImports().path.join(process.cwd(), 'schema.graphql')

let typegenFilePath: ConfiguredTypegen | null = null
let sdlFilePath: string | null = null
let typegenFilePath: ConfiguredTypegen | null = null
let sdlFilePath: string | null = null

if (outputs === undefined) {
if (isProductionStage()) {
sdlFilePath = defaultSDLFilePath
}
} else if (outputs === true) {
sdlFilePath = defaultSDLFilePath
} else if (typeof outputs === 'object') {
if (outputs.schema === true) {
if (outputs === undefined) {
if (isProductionStage()) {
sdlFilePath = defaultSDLFilePath
}
} else if (outputs === true) {
sdlFilePath = defaultSDLFilePath
} else if (typeof outputs.schema === 'string') {
sdlFilePath = assertAbsolutePath(outputs.schema, 'outputs.schema')
} else if (outputs.schema === undefined && isProductionStage()) {
}
// handle typegen configuration
if (typeof outputs.typegen === 'string') {
typegenFilePath = {
outputPath: assertAbsolutePath(outputs.typegen, 'outputs.typegen'),
} else if (typeof outputs === 'object') {
if (outputs.schema === true) {
sdlFilePath = defaultSDLFilePath
} else if (typeof outputs.schema === 'string') {
sdlFilePath = assertAbsolutePath(outputs.schema, 'outputs.schema')
} else if (outputs.schema === undefined && isProductionStage()) {
}
} else if (typeof outputs.typegen === 'object') {
typegenFilePath = {
...outputs.typegen,
outputPath: assertAbsolutePath(outputs.typegen.outputPath, 'outputs.typegen.outputPath'),
} as ConfiguredTypegen
if (outputs.typegen.globalsPath) {
typegenFilePath.globalsPath = assertAbsolutePath(
outputs.typegen.globalsPath,
'outputs.typegen.globalsPath'
)
// handle typegen configuration
if (typeof outputs.typegen === 'string') {
typegenFilePath = {
outputPath: assertAbsolutePath(outputs.typegen, 'outputs.typegen'),
}
} else if (typeof outputs.typegen === 'object') {
typegenFilePath = {
...outputs.typegen,
outputPath: assertAbsolutePath(outputs.typegen.outputPath, 'outputs.typegen.outputPath'),
} as ConfiguredTypegen
if (outputs.typegen.globalsPath) {
typegenFilePath.globalsPath = assertAbsolutePath(
outputs.typegen.globalsPath,
'outputs.typegen.globalsPath'
)
}
}
} else if (outputs !== false) {
console.warn(
`You should specify a configuration value for outputs in Nexus' makeSchema. ` +
`Provide one to remove this warning.`
)
}
return {
typegenFilePath,
sdlFilePath,
}
} else if (outputs !== false) {
console.warn(
`You should specify a configuration value for outputs in Nexus' makeSchema. ` +
`Provide one to remove this warning.`
)
}

return {
...rest,
nexusSchemaImportId: getOwnPackage().name,
outputs: {
typegen: shouldGenerateArtifacts ? typegenFilePath : null,
schema: shouldGenerateArtifacts ? sdlFilePath : null,
typegen: shouldGenerateArtifacts ? getOutputPaths().typegenFilePath : null,
schema: shouldGenerateArtifacts ? getOutputPaths().sdlFilePath : null,
},
}
}

function defaultShouldGenerateArtifacts() {
return Boolean(
typeof process === 'object' &&
typeof process.cwd === 'function' &&
(!process.env.NODE_ENV || process.env.NODE_ENV !== 'production')
)
}
24 changes: 11 additions & 13 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as fs from 'fs'
import {
GraphQLEnumType,
GraphQLInputObjectType,
Expand All @@ -22,7 +21,6 @@ import {
isWrappingType,
specifiedScalarTypes,
} from 'graphql'
import * as Path from 'path'
import { decorateType } from './definitions/decorateType'
import { isNexusMetaType, NexusMetaType, resolveNexusMetaType } from './definitions/nexusMeta'
import {
Expand All @@ -42,6 +40,7 @@ import {
TypingImport,
withNexusSymbol,
} from './definitions/_types'
import { nodeImports } from './node'

export const isInterfaceField = (type: GraphQLObjectType, fieldName: string) => {
return type.getInterfaces().some((i) => Boolean(i.getFields()[fieldName]))
Expand Down Expand Up @@ -150,7 +149,7 @@ export function eachObj<T>(obj: Record<string, T>, iter: (val: T, key: string, i
export const isObject = (obj: any): boolean => obj !== null && typeof obj === 'object'

export const assertAbsolutePath = (pathName: string, property: string) => {
if (!Path.isAbsolute(pathName)) {
if (!nodeImports().path.isAbsolute(pathName)) {
throw new Error(`Expected path for "${property}" to be an absolute path, saw "${pathName}"`)
}
return pathName
Expand Down Expand Up @@ -221,7 +220,7 @@ export function isPromiseLike(value: any): value is PromiseLike<any> {
export const typeScriptFileExtension = /(\.d)?\.ts$/

function makeRelativePathExplicitlyRelative(path: string) {
if (Path.isAbsolute(path)) return path
if (nodeImports().path.isAbsolute(path)) return path
if (path.startsWith('./')) return path
return `./${path}`
}
Expand All @@ -243,6 +242,7 @@ export function formatPathForModuleImport(path: string) {
}

export function relativePathTo(absolutePath: string, fromPath: string): string {
const Path = nodeImports().path
const filename = Path.basename(absolutePath)
const relative = Path.relative(Path.dirname(fromPath), Path.dirname(absolutePath))
return formatPathForModuleImport(Path.join(relative, filename))
Expand Down Expand Up @@ -477,20 +477,18 @@ export function casesHandled(x: never): never {
throw new Error(`A case was not handled for value: "${x}"`)
}

/** Quickly log objects */
export function dump(x: any) {
console.log(require('util').inspect(x, { depth: null }))
}

function isNodeModule(path: string) {
// Avoid treating absolute windows paths as Node packages e.g. D:/a/b/c
return !Path.isAbsolute(path) && /^([A-z0-9@])/.test(path)
return !nodeImports().path.isAbsolute(path) && /^([A-z0-9@])/.test(path)
}

export function resolveImportPath(rootType: TypingImport, typeName: string, outputPath: string) {
const rootTypePath = rootType.module

if (typeof rootTypePath !== 'string' || (!Path.isAbsolute(rootTypePath) && !isNodeModule(rootTypePath))) {
if (
typeof rootTypePath !== 'string' ||
(!nodeImports().path.isAbsolute(rootTypePath) && !isNodeModule(rootTypePath))
) {
throw new Error(
`Expected an absolute path or Node package for the root typing path of the type "${typeName}", saw "${rootTypePath}"`
)
Expand All @@ -502,15 +500,15 @@ export function resolveImportPath(rootType: TypingImport, typeName: string, outp
} catch (e) {
throw new Error(`Module "${rootTypePath}" for the type "${typeName}" does not exist`)
}
} else if (!fs.existsSync(rootTypePath)) {
} else if (!nodeImports().fs.existsSync(rootTypePath)) {
throw new Error(`Root typing path "${rootTypePath}" for the type "${typeName}" does not exist`)
}

if (isNodeModule(rootTypePath)) {
return rootTypePath
}

if (Path.isAbsolute(rootTypePath)) {
if (nodeImports().path.isAbsolute(rootTypePath)) {
return relativePathTo(rootTypePath, outputPath)
}

Expand Down
16 changes: 16 additions & 0 deletions tests/esm/esm-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { GraphQLSchema } from 'graphql'
import { makeSchema, queryType } from '../../dist'

const schema = makeSchema({
types: [
queryType({
definition(t) {
t.boolean('ok')
},
}),
],
})

if (!(schema instanceof GraphQLSchema)) {
throw new Error('Not a schema')
}
Loading

0 comments on commit 4d8e371

Please sign in to comment.