From 97b16fb4d77af20918ba1cd604d66dba55b4eb88 Mon Sep 17 00:00:00 2001 From: eybbus Date: Wed, 11 Nov 2026 12:58:27 +0100 Subject: [PATCH 1/5] build widget next script --- scripts/build-docker.sh | 5 +++++ scripts/build-widget-next.sh | 27 +++++++++++++++++++++++++++ scripts/generate-widget-stat.js | 5 ++++- scripts/release-widgets.sh | 4 +++- 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 scripts/build-widget-next.sh diff --git a/scripts/build-docker.sh b/scripts/build-docker.sh index 38b18a88e..a0bb4564f 100644 --- a/scripts/build-docker.sh +++ b/scripts/build-docker.sh @@ -13,6 +13,11 @@ mkdir -p dist mkdir -p dist/$NEXT_PUBLIC_PLANNER_ORG_ID echo "Running pnpm setup && pnpm build for $NEXT_PUBLIC_PLANNER_ORG_ID" pnpm setup + +if [ "$NEXT_PUBLIC_ENVIRONMENT" != "prod" ]; then + bash ./scripts/build-widget-next.sh +fi + pnpm build echo "Moving the output into the dist directory" diff --git a/scripts/build-widget-next.sh b/scripts/build-widget-next.sh new file mode 100644 index 000000000..1ae96e09a --- /dev/null +++ b/scripts/build-widget-next.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Builds the unreleased `next` widget for the current org from the current +# source, into public/widget//next. It is not checked in and not added to +# the manifests. Used for non-production builds (see build-docker.sh), so widget +# changes can be tested before they are released. + +set -e + +if [ -z "$NEXT_PUBLIC_PLANNER_ORG_ID" ]; then + echo "No NEXT_PUBLIC_PLANNER_ORG_ID found in environment." + exit 1 +fi + +echo "Building next widget for $NEXT_PUBLIC_PLANNER_ORG_ID" +WIDGET_VERSION=next pnpm build:widget + +ORG_DIR=$(node -p "require('lz-string').compressToEncodedURIComponent(process.env.NEXT_PUBLIC_PLANNER_ORG_ID)") +CSS_FILE="public/widget/$ORG_DIR/next/planner-web.css" + +# Same PostCSS workaround as in build-widget.sh +npx postcss --config . "$CSS_FILE" -o "$CSS_FILE" + +if grep -q 'token(' "$CSS_FILE"; then + echo "Unprocessed token() found in $CSS_FILE" + exit 1 +fi diff --git a/scripts/generate-widget-stat.js b/scripts/generate-widget-stat.js index 0fe10bd38..3899d3244 100644 --- a/scripts/generate-widget-stat.js +++ b/scripts/generate-widget-stat.js @@ -45,7 +45,10 @@ async function getExistingCreated(manifestFile) { async function getWidgetData(existingCreated) { const baseUrl = join(process.cwd(), '/public/widget', compressedOrgId); - const dirs = await readdir(baseUrl); + // Only released versions, not e.g. the unreleased `next` build. + const dirs = (await readdir(baseUrl)).filter((dir) => + /^\d+\.\d+\.\d+$/.test(dir), + ); // return all files from /public/widget based on dirs const allP = dirs.map(async (dir) => { diff --git a/scripts/release-widgets.sh b/scripts/release-widgets.sh index 68ef0dcd4..4aa1dd408 100644 --- a/scripts/release-widgets.sh +++ b/scripts/release-widgets.sh @@ -38,7 +38,9 @@ for ORG_DIR in "$BASE_DIR"/*/; do fi done - PREVIOUS=$(ls "$ORG_DIR" | grep -vxF "$VERSION" | sort -V | tail -n 1) + # Only released versions, not e.g. the unreleased `next` build + PREVIOUS=$(ls "$ORG_DIR" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | + grep -vxF "$VERSION" | sort -V | tail -n 1) if [[ -z "$PREVIOUS" ]]; then echo "$ORG_DIR: no previous version" CHANGED=true From 2a1e4ac72e3d6bc2a8a43e1505e2fac59e666e83 Mon Sep 17 00:00:00 2001 From: eybbus Date: Wed, 11 Nov 2026 12:58:55 +0100 Subject: [PATCH 2/5] feat: next widget version --- .gitignore | 3 ++ next.config.js | 31 +++++++++++- package.json | 1 + src/page-modules/widget/index.ts | 6 ++- src/page-modules/widget/server/index.ts | 21 +++++++- src/page-modules/widget/widget.module.css | 8 ++++ src/pages/widget/index.tsx | 58 +++++++++++++++++++++-- src/widget/vite.config.js | 6 ++- 8 files changed, 125 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 786e6cc75..c08230023 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,9 @@ src/components/icon/generated-icons.ts # Widget screenshots from `pnpm screenshot-widgets` /widget-screenshots/ +# Unreleased widget build from `pnpm generate-widget-next` +/public/widget/*/next/ + # Widget build used by `pnpm size:widget` /.widget-size/ diff --git a/next.config.js b/next.config.js index 9c2e698c8..4aa356c52 100644 --- a/next.config.js +++ b/next.config.js @@ -1,8 +1,28 @@ /** @type {import('next').NextConfig} */ const { version } = require('./package.json'); +const { existsSync } = require('fs'); +const { join } = require('path'); +const { compressToEncodedURIComponent } = require('lz-string'); + const orgId = process.env.NEXT_PUBLIC_PLANNER_ORG_ID; +/** + * Whether the unreleased `next` widget (scripts/build-widget-next.sh) was built + * before this build. Read at build time, as it is never built for production. + */ +function hasNextWidget() { + if (!orgId) return false; + return existsSync( + join( + __dirname, + 'public/widget', + compressToEncodedURIComponent(orgId), + 'next/planner-web.umd.js', + ), + ); +} + /** Sites allowed to show /embed pages in an iframe. */ function getEmbedFrameAncestors() { const orgData = orgId ? require(`./orgs/${orgId}.json`) : undefined; @@ -19,7 +39,10 @@ function getEmbedFrameAncestors() { */ const nextConfig = { output: 'standalone', - env: { PLANNER_WEB_VERSION: version }, + env: { + PLANNER_WEB_VERSION: version, + WIDGET_NEXT_AVAILABLE: hasNextWidget() ? 'true' : '', + }, transpilePackages: ['@atb-as/mapbox-shared', '@mrfylke/contact-form'], // Emit source maps for the server build so production stack traces resolve to original TypeScript experimental: { serverSourceMaps: true }, @@ -37,6 +60,12 @@ const nextConfig = { }, ], }, + // The `next` widget is rebuilt on every deploy under the same URL, so it + // must not be cached. Has to come after the rule above to override it. + { + source: '/widget/:org/next/:path*', + headers: [{ key: 'Cache-Control', value: 'no-cache' }], + }, { source: '/embed/:path+', headers: [ diff --git a/package.json b/package.json index 168c1be47..69cd7b761 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "prestart": "pnpm generate", "predev": "pnpm generate", "generate-widget": "dotenv -e .env.local -- sh ./scripts/build-widget.sh", + "generate-widget-next": "dotenv -e .env.local -- bash ./scripts/build-widget-next.sh", "generate-all-widgets": "sh ./scripts/build-all-widgets.sh", "screenshot-widgets": "sh ./scripts/screenshot-widgets.sh", "size:widget": "sh ./scripts/build-widget-size.sh && size-limit", diff --git a/src/page-modules/widget/index.ts b/src/page-modules/widget/index.ts index b88fc94b9..d12e02e3e 100644 --- a/src/page-modules/widget/index.ts +++ b/src/page-modules/widget/index.ts @@ -1,2 +1,6 @@ export { CopyMarkup, CopyMarkupLarge } from './copy-markup'; -export type { PlannerWidgetData, PlannerModule } from './server'; +export type { + PlannerWidgetData, + PlannerModule, + UnreleasedPlannerModule, +} from './server'; diff --git a/src/page-modules/widget/server/index.ts b/src/page-modules/widget/server/index.ts index ba7435cec..ff2fae941 100644 --- a/src/page-modules/widget/server/index.ts +++ b/src/page-modules/widget/server/index.ts @@ -4,8 +4,15 @@ import { compressToEncodedURIComponent } from 'lz-string'; export type PlannerWidgetData = { latest: PlannerModule; all: PlannerModule[]; + /** + * Unreleased widget built from the current source. Only in non-production + * builds, see scripts/build-widget-next.sh. + */ + next?: UnreleasedPlannerModule; }; +export type UnreleasedPlannerModule = Pick; + export type PlannerModule = { version: string; created: string; @@ -26,5 +33,17 @@ export async function getWidgetData(): Promise { `../available-widgets/${compressedOrgId}/manifest.json` ); - return data.default as PlannerWidgetData; + const widgetData = data.default as PlannerWidgetData; + if (!process.env.WIDGET_NEXT_AVAILABLE) return widgetData; + + return { ...widgetData, next: nextWidget }; } + +const nextWidget: UnreleasedPlannerModule = { + version: 'next', + urls: { + umd: `/widget/${compressedOrgId}/next/planner-web.umd.js`, + esm: `/widget/${compressedOrgId}/next/planner-web.mjs`, + css: `/widget/${compressedOrgId}/next/planner-web.css`, + }, +}; diff --git a/src/page-modules/widget/widget.module.css b/src/page-modules/widget/widget.module.css index c56b1ef15..0fe16d2c0 100644 --- a/src/page-modules/widget/widget.module.css +++ b/src/page-modules/widget/widget.module.css @@ -15,6 +15,14 @@ margin-bottom: token('spacing.medium'); } +.nextWidgetInfo { + margin-top: token('spacing.medium'); + padding: token('spacing.medium'); + border-radius: token('border.radius.regular'); + background-color: token('color.status.info.secondary.background'); + color: token('color.status.info.secondary.foreground.primary'); +} + .main details summary { cursor: pointer; margin: token('spacing.medium') 0; diff --git a/src/pages/widget/index.tsx b/src/pages/widget/index.tsx index 9d25ca3c2..ccf4cdf61 100644 --- a/src/pages/widget/index.tsx +++ b/src/pages/widget/index.tsx @@ -4,6 +4,7 @@ import { CopyMarkup, CopyMarkupLarge, type PlannerWidgetData, + type UnreleasedPlannerModule, } from '@atb/page-modules/widget'; import type { NextPage } from 'next'; import Head from 'next/head'; @@ -20,6 +21,8 @@ import { withAccessLogging } from '@atb/modules/logging'; type WidgetPagePropsContent = { data: PlannerWidgetData; + /** The widget version shown in the demo. */ + active: UnreleasedPlannerModule; }; export type WidgetPageProps = WithGlobalData; @@ -104,7 +107,7 @@ const outputCodeExample2 = html` `; -const WidgetPage: NextPage = ({ data, ...props }) => { +const WidgetPage: NextPage = ({ data, active, ...props }) => { const [isLoaded, setLoaded] = useState(false); const [html, setHtml] = useState(''); const lib = useRef(null); @@ -119,10 +122,10 @@ const WidgetPage: NextPage = ({ data, ...props }) => { {/* eslint-disable-next-line @next/next/no-css-tags */} - +