diff --git a/dotcom-rendering/makefile b/dotcom-rendering/makefile index 864b90c20b5..fc9dbaeb6f0 100644 --- a/dotcom-rendering/makefile +++ b/dotcom-rendering/makefile @@ -8,11 +8,11 @@ export SHELL := /usr/bin/env bash # messaging ######################################### define log - @node scripts/env/log $(1) + @node ../scripts/log $(1) endef define warn - @node scripts/env/log $(1) warn + @node ../scripts/log $(1) warn endef # deployment ######################################### @@ -134,9 +134,11 @@ lint: clean-dist install $(call log, "checking for lint errors") @yarn lint -lint-project: +lint-project: check-env $(call log, "linting project") - @node ../scripts/check-node-versions.mjs + @node scripts/check-node-versions.mjs + @node scripts/env/check-deps.js + @node scripts/env/check-files.js stylelint: clean-dist install $(call log, "checking for style lint errors") @@ -181,10 +183,8 @@ validate-build: # private check-env: # private $(call log, "checking environment") - @node scripts/env/check-node.js - @node scripts/env/check-yarn.js - @node scripts/env/check-deps.js - @node scripts/env/check-files.js + @cd .. && scripts/env/check-node + @cd .. && scripts/env/check-yarn clear: # private @clear diff --git a/scripts/check-node-versions.mjs b/dotcom-rendering/scripts/check-node-versions.mjs similarity index 86% rename from scripts/check-node-versions.mjs rename to dotcom-rendering/scripts/check-node-versions.mjs index c02a1f8c471..a903dd3e8a2 100644 --- a/scripts/check-node-versions.mjs +++ b/dotcom-rendering/scripts/check-node-versions.mjs @@ -3,13 +3,13 @@ import { readFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { log, warn } from '../dotcom-rendering/scripts/env/log.js'; +import { log, warn } from '../../scripts/log.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); process.chdir(resolve(__dirname, '..')); -const nvmrc = (await readFile('.nvmrc', 'utf-8')) +const nvmrc = (await readFile('../.nvmrc', 'utf-8')) // We don’t care about leading or trailing whitespace .trim(); @@ -30,15 +30,15 @@ if (!nodeVersion) { const requiredNodeVersionMatches = /** @type {const} @satisfies {ReadonlyArray<{filepath: string, pattern: RegExp}>}*/ ([ { - filepath: 'dotcom-rendering/Containerfile', + filepath: 'Containerfile', pattern: /^FROM node:(.+)-alpine$/m, }, { - filepath: 'dotcom-rendering/scripts/deploy/riff-raff.yaml', + filepath: 'scripts/deploy/riff-raff.yaml', pattern: /^ +Recipe: dotcom-rendering.*-node-(\d+\.\d+\.\d+)$/m, }, { - filepath: 'apps-rendering/riff-raff.yaml', + filepath: '../apps-rendering/riff-raff.yaml', pattern: /^ +Recipe: .+-mobile-node(\d+\.\d+\.\d+).*$/m, }, ]); diff --git a/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs b/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs index 8747cd80279..3d723a19405 100755 --- a/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs +++ b/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs @@ -3,7 +3,7 @@ import path from 'node:path'; import * as url from 'node:url'; import cpy from 'cpy'; import execa from 'execa'; -import { log, warn } from '../env/log.js'; +import { log, warn } from '../../../scripts/log.js'; const dirname = url.fileURLToPath(new URL('.', import.meta.url)); const target = path.resolve(dirname, '../..', 'target'); diff --git a/dotcom-rendering/scripts/env/check-deps.js b/dotcom-rendering/scripts/env/check-deps.js index 33534bfcb6a..ab26c2ea76f 100644 --- a/dotcom-rendering/scripts/env/check-deps.js +++ b/dotcom-rendering/scripts/env/check-deps.js @@ -1,7 +1,7 @@ const fs = require('node:fs'); const lockfile = require('@yarnpkg/lockfile'); +const { warn, log } = require('../../../scripts/log'); const pkg = require('../../package.json'); -const { warn, log } = require('./log'); if (pkg.devDependencies) { warn('Don’t use devDependencies'); diff --git a/dotcom-rendering/scripts/env/check-files.js b/dotcom-rendering/scripts/env/check-files.js index 8e349f43c6c..10bb69d7ce5 100644 --- a/dotcom-rendering/scripts/env/check-files.js +++ b/dotcom-rendering/scripts/env/check-files.js @@ -1,5 +1,5 @@ const execa = require('execa'); -const { warn } = require('./log'); +const { warn } = require('../../../scripts/log'); execa('find', ['src', '-type', 'f', '-name', '*index*.ts*']) .then(({ stdout }) => { diff --git a/dotcom-rendering/scripts/env/check-node.js b/dotcom-rendering/scripts/env/check-node.js deleted file mode 100644 index 8f22fde6698..00000000000 --- a/dotcom-rendering/scripts/env/check-node.js +++ /dev/null @@ -1,46 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/unbound-method -const { join } = require('node:path'); -const { promisify } = require('node:util'); -const readFile = promisify(require('node:fs').readFile); -const ensure = require('./ensure'); - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -(async () => { - try { - const [semver] = await ensure('semver'); - - const nodeVersion = /^v(\d+\.\d+\.\d+)/.exec(process.version)[1]; - const nvmrcVersion = ( - await readFile(join(__dirname, '..', '..', '..', '.nvmrc'), 'utf8') - ).trim(); - - if (!semver.satisfies(nodeVersion, nvmrcVersion)) { - const { warn, prompt, log } = require('./log'); - warn( - `dotcom-rendering requires Node v${nvmrcVersion}`, - `You are using v${nodeVersion ?? '(unknown)'}`, - ); - if (process.env.NVM_DIR) { - prompt('Run `nvm install` from the repo root and try again.'); - log( - 'See also: https://gist.github.com/sndrs/5940e9e8a3f506b287233ed65365befb', - ); - } else if (process.env.FNM_DIR) { - prompt( - 'It looks like you have fnm installed', - 'Run `fnm use` from the repo root and try again.', - ); - } else { - prompt( - `Using a Node version manager can make things easier.`, - `Our recommendation is fnm: https://github.com/Schniz/fnm`, - ); - } - process.exit(1); - } - } catch (e) { - // eslint-disable-next-line no-console - console.log(e); - process.exit(1); - } -})(); diff --git a/dotcom-rendering/scripts/env/check-yarn.js b/dotcom-rendering/scripts/env/check-yarn.js deleted file mode 100644 index 6b0ec02cb58..00000000000 --- a/dotcom-rendering/scripts/env/check-yarn.js +++ /dev/null @@ -1,32 +0,0 @@ -const { promisify } = require('node:util'); -const exec = promisify(require('node:child_process').execFile); -const ensure = require('./ensure'); - -// Yarn v1.x support .yarnrc, so we can use a local (check-in) copy of yarn -const YARN_MIN_VERSION = '1.x'; - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -(async () => { - try { - // This will fail if yarn isn't installed, and force into the catch, - // where we install yarn with NPM (mainly for CI) - const { stdout: version } = await exec('yarn', ['--version']); - - const [semver] = await ensure('semver'); - - if (!semver.satisfies(version, YARN_MIN_VERSION)) { - const { warn, prompt, log } = require('./log'); - warn( - `dotcom-rendering requires Yarn >=${YARN_MIN_VERSION}`, - `You are using v${version}`, - ); - prompt('Please upgrade yarn'); - log('https://classic.yarnpkg.com/en/docs/install'); - - process.exit(1); - } - } catch (e) { - require('./log').log(`Installing yarn`); - await exec('npm', ['i', '-g', `yarn@${YARN_MIN_VERSION}`]); - } -})(); diff --git a/dotcom-rendering/scripts/env/ensure.js b/dotcom-rendering/scripts/env/ensure.js deleted file mode 100644 index 9273ab01974..00000000000 --- a/dotcom-rendering/scripts/env/ensure.js +++ /dev/null @@ -1,37 +0,0 @@ -// provides a way to use packages in scripts when we don't know -// if they've been installed yet (with yarn) by temporarily -// installing with npm if node cannot resolve the package - -const { log } = require('./log'); - -module.exports = (...packages) => - new Promise((resolve) => { - try { - resolve(packages.map(require)); - } catch (e) { - log(`Pre-installing dependency (${packages.join(', ')})...`); - const npmInstallProcess = require('node:child_process') - .spawn('npm', [ - 'i', - ...packages, - '--no-save', - '--legacy-peer-deps', - '--package-lock=false', - ]) - .on('close', (code) => { - if (code !== 0) { - process.exit(code); - } - try { - resolve(packages.map(require)); - } catch (e2) { - // eslint-disable-next-line no-console - console.log(e2); - process.exit(1); - } - }) - .stderr.on('data', (data) => - console.error(Buffer.from(data).toString()), - ); - } - }); diff --git a/dotcom-rendering/scripts/gen-stories/get-stories.mjs b/dotcom-rendering/scripts/gen-stories/get-stories.mjs index 260969ce1d1..829ab2a634f 100644 --- a/dotcom-rendering/scripts/gen-stories/get-stories.mjs +++ b/dotcom-rendering/scripts/gen-stories/get-stories.mjs @@ -14,7 +14,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { log, success, warn } from '../env/log.js'; +import { log, success, warn } from '../../../scripts/log.js'; const STORIES_PATH = resolve( dirname(fileURLToPath(new URL(import.meta.url))), @@ -132,7 +132,7 @@ ${storyVariableName}.args = { config: ${JSON.stringify(config)} }; ${storyVariableName}.decorators = [lightDecorator({ display: ArticleDisplay.${displayName}, design: ArticleDesign.${designName}, - theme: {...ArticleSpecial, ...Pillar}.${theme.replace("Pillar", "")}, + theme: {...ArticleSpecial, ...Pillar}.${theme.replace('Pillar', '')}, })]; `; }; diff --git a/dotcom-rendering/scripts/perf/perf-test.js b/dotcom-rendering/scripts/perf/perf-test.js index d28120a0e49..f595b25de42 100644 --- a/dotcom-rendering/scripts/perf/perf-test.js +++ b/dotcom-rendering/scripts/perf/perf-test.js @@ -1,5 +1,5 @@ const execa = require('execa'); -const { warn, log } = require('../env/log'); +const { warn, log } = require('../../../scripts/log'); const run = async () => { try { diff --git a/scripts/env/check-node b/scripts/env/check-node new file mode 100755 index 00000000000..c650fb4e112 --- /dev/null +++ b/scripts/env/check-node @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +# Check whether the current node version matches the .nvmrc version, and offer some help if not. + +in_terminal() { test -t 1; } + +strip_colors() { + local text="$1" + echo "$text" | sed -e $'s/\x1b\[[0-9;]*m//g' +} + +log_with_color() { + local text="$1" + + # Check if output is a terminal and supports color + if in_terminal && [[ $(tput colors) -ge 8 ]]; then + # Terminal supports color, print the text as it is + echo -e "$text" + else + # Terminal does not support color, strip color codes and print + echo "$(strip_colors "$text")" + fi +} + +blue='\033[0;34m' +red='\033[0;31m' +dim='\033[2m' +bold='\033[1m' +reset='\033[0m' + +# get the node version from .nvmrc +nvmrc_contents=$(cat .nvmrc) + +# check that it's a valid version (matches x.y.z) +nvmrc_version_pattern="^[0-9]+\.[0-9]+\.[0-9]+$" +if [[ $nvmrc_contents =~ $nvmrc_version_pattern ]]; then + nvmrc_version=${BASH_REMATCH[0]} +else + log_with_color "${red}The Node version in .nvmrc is invalid${reset}" + log_with_color "${dim}It should match the pattern 'x.y.z'.${reset}" + # exit with failure + exit 1 +fi + +# Now we can check if the current version of Node matches the .nvmrc version + +# is _any_ node available? +if [[ -x "$(command -v node)" ]]; then + node_version=$(node -v) + node_version=${node_version:1} # remove the "v" in "v1.2.3" +fi + +# check the version we're using +# note that `node_version` will be empty if node wan't available above +if [ "$node_version" == "$nvmrc_version" ]; then + # we're using the correct version of node + log_with_color "${dim}Using Node ${blue}$node_version${reset}" + + # we're done, exit with success + exit 0 +fi + +# If we got here, we're using the wrong version of node, or There Is No Node. +# Try to help people load the correct version: +if in_terminal; then + log_with_color "${red}This project requires Node v$nvmrc_version${reset}" + if [[ -x "$(command -v fnm)" ]]; then + log_with_color "${dim}Run ${blue}fnm use${reset}${dim} to switch to the correct version.${reset}" + log_with_color "${dim}See ${blue}${dim}https://github.com/Schniz/fnm#shell-setup${reset}${dim} to automate this.${reset}" + elif [[ -x "$(command -v asdf)" ]]; then + log_with_color "${dim}Run ${blue}asdf install${reset}${dim} to switch to the correct version.${reset}" + elif [[ -x "$(which nvm)" ]]; then + log_with_color "${dim}Run ${blue}nvm install${reset}${dim} to switch to the correct version.${reset}" + else + log_with_color "${dim}Consider using ${bold}fnm${reset}${dim} to manage Node versions:${reset} ${blue}https://github.com/Schniz/fnm#installation${reset}${dim}.${reset}" + fi +else + # not in a terminal, so v possible we're running a husky script in a git gui + echo "Could not find Node v$nvmrc_version." + echo "You may need to load your Node version manager in a ~/.huskyrc file." + echo "See https://typicode.github.io/husky/troubleshooting.html#command-not-found." +fi + +# exit with failure +exit 1 + diff --git a/scripts/env/check-yarn b/scripts/env/check-yarn new file mode 100755 index 00000000000..800d9b9d1a3 --- /dev/null +++ b/scripts/env/check-yarn @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +/** + * Check that yarn is available. If not, ask the user to run `corepack enable` + * which will provide it. + * + * Note that any yarn@>1 will do, since it will defer to + * the copy in that lives in .yarn/releases (which is the version we want to + * use). + */ + +// no external deps can be used here, because this runs before deps are installed +const { execSync } = require('child_process'); +const { stdout } = require('process'); +const { colourise, log, warn } = require('../log'); + +// we can't use chalk, because this runs before deps are installed +// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 +const reset = '\x1b[0m'; +const blue = '\x1b[34m'; + +try { + const yarnVersion = execSync('yarn -v', { encoding: 'utf-8' }); + log(`Using yarn ${colourise(reset, colourise(blue, yarnVersion.trim()))}`); +} catch (e) { + warn(`Could not find yarn. Please run 'corepack enable'.`); + process.exit(1); +} diff --git a/dotcom-rendering/scripts/env/log.js b/scripts/log.js similarity index 77% rename from dotcom-rendering/scripts/env/log.js rename to scripts/log.js index 02ff6f44bb4..058dbe97577 100644 --- a/dotcom-rendering/scripts/env/log.js +++ b/scripts/log.js @@ -5,14 +5,18 @@ const capitalize = (str) => // we could use chalk, but this saves needing to pre-install it // if this is a first run +// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 const red = '\x1b[31m'; const yellow = '\x1b[33m'; -const green = '\u001b[32m'; +const green = '\x1b[32m'; const dim = '\x1b[2m'; const reset = '\x1b[0m'; +const colourise = (colour, str) => + process.stdout.isTTY ? `${colour}${str}${reset}` : str; + const logIt = (messages = [], color = dim) => { - console.log(`${color}%s${reset}`, capitalize(messages.join('\n'))); + console.log(colourise(color, capitalize(messages.join('\n')))); }; const log = (...messages) => logIt(messages); @@ -42,4 +46,5 @@ module.exports = { warn, prompt, success, + colourise, };