Skip to content

Commit 96bd727

Browse files
ambrauerart-alexeyenkoilliakovalenko
authored
[Next.js][Multi-site] Fetch site info during build (#1276)
* GraphQL site info service (#1227) * GraphQL site info service * some tweaks * more tweaks: debug, query * query fix for xm cloud * move template id to contstants * [Next.js][Multi-site] Dynamic site resolver (#1224) * [Next.js][Multi-site] Dynamic site resolver * Use SiteInfo from graphql-siteinfo-service Co-authored-by: Adam Brauer <[email protected]> * Plugin approach for JSS config generation. Added multisite plugin to set the "sites" prop. * Standardized comments in config plugins. Misc fixes. * Fixed tests and relocated headlessSiteGroupingTemplate constant (since this query is temp anyway) * Moved defaultLanguage of 'en' to defaultConfig * Simplify config plugin order. Delete multisite-sample.ts Co-authored-by: Artem Alexeyenko <[email protected]> Co-authored-by: Illia Kovalenko <[email protected]>
1 parent a75a453 commit 96bd727

File tree

18 files changed

+505
-95
lines changed

18 files changed

+505
-95
lines changed

packages/create-sitecore-jss/src/templates/nextjs-multisite/multisite-sample.ts

-1
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'dotenv/config';
2+
import chalk from 'chalk';
3+
import { ConfigPlugin, JssConfig } from '..';
4+
import { GraphQLSiteInfoService, SiteInfo } from '@sitecore-jss/sitecore-jss-nextjs';
5+
6+
/**
7+
* This plugin will set the "sites" config prop.
8+
* By default this will attempt to fetch site information directly from Sitecore (using the GraphQLSiteInfoService).
9+
* You could easily modify this to fetch from another source such as a static JSON file instead.
10+
*/
11+
class MultisitePlugin implements ConfigPlugin {
12+
order = 3;
13+
14+
async exec(config: JssConfig) {
15+
let sites: SiteInfo[] = [];
16+
const endpoint = process.env.GRAPH_QL_ENDPOINT || config.graphQLEndpoint;
17+
const apiKey = process.env.SITECORE_API_KEY || config.sitecoreApiKey;
18+
19+
if (!endpoint || !apiKey) {
20+
console.warn(
21+
chalk.yellow('Skipping site information fetch (missing GraphQL connection details)')
22+
);
23+
} else {
24+
console.log(`Fetching site information from ${endpoint}`);
25+
try {
26+
const siteInfoService = new GraphQLSiteInfoService({
27+
endpoint,
28+
apiKey,
29+
});
30+
sites = await siteInfoService.fetchSiteInfo();
31+
} catch (error) {
32+
console.error(chalk.red('Error fetching site information'));
33+
console.error(error);
34+
}
35+
}
36+
37+
return Object.assign({}, config, {
38+
sites: JSON.stringify(sites),
39+
});
40+
}
41+
}
42+
43+
export const multisitePlugin = new MultisitePlugin();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import 'dotenv/config';
2+
import chalk from 'chalk';
3+
import { constants } from '@sitecore-jss/sitecore-jss-nextjs';
4+
import { ConfigPlugin, JssConfig } from '..';
5+
6+
/**
7+
* This plugin will override the "sitecoreApiHost" config prop
8+
* for disconnected mode (using disconnected server).
9+
*/
10+
class DisconnectedPlugin implements ConfigPlugin {
11+
order = 3;
12+
13+
async exec(config: JssConfig) {
14+
const disconnected = process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED;
15+
16+
if (!disconnected) return config;
17+
18+
if (process.env.FETCH_WITH === constants.FETCH_WITH.GRAPHQL) {
19+
throw new Error(
20+
chalk.red(
21+
'GraphQL requests to Dictionary and Layout services are not supported in disconnected mode.'
22+
)
23+
);
24+
}
25+
26+
const port = process.env.PORT || 3000;
27+
28+
return Object.assign({}, config, {
29+
sitecoreApiHost: `http://localhost:${port}`,
30+
});
31+
}
32+
}
33+
34+
export const disconnectedPlugin = new DisconnectedPlugin();

packages/create-sitecore-jss/src/templates/nextjs/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"npm-run-all": "~4.1.5",
6868
"prettier": "^2.1.2",
6969
"ts-node": "^9.0.0",
70+
"tsconfig-paths": "^4.1.1",
7071
"typescript": "~4.3.5",
7172
"yaml-loader": "^0.6.0"
7273
},
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,21 @@
1-
import 'dotenv/config';
2-
import { generateConfig } from './generate-config';
3-
import { constants } from '@sitecore-jss/sitecore-jss-nextjs';
4-
import chalk from 'chalk';
51
/*
62
BOOTSTRAPPING
73
The bootstrap process runs before build, and generates JS that needs to be
8-
included into the build - specifically, the component name to component mapping,
9-
and the global config module.
4+
included into the build - specifically, plugins, the global config module,
5+
and the component name to component mapping.
106
*/
117

12-
const disconnected = process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED;
138
/*
14-
CONFIG GENERATION
15-
Generates the /src/temp/config.js file which contains runtime configuration
16-
that the app can import and use.
9+
PLUGINS GENERATION
1710
*/
18-
const port = process.env.PORT || 3000;
19-
const configOverride: { [key: string]: string } = {};
20-
if (disconnected) {
21-
if (process.env.FETCH_WITH === constants.FETCH_WITH.GRAPHQL) {
22-
throw new Error(
23-
chalk.red(
24-
'GraphQL requests to Dictionary and Layout services are not supported in disconnected mode.'
25-
)
26-
);
27-
}
28-
configOverride.sitecoreApiHost = `http://localhost:${port}`;
29-
}
30-
31-
generateConfig(configOverride);
11+
import './generate-plugins';
3212

3313
/*
34-
COMPONENT FACTORY GENERATION
14+
CONFIG GENERATION
3515
*/
36-
import './generate-component-factory';
16+
import './generate-config';
3717

3818
/*
39-
PLUGINS GENERATION
19+
COMPONENT FACTORY GENERATION
4020
*/
41-
import './generate-plugins';
21+
import './generate-component-factory';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// eslint-disable-next-line @typescript-eslint/no-var-requires
2+
const plugins = require('scripts/temp/config-plugins');
3+
4+
/**
5+
* JSS configuration object
6+
*/
7+
export interface JssConfig extends Record<string, string | undefined> {
8+
sitecoreApiKey?: string;
9+
sitecoreApiHost?: string;
10+
jssAppName?: string;
11+
graphQLEndpointPath?: string;
12+
defaultLanguage?: string;
13+
graphQLEndpoint?: string;
14+
}
15+
16+
export interface ConfigPlugin {
17+
/**
18+
* Detect order when the plugin should be called, e.g. 0 - will be called first (can be a plugin which data is required for other plugins)
19+
*/
20+
order: number;
21+
/**
22+
* A function which will be called during config generation
23+
* @param {JssConfig} config Current (accumulated) config
24+
*/
25+
exec(config: JssConfig): Promise<JssConfig>;
26+
}
27+
28+
export class JssConfigFactory {
29+
public async create(defaultConfig: JssConfig = {}): Promise<JssConfig> {
30+
return (Object.values(plugins) as ConfigPlugin[])
31+
.sort((p1, p2) => p1.order - p2.order)
32+
.reduce(
33+
(promise, plugin) => promise.then((config) => plugin.exec(config)),
34+
Promise.resolve(defaultConfig)
35+
);
36+
}
37+
}
38+
39+
export const jssConfigFactory = new JssConfigFactory();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ConfigPlugin, JssConfig } from '..';
2+
3+
/**
4+
* This plugin will set computed config props.
5+
* The "graphQLEndpoint" is an example of making a _computed_ config setting
6+
* based on other config settings.
7+
*/
8+
class ComputedPlugin implements ConfigPlugin {
9+
order = 2;
10+
11+
async exec(config: JssConfig) {
12+
return Object.assign({}, config, {
13+
graphQLEndpoint: `${config.sitecoreApiHost}${config.graphQLEndpointPath}`,
14+
});
15+
}
16+
}
17+
18+
export const computedPlugin = new ComputedPlugin();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ConfigPlugin, JssConfig } from '..';
2+
import packageConfig from 'package.json';
3+
4+
/**
5+
* This plugin will set config props based on package.json.
6+
*/
7+
class PackageJsonPlugin implements ConfigPlugin {
8+
order = 1;
9+
10+
async exec(config: JssConfig) {
11+
if (!packageConfig.config) return config;
12+
13+
return Object.assign({}, config, {
14+
jssAppName: packageConfig.config.appName,
15+
graphQLEndpointPath: packageConfig.config.graphQLEndpointPath,
16+
defaultLanguage: packageConfig.config.language,
17+
});
18+
}
19+
}
20+
21+
export const packageJsonPlugin = new PackageJsonPlugin();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ConfigPlugin, JssConfig } from '..';
2+
3+
/**
4+
* This plugin will set config props based on scjssconfig.json.
5+
* scjssconfig.json may not exist if you've never run `jss setup` (development)
6+
* or are depending on environment variables instead (production).
7+
*/
8+
class ScJssConfigPlugin implements ConfigPlugin {
9+
order = 1;
10+
11+
async exec(config: JssConfig) {
12+
let scJssConfig;
13+
try {
14+
scJssConfig = require('scjssconfig.json');
15+
} catch (e) {
16+
return config;
17+
}
18+
19+
if (!scJssConfig) return config;
20+
21+
return Object.assign({}, config, {
22+
sitecoreApiKey: scJssConfig.sitecore?.apiKey,
23+
sitecoreApiHost: scJssConfig.sitecore?.layoutServiceHost,
24+
});
25+
}
26+
}
27+
28+
export const scjssconfigPlugin = new ScJssConfigPlugin();
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,59 @@
11
import fs from 'fs';
22
import path from 'path';
33
import { constantCase } from 'constant-case';
4-
import packageConfig from '../package.json';
4+
import { JssConfig, jssConfigFactory } from './config';
55

6-
/* eslint-disable no-console */
6+
/*
7+
CONFIG GENERATION
8+
Generates the /src/temp/config.js file which contains runtime configuration
9+
that the app can import and use.
10+
*/
711

8-
/**
9-
* Generate config
10-
* The object returned from this function will be made available by importing src/temp/config.js.
11-
* This is executed prior to the build running, so it's a way to inject environment or build config-specific
12-
* settings as variables into the JSS app.
13-
* NOTE! Any configs returned here will be written into the client-side JS bundle. DO NOT PUT SECRETS HERE.
14-
* @param {object} configOverrides Keys in this object will override any equivalent global config keys.
15-
*/
16-
export function generateConfig(configOverrides?: { [key: string]: string }): void {
17-
const defaultConfig = {
18-
sitecoreApiKey: 'no-api-key-set',
19-
sitecoreApiHost: '',
20-
jssAppName: 'Unknown',
21-
};
12+
const defaultConfig: JssConfig = {
13+
sitecoreApiKey: 'no-api-key-set',
14+
sitecoreApiHost: '',
15+
jssAppName: 'Unknown',
16+
graphQLEndpointPath: '',
17+
defaultLanguage: 'en',
18+
};
2219

23-
// require + combine config sources
24-
const scjssConfig = transformScJssConfig();
25-
const packageJson = transformPackageConfig();
20+
generateConfig(defaultConfig);
2621

27-
// Object.assign merges the objects in order, so config overrides are performed as:
28-
// default config <-- scjssconfig.json <-- package.json <-- configOverrides
29-
// Optional: add any other dynamic config source (e.g. environment-specific config files).
30-
const config = Object.assign(defaultConfig, scjssConfig, packageJson, configOverrides);
31-
32-
// The GraphQL endpoint is an example of making a _computed_ config setting
33-
// based on other config settings.
34-
const computedConfig: { [key: string]: string } = {};
35-
computedConfig.graphQLEndpoint = '`${config.sitecoreApiHost}${config.graphQLEndpointPath}`';
22+
/**
23+
* Generates the JSS config based on config plugins (under ./config/plugins)
24+
* and then writes the config to disk.
25+
* @param {JssConfig} defaultConfig Default configuration.
26+
*/
27+
function generateConfig(defaultConfig: JssConfig): void {
28+
jssConfigFactory
29+
.create(defaultConfig)
30+
.then((config) => {
31+
writeConfig(config);
32+
})
33+
.catch((e) => {
34+
console.error('Error generating config');
35+
console.error(e);
36+
process.exit(1);
37+
});
38+
}
3639

40+
/**
41+
* Writes the config object to disk with support for environment variables.
42+
* @param {JssConfig} config JSS configuration to write.
43+
*/
44+
function writeConfig(config: JssConfig): void {
3745
let configText = `/* eslint-disable */
3846
// Do not edit this file, it is auto-generated at build time!
3947
// See scripts/bootstrap.ts to modify the generation of this file.
4048
const config = {};\n`;
4149

42-
// Set base configuration values, allowing override with environment variables
50+
// Set configuration values, allowing override with environment variables
4351
Object.keys(config).forEach((prop) => {
44-
configText += `config.${prop} = process.env.${constantCase(prop)} || "${config[prop]}",\n`;
45-
});
46-
// Set computed values, allowing override with environment variables
47-
Object.keys(computedConfig).forEach((prop) => {
48-
configText += `config.${prop} = process.env.${constantCase(prop)} || ${
49-
computedConfig[prop]
50-
};\n`;
52+
configText += `config.${prop} = process.env.${constantCase(prop)} || '${config[prop]}',\n`;
5153
});
5254
configText += `module.exports = config;`;
5355

5456
const configPath = path.resolve('src/temp/config.js');
5557
console.log(`Writing runtime config to ${configPath}`);
5658
fs.writeFileSync(configPath, configText, { encoding: 'utf8' });
5759
}
58-
59-
function transformScJssConfig() {
60-
// scjssconfig.json may not exist if you've never run `jss setup` (development)
61-
// or are depending on environment variables instead (production).
62-
let config;
63-
try {
64-
// eslint-disable-next-line global-require
65-
config = require('../scjssconfig.json');
66-
} catch (e) {
67-
return {};
68-
}
69-
70-
if (!config) return {};
71-
72-
return {
73-
sitecoreApiKey: config.sitecore.apiKey,
74-
sitecoreApiHost: config.sitecore.layoutServiceHost,
75-
};
76-
}
77-
78-
function transformPackageConfig() {
79-
if (!packageConfig.config) return {};
80-
81-
return {
82-
jssAppName: packageConfig.config.appName,
83-
graphQLEndpointPath: packageConfig.config.graphQLEndpointPath,
84-
defaultLanguage: packageConfig.config.language || 'en',
85-
};
86-
}

packages/create-sitecore-jss/src/templates/nextjs/scripts/generate-plugins.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ interface PluginFile {
3131
}
3232

3333
const pluginDefinitions = [
34+
{
35+
listPath: 'scripts/temp/config-plugins.ts',
36+
rootPath: 'scripts/config/plugins',
37+
moduleType: ModuleType.ESM,
38+
},
3439
{
3540
listPath: 'src/temp/sitemap-fetcher-plugins.ts',
3641
rootPath: 'src/lib/sitemap-fetcher/plugins',
@@ -75,7 +80,8 @@ function run(definitions: PluginDefinition[]) {
7580
* Modify this function to use a different convention.
7681
*/
7782
function writePlugins(listPath: string, rootPath: string, moduleType: ModuleType) {
78-
const pluginName = rootPath.split('/')[2];
83+
const segments = rootPath.split('/');
84+
const pluginName = segments[segments.length - 2];
7985
const plugins = getPluginList(rootPath, pluginName);
8086
let fileContent = '';
8187

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

0 commit comments

Comments
 (0)