Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: require all files that import nexus #833

Merged
merged 9 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ what follows is a stub
## Build Flow

1. The app layout is calculated
We discover things like where the entrypoint is, if any, and where graphql modules are, if any.
We discover things like where the entrypoint is, if any, and where [Nexus modules](/guides/project-layout?id=nexus-modules) are, if any.
1. Worktime plugins are loaded (see [Plugin Loading Flow](#plugin-loading-flow))
1. Typegen is acquired
This step is about processes that reflect upon the app's source code to extract type information that will be automatically used in other parts of the app. This approach is relatively novel among Node tools. There are dynamic and static processes. The static ones use the TypeScript compiler API while the dynamic ones literally run the app with node in a special reflective mode.
Expand All @@ -103,7 +103,7 @@ what follows is a stub

Static doesn't have to deal with the unpredictabilities of running an app and so has the benefit of being easier to reason about in a sense. It also has the benefit of extracting accurate type information using the native TS system whereas dynamic relies on building TS types from scratch. This makes static a fit for arbitrary code. On the downside, robust AST processing is hard work, and so, so far, static restricts how certain expressions can be written, otherwise AST traversal fails.

1. A start module is created in memory. It imports the entrypoint and all graphql modules. It registers an extension hook to transpile the TypeScript app on the fly as it is run. The transpilation uses the project's tsconfig but overrides target and module so that it is runnable by Node (10 and up). Specificaly es2015 target and commonjs module. For example if user had module of `esnext` the transpilation result would not be runnable by Node.
1. A start module is created in memory. It imports the entrypoint and all [Nexus modules](/guides/project-layout?id=nexus-modules). It registers an extension hook to transpile the TypeScript app on the fly as it is run. The transpilation uses the project's tsconfig but overrides target and module so that it is runnable by Node (10 and up). Specificaly es2015 target and commonjs module. For example if user had module of `esnext` the transpilation result would not be runnable by Node.
1. The start module is run in a sub-process for maximum isolation. (we're looking at running within workers [#752](https://github.com/graphql-nexus/nexus/issues/752))
1. In parallel, a TypeScript instance is created and the app source is statically analyzed to extract context types. This does not require running the app at all. TypeScript cache called tsbuildinfo is stored under `node_modules/.nexus`.

Expand Down
14 changes: 6 additions & 8 deletions docs/guides/project-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,19 @@ if `compilerOptions.noEmit` is set to `true` then Nexus will not output the buil

Nexus imposes a few requirements about how you structure your codebase.

### Schema Module(s)
### Nexus module(s)

##### Pattern

A `graphql` module or directory of modules `graphql.ts` `graphql/*.ts`.

This may be repeated multiple times in your source tree.
A file importing `nexus`. eg: `import { schema } from 'nexus'`

##### About

This convention is optional if entrypoint is present, required otherwise.
Nexus looks for modules that import `nexus` and uses codegen to statically import them before the server starts.

This is where you should write your GraphQL type definitions.
Beware if you have module-level side-effects coming from something else than Nexus, as these side-effects will always be run when your app starts.

In dev mode the modules are synchronously found and imported when the server starts. Conversely, at build time, codegen runs making the modules statically imported. This is done to support tree-shaking and decrease application start time.
> *Note: `require` is not supported.

### Entrypoint

Expand All @@ -58,4 +56,4 @@ A custom entrypoint can also be configured using the `--entrypoint` or `-e` CLI

##### About

This convention is optional if schema modules are present, required otherwise.
This convention is optional if Nexus modules are present, required otherwise.
4 changes: 2 additions & 2 deletions src/cli/commands/create/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ const templates: Record<TemplateName, TemplateCreator> = {
return {
files: [
{
path: path.join(internalConfig.sourceRoot, Layout.schema.CONVENTIONAL_SCHEMA_FILE_NAME),
path: path.join(internalConfig.sourceRoot, 'graphql.ts'),
content: stripIndent`
import { schema } from "nexus";

Expand Down Expand Up @@ -513,7 +513,7 @@ async function scaffoldBaseFiles(options: InternalConfig) {
// use(prisma())
`
),
fs.writeAsync(path.join(options.sourceRoot, Layout.schema.CONVENTIONAL_SCHEMA_FILE_NAME), ''),
fs.writeAsync(path.join(options.sourceRoot, 'graphql.ts'), ''),
// An exhaustive .gitignore tailored for Node can be found here:
// https://github.com/github/gitignore/blob/master/Node.gitignore
// We intentionally stay minimal here, as we want the default ignore file
Expand Down
67 changes: 42 additions & 25 deletions src/cli/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class Dev implements Command {
}

const entrypointPath = args['--entrypoint']
const reflectionMode = args['--reflection'] === true
let layout = await Layout.create({ entrypointPath })
const pluginReflectionResult = await Reflection.reflect(layout, { usedPlugins: true, onMainThread: true })

Expand Down Expand Up @@ -73,6 +74,14 @@ export class Dev implements Command {
dev: {
addToWatcherSettings: {},
async onBeforeWatcherStartOrRestart(change) {
if (change.type === 'change') {
const nexusModules = Layout.findNexusModules(
layout.tsConfig,
layout.app.exists ? layout.app.path : null
)
layout.update({ nexusModules })
}

if (
change.type === 'init' ||
change.type === 'add' ||
Expand All @@ -89,36 +98,16 @@ export class Dev implements Command {
}

runDebouncedReflection(layout)

return {
entrypointScript: getTranspiledStartModule(layout, reflectionMode),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So now before every restart the hook allows altering what entrypoint source is? Cool

Copy link
Collaborator Author

@Weakky Weakky May 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Makes me think it's dangerous to expose that on the plugin interface. Here, what this specifically enables is the ability to refresh the required schema modules in the start module whenever a file changes.

This used to not be required because we were looking for schema modules based on the filenames. So recomputing the layout whenever a file was added/deleted was enough.

But now that we find schema modules based on their import to Nexus, we need to recompute that on every file change. Here's a simple use-case where this is needed:

  1. You create a file (that file is still not considered a nexus module)
  2. You add import { schema } from 'nexus'
  3. The start module needs to be updated to contain a require to that file
  4. You comment out the import to nexus from that file
  5. The start module needs to be updated to no longer contain a require to that file

See, nexus modules are no longer tied to file creation/deletion anymore, but entirely based on their content

}
},
},
}

const startModule = createStartModuleContent({
registerTypeScript: {
...layout.tsConfig.content.options,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2015,
},
internalStage: 'dev',
runtimePluginManifests: [], // tree-shaking not needed
layout,
absoluteModuleImports: true,
})

const transpiledStartModule = transpileModule(startModule, {
...layout.tsConfig.content.options,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2015,
})

/**
* We use an empty script when in reflection mode so that the user's app doesn't run.
* The watcher will keep running though and so will reflection in the devPlugin.onBeforeWatcherStartOrRestart hook above
*/
const entrypointScript = args['--reflection'] ? '' : transpiledStartModule

const watcher = await createWatcher({
entrypointScript,
entrypointScript: getTranspiledStartModule(layout, reflectionMode),
sourceRoot: layout.sourceRoot,
cwd: process.cwd(),
plugins: [devPlugin].concat(worktimePlugins.map((p) => p.hooks)),
Expand All @@ -141,3 +130,31 @@ export class Dev implements Command {
`
}
}

function getTranspiledStartModule(layout: Layout.Layout, reflectionMode: boolean) {
/**
* We use an empty script when in reflection mode so that the user's app doesn't run.
* The watcher will keep running though and so will reflection in the devPlugin.onBeforeWatcherStartOrRestart hook above
*/
if (reflectionMode === true) {
return ''
}

const startModule = createStartModuleContent({
registerTypeScript: {
...layout.tsConfig.content.options,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2015,
},
internalStage: 'dev',
runtimePluginManifests: [], // tree-shaking not needed
layout,
absoluteModuleImports: true,
})

return transpileModule(startModule, {
...layout.tsConfig.content.options,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2015,
})
}
62 changes: 23 additions & 39 deletions src/lib/layout/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,12 @@ it('fails if no entrypoint and no graphql modules', async () => {

await ctx.scan()

expect(stripAnsi(mockedStdoutBuffer)).toMatchInlineSnapshot(`
"■ nexus:layout We could not find any graphql modules or app entrypoint
expect(mockedStdoutBuffer).toMatchInlineSnapshot(`
"■ nexus:layout We could not find any modules that imports 'nexus' or app.ts entrypoint
■ nexus:layout Please do one of the following:

1. Create a (graphql.ts file and write your GraphQL type definitions in it.
2. Create a graphql directory and write your GraphQL type definitions inside files there.
3. Create an app.ts file.
1. Create a file, import { schema } from 'nexus' and write your GraphQL type definitions in it.
2. Create an app.ts file.


--- process.exit(1) ---
Expand All @@ -214,20 +213,20 @@ it('fails if no entrypoint and no graphql modules', async () => {
expect(mockExit).toHaveBeenCalledWith(1)
})

it('finds nested graphql modules', async () => {
it('finds nested nexus modules', async () => {
ctx.setup({
...fsTsConfig,
src: {
'app.ts': '',
graphql: {
'1.ts': '',
'2.ts': '',
'1.ts': `import { schema } from 'nexus'`,
'2.ts': `import { schema } from 'nexus'`,
graphql: {
'3.ts': '',
'4.ts': '',
'3.ts': `import { schema } from 'nexus'`,
'4.ts': `import { schema } from 'nexus'`,
graphql: {
'5.ts': '',
'6.ts': '',
'5.ts': `import { schema } from 'nexus'`,
'6.ts': `import { schema } from 'nexus'`,
},
},
},
Expand All @@ -236,7 +235,7 @@ it('finds nested graphql modules', async () => {

const result = await ctx.scan()

expect(result.schemaModules).toMatchInlineSnapshot(`
expect(result.nexusModules).toMatchInlineSnapshot(`
Array [
"__DYNAMIC__/src/graphql/1.ts",
"__DYNAMIC__/src/graphql/2.ts",
Expand Down Expand Up @@ -324,39 +323,24 @@ it('fails if custom entrypoint is not a .ts file', async () => {
`)
})

it('does not take custom entrypoint as schema module if its named graphql.ts', async () => {
await ctx.setup({ ...fsTsConfig, 'graphql.ts': '', graphql: { 'user.ts': '' } })
const result = await ctx.scan({ entrypointPath: './graphql.ts' })
expect({
app: result.app,
schemaModules: result.schemaModules,
}).toMatchInlineSnapshot(`
Object {
"app": Object {
"exists": true,
"path": "__DYNAMIC__/graphql.ts",
},
"schemaModules": Array [
"__DYNAMIC__/graphql/user.ts",
],
}
`)
})

it('does not take custom entrypoint as schema module if its inside a graphql/ folder', async () => {
await ctx.setup({ ...fsTsConfig, graphql: { 'user.ts': '', 'graphql.ts': '' } })
const result = await ctx.scan({ entrypointPath: './graphql/graphql.ts' })
it('does not take custom entrypoint as nexus module if contains a nexus import', async () => {
await ctx.setup({
...fsTsConfig,
'app.ts': `import { schema } from 'nexus'`,
'graphql.ts': `import { schema } from 'nexus'`,
})
const result = await ctx.scan({ entrypointPath: './app.ts' })
expect({
app: result.app,
schemaModules: result.schemaModules,
nexusModules: result.nexusModules,
}).toMatchInlineSnapshot(`
Object {
"app": Object {
"exists": true,
"path": "__DYNAMIC__/graphql/graphql.ts",
"path": "__DYNAMIC__/app.ts",
},
"schemaModules": Array [
"__DYNAMIC__/graphql/user.ts",
"nexusModules": Array [
"__DYNAMIC__/graphql.ts",
],
}
`)
Expand Down
8 changes: 3 additions & 5 deletions src/lib/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ export {
loadDataFromParentProcess,
saveDataForChildProcess,
scanProjectType,
findNexusModules
} from './layout'

// todo refactor with TS 3.8 namespace re-export
// once https://github.com/prettier/prettier/issues/7263

import { CONVENTIONAL_SCHEMA_FILE_NAME, DIR_NAME, emptyExceptionMessage, MODULE_NAME } from './schema-modules'
import { emptyExceptionMessage } from './schema-modules'

export const schema = {
export const schemaModules = {
emptyExceptionMessage,
DIR_NAME,
MODULE_NAME,
CONVENTIONAL_SCHEMA_FILE_NAME,
}
Loading