diff --git a/plugin/package-lock.json b/plugin/package-lock.json index b3ee47b9..e994762b 100644 --- a/plugin/package-lock.json +++ b/plugin/package-lock.json @@ -9,11 +9,13 @@ "version": "3.8.0", "license": "MIT", "dependencies": { + "@gatsbyjs/reach-router": "^2.0.0", "@netlify/functions": "^1.6.0", "@netlify/ipx": "^1.4.6", "abortcontroller-polyfill": "^1.7.3", "chalk": "^4.1.2", "co-body": "^6.1.0", + "common-tags": "^1.8.2", "cookie": "^0.6.0", "download": "^8.0.0", "etag": "^1.8.1", @@ -30,7 +32,6 @@ "uuid": "^9.0.0" }, "devDependencies": { - "@gatsbyjs/reach-router": "^2.0.0", "@netlify/build": "^29.27.0", "@types/chance": "^1.1.3", "@types/fs-extra": "^9.0.12", @@ -47,10 +48,6 @@ }, "engines": { "node": ">=14.17.0" - }, - "peerDependencies": { - "@gatsbyjs/reach-router": "*", - "common-tags": "^1.8.2" } }, "node_modules/@ampproject/remapping": { @@ -3116,7 +3113,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gatsbyjs/reach-router/-/reach-router-2.0.1.tgz", "integrity": "sha512-gmSZniS9/phwgEgpFARMpNg21PkYDZEpfgEzvkgpE/iku4uvXqCrxr86fXbTpI9mkrhKS1SCTYmLGe60VdHcdQ==", - "dev": true, "dependencies": { "invariant": "^2.2.4", "prop-types": "^15.8.1" @@ -11175,7 +11171,6 @@ "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, "engines": { "node": ">=4.0.0" } @@ -17939,7 +17934,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "dependencies": { "loose-envify": "^1.0.0" } @@ -18737,8 +18731,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -19391,7 +19384,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -22401,7 +22393,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -22411,8 +22402,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/proper-lockfile": { "version": "4.1.2", @@ -28929,7 +28919,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gatsbyjs/reach-router/-/reach-router-2.0.1.tgz", "integrity": "sha512-gmSZniS9/phwgEgpFARMpNg21PkYDZEpfgEzvkgpE/iku4uvXqCrxr86fXbTpI9mkrhKS1SCTYmLGe60VdHcdQ==", - "dev": true, "requires": { "invariant": "^2.2.4", "prop-types": "^15.8.1" @@ -34733,8 +34722,7 @@ "common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==" }, "commondir": { "version": "1.0.1", @@ -39823,7 +39811,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -40401,8 +40388,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "4.1.0", @@ -40933,7 +40919,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -43082,7 +43067,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -43092,8 +43076,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, diff --git a/plugin/package.json b/plugin/package.json index 0aa0a754..2b6e1915 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -40,11 +40,13 @@ "prepare": "npm run build" }, "dependencies": { + "@gatsbyjs/reach-router": "^2.0.0", "@netlify/functions": "^1.6.0", "@netlify/ipx": "^1.4.6", "abortcontroller-polyfill": "^1.7.3", "chalk": "^4.1.2", "co-body": "^6.1.0", + "common-tags": "^1.8.2", "cookie": "^0.6.0", "download": "^8.0.0", "etag": "^1.8.1", @@ -61,7 +63,6 @@ "uuid": "^9.0.0" }, "devDependencies": { - "@gatsbyjs/reach-router": "^2.0.0", "@netlify/build": "^29.27.0", "@types/chance": "^1.1.3", "@types/fs-extra": "^9.0.12", @@ -75,9 +76,5 @@ "rimraf": "^5.0.0", "tmp-promise": "^3.0.3", "typescript": "^5.0.0" - }, - "peerDependencies": { - "@gatsbyjs/reach-router": "*", - "common-tags": "^1.8.2" } } diff --git a/plugin/src/helpers/functions.ts b/plugin/src/helpers/functions.ts index a5dd1d8a..cb794310 100644 --- a/plugin/src/helpers/functions.ts +++ b/plugin/src/helpers/functions.ts @@ -1,6 +1,7 @@ +/* eslint-disable max-lines */ import { NetlifyConfig, NetlifyPluginConstants } from '@netlify/build' -import { copy, copyFile, ensureDir, existsSync, rm, writeFile } from 'fs-extra' -import { resolve, join, relative } from 'pathe' +import { copy, ensureDir, existsSync, rm, writeFile, readFile } from 'fs-extra' +import { resolve, join, relative, dirname } from 'pathe' import { makeApiHandler, makeHandler } from '../templates/handlers' @@ -8,6 +9,81 @@ import { getGatsbyRoot } from './config' export type FunctionList = Array<'API' | 'SSR' | 'DSG'> +/** + * Adjust package imports in functions we produce to be relative. Those imported packages should always be dependencies + * of `@netlify/plugin-gatsby` and we can't rely on those imports being resolvable by accident (i.e. npm hoisting deps + * of this plugin in root node_modules) + */ +export const adjustRequiresToRelative = ( + template: string, + outputLocation: string, +): string => + // built files use CJS so targeting require here despite source files using ESM + template.replace(/require\(["'`]([^"'`]+)["'`]\)/g, (match, request) => { + if (request.startsWith('.')) { + return match + } + + let absolutePath + try { + absolutePath = dirname(require.resolve(`${request}/package.json`)) + } catch {} + + if (!absolutePath) { + absolutePath = require.resolve(request) + } + + if (absolutePath === request) { + // for builtins path will be the same as request + return match + } + const relativePath = `./${relative(dirname(outputLocation), absolutePath)}` + return `require('${relativePath}')` + }) + +const adjustFilesRequiresToRelative = async ( + filesToAdjustRequires: Set, +) => { + for (const file of filesToAdjustRequires) { + await writeFile( + file, + adjustRequiresToRelative(await readFile(file, 'utf8'), file), + ) + } +} + +const writeFileWithRelativeRequires = ( + outputPath: string, + source: string, +): Promise => + writeFile(outputPath, adjustRequiresToRelative(source, outputPath)) + +const copyWithRelativeRequires = async ( + src: string, + dest: string, +): Promise => { + const filesToAdjustRequires = new Set() + await copy(src, dest, { + filter: (_filterSrc, filterDest) => { + if (/\.[cm]?js$/.test(filterDest)) { + filesToAdjustRequires.add(filterDest) + } + return true + }, + }) + await adjustFilesRequiresToRelative(filesToAdjustRequires) +} + +const writeApiFunction = async ({ appDir, functionDir }) => { + const source = makeApiHandler(appDir) + // This is to ensure we're copying from the compiled js, not ts source + await copyWithRelativeRequires( + join(__dirname, '..', '..', 'lib', 'templates', 'api'), + functionDir, + ) + await writeFileWithRelativeRequires(join(functionDir, '__api.js'), source) +} + const writeFunction = async ({ renderMode, handlerName, @@ -16,23 +92,16 @@ const writeFunction = async ({ }) => { const source = makeHandler(appDir, renderMode) await ensureDir(join(functionsSrc, handlerName)) - await writeFile(join(functionsSrc, handlerName, `${handlerName}.js`), source) - await copyFile( + await writeFileWithRelativeRequires( + join(functionsSrc, handlerName, `${handlerName}.js`), + source, + ) + await copyWithRelativeRequires( join(__dirname, '..', '..', 'lib', 'templates', 'utils.js'), join(functionsSrc, handlerName, 'utils.js'), ) } -const writeApiFunction = async ({ appDir, functionDir }) => { - const source = makeApiHandler(appDir) - // This is to ensure we're copying from the compiled js, not ts source - await copy( - join(__dirname, '..', '..', 'lib', 'templates', 'api'), - functionDir, - ) - await writeFile(join(functionDir, '__api.js'), source) -} - export const writeFunctions = async ({ constants, netlifyConfig, @@ -92,13 +161,13 @@ export const setupImageCdn = async ({ await ensureDir(constants.INTERNAL_FUNCTIONS_SRC) - await copyFile( + await copyWithRelativeRequires( join(__dirname, '..', '..', 'src', 'templates', 'ipx.ts'), join(constants.INTERNAL_FUNCTIONS_SRC, '_ipx.ts'), ) if (NETLIFY_IMAGE_CDN === `true`) { - await copyFile( + await copyWithRelativeRequires( join(__dirname, '..', '..', 'src', 'templates', 'image.ts'), join(constants.INTERNAL_FUNCTIONS_SRC, '__image.ts'), ) @@ -169,3 +238,4 @@ export const deleteFunctions = async ({ } } } +/* eslint-enable max-lines */ diff --git a/plugin/src/templates/api/gatsbyFunction.ts b/plugin/src/templates/api/gatsbyFunction.ts index c3488d6a..2a4fd856 100644 --- a/plugin/src/templates/api/gatsbyFunction.ts +++ b/plugin/src/templates/api/gatsbyFunction.ts @@ -2,10 +2,7 @@ import { existsSync } from 'fs' import path from 'path' import process from 'process' -import { - match as reachRouterMatch, - matchPath as reachRouterMatchPath, -} from '@gatsbyjs/reach-router' +import { match as reachMatch } from '@gatsbyjs/reach-router' import { HandlerEvent } from '@netlify/functions' import bodyParser from 'co-body' import multer from 'multer' @@ -16,11 +13,6 @@ import { AugmentedGatsbyFunctionRequest, } from './utils' -/** - * Depending on the version of '@gatsbyjs/reach-router' installed, the 'match' method may not be defined. - * This check ensures that this continues to work as expected between v1 and v2 of the package. - */ -const reachMatch = reachRouterMatch || reachRouterMatchPath const parseForm = multer().any() type MulterReq = Parameters[0] type MulterRes = Parameters[1] diff --git a/plugin/src/templates/api/utils.ts b/plugin/src/templates/api/utils.ts index ae321233..a825992d 100644 --- a/plugin/src/templates/api/utils.ts +++ b/plugin/src/templates/api/utils.ts @@ -10,8 +10,7 @@ import { HandlerContext, } from '@netlify/functions' import cookie from 'cookie' -import type { GatsbyFunctionResponse } from 'gatsby' -import { GatsbyFunctionRequest } from 'gatsby' +import type { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby' import fetch, { Headers } from 'node-fetch' import statuses from 'statuses' diff --git a/plugin/test/unit/helpers/functions.spec.ts b/plugin/test/unit/helpers/functions.spec.ts index d4a5264b..e5a5bdd9 100644 --- a/plugin/test/unit/helpers/functions.spec.ts +++ b/plugin/test/unit/helpers/functions.spec.ts @@ -6,7 +6,10 @@ import Chance from 'chance' import { copy, existsSync } from 'fs-extra' import { dir as getTmpDir } from 'tmp-promise' -import { setupImageCdn } from '../../../src/helpers/functions' +import { + adjustRequiresToRelative, + setupImageCdn, +} from '../../../src/helpers/functions' const SAMPLE_PROJECT_DIR = `${__dirname}/../../../../demo` const TEST_TIMEOUT = 60_000 @@ -93,3 +96,40 @@ describe('setupImageCdn', () => { TEST_TIMEOUT, ) }) + +describe.skip('adjustRequiresToRelative', () => { + it('skips node builtins', () => { + expect(adjustRequiresToRelative('require("fs")', __filename)).toBe( + 'require("fs")', + ) + expect(adjustRequiresToRelative('require("node:fs")', __filename)).toBe( + 'require("node:fs")', + ) + }) + + it('skips already relative', () => { + expect(adjustRequiresToRelative('require("./sibling")', __filename)).toBe( + 'require("./sibling")', + ) + expect( + adjustRequiresToRelative('require("./nested/foo")', __filename), + ).toBe('require("./nested/foo")') + expect(adjustRequiresToRelative('require("../parent")', __filename)).toBe( + 'require("../parent")', + ) + }) + + it('handles packages', () => { + expect(adjustRequiresToRelative('require("node-fetch")', __filename)).toBe( + `require('./../../../node_modules/node-fetch/lib/index.js')`, + ) + expect(adjustRequiresToRelative(`require('multer')`, __filename)).toBe( + `require('./../../../node_modules/multer/index.js')`, + ) + expect( + adjustRequiresToRelative('require(`@gatsbyjs/reach-router`)', __filename), + ).toBe( + `require('./../../../node_modules/@gatsbyjs/reach-router/dist/index.js')`, + ) + }) +})