Skip to content

Commit 6607c9a

Browse files
authored
feat(build): treeshake build output (#866)
1 parent d10d1db commit 6607c9a

25 files changed

+759
-108
lines changed

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ what follows is a stub
110110
1. A new TypeScript instance is created so that the types generated in the previous step are picked up by the checker. This should be faster because it reuses the TypeScript cache created in the previous step.
111111
1. The app is type checked
112112
1. The app is transpiled
113-
1. The app is emitted into `node_modules/.build`. This convention keeps derived files in a well known generally ignored location.
113+
1. The app is emitted into `.nexus/build`. This convention keeps derived files in a well known generally ignored location.
114114
1. A production-oriented start module is generated differing in the following ways:
115115
- paths are relative
116116
- typescript not hooked into module extensions

docs/getting-started/migrate-from-nexus-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ You should only be working with the `nexus` CLI. Below shows the example scripts
108108
- "dev": "ts-node-dev --no-notify --respawn --transpileOnly src/server",
109109
+ "dev": "nexus dev",
110110
+ "build": "nexus build",
111-
+ "start": "node node_modules/.build"
111+
+ "start": "node .nexus/build"
112112
},
113113
```
114114

docs/guides/project-layout.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
- Out Root is the place where the transpiled TypeScript (to JavaScript) modules will be emitted to. The folder structure mimicks that of the source root.
2323
- Out Root is defined by setting `compilerOptions.outDir`.
24-
- If you do not specify it then Nexus will default to `node_modules/.build`. Unlike with `rootDir` Nexus will not scaffold the default into your `tsconfig.json` because its presence has no impact upon VSCode.
24+
- If you do not specify it then Nexus will default to `.nexus/build`. Unlike with `rootDir` Nexus will not scaffold the default into your `tsconfig.json` because its presence has no impact upon VSCode.
2525
- You can override its value interactively with `nexus build --out`.
2626

2727
##### Check-Only Builds

docs/references/recipes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ If you don't want to use a docker, here are some links to alternative approaches
137137

138138
```diff
139139
+++ package.json
140-
+ "start": "node node_modules/.build"
140+
+ "start": "node .nexus/build"
141141
```
142142

143143
3. In many cases this will be enough. Many deployment platforms will call into these scripts by default. You can customize where `build` outputs to if your deployment platform requires it. There are built in guides for `zeit` and `heroku` which will check your project is prepared for deployment to those respective platforms. Take advantage of them if applicable:

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@types/express": "^4.17.2",
3333
"@types/node-fetch": "^2.5.5",
3434
"@types/prompts": "^2.0.3",
35+
"@zeit/node-file-trace": "^0.5.1",
3536
"anymatch": "^3.1.1",
3637
"arg": "^4.1.3",
3738
"chalk": "^4.0.0",
@@ -41,7 +42,7 @@
4142
"dotenv": "^8.2.0",
4243
"express": "^4.17.1",
4344
"fp-ts": "^2.5.4",
44-
"fs-jetpack": "^2.2.3",
45+
"fs-jetpack": "^2.4.0",
4546
"get-port": "^5.1.0",
4647
"graphql": "^14.5.8",
4748
"http-errors": "^1.7.3",

src/cli/commands/build.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const BUILD_ARGS = {
1414
'--stage': String,
1515
'--entrypoint': String,
1616
'-e': '--entrypoint',
17+
'--no-bundle': Boolean,
1718
'--help': Boolean,
1819
'-h': '--help',
1920
}
@@ -36,6 +37,7 @@ export class Build implements Command {
3637
output: args['--output'],
3738
stage: args['--stage'],
3839
entrypoint: args['--entrypoint'],
40+
asBundle: args['--no-bundle'] !== true,
3941
})
4042
}
4143

@@ -49,6 +51,7 @@ export class Build implements Command {
4951
-o, --output Relative path to output directory
5052
-e, --entrypoint Custom entrypoint to your app (default: app.ts)
5153
-d, --deployment Enable custom build for some deployment platforms (${formattedSupportedDeployTargets})
54+
--no-bundle Do not output build as a bundle
5255
-h, --help Show this help message
5356
`
5457
}

src/cli/commands/create/app.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { tsconfigTemplate } from '../../../lib/layout/tsconfig'
99
import { rootLogger } from '../../../lib/nexus-logger'
1010
import { ownPackage } from '../../../lib/own-package'
1111
import * as PackageManager from '../../../lib/package-manager'
12-
import * as Plugin from '../../../lib/plugin'
12+
import * as PluginRuntime from '../../../lib/plugin'
13+
import * as PluginWorktime from '../../../lib/plugin/worktime'
1314
import * as proc from '../../../lib/process'
1415
import { createGitRepository, CWDProjectNameOrGenerate } from '../../../lib/utils'
1516

@@ -52,8 +53,8 @@ export async function runLocalHandOff(): Promise<void> {
5253

5354
const parentData = await loadDataFromParentProcess()
5455
const layout = await Layout.create()
55-
const pluginM = await Plugin.getUsedPlugins(layout)
56-
const plugins = Plugin.importAndLoadWorktimePlugins(pluginM, layout)
56+
const pluginM = await PluginWorktime.getUsedPlugins(layout)
57+
const plugins = PluginRuntime.importAndLoadWorktimePlugins(pluginM, layout)
5758
log.trace('plugins', { plugins })
5859

5960
// TODO select a template
@@ -460,6 +461,7 @@ const templates: Record<TemplateName, TemplateCreator> = {
460461
*/
461462
async function scaffoldBaseFiles(options: InternalConfig) {
462463
const appEntrypointPath = path.join(options.sourceRoot, 'app.ts')
464+
const sourceRootRelative = path.relative(options.projectRoot, options.sourceRoot)
463465

464466
await Promise.all([
465467
// Empty app and graphql module.
@@ -535,7 +537,7 @@ async function scaffoldBaseFiles(options: InternalConfig) {
535537
format: "npx prettier --write './**/*.{ts,md}'",
536538
dev: 'nexus dev',
537539
build: 'nexus build',
538-
start: 'node node_modules/.build',
540+
start: `node .nexus/build/${sourceRootRelative}`,
539541
},
540542
prettier: {
541543
semi: false,
@@ -550,7 +552,7 @@ async function scaffoldBaseFiles(options: InternalConfig) {
550552
fs.writeAsync(
551553
'tsconfig.json',
552554
tsconfigTemplate({
553-
sourceRootRelative: path.relative(options.projectRoot, options.sourceRoot),
555+
sourceRootRelative,
554556
outRootRelative: Layout.DEFAULT_BUILD_FOLDER_PATH_RELATIVE_TO_PROJECT_ROOT,
555557
})
556558
),
@@ -583,8 +585,8 @@ async function scaffoldBaseFiles(options: InternalConfig) {
583585
const ENV_PARENT_DATA = 'NEXUS_CREATE_DATA'
584586

585587
type ParentData = {
586-
database?: Plugin.OnAfterBaseSetupLens['database']
587-
connectionURI?: Plugin.OnAfterBaseSetupLens['connectionURI']
588+
database?: PluginRuntime.OnAfterBaseSetupLens['database']
589+
connectionURI?: PluginRuntime.OnAfterBaseSetupLens['connectionURI']
588590
}
589591

590592
async function loadDataFromParentProcess(): Promise<ParentData> {

src/lib/build/build.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { stripIndent } from 'common-tags'
22
import * as FS from 'fs-jetpack'
33
import * as Path from 'path'
44
import * as Layout from '../../lib/layout'
5-
import { compile, createTSProgram, deleteTSIncrementalFile } from '../../lib/tsc'
5+
import { emitTSProgram, createTSProgram, deleteTSIncrementalFile } from '../../lib/tsc'
66
import {
77
createStartModuleContent,
88
prepareStartModule,
@@ -18,6 +18,7 @@ import {
1818
normalizeTarget,
1919
validateTarget,
2020
} from './deploy-target'
21+
import { bundle } from './bundle'
2122

2223
const log = rootLogger.child('build')
2324

@@ -26,6 +27,8 @@ interface BuildSettings {
2627
output?: string
2728
stage?: string
2829
entrypoint?: string
30+
asBundle: boolean
31+
cwd?: string
2932
}
3033

3134
export async function buildNexusApp(settings: BuildSettings) {
@@ -36,8 +39,10 @@ export async function buildNexusApp(settings: BuildSettings) {
3639
const buildOutput = settings.output ?? computeBuildOutputFromTarget(deploymentTarget) ?? undefined
3740

3841
const layout = await Layout.create({
39-
buildOutput,
42+
buildOutputDir: buildOutput,
43+
asBundle: settings.asBundle,
4044
entrypointPath: settings.entrypoint,
45+
cwd: settings.cwd,
4146
})
4247

4348
/**
@@ -85,7 +90,7 @@ export async function buildNexusApp(settings: BuildSettings) {
8590
// incremental builder type of program so that the cache from the previous
8691
// run of TypeScript should make re-building up this one cheap.
8792

88-
compile(tsBuilder, layout, { removePreviousBuild: false })
93+
emitTSProgram(tsBuilder, layout, { removePreviousBuild: false })
8994

9095
const gotManifests = Plugin.getPluginManifests(plugins)
9196

@@ -105,12 +110,25 @@ export async function buildNexusApp(settings: BuildSettings) {
105110
})
106111
),
107112
})
113+
114+
if (layout.build.bundleOutputDir) {
115+
log.info('bundling app')
116+
await bundle({
117+
base: layout.projectRoot,
118+
bundleOutputDir: layout.build.bundleOutputDir,
119+
entrypoint: layout.build.startModuleOutPath,
120+
tsOutputDir: layout.build.tsOutputDir,
121+
tsRootDir: layout.tsConfig.content.options.rootDir!,
122+
plugins: pluginReflection.plugins,
123+
})
124+
await FS.removeAsync(layout.build.tsOutputDir)
125+
}
108126
}
109127

110128
const buildOutputLog =
111129
layout.tsConfig.content.options.noEmit === true
112130
? 'no emit'
113-
: Path.relative(layout.projectRoot, layout.buildOutput)
131+
: Path.relative(layout.projectRoot, layout.build.bundleOutputDir ?? layout.build.tsOutputDir)
114132

115133
log.info('success', {
116134
buildOutput: buildOutputLog,
@@ -138,9 +156,9 @@ export async function writeStartModule({
138156
// module. For example we can alias it, or, we can rename it e.g.
139157
// `index_original.js`. For now we just error out and ask the user to not name
140158
// their module index.ts.
141-
if (FS.exists(layout.startModuleInPath)) {
159+
if (FS.exists(layout.build.startModuleInPath)) {
142160
fatal(stripIndent`
143-
Found ${layout.startModuleInPath}
161+
Found ${layout.build.startModuleInPath}
144162
Nexus reserves the source root module name ${START_MODULE_NAME}.js for its own use.
145163
Please change your app layout to not have this module.
146164
This is a temporary limitation that we intend to remove in the future.
@@ -149,5 +167,5 @@ export async function writeStartModule({
149167
}
150168

151169
log.trace('Writing start module to disk')
152-
await FS.writeAsync(layout.startModuleOutPath, startModule)
170+
await FS.writeAsync(layout.build.startModuleOutPath, startModule)
153171
}

src/lib/build/bundle.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { NodeFileTraceReasons } from '@zeit/node-file-trace'
2+
import * as Path from 'path'
3+
import { traceFiles } from './bundle'
4+
5+
const base = Path.dirname(require.resolve('../../../package.json'))
6+
const entrypoint = Path.join(base, 'dist', 'index.js')
7+
8+
it('should not bundle typescript', async () => {
9+
const { reasons } = await traceFiles({
10+
base,
11+
entrypoint,
12+
plugins: [],
13+
})
14+
const isTypescriptBundled = isModuleBundled('node_modules/typescript/lib/typescript.js', reasons)
15+
16+
expect(isTypescriptBundled).toMatchInlineSnapshot(`false`)
17+
})
18+
19+
it('should not bundle any of the cli', async () => {
20+
const { files } = await traceFiles({
21+
base,
22+
entrypoint,
23+
plugins: [],
24+
})
25+
26+
const cliFiles = Array.from(files.keys()).filter((f) => f.includes('dist/cli'))
27+
28+
expect(cliFiles).toMatchInlineSnapshot(`Array []`)
29+
})
30+
31+
function walkParents(parents: string[], reasons: NodeFileTraceReasons, path: Array<string | string[]>): void {
32+
if (parents.length === 0) {
33+
return
34+
}
35+
36+
if (parents.length === 1) {
37+
path.push(parents[0])
38+
} else {
39+
path.push(parents)
40+
}
41+
42+
parents.forEach((p) => {
43+
const module = reasons[p]
44+
walkParents(module.parents, reasons, path)
45+
})
46+
}
47+
48+
export function isModuleBundled(
49+
moduleId: string,
50+
reasons: NodeFileTraceReasons
51+
): false | { reason: Array<string | string[]> } {
52+
const module = reasons[moduleId]
53+
54+
if (!module) {
55+
return false
56+
}
57+
58+
let path: (string | string[])[] = [moduleId]
59+
60+
walkParents(module.parents, reasons, path)
61+
62+
return { reason: path.reverse() }
63+
}

0 commit comments

Comments
 (0)