Repository: netlify/build
Pull request #3964 (2021-12-16 22:29:10)

fix: use ESM-compatible JSON imports
Part of https://github.com/netlify/build/issues/3742

JSON imports are not available yet with pure ES modules without using experimental flags. As part of the ES modules migration, this PR refactors them so they are compatible. 

---

For us to review and ship your PR efficiently, please perform the following steps:

- [x] Open a [bug/issue](https://github.com/netlify/build/issues/new/choose) before writing your code 🧑‍💻. This ensures we can discuss the changes and get feedback from everyone that should be involved. If you\`re fixing a typo or something that\`s on fire 🔥 (e.g. incident related), you can skip this step.
- [x] Read the [contribution guidelines](../CONTRIBUTING.md) 📖. This ensures your code follows our style guide and
      passes our tests.
- [x] Update or add tests (if any source code was changed or added) 🧪
- [x] Update or add documentation (if features were changed or added) 📝
- [x] Make sure the status checks below are successful ✅


Patch:
diff --git a/packages/build/src/core/constants.js b/packages/build/src/core/constants.js
index 63970f78c..f874dc56c 100644
--- a/packages/build/src/core/constants.js
+++ b/packages/build/src/core/constants.js
@@ -5,7 +5,7 @@ const { relative, normalize } = require('path')
 const mapObj = require('map-obj')
 const pathExists = require('path-exists')
 
-const { version } = require('../../package.json')
+const { ROOT_PACKAGE_JSON } = require('../utils/json')
 
 const cacheUtilsPromise = import('@netlify/cache-utils')
 
@@ -35,7 +35,7 @@ const getConstants = async function ({
     // Boolean indicating whether the build was run locally (Netlify CLI) or in the production CI
     IS_LOCAL: isLocal,
     // The version of Netlify Build
-    NETLIFY_BUILD_VERSION: version,
+    NETLIFY_BUILD_VERSION: ROOT_PACKAGE_JSON.version,
     // The Netlify Site ID
     SITE_ID: siteId,
     // The Netlify API access token
diff --git a/packages/build/src/error/monitor/start.js b/packages/build/src/error/monitor/start.js
index bd72e7cb4..bc7349d55 100644
--- a/packages/build/src/error/monitor/start.js
+++ b/packages/build/src/error/monitor/start.js
@@ -3,8 +3,8 @@
 const Bugsnag = require('@bugsnag/js')
 const memoizeOne = require('memoize-one')
 
-const { name, version } = require('../../../package.json')
 const { log } = require('../../log/logger')
+const { ROOT_PACKAGE_JSON } = require('../../utils/json')
 
 const projectRoot = `${__dirname}/../../..`
 
@@ -20,8 +20,8 @@ const startErrorMonitor = function ({ flags: { mode }, logs, bugsnagKey }) {
   try {
     const errorMonitor = startBugsnag({
       apiKey: bugsnagKey,
-      appVersion: `${name} ${version}`,
-      appType: name,
+      appVersion: `${ROOT_PACKAGE_JSON.name} ${ROOT_PACKAGE_JSON.version}`,
+      appType: ROOT_PACKAGE_JSON.name,
       releaseStage,
       logger,
       projectRoot,
diff --git a/packages/build/src/log/messages/core.js b/packages/build/src/log/messages/core.js
index 6be92cc52..4d5bc7088 100644
--- a/packages/build/src/log/messages/core.js
+++ b/packages/build/src/log/messages/core.js
@@ -3,10 +3,10 @@
 const { link } = require('ansi-escapes')
 const prettyMs = require('pretty-ms')
 
-const { name, version } = require('../../../package.json')
 const { getFullErrorInfo } = require('../../error/parse/parse')
 const { serializeLogError } = require('../../error/parse/serialize_log')
 const { roundTimerToMillisecs } = require('../../time/measure')
+const { ROOT_PACKAGE_JSON } = require('../../utils/json')
 const { getLogHeaderFunc } = require('../header_func')
 const { log, logMessage, logWarning, logHeader, logSubHeader, logWarningArray } = require('../logger')
 const { logOldCliVersionError } = require('../old_version')
@@ -17,7 +17,7 @@ const { logConfigOnError } = require('./config')
 const logBuildStart = function (logs) {
   logHeader(logs, 'Netlify Build')
   logSubHeader(logs, 'Version')
-  logMessage(logs, `${name} ${version}`)
+  logMessage(logs, `${ROOT_PACKAGE_JSON.name} ${ROOT_PACKAGE_JSON.version}`)
 }
 
 const logBuildError = async function ({ error, netlifyConfig, mode, logs, debug, testOpts }) {
diff --git a/packages/build/src/log/old_version.js b/packages/build/src/log/old_version.js
index 1bc745b0d..3b41b0de5 100644
--- a/packages/build/src/log/old_version.js
+++ b/packages/build/src/log/old_version.js
@@ -4,7 +4,7 @@ const { stdout } = require('process')
 
 const UpdateNotifier = require('update-notifier')
 
-const CORE_PACKAGE_JSON = require('../../package.json')
+const { ROOT_PACKAGE_JSON } = require('../utils/json')
 
 // Many build errors happen in local builds that do not use the latest version
 // of `@netlify/build`. We print a warning message on those.
@@ -32,10 +32,10 @@ const getCorePackageJson = function (testOpts) {
     // eslint-disable-next-line fp/no-mutation
     stdout.isTTY = true
 
-    return { ...CORE_PACKAGE_JSON, version: '0.0.1' }
+    return { ...ROOT_PACKAGE_JSON, version: '0.0.1' }
   }
 
-  return CORE_PACKAGE_JSON
+  return ROOT_PACKAGE_JSON
 }
 
 const OLD_VERSION_MESSAGE = `Please update netlify-cli to its latest version.
diff --git a/packages/build/src/plugins/compatibility.js b/packages/build/src/plugins/compatibility.js
index 373c2f61b..ec84a5d1b 100644
--- a/packages/build/src/plugins/compatibility.js
+++ b/packages/build/src/plugins/compatibility.js
@@ -3,6 +3,7 @@ const pEvery = require('p-every')
 const pLocate = require('p-locate')
 const { satisfies, clean: cleanVersion } = require('semver')
 
+const { importJsonFile } = require('../utils/json')
 const { resolvePath } = require('../utils/resolve')
 
 // Retrieve the `expectedVersion` of a plugin:
@@ -107,8 +108,7 @@ const siteDependencyTest = async function ({ dependencyName, allowedVersion, sit
   try {
     // if this is a range we need to get the exact version
     const packageJsonPath = await resolvePath(`${dependencyName}/package.json`, buildDir)
-    // eslint-disable-next-line node/global-require, import/no-dynamic-require
-    const { version } = require(packageJsonPath)
+    const { version } = await importJsonFile(packageJsonPath)
     return satisfies(version, allowedVersion)
   } catch (error) {
     return false
diff --git a/packages/build/src/plugins/expected_version.js b/packages/build/src/plugins/expected_version.js
index 2bfb77ecb..024faf411 100644
--- a/packages/build/src/plugins/expected_version.js
+++ b/packages/build/src/plugins/expected_version.js
@@ -3,6 +3,7 @@
 const { satisfies } = require('semver')
 
 const { addErrorInfo } = require('../error/info')
+const { importJsonFile } = require('../utils/json')
 const { resolvePath } = require('../utils/resolve')
 
 const { getExpectedVersion } = require('./compatibility')
@@ -95,8 +96,7 @@ const isMissingVersion = async function ({ autoPluginsDir, packageName, pluginPa
 
 const getAutoPluginVersion = async function (packageName, autoPluginsDir) {
   const packageJsonPath = await resolvePath(`${packageName}/package.json`, autoPluginsDir)
-  // eslint-disable-next-line node/global-require, import/no-dynamic-require
-  const { version } = require(packageJsonPath)
+  const { version } = await importJsonFile(packageJsonPath)
   return version
 }
 
diff --git a/packages/build/src/plugins/node_version.js b/packages/build/src/plugins/node_version.js
index cfa9904c2..d4d020b17 100644
--- a/packages/build/src/plugins/node_version.js
+++ b/packages/build/src/plugins/node_version.js
@@ -4,10 +4,8 @@ const { version: currentVersion, execPath } = require('process')
 
 const { satisfies, clean: cleanVersion } = require('semver')
 
-const {
-  engines: { node: nodeVersionSupportedRange },
-} = require('../../package.json')
 const { addErrorInfo } = require('../error/info')
+const { ROOT_PACKAGE_JSON } = require('../utils/json')
 
 // Local plugins and `package.json`-installed plugins use user's preferred Node.js version if higher than our minimum
 // supported version. Else default to the system Node version.
@@ -29,7 +27,7 @@ const addPluginNodeVersion = function ({
   nodePath,
 }) {
   return (loadedFrom === 'local' || loadedFrom === 'package.json') &&
-    satisfies(userNodeVersion, nodeVersionSupportedRange)
+    satisfies(userNodeVersion, ROOT_PACKAGE_JSON.engines.node)
     ? { ...pluginOptions, nodePath, nodeVersion: userNodeVersion }
     : { ...pluginOptions, nodePath: execPath, nodeVersion: currentNodeVersion }
 }
diff --git a/packages/build/src/plugins/options.js b/packages/build/src/plugins/options.js
index 3b50a0b7d..7bbaafe25 100644
--- a/packages/build/src/plugins/options.js
+++ b/packages/build/src/plugins/options.js
@@ -2,9 +2,9 @@
 
 const { dirname } = require('path')
 
-const corePackageJson = require('../../package.json')
 const { installLocalPluginsDependencies } = require('../install/local')
 const { measureDuration } = require('../time/main')
+const { ROOT_PACKAGE_JSON } = require('../utils/json')
 const { getPackageJson } = require('../utils/package')
 
 const { useManifest } = require('./manifest/main')
@@ -83,10 +83,9 @@ const isNotRedundantCorePlugin = function (pluginOptionsA, index, pluginsOptions
 // Retrieve information about @netlify/build when an error happens there and not
 // in a plugin
 const getSpawnInfo = function () {
-  const { name } = corePackageJson
   return {
-    plugin: { packageName: name, pluginPackageJson: corePackageJson },
-    location: { event: 'load', packageName: name, loadedFrom: 'core', origin: 'core' },
+    plugin: { packageName: ROOT_PACKAGE_JSON.name, pluginPackageJson: ROOT_PACKAGE_JSON },
+    location: { event: 'load', packageName: ROOT_PACKAGE_JSON.name, loadedFrom: 'core', origin: 'core' },
   }
 }
 
diff --git a/packages/build/src/telemetry/main.js b/packages/build/src/telemetry/main.js
index 4156a7308..37b5da6c5 100644
--- a/packages/build/src/telemetry/main.js
+++ b/packages/build/src/telemetry/main.js
@@ -5,9 +5,9 @@ const { platform } = require('process')
 const got = require('got')
 const osName = require('os-name')
 
-const { version: buildVersion } = require('../../package.json')
 const { addErrorInfo } = require('../error/info')
 const { roundTimerToMillisecs } = require('../time/measure')
+const { ROOT_PACKAGE_JSON } = require('../utils/json')
 
 const DEFAULT_TELEMETRY_TIMEOUT = 1200
 const DEFAULT_TELEMETRY_CONFIG = {
@@ -89,7 +89,7 @@ const getPayload = function ({
       buildId,
       status,
       steps: stepsCount,
-      buildVersion,
+      buildVersion: ROOT_PACKAGE_JSON.version,
       // We're passing the node version set by the buildbot/user which will run the `build.command` and
       // the `package.json`/locally defined plugins
       nodeVersion: userNodeVersion,
diff --git a/packages/build/src/utils/json.js b/packages/build/src/utils/json.js
new file mode 100644
index 000000000..6ed5fb5af
--- /dev/null
+++ b/packages/build/src/utils/json.js
@@ -0,0 +1,22 @@
+'use strict'
+
+const { promises: fs, readFileSync } = require('fs')
+
+const ROOT_PACKAGE_JSON_PATH = `${__dirname}/../../package.json`
+
+// TODO: Replace with dynamic `import()` once it is supported without
+// experimental flags
+const importJsonFile = async function (filePath) {
+  const fileContents = await fs.readFile(filePath, 'utf8')
+  return JSON.parse(fileContents)
+}
+
+const importJsonFileSync = function (filePath) {
+  // Use sync I/O so it is easier to migrate to `import()` later on
+  const fileContents = readFileSync(filePath, 'utf8')
+  return JSON.parse(fileContents)
+}
+
+const ROOT_PACKAGE_JSON = importJsonFileSync(ROOT_PACKAGE_JSON_PATH)
+
+module.exports = { importJsonFile, ROOT_PACKAGE_JSON }


Test patch:
diff --git a/packages/build/tests/core/tests.js b/packages/build/tests/core/tests.js
index faf7f3b0a..c123f5ea5 100644
--- a/packages/build/tests/core/tests.js
+++ b/packages/build/tests/core/tests.js
@@ -14,6 +14,7 @@ const sinon = require('sinon')
 const { tmpName } = require('tmp-promise')
 
 const { version: netlifyBuildVersion } = require('../../package.json')
+const { importJsonFile } = require('../../src/utils/json')
 const { runFixtureConfig } = require('../helpers/config')
 const { removeDir } = require('../helpers/dir')
 const { runFixture, FIXTURES_DIR } = require('../helpers/main')
@@ -517,8 +518,7 @@ test('Generates a `manifest.json` file when running outside of buildbot', async
 
   t.true(await pathExists(manifestPath))
 
-  // eslint-disable-next-line import/no-dynamic-require, node/global-require
-  const { functions, timestamp, version: manifestVersion } = require(manifestPath)
+  const { functions, timestamp, version: manifestVersion } = await importJsonFile(manifestPath)
 
   t.is(functions.length, 3)
   t.is(typeof timestamp, 'number')
@@ -538,8 +538,7 @@ test('Generates a `manifest.json` file when the `buildbot_create_functions_manif
 
   t.true(await pathExists(manifestPath))
 
-  // eslint-disable-next-line import/no-dynamic-require, node/global-require
-  const { functions, timestamp, version: manifestVersion } = require(manifestPath)
+  const { functions, timestamp, version: manifestVersion } = await importJsonFile(manifestPath)
 
   t.is(functions.length, 3)
   t.is(typeof timestamp, 'number')
diff --git a/packages/build/tests/edge_handlers/tests.js b/packages/build/tests/edge_handlers/tests.js
index 64b38a395..bd1619f34 100644
--- a/packages/build/tests/edge_handlers/tests.js
+++ b/packages/build/tests/edge_handlers/tests.js
@@ -3,6 +3,7 @@
 const test = require('ava')
 const { spy } = require('sinon')
 
+const { importJsonFile } = require('../../src/utils/json')
 const { removeDir } = require('../helpers/dir')
 const { runFixture, FIXTURES_DIR } = require('../helpers/main')
 
@@ -16,7 +17,7 @@ const getEdgeHandlersPaths = function (fixtureName) {
 }
 
 const loadEdgeHandlerBundle = async function ({ outputDir, manifestPath }) {
-  const bundlePath = getEdgeHandlerBundlePath({ outputDir, manifestPath })
+  const bundlePath = await getEdgeHandlerBundlePath({ outputDir, manifestPath })
 
   try {
     return requireEdgeHandleBundle(bundlePath)
@@ -25,9 +26,8 @@ const loadEdgeHandlerBundle = async function ({ outputDir, manifestPath }) {
   }
 }
 
-const getEdgeHandlerBundlePath = function ({ outputDir, manifestPath }) {
-  // eslint-disable-next-line node/global-require, import/no-dynamic-require
-  const { sha } = require(manifestPath)
+const getEdgeHandlerBundlePath = async function ({ outputDir, manifestPath }) {
+  const { sha } = await importJsonFile(manifestPath)
   return `${outputDir}/${sha}`
 }