diff --git a/packages/portal-proto/next.config.js b/packages/portal-proto/next.config.js index aba814a10..f1f16d2ef 100644 --- a/packages/portal-proto/next.config.js +++ b/packages/portal-proto/next.config.js @@ -1,3 +1,19 @@ +// Optional config overrides supplied at runtime as a JSON object via +// NEXT_CONFIG_OVERRIDES. Its `turbopack`, `connectSrc`, and `env` properties are +// spread into the corresponding entries below (overrides take precedence). This +// env var is unset in normal `next dev`, `next build`, `next start`, and the +// Docker image, so the Data Portal build depends on no external config code. +// Local tooling can inject overrides — e.g. src/features/proteinpaint/dev.sh runs +// ppNextConfig.mjs to point Turbopack at a local proteinpaint client build. +let overrides = {}; +if (process.env.NEXT_CONFIG_OVERRIDES) { + try { + overrides = JSON.parse(process.env.NEXT_CONFIG_OVERRIDES); + } catch (e) { + throw new Error(`Invalid NEXT_CONFIG_OVERRIDES JSON: ${e.message}`); + } +} + /** * This basePath defines root of the application. This must match * the intended deployment path. For example, the basePath of "/v2" @@ -14,21 +30,9 @@ const connectSrc = [ "https://storage.googleapis.com/idc-index-data-artifacts/", // Uncomment to use mock server for testing //"https://localhost:3100", + ...(overrides.connectSrc || []), ]; -if (process.env.NODE_ENV == "development") { - // in SJ dev environment, this would point to a local PP server instance - const PROTEINPAINT_API = - process.env.PROTEINPAINT_API || - process.env.NEXT_PUBLIC_PROTEINPAINT_API || - ""; - const PROTEINPAINT_HOST = - PROTEINPAINT_API.split("://")[1]?.split("/")[0] || ""; - - if (PROTEINPAINT_HOST && !connectSrc.includes(`https://${PROTEINPAINT_HOST}`)) - connectSrc.push(`https://${PROTEINPAINT_HOST}`); -} - // Fallback if Docker is not run: This calls git directly const buildHash = () => { try { @@ -56,7 +60,6 @@ const cspHeader = ` frame-ancestors 'none'; upgrade-insecure-requests; `; - // @ts-check /** * @type {import('next').NextConfig} @@ -69,6 +72,7 @@ module.exports = { as: "*.js", }, }, + ...(overrides.turbopack || {}), }, i18n: { locales: ["en"], @@ -82,9 +86,12 @@ module.exports = { }, allowedDevOrigins: ["localhost.gdc.cancer.gov"], env: { - // passed via command line, `PROTEINPAINT_API=... npm run dev` - PROTEINPAINT_API: - process.env.PROTEINPAINT_API || process.env.NEXT_PUBLIC_PROTEINPAINT_API, + // e.g. PROTEINPAINT_API, supplied via NEXT_CONFIG_OVERRIDES.env or process.env + ...(overrides.env || { + PROTEINPAINT_API: + process.env.PROTEINPAINT_API || + process.env.NEXT_PUBLIC_PROTEINPAINT_API, + }), NEXT_PUBLIC_APP_VERSION: process.env.npm_package_version, // NEXT_PUBLIC_BUILD_SHORT_SHA is passed from gitlab to docker when docker is not run it tries to get it directly from git NEXT_PUBLIC_APP_HASH: diff --git a/packages/portal-proto/src/features/proteinpaint/dev.sh b/packages/portal-proto/src/features/proteinpaint/dev.sh index b9282717f..d2d1450f6 100755 --- a/packages/portal-proto/src/features/proteinpaint/dev.sh +++ b/packages/portal-proto/src/features/proteinpaint/dev.sh @@ -6,32 +6,47 @@ # assumes that the proteinpaint folder is a sibling dir of gff if [[ "$1" == "unlink" ]]; then - # to test the published client package before submitting a PR with an updated pp-client version - npm unlink ../proteinpaint/client + # to test the published client package before submitting a PR with an updated pp-client version. + # Clear any local-dev vars inherited from the shell so next.config.js does NOT apply the + # local client override — unlink mode must resolve the published package. + unset PP_CLIENT_DIST NEXT_CONFIG_OVERRIDES + npm uninstall @sjcrh/proteinpaint-client --save --workspace=packages/portal-proto npm install @sjcrh/proteinpaint-client --save --save-exact --workspace=packages/portal-proto + + # sometimes the nextjs bundle cache is stale after changing the client package + rm -rf packages/portal-proto/.next + + # run the following in a separate tab + # local-ssl-proxy --config ssl-proxy.json --cert localhost.pem --key localhost-key.pem + # then from the gff dir + PROTEINPAINT_API=https://localhost.gdc.cancer.gov:3011 PORT=3001 npm run dev else - # to test the local PP client code - - # !!! NOTE: turbopack used to find the @sjrch/proteintpaint under node_mmodules, but has stopped working !!! - # a temporary fix is to do the following: - # 1. run `npm run dev` from the sjpp or proteinpaint repo/directory - # 2. on rebundling from the client dir, run `cp -r dist ~/dev/gdc-frontend-framework/node_modules/"@sjcrh"/proteinpaint-client/` - # - npm unlink ../proteinpaint/client # change back to `npm link` when fixed - # An issue with npm link and workspaces: the non-linked @sjcrh/proteinpaint-client package - # may be moved to portal-proto/node_modules, creating 2 separate modules of the same package, - # must ensure only the linked module is used for bundling so delete - rm -rf packages/portal-proto/node_modules/@sjcrh - # also not able to do a simpler - # `cd packages/portal-proto && npm link path/to/proteinpaint/client`, - # where the linked module would be in portal-proto/node_modules instead of the - # other way around -fi + # to test the local PP client code: + # 1. build the client in watch mode from the sjpp repo which has proteinpaint as a submodule: + # cd ../../sjpp && npm run dev + # 2. ppNextConfig.mjs (run below) prints a NEXT_CONFIG_OVERRIDES JSON object that + # next.config.js spreads: `turbopack` points Turbopack at the local client dist, + # and `env`/`connectSrc` supply PROTEINPAINT_API and allow its host in the CSP + # (see next.config.js). No npm link or manual `cp -r dist ...` into node_modules is + # needed. A browser refresh after a client rebuild is usually enough to pick up changes. + # PP_CLIENT_DIST may point at the client repo root, its dist dir, or app.js itself. + # Resolve to an absolute path (via a subshell) so it is unaffected by the cwd + # that `lerna run dev` uses for the next process. + if ! PP_CLIENT_DIST="$(cd ../proteinpaint/client && pwd -P)"; then + echo "error: ../proteinpaint/client not found (expected sibling of this repo)" >&2 + exit 1 + fi + export PP_CLIENT_DIST + + # sometimes the nextjs bundle cache is stale after switching the client source + rm -rf packages/portal-proto/.next -# sometimes the nextjs bundle cache are stale after npm link -rm -rf packages/portal-proto/.next + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" -# run the following tab in a separate tab -# local-ssl-proxy --config ssl-proxy.json --cert localhost.pem --key localhost-key.pem -# then from the gff dir -PROTEINPAINT_API=https://localhost.gdc.cancer.gov:3011 PORT=3001 npm run dev + # run the following in a separate tab + # local-ssl-proxy --config ssl-proxy.json --cert localhost.pem --key localhost-key.pem + # then from the gff dir + PROTEINPAINT_API=https://localhost.gdc.cancer.gov:3011 NEXT_CONFIG_OVERRIDES="$("$SCRIPT_DIR/ppNextConfig.mjs")" \ + PORT=3001 npm run dev +fi diff --git a/packages/portal-proto/src/features/proteinpaint/ppNextConfig.mjs b/packages/portal-proto/src/features/proteinpaint/ppNextConfig.mjs new file mode 100755 index 000000000..e6ba6fcaa --- /dev/null +++ b/packages/portal-proto/src/features/proteinpaint/ppNextConfig.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +import path from "path"; +import { existsSync } from "fs"; +import { fileURLToPath } from "url"; + +const CLIENT_PKG = "@sjcrh/proteinpaint-client"; + +// Default proteinpaint API for local SJ development; override with PROTEINPAINT_API. +const DEFAULT_PROTEINPAINT_API = "https://localhost.gdc.cancer.gov:3011"; + +// Deepest directory shared by two absolute paths. +const commonAncestor = (a, b) => { + const as = a.split(path.sep); + const bs = b.split(path.sep); + const out = []; + for (let i = 0; i < Math.min(as.length, bs.length) && as[i] === bs[i]; i++) + out.push(as[i]); + return out.join(path.sep) || path.sep; +}; + +// Absolute path to the client's built entry (dist/app.js) from a base that may +// point at the client repo root, the dist directory, or app.js itself. Prefer +// whichever candidate exists; otherwise infer from the directory name so it also +// works before a first build. +const clientAppJs = (base) => { + const abs = path.resolve(base); + if (abs.endsWith(`${path.sep}app.js`)) return abs; + const asDistDir = path.join(abs, "app.js"); // base is .../client/dist + const asRepoRoot = path.join(abs, "dist", "app.js"); // base is .../client + if (existsSync(asRepoRoot)) return asRepoRoot; + if (existsSync(asDistDir)) return asDistDir; + return path.basename(abs) === "dist" ? asDistDir : asRepoRoot; +}; + +/** + * Turbopack config fragment that bundles a local proteinpaint/client build + * instead of the published proteinpaint-client dependency, replacing the old + * `npm link` + `cp -r dist` workflow that Turbopack no longer supports in a + * workspace. Nested under `turbopack` in the JSON printed by this CLI and passed + * to next.config.js via NEXT_CONFIG_OVERRIDES (see dev.sh), so the Data Portal + * build never depends on this dev-only module. + * + * - resolveAlias: aliased to dist/app.js (not the package dir) so its sibling + * chunk-*.js files resolve from the real repo dist and rebuilds are live. + * The value must be project-root-relative — Turbopack rejects absolute paths. + * - root: widened to the ancestor shared with the client, since the client + * typically lives outside this repo and Turbopack won't resolve files outside + * its root. + * + * @param projectDir - absolute path to the Next project root (the directory + * containing next.config.js). + * @param clientDir - path to the local client (repo root, dist dir, or app.js). + * @returns a fragment to spread into next.config.js `turbopack`. + */ +export function turbopackConfig(projectDir, clientDir) { + if (!clientDir) return {}; + + const appJs = clientAppJs(clientDir); + let rel = path.relative(projectDir, appJs); + if (!rel.startsWith(".")) rel = "./" + rel; + + return { + root: commonAncestor(projectDir, appJs), + resolveAlias: { [CLIENT_PKG]: rel }, + }; +} + +// CLI: print a NEXT_CONFIG_OVERRIDES JSON object for next.config.js, e.g. +// NEXT_CONFIG_OVERRIDES="$(node ppNextConfig.mjs)" npm run dev +// It supplies everything local proteinpaint development needs: +// - turbopack: alias to the local client build (PP_CLIENT_DIST, else the +// conventional sibling ../proteinpaint/client) +// - env: PROTEINPAINT_API (PROTEINPAINT_API env, else the SJ dev default) +// - connectSrc: the API host, added to the CSP connect-src directive +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); + const projectDir = path.resolve(scriptDir, "../../.."); // packages/portal-proto + const clientDir = + process.env.PP_CLIENT_DIST || + path.resolve(projectDir, "../../../proteinpaint/client"); + + const proteinpaintApi = + process.env.PROTEINPAINT_API || DEFAULT_PROTEINPAINT_API; + const apiHost = proteinpaintApi.split("://")[1]?.split("/")[0] || ""; + + process.stdout.write( + JSON.stringify({ + turbopack: turbopackConfig(projectDir, clientDir), + connectSrc: apiHost ? [`https://${apiHost}`] : [], + env: { PROTEINPAINT_API: proteinpaintApi }, + }), + ); +}