Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/

33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,35 @@ Check that NEXT_PUBLIC_PLANNER_ORG_ID is set in your .env.local file, and run
pnpm generate-widget
```

This will place asset inside `public/widget/<VERSION>` which will be reachable
through `http://localhost:3000/widget` or
`http://localhost:3000/widget/<VERSION>` (omitting the version will default to
the most recent one).
This will place assets inside `public/widget/<ORG>/<VERSION>`, using the version
from `package.json`, and add the version to the manifest in
`src/page-modules/widget/available-widgets`. The widget is then reachable
through `http://localhost:3000/widget`, or in fullscreen through
`http://localhost:3000/widget/<VERSION>`.

`/widget` shows the most recent version, unless the unreleased widget has been
built (see below).

### Testing unreleased widget changes

New widget versions are only generated on the release PR. Until then, the latest
widget version does not include widget changes merged to `main`, so they can't
be tested on staging. Staging therefore also has an unreleased widget, built
from the current code on every deploy:

- `/widget` shows the unreleased widget, with a link to switch to the latest
released version.
- `/widget/next` shows it in fullscreen.

It is built as the version `next` into `public/widget/<ORG>/next`. This folder
is not checked in, not listed in the manifests and not built for production, so
`next` should never be used in integrations.

To get it locally, run this and restart `pnpm dev`:

```
pnpm generate-widget-next
```

### Releases and Widget Development

Expand Down
31 changes: 30 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 },
Expand All @@ -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: [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 -- sh ./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",
Expand Down
5 changes: 5 additions & 0 deletions scripts/build-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
sh ./scripts/build-widget-next.sh
fi

pnpm build

echo "Moving the output into the dist directory"
Expand Down
27 changes: 27 additions & 0 deletions scripts/build-widget-next.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

# Builds the unreleased `next` widget for the current org from the current
# source, into public/widget/<org>/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
5 changes: 4 additions & 1 deletion scripts/generate-widget-stat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
4 changes: 3 additions & 1 deletion scripts/release-widgets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/page-modules/widget/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export { CopyMarkup, CopyMarkupLarge } from './copy-markup';
export type { PlannerWidgetData, PlannerModule } from './server';
export type {
PlannerWidgetData,
PlannerModule,
UnreleasedPlannerModule,
} from './server';
21 changes: 20 additions & 1 deletion src/page-modules/widget/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlannerModule, 'version' | 'urls'>;

export type PlannerModule = {
version: string;
created: string;
Expand All @@ -26,5 +33,17 @@ export async function getWidgetData(): Promise<PlannerWidgetData> {
`../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`,
},
};
8 changes: 8 additions & 0 deletions src/page-modules/widget/widget.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
58 changes: 54 additions & 4 deletions src/pages/widget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<WidgetPagePropsContent>;

Expand Down Expand Up @@ -104,7 +107,7 @@ const outputCodeExample2 = html`
</script>
`;

const WidgetPage: NextPage<WidgetPageProps> = ({ data, ...props }) => {
const WidgetPage: NextPage<WidgetPageProps> = ({ data, active, ...props }) => {
const [isLoaded, setLoaded] = useState(false);
const [html, setHtml] = useState('');
const lib = useRef<PlannerWebOutput | null>(null);
Expand All @@ -119,10 +122,10 @@ const WidgetPage: NextPage<WidgetPageProps> = ({ data, ...props }) => {
<DefaultLayout {...props}>
<Head>
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href={data.latest.urls.css} />
<link rel="stylesheet" href={active.urls.css} />
</Head>
<Script
src={data.latest.urls.umd}
src={active.urls.umd}
Comment thread
eybbus marked this conversation as resolved.
Resolved
strategy="lazyOnload"
onLoad={() => {
setLoaded(true);
Expand All @@ -143,6 +146,8 @@ const WidgetPage: NextPage<WidgetPageProps> = ({ data, ...props }) => {
<main className={style.main}>
<h1>Widget documentation</h1>

{data.next && <NextWidgetInfo data={data} active={active} />}

{!isLoaded && <p>Loading...</p>}

{isLoaded && lib.current && (
Expand All @@ -155,18 +160,63 @@ const WidgetPage: NextPage<WidgetPageProps> = ({ data, ...props }) => {

export default WidgetPage;

/**
* Shows the "next" version by default. On testing envs only if available
*/
export const getServerSideProps = withAccessLogging(
withGlobalData<WidgetPagePropsContent>(async function () {
withGlobalData<WidgetPagePropsContent>(async function ({ query }) {
const data = await getWidgetData();

const showNext = data.next && query.version !== RELEASED_QUERY_VALUE;

return {
props: {
data,
active: showNext && data.next ? data.next : data.latest,
},
};
}),
);

const RELEASED_QUERY_VALUE = 'released';

/**
* Shown where the unreleased `next` widget exists (not in production). Uses
* plain links to reload the page, as two widget versions can't be loaded at
* the same time.
*/
function NextWidgetInfo({
data,
active,
}: {
data: PlannerWidgetData;
active: UnreleasedPlannerModule;
}) {
const isNext = active.version === data.next?.version;
const buildId = process.env.NEXT_PUBLIC_BUILD_ID;

return (
<p className={style.nextWidgetInfo}>
{isNext ? (
<>
<strong>Unreleased widget</strong>
{buildId ? ` (${buildId.slice(0, 8)})` : ''}.{' '}
<a href={`/widget?version=${RELEASED_QUERY_VALUE}`}>
Show v{data.latest.version}
</a>
</>
) : (
<>
<strong>Released widget v{data.latest.version}</strong>.{' '}
{/* Needs a full page load to swap the widget script. */}
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
<a href="/widget">Show unreleased</a>
</>
)}
</p>
);
}

function WidgetContent({
html,
lib,
Expand Down
5 changes: 3 additions & 2 deletions src/widget/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ if (!orgId) {
}
const compressedOrgId = compressToEncodedURIComponent(orgId);

const widgetVersion = process.env.WIDGET_VERSION || version;
const ROOT_DIR = resolve(__dirname, '../..');
const CSS_FILE_NAME = 'planner-web.css';
// `token(` as a function call, not as the end of another name like `my-token(`.
Expand Down Expand Up @@ -89,7 +90,7 @@ export default defineConfig({
],
define: {
'process.env': {
MODULE_VERSION: version,
MODULE_VERSION: widgetVersion,
COMPRESSED_ORG: compressedOrgId,
ORG_ID: orgId,
},
Expand All @@ -104,7 +105,7 @@ export default defineConfig({
},
outDir: resolve(
__dirname,
`../../public/widget/${compressedOrgId}/${version}`,
`../../public/widget/${compressedOrgId}/${widgetVersion}`,
),
rollupOptions: {
output: {
Expand Down
Loading