Skip to content

Commit ea9d23f

Browse files
committed
Forwardport of several new additions in the Seven side.
1 parent a5e2e35 commit ea9d23f

17 files changed

+445
-25
lines changed

packages/registry/__tests__/addon-registry.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ describe('AddonRegistry - Project', () => {
100100
},
101101
'test-released-addon': {
102102
basePath: `${base}/node_modules/test-released-addon`,
103+
hasServerConfig: false,
103104
isPublishedPackage: true,
104105
modulePath: `${base}/node_modules/test-released-addon`,
105106
name: 'test-released-addon',
@@ -112,6 +113,7 @@ describe('AddonRegistry - Project', () => {
112113
},
113114
'test-released-source-addon': {
114115
basePath: `${base}/node_modules/test-released-source-addon`,
116+
hasServerConfig: false,
115117
isPublishedPackage: true,
116118
modulePath: `${base}/node_modules/test-released-source-addon/src`,
117119
name: 'test-released-source-addon',
@@ -126,6 +128,7 @@ describe('AddonRegistry - Project', () => {
126128
'test-released-unmentioned': {
127129
addons: [],
128130
basePath: `${base}/node_modules/test-released-unmentioned`,
131+
hasServerConfig: false,
129132
isPublishedPackage: true,
130133
modulePath: `${base}/node_modules/test-released-unmentioned`,
131134
name: 'test-released-unmentioned',
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import {
2+
getAddonsLoaderCode,
3+
nameFromPackage,
4+
} from '../src/addon-registry/create-addons-loader-server';
5+
import { describe, expect, test } from 'vitest';
6+
7+
describe('create-addons-loader code generation', () => {
8+
test('no addon creates simple loader, default = no loadProjectConfig', () => {
9+
const code = getAddonsLoaderCode([], []);
10+
expect(code).toBe(`/*
11+
Don't change this file manually.
12+
It is autogenerated by @plone/registry.
13+
Instead, change the "addons" registration in the app.
14+
*/
15+
16+
const safeWrapper = (func) => (config) => {
17+
const res = func(config);
18+
if (typeof res === 'undefined') {
19+
throw new Error("Configuration function doesn't return config");
20+
}
21+
return res;
22+
};
23+
24+
const load = (config) => {
25+
const addonLoaders = [];
26+
if (!addonLoaders.every((el) => typeof el === 'function')) {
27+
throw new TypeError(
28+
'Each addon has to provide a function applying its configuration to the projects configuration.',
29+
);
30+
}
31+
return addonLoaders.reduce((acc, apply) => safeWrapper(apply)(acc), config);
32+
};
33+
export default load;
34+
`);
35+
});
36+
37+
test('no addon creates simple loader, loadProjectConfig set to true', () => {
38+
const code = getAddonsLoaderCode([], []);
39+
expect(code).toBe(`/*
40+
Don't change this file manually.
41+
It is autogenerated by @plone/registry.
42+
Instead, change the "addons" registration in the app.
43+
*/
44+
45+
const safeWrapper = (func) => (config) => {
46+
const res = func(config);
47+
if (typeof res === 'undefined') {
48+
throw new Error("Configuration function doesn't return config");
49+
}
50+
return res;
51+
};
52+
53+
const load = (config) => {
54+
const addonLoaders = [];
55+
if (!addonLoaders.every((el) => typeof el === 'function')) {
56+
throw new TypeError(
57+
'Each addon has to provide a function applying its configuration to the projects configuration.',
58+
);
59+
}
60+
return addonLoaders.reduce((acc, apply) => safeWrapper(apply)(acc), config);
61+
};
62+
export default load;
63+
`);
64+
});
65+
66+
test('one addon creates loader when hasServerConfig is true', () => {
67+
const code = getAddonsLoaderCode(
68+
['volto-addon1'],
69+
[{ name: 'volto-addon1', hasServerConfig: true }],
70+
);
71+
expect(
72+
code.indexOf("import voltoAddon1 from 'volto-addon1/config/server';") > 0,
73+
).toBe(true);
74+
});
75+
76+
test('two addons create loaders when both are server-enabled', () => {
77+
const code = getAddonsLoaderCode(
78+
['volto-addon1', 'volto-addon2'],
79+
[
80+
{ name: 'volto-addon1', hasServerConfig: true },
81+
{ name: 'volto-addon2', hasServerConfig: true },
82+
],
83+
);
84+
expect(
85+
code.indexOf(`
86+
import voltoAddon1 from 'volto-addon1/config/server';
87+
import voltoAddon2 from 'volto-addon2/config/server';`) > 0,
88+
).toBe(true);
89+
});
90+
91+
test('one addon plus one extra creates loader when server-enabled', () => {
92+
const code = getAddonsLoaderCode(
93+
['volto-addon1:loadExtra1'],
94+
[{ name: 'volto-addon1', hasServerConfig: true }],
95+
);
96+
expect(
97+
code.indexOf(`
98+
import voltoAddon1, { loadExtra1 as loadExtra10 } from 'volto-addon1/config/server';
99+
`) > 0,
100+
).toBe(true);
101+
});
102+
103+
test('one addon plus two extras creates loader when server-enabled', () => {
104+
const code = getAddonsLoaderCode(
105+
['volto-addon1:loadExtra1,loadExtra2'],
106+
[{ name: 'volto-addon1', hasServerConfig: true }],
107+
);
108+
expect(
109+
code.indexOf(`
110+
import voltoAddon1, { loadExtra1 as loadExtra10, loadExtra2 as loadExtra21 } from 'volto-addon1/config/server';
111+
`) > 0,
112+
).toBe(true);
113+
});
114+
115+
test('two addons plus extras creates loader when server-enabled', () => {
116+
const code = getAddonsLoaderCode(
117+
[
118+
'volto-addon1:loadExtra1,loadExtra2',
119+
'volto-addon2:loadExtra3,loadExtra4',
120+
],
121+
[
122+
{ name: 'volto-addon1', hasServerConfig: true },
123+
{ name: 'volto-addon2', hasServerConfig: true },
124+
],
125+
);
126+
expect(
127+
code.indexOf(`
128+
import voltoAddon1, { loadExtra1 as loadExtra10, loadExtra2 as loadExtra21 } from 'volto-addon1/config/server';
129+
import voltoAddon2, { loadExtra3 as loadExtra32, loadExtra4 as loadExtra43 } from 'volto-addon2/config/server';
130+
`) > 0,
131+
).toBe(true);
132+
});
133+
});
134+
135+
describe('create-addons-loader default name generation', () => {
136+
const getName = nameFromPackage;
137+
138+
test('passing a simple word returns a word', () => {
139+
expect(getName('something')).toBe('something');
140+
});
141+
142+
test('passing a kebab-name returns a word', () => {
143+
expect(getName('volto-something-else')).toBe('voltoSomethingElse');
144+
});
145+
146+
test('passing a simple relative path returns random string', () => {
147+
const rand = getName('../../');
148+
expect(rand.length).toBe(10);
149+
expect(new RegExp(/[abcdefghjk]+/).exec(rand)[0].length > 0).toBe(true);
150+
});
151+
test('passing a tilda relative path with addon strips tilda', () => {
152+
const name = getName('~/addons/volto-addon1');
153+
expect(name).toBe('addonsvoltoAddon1');
154+
});
155+
test('passing a namespace package strips @', () => {
156+
const name = getName('@plone/volto-addon1');
157+
expect(name).toBe('plonevoltoAddon1');
158+
});
159+
test('passing a tilda relative path strips tilda', () => {
160+
const name = getName('~/../');
161+
expect(name.length).toBe(10);
162+
expect(new RegExp(/[abcdefghjk]+/).exec(name)[0].length > 0).toBe(true);
163+
});
164+
test('passing a backspaced path strips backspace', () => {
165+
const name = getName('c:\\nodeprojects');
166+
expect(name).toBe('cnodeprojects');
167+
});
168+
});
169+
test('addon without server config does not create import', () => {
170+
const code = getAddonsLoaderCode(
171+
['volto-addon1'],
172+
[{ name: 'volto-addon1', hasServerConfig: false }],
173+
);
174+
expect(
175+
code.indexOf("import voltoAddon1 from 'volto-addon1/config/server';") > 0,
176+
).toBe(false);
177+
});

packages/registry/bin/init-loaders.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,69 @@
33
// the unit tests before they run.
44
// It was used when React Router was evaluating `routes.ts` before running vite build.
55
// See https://github.com/remix-run/react-router/issues/13078#issuecomment-2863445977
6+
import fs from 'fs';
67
import path from 'path';
8+
import { createServer } from 'vite';
79
import { AddonRegistry } from '@plone/registry/addon-registry';
810
import { createAddonsLoader } from '@plone/registry/create-addons-loader';
11+
import { createAddonsServerLoader } from '@plone/registry/create-addons-loader-server';
912
import { createAddonsStyleLoader } from '@plone/registry/create-addons-styles-loader';
13+
import { createAddonsLocalesLoader } from '@plone/registry/create-addons-locales-loader';
14+
import { PloneRegistryVitePlugin } from '@plone/registry/vite-plugin';
15+
import config from '@plone/registry';
1016

11-
function initPloneRegistryLoaders() {
17+
async function evaluateAddons(addonsLoaderPath) {
18+
const projectRootPath = path.resolve('.');
19+
20+
const ploneDir = path.join(projectRootPath, '.plone');
21+
22+
const server = await createServer({
23+
root: projectRootPath,
24+
configFile: false,
25+
server: { middlewareMode: true },
26+
plugins: [PloneRegistryVitePlugin()],
27+
});
28+
29+
try {
30+
const { default: loader, addonsInfo } =
31+
await server.ssrLoadModule(addonsLoaderPath);
32+
33+
fs.writeFileSync(
34+
path.join(ploneDir, 'registry.routes.json'),
35+
JSON.stringify(loader(config).routes, null, 2),
36+
);
37+
fs.writeFileSync(
38+
path.join(ploneDir, 'registry.addonsInfo.json'),
39+
JSON.stringify(addonsInfo, null, 2),
40+
);
41+
} finally {
42+
await server.close();
43+
}
44+
}
45+
46+
async function initPloneRegistryLoaders() {
1247
const projectRootPath = path.resolve('.');
1348
const { registry, shadowAliases } = AddonRegistry.init(projectRootPath);
1449

50+
const ploneDir = path.join(projectRootPath, '.plone');
51+
if (!fs.existsSync(ploneDir)) {
52+
fs.mkdirSync(ploneDir, { recursive: true });
53+
}
54+
1555
const addonsLoaderPath = createAddonsLoader(
1656
registry.getAddonDependencies(),
1757
registry.getAddons(),
1858
{ tempInProject: true },
1959
);
2060

61+
await evaluateAddons(addonsLoaderPath);
62+
63+
createAddonsServerLoader(
64+
registry.getAddonDependencies(),
65+
registry.getAddons(),
66+
);
2167
createAddonsStyleLoader(registry);
68+
createAddonsLocalesLoader(registry);
2269

2370
return { registry, shadowAliases, addonsLoaderPath };
2471
}

packages/registry/docs/conceptual-guides/add-on-loader.md

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This should be a JavaScript or TypeScript file, such as {file}`index.ts` or {fil
1515

1616
```json
1717
{
18-
"main": "index.ts",
18+
"main": "index.ts"
1919
}
2020
```
2121

@@ -33,7 +33,7 @@ export default function loadConfig(config: ConfigType) {
3333
`@plone/registry` has a helper utility `createAddonsLoader` which generates the add-ons loader file.
3434
That file contains the code needed to load the add-ons configuration of all the registered add-ons, keeping the order in which they were defined.
3535

36-
This loader is a JavaScript file and it is placed in the root of your application.
36+
This loader is a JavaScript file and it is placed in the {file}`.plone` directory in the root of your application.
3737
By default, it's called {file}`registry.loader.js`.
3838

3939
```{important}
@@ -46,23 +46,23 @@ The add-ons loader generator is meant to be run before bundling your app or by t
4646
The `@plone/registry` Vite plugin generates this file, so the framework can load it during app bootstrap time, as shown below.
4747

4848
```js
49-
const projectRootPath = path.resolve('.');
50-
const { registry, shadowAliases } = AddonRegistry.init(projectRootPath);
51-
52-
createAddonsLoader(
53-
registry.getAddonDependencies(),
54-
registry.getAddons(),
55-
{ tempInProject: true },
56-
);
49+
import { createAddonsLoader } from '@plone/registry/create-addons-loader';
50+
51+
const projectRootPath = path.resolve('.');
52+
const { registry, shadowAliases } = AddonRegistry.init(projectRootPath);
53+
54+
createAddonsLoader(registry.getAddonDependencies(), registry.getAddons(), {
55+
tempInProject: true,
56+
});
5757
```
5858

59-
This will create {file}`registry.loader.js` in the root of your app.
59+
This will create {file}`registry.loader.js` in the {file}`.plone` directory in the root of your app.
6060

6161
Afterwards, configure your app to load the code during the app bootstrap, as early as possible in both your client and server code, and as a module side-effect, as shown in the following example.
6262

6363
```js
6464
import config from '@plone/registry';
65-
import applyAddonConfiguration from './registry.loader';
65+
import applyAddonConfiguration from './.plone/registry.loader';
6666

6767
applyAddonConfiguration(config);
6868
```
@@ -73,6 +73,70 @@ If you use a non-Vite framework, you will have to build your own integration.
7373
You can take the implementation of the Vite plugin as reference.
7474
```
7575

76+
## Add-ons server configuration loader
77+
78+
Add-ons can also provide a server-side configuration loader.
79+
To do so, create a file {file}`config/server.ts` at the root of your add-on package.
80+
This is useful when your add-on needs to load configuration that is only relevant on the server side, such as API endpoints or server-only features.
81+
This file should contain a default export with a function with the same signature as the client-side loader.
82+
83+
```ts
84+
import type { ConfigType } from '@plone/registry';
85+
86+
export default function loadConfig(config: ConfigType) {
87+
// You can mutate the configuration object in here
88+
return config;
89+
}
90+
```
91+
92+
When registering your add-on, `@plone/registry` will detect the presence of this file and include it in the generated server-side add-ons loader file, which is called {file}`registry.loader.server.js` by default.
93+
94+
```{note}
95+
The server-side configuration loader is optional.
96+
If your add-on does not need to load any server-side configuration, you can omit this file.
97+
```
98+
99+
`@plone/registry` has a helper utility `createAddonsServerLoader` which generates the add-ons loader file.
100+
That file contains the code needed to load the add-ons configuration of all the registered add-ons, keeping the order in which they were defined.
101+
102+
This loader is a JavaScript file and it is placed in the {file}`.plone` directory in the root of your application.
103+
By default, it's called {file}`registry.loader.server.js`.
104+
105+
```{important}
106+
This file is generated and maintained by `@plone/registry`.
107+
You should neither modify it nor add your own styles in here.
108+
It will be overwritten in the next bundler run.
109+
```
110+
111+
The add-ons loader generator is meant to be run before bundling your app or by the bundler itself when it runs.
112+
The `@plone/registry` Vite plugin generates this file, so the framework can load it during app bootstrap time, as shown below.
113+
114+
```js
115+
import { createAddonsServerLoader } from '@plone/registry/create-addons-loader-server';
116+
117+
const projectRootPath = path.resolve('.');
118+
const { registry, shadowAliases } = AddonRegistry.init(projectRootPath);
119+
120+
createAddonsServerLoader(registry.getAddonDependencies(), registry.getAddons());
121+
```
122+
123+
This will create {file}`registry.loader.js` in the {file}`.plone` directory in the root of your app.
124+
125+
Afterwards, configure your app to load the code during the app bootstrap, as early as possible in both your client and server code, and as a module side-effect, as shown in the following example.
126+
127+
```js
128+
import config from '@plone/registry';
129+
import applyServerAddonConfiguration from './.plone/registry.loader.server';
130+
131+
applyServerAddonConfiguration(config);
132+
```
133+
134+
```{note}
135+
If you use a Vite-powered framework, use the `@plone/registry` Vite plugin.
136+
If you use a non-Vite framework, you will have to build your own integration.
137+
You can take the implementation of the Vite plugin as reference.
138+
```
139+
76140
## Provide optional add-on configurations
77141

78142
You can export additional configuration functions from your add-ons configuration loader file.

0 commit comments

Comments
 (0)