diff --git a/dotcom-rendering/makefile b/dotcom-rendering/makefile index fc9dbaeb6f0..864b90c20b5 100644 --- a/dotcom-rendering/makefile +++ b/dotcom-rendering/makefile @@ -8,11 +8,11 @@ export SHELL := /usr/bin/env bash # messaging ######################################### define log - @node ../scripts/log $(1) + @node scripts/env/log $(1) endef define warn - @node ../scripts/log $(1) warn + @node scripts/env/log $(1) warn endef # deployment ######################################### @@ -134,11 +134,9 @@ lint: clean-dist install $(call log, "checking for lint errors") @yarn lint -lint-project: check-env +lint-project: $(call log, "linting project") - @node scripts/check-node-versions.mjs - @node scripts/env/check-deps.js - @node scripts/env/check-files.js + @node ../scripts/check-node-versions.mjs stylelint: clean-dist install $(call log, "checking for style lint errors") @@ -183,8 +181,10 @@ validate-build: # private check-env: # private $(call log, "checking environment") - @cd .. && scripts/env/check-node - @cd .. && scripts/env/check-yarn + @node scripts/env/check-node.js + @node scripts/env/check-yarn.js + @node scripts/env/check-deps.js + @node scripts/env/check-files.js clear: # private @clear diff --git a/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs b/dotcom-rendering/scripts/deploy/build-riffraff-bundle.mjs index 3d723a19405..8747cd80279 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 '../../../scripts/log.js'; +import { log, warn } from '../env/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 ab26c2ea76f..33534bfcb6a 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 10bb69d7ce5..8e349f43c6c 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('../../../scripts/log'); +const { warn } = require('./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 new file mode 100644 index 00000000000..8f22fde6698 --- /dev/null +++ b/dotcom-rendering/scripts/env/check-node.js @@ -0,0 +1,46 @@ +// 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 new file mode 100644 index 00000000000..6b0ec02cb58 --- /dev/null +++ b/dotcom-rendering/scripts/env/check-yarn.js @@ -0,0 +1,32 @@ +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 new file mode 100644 index 00000000000..9273ab01974 --- /dev/null +++ b/dotcom-rendering/scripts/env/ensure.js @@ -0,0 +1,37 @@ +// 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/scripts/log.js b/dotcom-rendering/scripts/env/log.js similarity index 77% rename from scripts/log.js rename to dotcom-rendering/scripts/env/log.js index 058dbe97577..02ff6f44bb4 100644 --- a/scripts/log.js +++ b/dotcom-rendering/scripts/env/log.js @@ -5,18 +5,14 @@ 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 = '\x1b[32m'; +const green = '\u001b[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(colourise(color, capitalize(messages.join('\n')))); + console.log(`${color}%s${reset}`, capitalize(messages.join('\n'))); }; const log = (...messages) => logIt(messages); @@ -46,5 +42,4 @@ module.exports = { warn, prompt, success, - colourise, }; diff --git a/dotcom-rendering/scripts/gen-stories/get-stories.mjs b/dotcom-rendering/scripts/gen-stories/get-stories.mjs index 829ab2a634f..260969ce1d1 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 '../../../scripts/log.js'; +import { log, success, warn } from '../env/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 f595b25de42..d28120a0e49 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('../../../scripts/log'); +const { warn, log } = require('../env/log'); const run = async () => { try { diff --git a/dotcom-rendering/scripts/check-node-versions.mjs b/scripts/check-node-versions.mjs similarity index 86% rename from dotcom-rendering/scripts/check-node-versions.mjs rename to scripts/check-node-versions.mjs index a903dd3e8a2..c02a1f8c471 100644 --- a/dotcom-rendering/scripts/check-node-versions.mjs +++ b/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 '../../scripts/log.js'; +import { log, warn } from '../dotcom-rendering/scripts/env/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: 'Containerfile', + filepath: 'dotcom-rendering/Containerfile', pattern: /^FROM node:(.+)-alpine$/m, }, { - filepath: 'scripts/deploy/riff-raff.yaml', + filepath: 'dotcom-rendering/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/scripts/env/check-node b/scripts/env/check-node deleted file mode 100755 index c650fb4e112..00000000000 --- a/scripts/env/check-node +++ /dev/null @@ -1,86 +0,0 @@ -#!/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 deleted file mode 100755 index 800d9b9d1a3..00000000000 --- a/scripts/env/check-yarn +++ /dev/null @@ -1,28 +0,0 @@ -#!/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); -}