Skip to content

Commit 6b4f995

Browse files
Merge pull request #7 from netlify/test-workflow
feat: transfer core tests + accompanying logic
2 parents 14d425d + a316e25 commit 6b4f995

File tree

34 files changed

+9818
-2514
lines changed

34 files changed

+9818
-2514
lines changed

.github/workflows/test.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Plugin Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
build:
10+
runs-on: ${{ matrix.os }}
11+
12+
strategy:
13+
matrix:
14+
os: [ubuntu-latest, macOS-latest, windows-latest]
15+
node-version: [12, '*']
16+
exclude:
17+
- os: macOS-latest
18+
node-version: 12
19+
- os: windows-latest
20+
node-version: 12
21+
fail-fast: false
22+
23+
steps:
24+
- uses: actions/checkout@v2
25+
- name: Use Node.js ${{ matrix.node-version }}
26+
uses: actions/setup-node@v2
27+
with:
28+
node-version: ${{ matrix.node-version }}
29+
check-latest: true
30+
- name: NPM Install
31+
run: npm ci
32+
- name: Linting
33+
run: npm run format:ci
34+
if: "${{ matrix.node-version == '*' }}"
35+
- name: Run tests against next@latest
36+
run: npm test
37+
- name: Install Next.js Canary
38+
run: npm install -D next@canary --legacy-peer-deps
39+
- name: Run tests against next@canary
40+
run: npm test

package-lock.json

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

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"prepublishOnly:checkout": "git checkout main",
2424
"prepublishOnly:pull": "git pull",
2525
"prepublishOnly:install": "npm ci",
26-
"prepublishOnly:test": "npm test"
26+
"prepublishOnly:test": "npm test",
27+
"test": "next build test/sample && jest"
2728
},
2829
"config": {
2930
"eslint": "--cache --format=codeframe --max-warnings=0 \"{src,scripts,tests,.github}/**/*.{js,md,html}\" \"*.{js,md,html}\" \".*.{js,md,html}\"",
@@ -65,12 +66,15 @@
6566
},
6667
"devDependencies": {
6768
"@netlify/eslint-config-node": "^3.2.6",
69+
"cpy": "^8.1.2",
6870
"eslint-config-next": "^11.0.0",
6971
"husky": "^4.3.0",
7072
"jest": "^27.0.0",
73+
"path-exists": "^4.0.0",
7174
"prettier": "^2.1.2",
7275
"react": "^17.0.1",
73-
"react-dom": "^17.0.1"
76+
"react-dom": "^17.0.1",
77+
"tmp-promise": "^3.0.2"
7478
},
7579
"husky": {
7680
"hooks": {

src/constants.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const HANDLER_FUNCTION_NAME = '___netlify-handler'
2+
const ODB_FUNCTION_NAME = '___netlify-odb-handler'
3+
4+
module.exports = {
5+
HANDLER_FUNCTION_NAME,
6+
ODB_FUNCTION_NAME,
7+
}

src/helpers/generateFunctions.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const path = require('path')
2+
3+
const { copyFile, ensureDir, writeFile } = require('fs-extra')
4+
5+
const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } = require('../constants')
6+
7+
const getHandler = require('./getHandler')
8+
9+
const DEFAULT_FUNCTIONS_SRC = 'netlify/functions'
10+
11+
const generateFunctions = async ({ FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC }) => {
12+
const FUNCTION_DIR = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC
13+
const bridgeFile = require.resolve('@vercel/node/dist/bridge')
14+
15+
const writeHandler = async (func, isODB) => {
16+
const handlerSource = await getHandler(isODB)
17+
await ensureDir(path.join(FUNCTION_DIR, func))
18+
await writeFile(path.join(FUNCTION_DIR, func, `${func}.js`), handlerSource)
19+
await copyFile(bridgeFile, path.join(FUNCTION_DIR, func, 'bridge.js'))
20+
}
21+
22+
await writeHandler(HANDLER_FUNCTION_NAME, false)
23+
await writeHandler(ODB_FUNCTION_NAME, true)
24+
}
25+
26+
module.exports = generateFunctions

src/helpers/isStaticExportProject.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const usesBuildCommand = require('./usesBuildCommand')
2+
3+
// Takes 1. Netlify config's build details and
4+
// 2. the project's package.json scripts to determine if
5+
// the Next.js app uses static HTML export
6+
const isStaticExportProject = ({ build, scripts }) => {
7+
const NEXT_EXPORT_COMMAND = 'next export'
8+
9+
const isStaticExport = usesBuildCommand({ build, scripts, command: NEXT_EXPORT_COMMAND })
10+
11+
if (isStaticExport) {
12+
console.log(
13+
'NOTE: Static HTML export Next.js projects (projects that use `next export`) do not require most of this plugin. For these sites, this plugin *only* caches builds.',
14+
)
15+
}
16+
17+
return isStaticExport
18+
}
19+
20+
module.exports = isStaticExportProject

src/helpers/setIncludedFiles.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } = require('../constants')
2+
3+
const setIncludedFiles = ({ netlifyConfig, distDir }) => {
4+
// Serverless functions need parts of build dist in runtime env
5+
;[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {
6+
if (!netlifyConfig.functions[functionName]) {
7+
netlifyConfig.functions[functionName] = {}
8+
}
9+
if (!netlifyConfig.functions[functionName].included_files) {
10+
netlifyConfig.functions[functionName].included_files = []
11+
}
12+
netlifyConfig.functions[functionName].included_files.push(
13+
`${distDir}/server/**`,
14+
`${distDir}/*.json`,
15+
`${distDir}/BUILD_ID`,
16+
)
17+
})
18+
}
19+
20+
module.exports = setIncludedFiles

src/helpers/shouldSkipPlugin.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Checks all the cases for which the plugin should do nothing
2+
const { redBright, yellowBright } = require('chalk')
3+
4+
const isStaticExportProject = require('./isStaticExportProject')
5+
const usesBuildCommand = require('./usesBuildCommand')
6+
7+
const shouldSkipPlugin = ({ netlifyConfig, packageJson, failBuild }) => {
8+
const hasNoPackageJson = Object.keys(packageJson).length === 0
9+
if (hasNoPackageJson) {
10+
return failBuild('Could not find a package.json for this project')
11+
}
12+
13+
// The env var skips the auto-detection
14+
const envVar = process.env.NEXT_PLUGIN_FORCE_RUN
15+
if (envVar === 'false' || envVar === '0' || envVar === false) {
16+
console.log(
17+
yellowBright`The env var NEXT_PLUGIN_FORCE_RUN was set to ${JSON.stringify(
18+
envVar,
19+
)}, so auto-detection is disabled and the plugin will not run`,
20+
)
21+
return true
22+
}
23+
if (envVar === 'true' || envVar === '1' || envVar === true) {
24+
console.log(
25+
yellowBright`The env var NEXT_PLUGIN_FORCE_RUN was set to ${JSON.stringify(
26+
envVar,
27+
)}, so auto-detection is disabled and the plugin will run regardless`,
28+
)
29+
return false
30+
}
31+
// Otherwise use auto-detection
32+
33+
const { build } = netlifyConfig
34+
const { scripts = {} } = packageJson
35+
36+
if (!build.command) {
37+
console.log(
38+
redBright`⚠️ Warning: No build command specified in the site's Netlify config, so plugin will not run. This deploy will fail unless you have already exported the site. ⚠️`,
39+
)
40+
return true
41+
}
42+
43+
if (usesBuildCommand({ build, scripts, command: 'build-storybook' })) {
44+
console.log(
45+
yellowBright`Site seems to be building a Storybook rather than the Next.js site, so the Essential Next.js plugin will not run. If this is incorrect, set NEXT_PLUGIN_FORCE_RUN to true`,
46+
)
47+
return true
48+
}
49+
50+
return isStaticExportProject({ build, scripts })
51+
}
52+
53+
module.exports = shouldSkipPlugin

src/helpers/usesBuildCommand.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const parseNpmScript = require('@netlify/parse-npm-script')
2+
3+
const COMMAND_PLACEHOLDER = '___netlifybuildcommand'
4+
5+
// Does the build command include this value, either directly or via an npm script?
6+
const usesBuildCommand = ({ build, scripts, command }) => {
7+
if (!build.command) return false
8+
9+
if (build.command.includes(command)) {
10+
return true
11+
}
12+
13+
if (!build.command.includes('npm run') && !build.command.includes('yarn')) {
14+
return false
15+
}
16+
17+
// Insert a fake script to represent the build command
18+
19+
const commands = { ...scripts, [COMMAND_PLACEHOLDER]: build.command }
20+
21+
// This resolves the npm script that is actually being run
22+
try {
23+
const { raw } = parseNpmScript({ scripts: commands }, COMMAND_PLACEHOLDER)
24+
return raw.some((script) => script.includes(command))
25+
} catch (error) {
26+
console.error(
27+
`Static export detection disabled because we could not parse your build command: ${error.message}
28+
The build command is "${build.command}" and the available npm scripts are: ${Object.keys(scripts)
29+
.map((script) => `"${script}"`)
30+
.join(', ')}
31+
If the site does use static export then you can set the env var NEXT_PLUGIN_FORCE_RUN to "false" or uninstall the plugin. See https://ntl.fyi/remove-plugin for instructions.`,
32+
)
33+
}
34+
}
35+
36+
module.exports = usesBuildCommand

src/helpers/validateNextUsage.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const { yellowBright } = require('chalk')
2+
const { lt: ltVersion, gte: gteVersion } = require('semver')
3+
4+
const getNextRoot = require('./getNextRoot')
5+
const resolveNextModule = require('./resolveNextModule')
6+
7+
// Ensure Next.js is available.
8+
// We use `peerDependencies` instead of `dependencies` so that users can choose
9+
// the Next.js version. However, this requires them to install "next" in their
10+
// site.
11+
const validateNextUsage = function ({ failBuild, netlifyConfig }) {
12+
const nextRoot = getNextRoot({ netlifyConfig })
13+
// Because we don't know the monorepo structure, we try to resolve next both locally and in the next root
14+
if (!hasPackage('next', nextRoot)) {
15+
return failBuild(
16+
`This site does not seem to be using Next.js. Please run "npm install next" in the repository.
17+
If you are using a monorepo, please see the docs on configuring your site: https://ntl.fyi/next-monorepos`,
18+
)
19+
}
20+
21+
// We cannot load `next` at the top-level because we validate whether the
22+
// site is using `next` inside `onPreBuild`.
23+
// Old Next.js versions are not supported
24+
// eslint-disable-next-line import/no-dynamic-require
25+
const { version } = require(resolveNextModule(`next/package.json`, nextRoot))
26+
27+
console.log(`Using Next.js ${yellowBright(version)}`)
28+
29+
if (ltVersion(version, MIN_VERSION)) {
30+
return failBuild(`Please upgrade to Next.js ${MIN_VERSION} or later.`)
31+
}
32+
33+
// Recent Next.js versions are sometimes unstable and we might not officially
34+
// support them yet. However, they might still work for some users, so we
35+
// only print a warning
36+
if (gteVersion(version, MIN_EXPERIMENTAL_VERSION)) {
37+
console.log(yellowBright(`Warning: support for Next.js >=${MIN_EXPERIMENTAL_VERSION} is experimental`))
38+
}
39+
}
40+
41+
const MIN_VERSION = '11.1.2'
42+
const MIN_EXPERIMENTAL_VERSION = '11.2.0'
43+
44+
const hasPackage = function (packageName, nextRoot) {
45+
try {
46+
resolveNextModule(`${packageName}/package.json`, nextRoot)
47+
return true
48+
} catch (error) {
49+
return false
50+
}
51+
}
52+
53+
module.exports = validateNextUsage

src/helpers/verifyBuildTarget.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const { yellowBright } = require('chalk')
22

33
const verifyBuildTarget = (target) => {
44
if (target !== 'server') {
5-
// TO-DO: This will only work for Next > 11.1.2
5+
// NOTE: This will only work for Next > 11.1.2
66
console.log(yellowBright`Forcing site to build with target: server`)
77
process.env.NEXT_PRIVATE_TARGET = 'server'
88
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const { satisfies } = require('semver')
2+
3+
// This is when the esbuild dynamic import support was added
4+
const REQUIRED_BUILD_VERSION = '>=15.11.5'
5+
6+
const verifyNetlifyBuildVersion = ({ IS_LOCAL, NETLIFY_BUILD_VERSION, failBuild }) => {
7+
// We check for build version because that's what's available to us, but prompt about the cli because that's what they can upgrade
8+
if (IS_LOCAL && !satisfies(NETLIFY_BUILD_VERSION, REQUIRED_BUILD_VERSION, { includePrerelease: true })) {
9+
return failBuild(
10+
`This version of the Essential Next.js plugin requires netlify-cli@4.4.2 or higher. Please upgrade and try again.
11+
You can do this by running: "npm install -g netlify-cli@latest" or "yarn global add netlify-cli@latest"`,
12+
)
13+
}
14+
}
15+
16+
module.exports = verifyNetlifyBuildVersion

src/helpers/verifyPublishDir.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const path = require('path')
2+
3+
const verifyPublishDir = ({ netlifyConfig, siteRoot, distDir, failBuild }) => {
4+
const { publish } = netlifyConfig.build
5+
const nextSiteNotInProjectRoot = siteRoot !== process.cwd()
6+
7+
// Publish dir needs to match distDir
8+
if (
9+
!publish ||
10+
(nextSiteNotInProjectRoot && publish !== path.join(siteRoot, distDir)) ||
11+
(!nextSiteNotInProjectRoot && publish !== path.join(process.cwd(), distDir))
12+
) {
13+
return failBuild(
14+
`You set your publish directory to "${publish}". Your publish directory should be set to your distDir (defaults to .next or is configured in your next.config.js). If your site is rooted in a subdirectory, your publish directory should be {yourSiteRoot}/{distDir}.`,
15+
)
16+
}
17+
}
18+
19+
module.exports = verifyPublishDir

src/helpers/writeRedirects.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ const getNetlifyRoutes = (nextRoute) => {
4545
return netlifyRoutes
4646
}
4747

48-
const writeRedirects = async ({ publishDir = 'out', nextRoot = process.cwd(), netlifyConfig }) => {
49-
const { dynamicRoutes } = await readJSON(path.join(nextRoot, '.next', 'prerender-manifest.json'))
48+
const writeRedirects = async ({ siteRoot = process.cwd(), netlifyConfig }) => {
49+
const { dynamicRoutes } = await readJSON(path.join(siteRoot, '.next', 'prerender-manifest.json'))
5050

5151
const redirects = []
5252
Object.entries(dynamicRoutes).forEach(([route, { dataRoute, fallback }]) => {
@@ -76,7 +76,7 @@ const writeRedirects = async ({ publishDir = 'out', nextRoot = process.cwd(), ne
7676
// /_next/static/* /static/:splat 200
7777
// /* ${HANDLER_FUNCTION_PATH} 200
7878
// `;
79-
// await writeFile(path.join(nextRoot, publishDir, "_redirects"), odbRedirects);
79+
// await writeFile(path.join(siteRoot, publishDir, "_redirects"), odbRedirects);
8080
}
8181

8282
module.exports = writeRedirects

0 commit comments

Comments
 (0)