diff --git a/build-tools/tasks/generate-environment.js b/build-tools/tasks/generate-environment.js
index f43ecd1b4a..6a12bac23c 100644
--- a/build-tools/tasks/generate-environment.js
+++ b/build-tools/tasks/generate-environment.js
@@ -1,45 +1,69 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
+
+// Generates the `internal/environment` files for each theme.
+// For every theme we write the same set of values in three forms:
+// - environment.json raw values
+// - environment.js runtime module, e.g. `export var THEME = "default";`
+// - environment.d.ts type declarations, e.g. `export const THEME: string;`
+
const path = require('path');
const { writeFile } = require('../utils/files');
const themes = require('../utils/themes');
const workspace = require('../utils/workspace');
const ALWAYS_VISUAL_REFRESH = process.env.ALWAYS_VISUAL_REFRESH === 'true';
-const INCLUDE_ONE_THEME = process.env.INCLUDE_ONE_THEME === 'true';
-function writeEnvironmentFile(theme) {
- const filepath = 'internal/environment';
- const values = {
+// The build-time constants exposed to the component source for a given theme.
+function getEnvironmentValues(theme) {
+ return {
PACKAGE_SOURCE: workspace.packageSource,
PACKAGE_VERSION: workspace.packageVersion,
GIT_SHA: workspace.gitCommitVersion,
THEME: theme.name,
SYSTEM: 'core',
ALWAYS_VISUAL_REFRESH: !!theme.alwaysVisualRefresh || ALWAYS_VISUAL_REFRESH,
- INCLUDE_ONE_THEME: INCLUDE_ONE_THEME,
+ INCLUDED_THEMES: theme.includedThemes ?? [],
};
- const basePath = path.join(theme.outputPath, filepath);
+}
+
+function toTypeScriptType(value) {
+ if (Array.isArray(value)) {
+ return 'string[]';
+ }
+ if (typeof value === 'boolean') {
+ return 'boolean';
+ }
+ return 'string';
+}
+
+// Source for environment.js, e.g. `export var THEME = "default";`
+function toModuleSource(values) {
+ return Object.entries(values)
+ .map(([name, value]) => `export var ${name} = ${JSON.stringify(value)};`)
+ .join('\n');
+}
+
+// Source for environment.d.ts, e.g. `export const THEME: string;`
+function toDeclarationsSource(values) {
+ return Object.entries(values)
+ .map(([name, value]) => `export const ${name}: ${toTypeScriptType(value)};`)
+ .join('\n');
+}
+
+function writeEnvironmentFile(theme) {
+ const values = getEnvironmentValues(theme);
+ const basePath = path.join(theme.outputPath, 'internal/environment');
console.log('Environment values\n', JSON.stringify(values, null, 2));
writeFile(`${basePath}.json`, JSON.stringify(values, null, 2));
- writeFile(
- `${basePath}.js`,
- Object.entries(values)
- .map(([key, value]) => `export var ${key} = ${JSON.stringify(value)};`)
- .join('\n')
- );
- writeFile(
- `${basePath}.d.ts`,
- Object.keys(values)
- .map(key => `export const ${key}: string;`)
- .join('\n')
- );
+ writeFile(`${basePath}.js`, toModuleSource(values));
+ writeFile(`${basePath}.d.ts`, toDeclarationsSource(values));
}
module.exports = function generateEnvironment() {
- themes.forEach(theme => writeEnvironmentFile(theme));
+ themes.forEach(writeEnvironmentFile);
return Promise.resolve();
};
diff --git a/build-tools/utils/themes.js b/build-tools/utils/themes.js
index 6564bf32d4..0817937478 100644
--- a/build-tools/utils/themes.js
+++ b/build-tools/utils/themes.js
@@ -3,7 +3,36 @@
const path = require('path');
const workspace = require('./workspace');
-const INCLUDE_ONE_THEME = process.env.INCLUDE_ONE_THEME === 'true';
+// Secondary themes are layered on top of the primary theme as opt-in overrides. Each entry maps a
+// theme id to its style-dictionary entry point. To add a new theme, register it here, then include
+// it via the THEMES env var.
+const SECONDARY_THEME_PATHS = {
+ 'visual-refresh': './visual-refresh-secondary/index.js',
+ 'one-theme': './one-theme/index.js',
+};
+
+// Secondary themes included when THEMES is not set. Keeps visual refresh in, one-theme opt-in.
+const DEFAULT_THEMES = 'visual-refresh';
+
+// Resolves which secondary themes to compile from the comma-separated THEMES env var, e.g.
+// `THEMES=visual-refresh,one-theme`. Throws if a requested theme is not registered above.
+function resolveIncludedThemes() {
+ const themeIds = (process.env.THEMES ?? DEFAULT_THEMES)
+ .split(',')
+ .map(id => id.trim())
+ .filter(Boolean);
+
+ for (const id of themeIds) {
+ if (!SECONDARY_THEME_PATHS[id]) {
+ const availableThemes = Object.keys(SECONDARY_THEME_PATHS).join(', ');
+ throw new Error(`Unknown theme "${id}" in THEMES env var. Available themes: ${availableThemes}.`);
+ }
+ }
+
+ return themeIds;
+}
+
+const includedThemes = resolveIncludedThemes();
const themes = [
// This is the default Cloudscape theme, which is best used with Visual Refresh enabled (by default)
@@ -15,10 +44,8 @@ const themes = [
designTokensPackageJson: { name: '@cloudscape-design/design-tokens' },
outputPath: path.join(workspace.targetPath, 'components'),
primaryThemePath: './classic/index.js',
- secondaryThemePaths: [
- './visual-refresh-secondary/index.js',
- ...(INCLUDE_ONE_THEME ? ['./one-theme/index.js'] : []),
- ],
+ includedThemes,
+ secondaryThemePaths: includedThemes.map(id => SECONDARY_THEME_PATHS[id]),
},
];
diff --git a/pages/app/app-context.tsx b/pages/app/app-context.tsx
index 4807fdf5fe..634c9b9325 100644
--- a/pages/app/app-context.tsx
+++ b/pages/app/app-context.tsx
@@ -12,7 +12,7 @@ interface AppUrlParams {
density: Density;
direction: 'ltr' | 'rtl';
visualRefresh: boolean;
- oneTheme: boolean;
+ theme?: string;
motionDisabled: boolean;
appLayoutWidget: boolean;
mode?: Mode;
@@ -33,7 +33,6 @@ const appContextDefaults: AppContextType = {
density: Density.Comfortable,
direction: 'ltr',
visualRefresh: THEME === 'default',
- oneTheme: false,
motionDisabled: false,
appLayoutWidget: false,
},
diff --git a/pages/app/components/header.tsx b/pages/app/components/header.tsx
index a8ddc58445..b99df8af24 100644
--- a/pages/app/components/header.tsx
+++ b/pages/app/components/header.tsx
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useContext } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, useLocation } from 'react-router-dom';
import clsx from 'clsx';
import AppContext from '../app-context';
@@ -11,11 +11,13 @@ import styles from './header.scss';
export default function Header({ sticky }: { sticky?: boolean }) {
const { mode } = useContext(AppContext);
+ // Preserve the query string (theme, density, motion, …) when navigating back to the index.
+ const { search } = useLocation();
return (
<>
{/* #h selector for compatibility with global navigation */}