diff --git a/package.json b/package.json index a0283f6..119adfa 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,16 @@ "version": "1.0.0", "private": true, "scripts": { - "build": "SKIP_E2E_DEPS=true scripts/install.sh && scripts/build.mjs", + "build": "SKIP_E2E_DEPS=true scripts/install.mjs && scripts/build.mjs", "clean": "scripts/clean.mjs", - "install": "scripts/install.sh --force", - "lint": "scripts/install.sh && scripts/lint.mjs", - "start": "SKIP_E2E_DEPS=true scripts/install.sh && scripts/start.sh", - "test": "scripts/install.sh && scripts/lint.mjs && scripts/test.sh", + "install": "scripts/install.mjs --force", + "lint": "scripts/install.mjs && scripts/lint.mjs", + "start": "SKIP_E2E_DEPS=true scripts/install.mjs && scripts/start.mjs", + "test": "scripts/install.mjs && scripts/lint.mjs && scripts/test.sh", "format": "npm --prefix=backend run format --quiet && npm --prefix=frontend run format --quiet && npm --prefix=e2e run format --quiet", "test:backend": "npm --prefix=backend test --quiet --", "test:frontend": "npm --prefix=frontend test --quiet --", "test:frontend:watch": "npm --prefix=frontend test --quiet -- --watch", - "test:e2e": "scripts/install.sh && scripts/e2e.sh" + "test:e2e": "scripts/install.mjs && scripts/e2e.sh" } } diff --git a/scripts/build.mjs b/scripts/build.mjs index 9f2d603..98b53fe 100755 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -14,10 +14,13 @@ import { import { stat, rename, chmod } from 'node:fs/promises'; import { runTask, runTaskPrefixOutput } from './helpers/proc.mjs'; -const PARALLEL_BUILD = process.env['PARALLEL_BUILD'] !== 'false'; +const PARALLEL_BUILD = (process.env['PARALLEL_BUILD'] ?? 'true') === 'true'; const KEEP_DEPS = process.argv.slice(2).includes('--keep-deps'); -const packages = ['frontend', 'backend']; +const packages = [ + { dir: 'frontend', format: '35' }, + { dir: 'backend', format: '36' }, +]; const builddir = join(basedir, 'build'); const staticdir = join(builddir, 'static'); @@ -31,12 +34,14 @@ try { process.exit(1); } -async function buildPackage(pkg) { - log(`Building ${pkg}...`); +async function buildPackage({ dir, format }) { + log(`Building ${dir}...`); await runTaskPrefixOutput({ command: 'npm', - args: ['--prefix', join(basedir, pkg), 'run', 'build', '--quiet'], - outputPrefix: pkg, + args: ['run', 'build', '--quiet'], + cwd: join(basedir, dir), + outputPrefix: dir, + prefixFormat: format, }); } diff --git a/scripts/helpers/proc.mjs b/scripts/helpers/proc.mjs index 67d421a..6386424 100644 --- a/scripts/helpers/proc.mjs +++ b/scripts/helpers/proc.mjs @@ -2,8 +2,15 @@ import { spawn, execFile } from 'node:child_process'; const MAX_LINE_BUFFER = 10000; -export function propagateStreamWithPrefix(stream, target, prefix) { - const pre = target.isTTY ? `\u001B[35m${prefix}:\u001B[0m ` : `${prefix}: `; +export function propagateStreamWithPrefix( + stream, + target, + prefix, + prefixFormat, +) { + const pre = target.isTTY + ? `\u001B[${prefixFormat}m${prefix}:\u001B[0m ` + : `${prefix}: `; const suf = target.isTTY ? '\u001B[0m\n' : '\n'; const curLine = Buffer.alloc(MAX_LINE_BUFFER); let curLineP = 0; @@ -60,6 +67,7 @@ export function runTaskPrefixOutput({ args, outputTarget = process.stderr, outputPrefix = command, + prefixFormat = '35', ...options } = {}) { return new Promise((resolve, reject) => { @@ -67,8 +75,18 @@ export function runTaskPrefixOutput({ ...options, stdio: ['ignore', 'pipe', 'pipe'], }); - propagateStreamWithPrefix(proc.stdio[1], outputTarget, outputPrefix); - propagateStreamWithPrefix(proc.stdio[2], outputTarget, outputPrefix); + propagateStreamWithPrefix( + proc.stdio[1], + outputTarget, + outputPrefix, + prefixFormat, + ); + propagateStreamWithPrefix( + proc.stdio[2], + outputTarget, + outputPrefix, + prefixFormat, + ); proc.on('error', reject); proc.on('exit', handleExit(resolve, reject)); }); diff --git a/scripts/install.mjs b/scripts/install.mjs new file mode 100755 index 0000000..7b031bb --- /dev/null +++ b/scripts/install.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +import { join } from 'node:path'; +import { basedir, deleteDirectory, log, readJSON } from './helpers/io.mjs'; +import { stat } from 'node:fs/promises'; +import { runTask } from './helpers/proc.mjs'; + +const SKIP_E2E_DEPS = (process.env['SKIP_E2E_DEPS'] ?? 'false') === 'true'; +const FORCE = process.argv.slice(2).includes('--force'); + +const packageJson = await readJSON(join(basedir, 'package.json')); +const dependencyKeys = Object.keys(packageJson).filter((k) => + k.toLowerCase().includes('dependencies'), +); +if (dependencyKeys.length > 0) { + log(` +Dependencies should not be installed in root package.json! +- remove ${dependencyKeys.map((v) => `"${v}"`).join(', ')} +- add the dependencies to the desired subproject instead +- re-run install +`); + deleteDirectory(join(basedir, 'node_modules')); + process.exit(1); +} + +async function installPackage(pkg) { + const s = await stat(join(basedir, pkg, 'node_modules')).catch(() => null); + if (s === null || FORCE) { + log(`Installing ${pkg} dependencies...`); + await runTask('npm', ['install', '--quiet'], { + cwd: join(basedir, pkg), + env: { ...process.env, DISABLE_OPENCOLLECTIVE: '1' }, + }); + } +} + +await installPackage('frontend'); +await installPackage('backend'); +if (!SKIP_E2E_DEPS) { + await installPackage('e2e'); +} diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100755 index b6569e6..0000000 --- a/scripts/install.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh -set -e; - -BASEDIR="$(dirname "$0")/.."; -if echo " $* " | grep ' --force ' > /dev/null; then - FORCE='true'; -fi; - -if grep 'ependencies"' "$BASEDIR/package.json" > /dev/null; then - echo >&2; - echo >&2; - echo 'Dependencies should not be installed in root package.json!' >&2; - echo '- remove the dependencies and devDependencies' >&2; - echo '- add the dependencies to the desired subproject instead' >&2; - echo '- re-run install' >&2; - echo >&2; - echo >&2; - rm -rf "$BASEDIR/node_modules"; - exit 1; -fi - -install_subproject() { - local PROJECT="$1"; - if [ ! -d "$BASEDIR/$PROJECT/node_modules" ] || [ "$FORCE" == 'true' ]; then - echo; - echo "Installing $PROJECT dependencies..."; - DISABLE_OPENCOLLECTIVE=1 \ - npm --prefix="$BASEDIR/$PROJECT" install --quiet; - fi; -} - -install_subproject 'frontend'; -install_subproject 'backend'; -if [ "${SKIP_E2E_DEPS:-false}" != 'true' ]; then - install_subproject 'e2e'; -fi; diff --git a/scripts/start.mjs b/scripts/start.mjs new file mode 100755 index 0000000..b92ac8f --- /dev/null +++ b/scripts/start.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +import { join } from 'node:path'; +import { basedir, log } from './helpers/io.mjs'; +import { runTaskPrefixOutput } from './helpers/proc.mjs'; + +const forceMockSSO = process.argv.slice(2).includes('--mock-sso'); +const apiPort = Number.parseInt(process.env['PORT'] ?? '5000'); +const appPort = apiPort + 1; + +const frontendEnv = { + ...process.env, + PORT: String(appPort), +}; + +const backendEnv = { + ...process.env, + PORT: String(apiPort), + FORWARD_HOST: `http://localhost:${appPort}`, + SERVER_BIND_ADDRESS: 'localhost', +}; + +if ( + forceMockSSO || + (!process.env['SSO_GOOGLE_CLIENT_ID'] && + !process.env['SSO_GITHUB_CLIENT_ID'] && + !process.env['SSO_GITLAB_CLIENT_ID']) +) { + log('Using mock authentication provider'); + const mockSSOPort = apiPort + 2; + const mockSSOHost = `http://localhost:${mockSSOPort}`; + backendEnv['MOCK_SSO_PORT'] = mockSSOPort; + backendEnv['SSO_GOOGLE_CLIENT_ID'] = 'mock-client-id'; + backendEnv['SSO_GOOGLE_AUTH_URL'] = `${mockSSOHost}/auth`; + backendEnv['SSO_GOOGLE_TOKEN_INFO_URL'] = `${mockSSOHost}/tokeninfo`; +} + +log('Starting application...'); +await Promise.all([ + runTaskPrefixOutput({ + command: 'npm', + args: ['start', '--quiet'], + cwd: join(basedir, 'frontend'), + env: frontendEnv, + outputPrefix: 'frontend', + prefixFormat: '35', + }), + runTaskPrefixOutput({ + command: 'npm', + args: ['start', '--quiet'], + cwd: join(basedir, 'backend'), + env: backendEnv, + outputPrefix: 'backend', + prefixFormat: '36', + }), +]); diff --git a/scripts/start.sh b/scripts/start.sh deleted file mode 100755 index 0c8dfcc..0000000 --- a/scripts/start.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh -set -e; - -BASEDIR="$(dirname "$0")/.."; - -API_PORT="${PORT:-5000}"; -APP_PORT="$((API_PORT + 1))"; - -MOCK_SSO='true'; -if [ -n "$SSO_GOOGLE_CLIENT_ID" ] || [ -n "$SSO_GITHUB_CLIENT_ID" ] || [ -n "$SSO_GITLAB_CLIENT_ID" ]; then - MOCK_SSO='false'; -fi; -if echo " $* " | grep ' --mock-sso ' > /dev/null; then - MOCK_SSO='true'; -fi; - -if [ "$MOCK_SSO" == 'true' ]; then - echo 'Using mock authentication provider'; - MOCK_SSO_PORT="$((API_PORT + 2))"; - MOCK_SSO_HOST="http://localhost:$MOCK_SSO_PORT"; - export SSO_GOOGLE_CLIENT_ID='mock-client-id'; - export SSO_GOOGLE_AUTH_URL="$MOCK_SSO_HOST/auth"; - export SSO_GOOGLE_TOKEN_INFO_URL="$MOCK_SSO_HOST/tokeninfo"; -fi; - -echo 'Running frontend...'; - -# Continue with script after failure so everything closes down nicely -set +e; - -PORT="$APP_PORT" \ -npm --prefix="$BASEDIR/frontend" start --quiet & APP_PID="$!"; - -trap "kill '$APP_PID'; wait '$APP_PID' > /dev/null" EXIT; - -echo 'Running backend...'; - -PORT="$API_PORT" \ -FORWARD_HOST="http://localhost:$APP_PORT" \ -MOCK_SSO_PORT="$MOCK_SSO_PORT" \ -SERVER_BIND_ADDRESS="localhost" \ -npm --prefix="$BASEDIR/backend" start --quiet;