Skip to content

Commit e2b974f

Browse files
feat: use nft bundler (#51)
* feat: handle tracing with fake resolve file * feat: use nft bundler and simplify config * fix: update tests * chore: upgrade demo next and exclude more files * fix: changes from review * Apply suggestions from code review Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com> * chore: sort * fix: use glob for files * fix: remove ls * fix: indent * fix: posix * fix: test paths * fix: normalise paths Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com>
1 parent 19197f2 commit e2b974f

File tree

12 files changed

+365
-308
lines changed

12 files changed

+365
-308
lines changed

demo/local-plugin/package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 166 additions & 167 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@
5959
"chalk": "^4.1.2",
6060
"fs-extra": "^10.0.0",
6161
"moize": "^6.1.0",
62-
"next": "^11.1.0",
62+
"next": "^11.1.3-canary.65",
6363
"outdent": "^0.8.0",
64-
"semver": "^7.3.5"
64+
"semver": "^7.3.5",
65+
"slash": "^3.0.0",
66+
"tiny-glob": "^0.2.9"
6567
},
6668
"devDependencies": {
6769
"@netlify/eslint-config-node": "^3.3.0",

src/helpers/cache.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
const { join } = require('path')
1+
const {
2+
posix: { join },
3+
} = require('path')
24

35
exports.restoreCache = async ({ cache, publish }) => {
46
const cacheDir = join(publish, 'cache')

src/helpers/config.js

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -110,43 +110,26 @@ exports.getNextConfig = async function getNextConfig({ publish, failBuild = defa
110110
}
111111
}
112112

113-
exports.setIncludedFiles = ({ netlifyConfig, publish }) => {
114-
// Serverless functions need parts of build dist in runtime env
113+
exports.configureHandlerFunctions = ({ netlifyConfig, publish }) => {
115114
;[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {
116-
if (!netlifyConfig.functions[functionName]) {
117-
netlifyConfig.functions[functionName] = {}
118-
}
119-
if (!netlifyConfig.functions[functionName].included_files) {
120-
netlifyConfig.functions[functionName].included_files = []
121-
}
115+
netlifyConfig.functions[functionName] ||= { included_files: [], external_node_modules: [] }
116+
netlifyConfig.functions[functionName].node_bundler = 'nft'
117+
netlifyConfig.functions[functionName].included_files ||= []
122118
netlifyConfig.functions[functionName].included_files.push(
123119
`${publish}/server/**`,
124120
`${publish}/serverless/**`,
125121
`${publish}/*.json`,
126122
`${publish}/BUILD_ID`,
123+
'!node_modules/@next/swc-*/**/*',
124+
'!node_modules/next/dist/compiled/@ampproject/toolbox-optimizer/**/*',
125+
'!node_modules/next/dist/pages/**/*',
126+
`!node_modules/next/dist/server/lib/squoosh/**/*.wasm`,
127+
`!node_modules/next/dist/next-server/server/lib/squoosh/**/*.wasm`,
128+
'!node_modules/next/dist/compiled/webpack/(bundle4|bundle5).js',
129+
'!node_modules/react/**/*.development.js',
130+
'!node_modules/react-dom/**/*.development.js',
131+
'!node_modules/use-subscription/**/*.development.js',
132+
'!node_modules/sharp/**/*',
127133
)
128134
})
129135
}
130-
131-
exports.setBundler = ({ netlifyConfig, target }) => {
132-
if (target === 'serverless') {
133-
// Any bundler works with serverless
134-
return
135-
}
136-
137-
// Not quite such a simple a check, as we need to check both the default bundler
138-
// and whether any of the individual functions are using the esbuild bundler.
139-
const handlers = [HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME]
140-
const defaultIsEsbuild = Boolean(netlifyConfig.functions['*']?.node_bundler === 'esbuild')
141-
const handlerBundlers = handlers.map((func) => netlifyConfig.functions[func]?.node_bundler)
142-
if (
143-
(defaultIsEsbuild && !handlerBundlers.every((bundler) => bundler === 'zisi')) ||
144-
handlerBundlers.includes('esbuild')
145-
) {
146-
console.warn(`esbuild is not supported for target=${target}. Setting to "zisi"`)
147-
}
148-
handlers.forEach((func) => {
149-
netlifyConfig.functions[func] ||= {}
150-
netlifyConfig.functions[func].node_bundler = 'zisi'
151-
})
152-
}

src/helpers/functions.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { copyFile, ensureDir, writeFile, writeJSON } = require('fs-extra')
44

55
const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } = require('../constants')
66
const getHandler = require('../templates/getHandler')
7+
const { getPageResolver } = require('../templates/getPageResolver')
78

89
const DEFAULT_FUNCTIONS_SRC = 'netlify/functions'
910

@@ -28,6 +29,26 @@ exports.generateFunctions = async (
2829
await writeHandler(ODB_FUNCTION_NAME, true)
2930
}
3031

32+
/**
33+
* Writes a file in each function directory that contains references to every page entrypoint.
34+
* This is just so that the nft bundler knows about them. We'll eventually do this better.
35+
*/
36+
exports.generatePagesResolver = async ({
37+
constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC },
38+
netlifyConfig,
39+
target,
40+
}) => {
41+
const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC
42+
43+
const jsSource = await getPageResolver({
44+
netlifyConfig,
45+
target,
46+
})
47+
48+
await writeFile(join(functionsPath, ODB_FUNCTION_NAME, 'pages.js'), jsSource)
49+
await writeFile(join(functionsPath, HANDLER_FUNCTION_NAME, 'pages.js'), jsSource)
50+
}
51+
3152
// Move our next/image function into the correct functions directory
3253
exports.setupImageFunction = async ({
3354
constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC },

src/helpers/verification.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ exports.checkNextSiteHasBuilt = ({ publish, failBuild }) => {
4646
}
4747

4848
exports.checkForRootPublish = ({ publish, failBuild }) => {
49-
if (path.normalize(publish) === path.normalize(process.cwd())) {
49+
if (path.resolve(publish) === path.resolve('.')) {
5050
failBuild(outdent`
5151
Your publish directory is pointing to the base directory of your site. This is not supported for Next.js sites, and is probably a mistake.
5252
In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config, or the Next site is in a subdirectory.`)

src/index.js

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
// @ts-check
22

3-
const { join } = require('path')
3+
const { join, relative } = require('path')
44

55
const { copy, existsSync } = require('fs-extra')
66

7-
const { ODB_FUNCTION_NAME, HANDLER_FUNCTION_NAME } = require('./constants')
87
const { restoreCache, saveCache } = require('./helpers/cache')
9-
const { getNextConfig, setIncludedFiles, generateRedirects, setBundler } = require('./helpers/config')
10-
const { generateFunctions, setupImageFunction } = require('./helpers/functions')
8+
const { getNextConfig, configureHandlerFunctions, generateRedirects } = require('./helpers/config')
9+
const { generateFunctions, setupImageFunction, generatePagesResolver } = require('./helpers/functions')
1110
const {
1211
verifyNetlifyBuildVersion,
1312
checkNextSiteHasBuilt,
@@ -44,13 +43,13 @@ module.exports = {
4443

4544
const { appDir, basePath, i18n, images, target } = await getNextConfig({ publish, failBuild })
4645

47-
setBundler({ netlifyConfig, target })
48-
4946
verifyBuildTarget(target)
5047

51-
setIncludedFiles({ netlifyConfig, publish })
48+
configureHandlerFunctions({ netlifyConfig, publish: relative(process.cwd(), publish) })
5249

5350
await generateFunctions(constants, appDir)
51+
await generatePagesResolver({ netlifyConfig, target, constants })
52+
5453
const publicDir = join(appDir, 'public')
5554
if (existsSync(publicDir)) {
5655
await copy(publicDir, `${publish}/`)
@@ -64,14 +63,7 @@ module.exports = {
6463
})
6564
},
6665

67-
async onPostBuild({ netlifyConfig, constants: { FUNCTIONS_DIST = '.netlify/functions' }, utils: { run, cache } }) {
68-
// Remove swc binaries from the zipfile if present. Yes, it's a hack, but it drops >10MB from the zipfile when bundling with zip-it-and-ship-it
69-
for (const func of [ODB_FUNCTION_NAME, HANDLER_FUNCTION_NAME]) {
70-
await run(`zip`, [`-d`, join(FUNCTIONS_DIST, `${func}.zip`), '*node_modules/@next/swc-*']).catch(() => {
71-
// This throws if there's none of these in the zipfile, so ignore it
72-
})
73-
}
74-
66+
async onPostBuild({ netlifyConfig, utils: { cache } }) {
7567
return saveCache({ cache, publish: netlifyConfig.build.publish })
7668
},
7769
}

src/templates/getHandler.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ const makeHandler =
1515
// Probably an old version of next
1616
}
1717

18+
// This is just so nft knows about the page entrypoints
19+
try {
20+
// eslint-disable-next-line node/no-missing-require
21+
require.resolve('./pages.js')
22+
} catch {}
23+
1824
if (!NextServer) {
1925
try {
2026
// next < 11.0.1

src/templates/getPageResolver.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const {
2+
posix: { resolve, join, relative },
3+
} = require('path')
4+
5+
const { outdent } = require('outdent')
6+
const slash = require('slash')
7+
const glob = require('tiny-glob')
8+
9+
const { HANDLER_FUNCTION_NAME } = require('../constants')
10+
11+
// Generate a file full of require.resolve() calls for all the pages in the
12+
// build. This is used by the nft bundler to find all the pages.
13+
14+
exports.getPageResolver = async ({ netlifyConfig, target }) => {
15+
const functionDir = resolve(join('.netlify', 'functions', HANDLER_FUNCTION_NAME))
16+
const root = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless', 'pages')
17+
18+
const pages = await glob('**/*.js', {
19+
cwd: root,
20+
dot: true,
21+
})
22+
const pageFiles = pages.map((page) => `require.resolve('${relative(functionDir, join(root, slash(page)))}')`).sort()
23+
24+
return outdent`
25+
// This file is purely to allow nft to know about these pages. It should be temporary.
26+
exports.resolvePages = () => {
27+
try {
28+
${pageFiles.join('\n ')}
29+
} catch {}
30+
}
31+
`
32+
}

test/__snapshots__/index.js.snap

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,71 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`onBuild() generates a file referencing all page sources 1`] = `
4+
"// This file is purely to allow nft to know about these pages. It should be temporary.
5+
exports.resolvePages = () => {
6+
try {
7+
require.resolve('../../../.next/server/pages/_app.js')
8+
require.resolve('../../../.next/server/pages/_document.js')
9+
require.resolve('../../../.next/server/pages/_error.js')
10+
require.resolve('../../../.next/server/pages/api/enterPreview.js')
11+
require.resolve('../../../.next/server/pages/api/exitPreview.js')
12+
require.resolve('../../../.next/server/pages/api/hello-background.js')
13+
require.resolve('../../../.next/server/pages/api/hello.js')
14+
require.resolve('../../../.next/server/pages/api/shows/[...params].js')
15+
require.resolve('../../../.next/server/pages/api/shows/[id].js')
16+
require.resolve('../../../.next/server/pages/deep/import.js')
17+
require.resolve('../../../.next/server/pages/getServerSideProps/[id].js')
18+
require.resolve('../../../.next/server/pages/getServerSideProps/all/[[...slug]].js')
19+
require.resolve('../../../.next/server/pages/getServerSideProps/static.js')
20+
require.resolve('../../../.next/server/pages/getStaticProps/[id].js')
21+
require.resolve('../../../.next/server/pages/getStaticProps/static.js')
22+
require.resolve('../../../.next/server/pages/getStaticProps/with-revalidate.js')
23+
require.resolve('../../../.next/server/pages/getStaticProps/withFallback/[...slug].js')
24+
require.resolve('../../../.next/server/pages/getStaticProps/withFallback/[id].js')
25+
require.resolve('../../../.next/server/pages/getStaticProps/withFallbackBlocking/[id].js')
26+
require.resolve('../../../.next/server/pages/getStaticProps/withRevalidate/[id].js')
27+
require.resolve('../../../.next/server/pages/getStaticProps/withRevalidate/withFallback/[id].js')
28+
require.resolve('../../../.next/server/pages/index.js')
29+
require.resolve('../../../.next/server/pages/previewTest.js')
30+
require.resolve('../../../.next/server/pages/shows/[...params].js')
31+
require.resolve('../../../.next/server/pages/shows/[id].js')
32+
} catch {}
33+
}"
34+
`;
35+
36+
exports[`onBuild() generates a file referencing all page sources 2`] = `
37+
"// This file is purely to allow nft to know about these pages. It should be temporary.
38+
exports.resolvePages = () => {
39+
try {
40+
require.resolve('../../../.next/server/pages/_app.js')
41+
require.resolve('../../../.next/server/pages/_document.js')
42+
require.resolve('../../../.next/server/pages/_error.js')
43+
require.resolve('../../../.next/server/pages/api/enterPreview.js')
44+
require.resolve('../../../.next/server/pages/api/exitPreview.js')
45+
require.resolve('../../../.next/server/pages/api/hello-background.js')
46+
require.resolve('../../../.next/server/pages/api/hello.js')
47+
require.resolve('../../../.next/server/pages/api/shows/[...params].js')
48+
require.resolve('../../../.next/server/pages/api/shows/[id].js')
49+
require.resolve('../../../.next/server/pages/deep/import.js')
50+
require.resolve('../../../.next/server/pages/getServerSideProps/[id].js')
51+
require.resolve('../../../.next/server/pages/getServerSideProps/all/[[...slug]].js')
52+
require.resolve('../../../.next/server/pages/getServerSideProps/static.js')
53+
require.resolve('../../../.next/server/pages/getStaticProps/[id].js')
54+
require.resolve('../../../.next/server/pages/getStaticProps/static.js')
55+
require.resolve('../../../.next/server/pages/getStaticProps/with-revalidate.js')
56+
require.resolve('../../../.next/server/pages/getStaticProps/withFallback/[...slug].js')
57+
require.resolve('../../../.next/server/pages/getStaticProps/withFallback/[id].js')
58+
require.resolve('../../../.next/server/pages/getStaticProps/withFallbackBlocking/[id].js')
59+
require.resolve('../../../.next/server/pages/getStaticProps/withRevalidate/[id].js')
60+
require.resolve('../../../.next/server/pages/getStaticProps/withRevalidate/withFallback/[id].js')
61+
require.resolve('../../../.next/server/pages/index.js')
62+
require.resolve('../../../.next/server/pages/previewTest.js')
63+
require.resolve('../../../.next/server/pages/shows/[...params].js')
64+
require.resolve('../../../.next/server/pages/shows/[id].js')
65+
} catch {}
66+
}"
67+
`;
68+
369
exports[`onBuild() writes correct redirects to netlifyConfig 1`] = `
470
Array [
571
Object {

0 commit comments

Comments
 (0)