Skip to content

Commit 0698db4

Browse files
authored
feat: require all files that import nexus (#833)
1 parent 6607c9a commit 0698db4

File tree

13 files changed

+137
-128
lines changed

13 files changed

+137
-128
lines changed

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ what follows is a stub
9494
## Build Flow
9595

9696
1. The app layout is calculated
97-
We discover things like where the entrypoint is, if any, and where graphql modules are, if any.
97+
We discover things like where the entrypoint is, if any, and where [Nexus modules](/guides/project-layout?id=nexus-modules) are, if any.
9898
1. Worktime plugins are loaded (see [Plugin Loading Flow](#plugin-loading-flow))
9999
1. Typegen is acquired
100100
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.
@@ -103,7 +103,7 @@ what follows is a stub
103103

104104
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.
105105

106-
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.
106+
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.
107107
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))
108108
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`.
109109

docs/guides/project-layout.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,19 @@ if `compilerOptions.noEmit` is set to `true` then Nexus will not output the buil
3232

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

35-
### Schema Module(s)
35+
### Nexus module(s)
3636

3737
##### Pattern
3838

39-
A `graphql` module or directory of modules `graphql.ts` `graphql/*.ts`.
40-
41-
This may be repeated multiple times in your source tree.
39+
A file importing `nexus`. eg: `import { schema } from 'nexus'`
4240

4341
##### About
4442

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

47-
This is where you should write your GraphQL type definitions.
45+
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.
4846

49-
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.
47+
> *Note: `require` is not supported.
5048
5149
### Entrypoint
5250

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

5957
##### About
6058

61-
This convention is optional if schema modules are present, required otherwise.
59+
This convention is optional if Nexus modules are present, required otherwise.

src/cli/commands/create/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ const templates: Record<TemplateName, TemplateCreator> = {
385385
return {
386386
files: [
387387
{
388-
path: path.join(internalConfig.sourceRoot, Layout.schema.CONVENTIONAL_SCHEMA_FILE_NAME),
388+
path: path.join(internalConfig.sourceRoot, 'graphql.ts'),
389389
content: stripIndent`
390390
import { schema } from "nexus";
391391
@@ -513,7 +513,7 @@ async function scaffoldBaseFiles(options: InternalConfig) {
513513
// use(prisma())
514514
`
515515
),
516-
fs.writeAsync(path.join(options.sourceRoot, Layout.schema.CONVENTIONAL_SCHEMA_FILE_NAME), ''),
516+
fs.writeAsync(path.join(options.sourceRoot, 'graphql.ts'), ''),
517517
// An exhaustive .gitignore tailored for Node can be found here:
518518
// https://github.com/github/gitignore/blob/master/Node.gitignore
519519
// We intentionally stay minimal here, as we want the default ignore file

src/cli/commands/dev.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class Dev implements Command {
3737
}
3838

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

@@ -73,6 +74,14 @@ export class Dev implements Command {
7374
dev: {
7475
addToWatcherSettings: {},
7576
async onBeforeWatcherStartOrRestart(change) {
77+
if (change.type === 'change') {
78+
const nexusModules = Layout.findNexusModules(
79+
layout.tsConfig,
80+
layout.app.exists ? layout.app.path : null
81+
)
82+
layout.update({ nexusModules })
83+
}
84+
7685
if (
7786
change.type === 'init' ||
7887
change.type === 'add' ||
@@ -89,36 +98,16 @@ export class Dev implements Command {
8998
}
9099

91100
runDebouncedReflection(layout)
101+
102+
return {
103+
entrypointScript: getTranspiledStartModule(layout, reflectionMode),
104+
}
92105
},
93106
},
94107
}
95108

96-
const startModule = createStartModuleContent({
97-
registerTypeScript: {
98-
...layout.tsConfig.content.options,
99-
module: ts.ModuleKind.CommonJS,
100-
target: ts.ScriptTarget.ES2015,
101-
},
102-
internalStage: 'dev',
103-
runtimePluginManifests: [], // tree-shaking not needed
104-
layout,
105-
absoluteModuleImports: true,
106-
})
107-
108-
const transpiledStartModule = transpileModule(startModule, {
109-
...layout.tsConfig.content.options,
110-
module: ts.ModuleKind.CommonJS,
111-
target: ts.ScriptTarget.ES2015,
112-
})
113-
114-
/**
115-
* We use an empty script when in reflection mode so that the user's app doesn't run.
116-
* The watcher will keep running though and so will reflection in the devPlugin.onBeforeWatcherStartOrRestart hook above
117-
*/
118-
const entrypointScript = args['--reflection'] ? '' : transpiledStartModule
119-
120109
const watcher = await createWatcher({
121-
entrypointScript,
110+
entrypointScript: getTranspiledStartModule(layout, reflectionMode),
122111
sourceRoot: layout.sourceRoot,
123112
cwd: process.cwd(),
124113
plugins: [devPlugin].concat(worktimePlugins.map((p) => p.hooks)),
@@ -141,3 +130,31 @@ export class Dev implements Command {
141130
`
142131
}
143132
}
133+
134+
function getTranspiledStartModule(layout: Layout.Layout, reflectionMode: boolean) {
135+
/**
136+
* We use an empty script when in reflection mode so that the user's app doesn't run.
137+
* The watcher will keep running though and so will reflection in the devPlugin.onBeforeWatcherStartOrRestart hook above
138+
*/
139+
if (reflectionMode === true) {
140+
return ''
141+
}
142+
143+
const startModule = createStartModuleContent({
144+
registerTypeScript: {
145+
...layout.tsConfig.content.options,
146+
module: ts.ModuleKind.CommonJS,
147+
target: ts.ScriptTarget.ES2015,
148+
},
149+
internalStage: 'dev',
150+
runtimePluginManifests: [], // tree-shaking not needed
151+
layout,
152+
absoluteModuleImports: true,
153+
})
154+
155+
return transpileModule(startModule, {
156+
...layout.tsConfig.content.options,
157+
module: ts.ModuleKind.CommonJS,
158+
target: ts.ScriptTarget.ES2015,
159+
})
160+
}

src/lib/layout/index.spec.ts

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,12 @@ it('fails if no entrypoint and no graphql modules', async () => {
198198

199199
await ctx.scan()
200200

201-
expect(stripAnsi(mockedStdoutBuffer)).toMatchInlineSnapshot(`
202-
"■ nexus:layout We could not find any graphql modules or app entrypoint
201+
expect(mockedStdoutBuffer).toMatchInlineSnapshot(`
202+
"■ nexus:layout We could not find any modules that imports 'nexus' or app.ts entrypoint
203203
■ nexus:layout Please do one of the following:
204204
205-
1. Create a (graphql.ts file and write your GraphQL type definitions in it.
206-
2. Create a graphql directory and write your GraphQL type definitions inside files there.
207-
3. Create an app.ts file.
205+
1. Create a file, import { schema } from 'nexus' and write your GraphQL type definitions in it.
206+
2. Create an app.ts file.
208207
209208
210209
--- process.exit(1) ---
@@ -214,20 +213,20 @@ it('fails if no entrypoint and no graphql modules', async () => {
214213
expect(mockExit).toHaveBeenCalledWith(1)
215214
})
216215

217-
it('finds nested graphql modules', async () => {
216+
it('finds nested nexus modules', async () => {
218217
ctx.setup({
219218
...fsTsConfig,
220219
src: {
221220
'app.ts': '',
222221
graphql: {
223-
'1.ts': '',
224-
'2.ts': '',
222+
'1.ts': `import { schema } from 'nexus'`,
223+
'2.ts': `import { schema } from 'nexus'`,
225224
graphql: {
226-
'3.ts': '',
227-
'4.ts': '',
225+
'3.ts': `import { schema } from 'nexus'`,
226+
'4.ts': `import { schema } from 'nexus'`,
228227
graphql: {
229-
'5.ts': '',
230-
'6.ts': '',
228+
'5.ts': `import { schema } from 'nexus'`,
229+
'6.ts': `import { schema } from 'nexus'`,
231230
},
232231
},
233232
},
@@ -236,7 +235,7 @@ it('finds nested graphql modules', async () => {
236235

237236
const result = await ctx.scan()
238237

239-
expect(result.schemaModules).toMatchInlineSnapshot(`
238+
expect(result.nexusModules).toMatchInlineSnapshot(`
240239
Array [
241240
"__DYNAMIC__/src/graphql/1.ts",
242241
"__DYNAMIC__/src/graphql/2.ts",
@@ -324,39 +323,24 @@ it('fails if custom entrypoint is not a .ts file', async () => {
324323
`)
325324
})
326325

327-
it('does not take custom entrypoint as schema module if its named graphql.ts', async () => {
328-
await ctx.setup({ ...fsTsConfig, 'graphql.ts': '', graphql: { 'user.ts': '' } })
329-
const result = await ctx.scan({ entrypointPath: './graphql.ts' })
330-
expect({
331-
app: result.app,
332-
schemaModules: result.schemaModules,
333-
}).toMatchInlineSnapshot(`
334-
Object {
335-
"app": Object {
336-
"exists": true,
337-
"path": "__DYNAMIC__/graphql.ts",
338-
},
339-
"schemaModules": Array [
340-
"__DYNAMIC__/graphql/user.ts",
341-
],
342-
}
343-
`)
344-
})
345-
346-
it('does not take custom entrypoint as schema module if its inside a graphql/ folder', async () => {
347-
await ctx.setup({ ...fsTsConfig, graphql: { 'user.ts': '', 'graphql.ts': '' } })
348-
const result = await ctx.scan({ entrypointPath: './graphql/graphql.ts' })
326+
it('does not take custom entrypoint as nexus module if contains a nexus import', async () => {
327+
await ctx.setup({
328+
...fsTsConfig,
329+
'app.ts': `import { schema } from 'nexus'`,
330+
'graphql.ts': `import { schema } from 'nexus'`,
331+
})
332+
const result = await ctx.scan({ entrypointPath: './app.ts' })
349333
expect({
350334
app: result.app,
351-
schemaModules: result.schemaModules,
335+
nexusModules: result.nexusModules,
352336
}).toMatchInlineSnapshot(`
353337
Object {
354338
"app": Object {
355339
"exists": true,
356-
"path": "__DYNAMIC__/graphql/graphql.ts",
340+
"path": "__DYNAMIC__/app.ts",
357341
},
358-
"schemaModules": Array [
359-
"__DYNAMIC__/graphql/user.ts",
342+
"nexusModules": Array [
343+
"__DYNAMIC__/graphql.ts",
360344
],
361345
}
362346
`)

src/lib/layout/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,14 @@ export {
99
loadDataFromParentProcess,
1010
saveDataForChildProcess,
1111
scanProjectType,
12+
findNexusModules
1213
} from './layout'
1314

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

17-
import { CONVENTIONAL_SCHEMA_FILE_NAME, DIR_NAME, emptyExceptionMessage, MODULE_NAME } from './schema-modules'
18+
import { emptyExceptionMessage } from './schema-modules'
1819

19-
export const schema = {
20+
export const schemaModules = {
2021
emptyExceptionMessage,
21-
DIR_NAME,
22-
MODULE_NAME,
23-
CONVENTIONAL_SCHEMA_FILE_NAME,
2422
}

0 commit comments

Comments
 (0)