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
7 changes: 7 additions & 0 deletions packages/cmsui/components/SiteOverview/SiteOverview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import SiteOverview from './SiteOverview';

vi.mock('@plone/registry/app-info', () => ({
default: { name: '@plone/aurora', version: '1.0.0-alpha.23', addons: [] },
}));

const system = {
'@id': 'http://localhost:8080/Plone/@system',
cmf_version: '3.8',
Expand Down Expand Up @@ -35,6 +39,9 @@ describe('SiteOverview', () => {
);
expect(screen.getByText('6.2.2')).toBeInTheDocument();
expect(screen.getByText('10.0.4')).toBeInTheDocument();
// The app's version comes from `@plone/registry/app-info`.
expect(screen.getByText('Plone Aurora')).toBeInTheDocument();
expect(screen.getByText('1.0.0-alpha.23')).toBeInTheDocument();
// Only the version number of Python, without its build details.
expect(screen.getByText('3.13.15')).toBeInTheDocument();
expect(screen.getByText('cmsui.siteOverview.upToDate')).toBeInTheDocument();
Expand Down
7 changes: 5 additions & 2 deletions packages/cmsui/components/SiteOverview/SiteOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@plone/quanta';
import type { GetSiteResponse, GetSystemResponse } from '@plone/types';
import PackageJson from '../../../../apps/aurora/package.json';
import appInfo from '@plone/registry/app-info';

interface SiteOverviewProps {
system: GetSystemResponse;
Expand Down Expand Up @@ -59,7 +59,10 @@ const SiteOverview = ({

const versions: [string, string | undefined][] = [
['Plone', system.plone_version],
['Plone Aurora', PackageJson.version],
[
appInfo.name === '@plone/aurora' ? 'Plone Aurora' : appInfo.name,
appInfo.version,
],
['plone.restapi', system.plone_restapi_version],
['Zope', system.zope_version],
['Python', pythonVersion],
Expand Down
1 change: 1 addition & 0 deletions packages/cmsui/news/+app-info.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Read the version of the app for the site overview from `@plone/registry/app-info` instead of importing the Aurora app's `package.json`, so it works in any app. @sneridagh
79 changes: 79 additions & 0 deletions packages/registry/__tests__/create-app-info-loader.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, test } from 'vitest';
import {
createAppInfoLoader,
getAppInfo,
getAppInfoLoaderCode,
} from '../src/addon-registry/create-app-info-loader';

const tmpDirs = [];

afterEach(() => {
tmpDirs.splice(0).forEach((dir) => fs.rmSync(dir, { recursive: true }));
});

/**
* A minimal stand-in for `AddonRegistry`, with add-ons carrying the paths the
* real registry has.
*/
function fakeRegistry() {
const projectRootPath = fs.mkdtempSync(
path.join(os.tmpdir(), 'registry-app-info-'),
);
tmpDirs.push(projectRootPath);
return {
projectRootPath,
packageJson: { name: 'my-app', version: '2.1.0', addons: ['addon-a'] },
getAddons: () => [
{
name: 'addon-a',
version: '1.0.0',
modulePath: '/home/me/app/node_modules/addon-a/src',
packageJson: '/home/me/app/node_modules/addon-a/package.json',
},
{ name: 'addon-b', modulePath: '/home/me/app/packages/addon-b' },
],
};
}

describe('getAppInfo', () => {
test('has the app and its add-ons, in order, without paths', () => {
expect(getAppInfo(fakeRegistry())).toEqual({
name: 'my-app',
version: '2.1.0',
addons: [
{ name: 'addon-a', version: '1.0.0' },
{ name: 'addon-b', version: '' },
],
});
});
});

describe('createAppInfoLoader', () => {
test('writes a module that exports the app info', () => {
const registry = fakeRegistry();
const loaderPath = createAppInfoLoader(registry);

expect(loaderPath).toBe(
path.join(registry.projectRootPath, '.plone', 'registry.appInfo.js'),
);
const code = fs.readFileSync(loaderPath, 'utf-8');
expect(code).toBe(getAppInfoLoaderCode(getAppInfo(registry)));
expect(code).not.toContain('/home/me');
expect(code).toContain(
`export default ${JSON.stringify(getAppInfo(registry), null, 2)};`,
);
});

test('does not write the file again when it does not change', () => {
const registry = fakeRegistry();
const loaderPath = createAppInfoLoader(registry);
fs.utimesSync(loaderPath, new Date(0), new Date(0));

createAppInfoLoader(registry);

expect(fs.statSync(loaderPath).mtimeMs).toBe(0);
});
});
15 changes: 15 additions & 0 deletions packages/registry/docs/conceptual-guides/add-on-loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,18 @@ The additional comma-separated names should be exported from the add-on configur
The main configuration function should be exported as the default.
An add-on's default configuration method will always be loaded.
```

## Read the app and add-on versions

The `@plone/registry` Vite plugin also generates the `@plone/registry/app-info` module.
Its default export holds the name and version of the app, from its {file}`package.json`, and of each of its add-ons, in the order they load.

```ts
import appInfo from '@plone/registry/app-info';

appInfo.version; // '1.0.0'
appInfo.addons; // [{ name: 'my-volto-add-on-name', version: '2.1.0' }, …]
```

It has no file system paths, so both server and client code can import it, for example to show the versions in a control panel.
Outside of an app built with the Vite plugin, such as in unit tests or Storybook, the import resolves to an empty app with no add-ons.
1 change: 1 addition & 0 deletions packages/registry/news/+app-info.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added the `@plone/registry/app-info` virtual module, with the name and version of the app and of its add-ons, without file system paths, so client code can show them. @sneridagh
8 changes: 8 additions & 0 deletions packages/registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,21 @@
"import": "./dist/addon-registry/create-plate-plugins-loader.js",
"default": "./dist/addon-registry/create-plate-plugins-loader.cjs"
},
"./create-app-info-loader": {
"import": "./dist/addon-registry/create-app-info-loader.js",
"default": "./dist/addon-registry/create-app-info-loader.cjs"
},
"./vite-plugin": {
"import": "./vite-plugin.js",
"types": "./vite-plugin.d.ts"
},
"./addons-loader": {
"types": "./vite-plugin.d.ts"
},
"./app-info": {
"types": "./virtual/app-info.d.ts",
"default": "./virtual/app-info.js"
},
"./blocks-docs": {
"types": "./virtual/blocks-docs.d.ts",
"default": "./virtual/blocks-docs.js"
Expand Down
71 changes: 71 additions & 0 deletions packages/registry/src/addon-registry/create-app-info-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import fs from 'fs';
import path from 'path';

/*
* The name and version of the app and of its add-ons, collected at build time
* for `@plone/registry/app-info`. It only has public facts: no paths of the
* machine that built the app, since the browser loads this module too.
*/
export type AppInfo = {
name: string;
version: string;
addons: Array<{ name: string; version: string }>;
};

export type AppInfoRegistry = {
packageJson: { name?: string; version?: string };
getAddons: () => Array<{ name: string; version?: string } | undefined>;
};

export const APP_INFO_LOADER_FILE_NAME = 'registry.appInfo.js';

/*
* The app and its add-ons, in add-on order.
*/
export function getAppInfo(registry: AppInfoRegistry): AppInfo {
return {
name: registry.packageJson.name ?? '',
version: registry.packageJson.version ?? '',
addons: registry
.getAddons()
.filter((addon) => Boolean(addon))
.map((addon) => ({
name: addon!.name,
version: addon!.version ?? '',
})),
};
}

/*
* The code of a module that exports the app info.
*/
export function getAppInfoLoaderCode(appInfo: AppInfo): string {
return `/*
Don't change this file manually.
It is autogenerated by @plone/registry.
Instead, change the "name", "version" or "addons" in the app's package.json.
*/
export default ${JSON.stringify(appInfo, null, 2)};
`;
}

/*
* Writes the app info module in the project's `.plone` folder and returns its
* path. The file is only written when it changes.
*/
export function createAppInfoLoader(
registry: AppInfoRegistry & { projectRootPath: string },
): string {
const ploneDir = path.join(registry.projectRootPath, '.plone');
fs.mkdirSync(ploneDir, { recursive: true });

const loaderPath = path.join(ploneDir, APP_INFO_LOADER_FILE_NAME);
const code = getAppInfoLoaderCode(getAppInfo(registry));
const existing = fs.existsSync(loaderPath)
? fs.readFileSync(loaderPath, 'utf-8')
: null;
if (existing !== code) {
fs.writeFileSync(loaderPath, code);
}
return loaderPath;
}
1 change: 1 addition & 0 deletions packages/registry/virtual/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ Each file must:
| ---------------------------------------------------------------- | ------------------ |
| `@plone/registry/plate-editor`, `@plone/registry/plate-renderer` | `plate-plugins.js` |
| `@plone/registry/blocks-docs` | `blocks-docs.js` |
| `@plone/registry/app-info` | `app-info.js` |
14 changes: 14 additions & 0 deletions packages/registry/virtual/app-info.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* The name and version of the app (from its `package.json`) and of each of
* its add-ons, in add-on order, collected at build time by the Plone registry
* Vite plugin. It has no paths, so client code can import it.
*/
export type AppInfo = {
name: string;
version: string;
addons: Array<{ name: string; version: string }>;
};

declare const appInfo: AppInfo;

export default appInfo;
3 changes: 3 additions & 0 deletions packages/registry/virtual/app-info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Outside of an app built with the Plone registry Vite plugin (in the unit
// tests or the Storybook of a package), there is no app and no add-ons.
export default { name: '', version: '', addons: [] };
9 changes: 9 additions & 0 deletions packages/registry/vite-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createAddonsStyleLoader } from '@plone/registry/create-addons-styles-lo
import { createAddonsLocalesLoader } from '@plone/registry/create-addons-locales-loader';
import { createBlocksDocsLoader } from '@plone/registry/create-blocks-docs-loader';
import { createPlatePluginsLoaders } from '@plone/registry/create-plate-plugins-loader';
import { createAppInfoLoader } from '@plone/registry/create-app-info-loader';
import { pathToFileURL } from 'node:url';

const titleCase = (w) => w.slice(0, 1).toUpperCase() + w.slice(1, w.length);
Expand Down Expand Up @@ -168,6 +169,10 @@ export const PloneRegistryVitePlugin = () => {
// import these loaders.
const platePluginsLoaders = createPlatePluginsLoaders(registry);

// The name and version of the app and of its add-ons, without any path,
// so that the client can show them.
const appInfoLoaderPath = createAppInfoLoader(registry);

const [addonsThemeLoaderVariablesPath, addonsThemeLoaderMainPath] =
createThemeAddonsLoader(registry.getCustomThemeAddons());

Expand Down Expand Up @@ -221,6 +226,10 @@ export const PloneRegistryVitePlugin = () => {
find: '@plone/registry/addons-loader',
replacement: addonsLoaderPath,
},
{
find: '@plone/registry/app-info',
replacement: appInfoLoaderPath,
},
{
find: '@plone/registry/blocks-docs',
replacement: blocksDocsLoaderPath,
Expand Down
Loading