mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-28 17:43:07 +08:00
feat(client): support settings-default entry mode (#10267)
This commit is contained in:
+6
-2
@@ -48,6 +48,10 @@ _Avoid_: hybrid modern-only, redirect-all
|
||||
An **App client entry mode** where legacy client document entries hand off to the **Modern client public path**; in production this is mainly a browser-side handoff, while dev additionally avoids running the legacy client dev server and redirects non-`/v/` entries into `/v/`.
|
||||
_Avoid_: compatible modern-only, route-mapped modern-only
|
||||
|
||||
**Settings-default**:
|
||||
An **App client entry mode** where the app root hands off to the standalone Settings application at `${APP_PUBLIC_PATH}settings/`, while the **Modern client** and legacy deep links remain available at their own paths.
|
||||
_Avoid_: settings-only, no-portal mode
|
||||
|
||||
**Client document entry request**:
|
||||
An HTTP request whose job is to load a client HTML entry, not an API, websocket, upload, dist, or plugin-static resource.
|
||||
_Avoid_: all frontend request, browser request
|
||||
@@ -58,9 +62,9 @@ _Avoid_: all frontend request, browser request
|
||||
- The **Legacy client** is served at the **App public path**; the **Modern client** is served at the **Modern client public path** nested inside it
|
||||
- **App public path** and **Modern client prefix** vary independently; both default such that the modern client lands at `/v/`
|
||||
- The **Site root** is not always the **App public path**
|
||||
- The **App client entry mode** chooses the default entry behavior independently from the concrete route trees owned by the **Legacy client** and the **Modern client**
|
||||
- The **App client entry mode** chooses the default entry behavior independently from the concrete route trees owned by the **Legacy client**, the **Modern client**, and the standalone Settings application
|
||||
- A **Legacy client** deep link is not assumed to have a one-to-one **Modern client** deep link
|
||||
- In production, the **Legacy client** shell is the main place where **App client entry mode** hands off document entries to the **Modern client**
|
||||
- In production, the **Legacy client** shell is the main place where **App client entry mode** hands off document entries to the configured default client
|
||||
- In dev, only **Modern-only** adds extra runtime handling so the legacy dev server is not started and non-`/v/` entries are redirected into the modern dev entry
|
||||
|
||||
## Example dialogue
|
||||
|
||||
@@ -75,7 +75,7 @@ function runBrowserChecker(options: RunBrowserCheckerOptions) {
|
||||
}
|
||||
|
||||
describe('v2 browser checker', () => {
|
||||
it.each([undefined, 'modern-default', 'modern-only'] as const)(
|
||||
it.each([undefined, 'modern-default', 'modern-only', 'settings-default'] as const)(
|
||||
'lets the modern client handle its root for entry mode %s',
|
||||
(appClientEntryMode) => {
|
||||
expect(
|
||||
@@ -87,7 +87,7 @@ describe('v2 browser checker', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([undefined, 'modern-default', 'modern-only'] as const)(
|
||||
it.each([undefined, 'modern-default', 'modern-only', 'settings-default'] as const)(
|
||||
'lets scoped modern clients handle their roots for entry mode %s',
|
||||
(appClientEntryMode) => {
|
||||
expect(
|
||||
|
||||
@@ -54,7 +54,7 @@ if (basename !== '/' && currentPath === basenameWithoutTrailingSlash) {
|
||||
// This client-side redirect is still needed because legacy `index.html` is
|
||||
// not always served through the node gateway. In nginx/static delivery paths
|
||||
// the browser may already be running the legacy shell by the time entry-mode
|
||||
// logic is evaluated, so the last hop into the modern entry has to be
|
||||
// logic is evaluated, so the last hop into the configured default entry has to be
|
||||
// recoverable in the browser as well.
|
||||
const normalizedPath = normalizePathname(currentPath);
|
||||
const relativePath =
|
||||
@@ -66,15 +66,22 @@ if (basename !== '/' && currentPath === basenameWithoutTrailingSlash) {
|
||||
? normalizePathname(normalizedPath.slice(basename.length - 1))
|
||||
: null;
|
||||
const modernBase = `${trimTrailingSlash(basename)}/${modernClientPrefix}/`.replace(/\/{2,}/g, '/');
|
||||
const settingsBase = `${basename}settings/`;
|
||||
const isModernDefault = appClientEntryMode === 'modern-default';
|
||||
const isModernOnly = appClientEntryMode === 'modern-only';
|
||||
const isSettingsDefault = appClientEntryMode === 'settings-default';
|
||||
const targetBase = isSettingsDefault ? settingsBase : modernBase;
|
||||
if (
|
||||
relativePath &&
|
||||
isClientDocumentEntryPath(relativePath) &&
|
||||
(isModernDefault || isModernOnly) &&
|
||||
!normalizedPath.startsWith(modernBase)
|
||||
(isModernDefault || isModernOnly || isSettingsDefault) &&
|
||||
!normalizedPath.startsWith(targetBase)
|
||||
) {
|
||||
const targetPath = isModernDefault
|
||||
const targetPath = isSettingsDefault
|
||||
? relativePath === '/' || relativePath === '/index.html'
|
||||
? settingsBase
|
||||
: null
|
||||
: isModernDefault
|
||||
? relativePath === '/' || relativePath === '/index.html'
|
||||
? relativePath === '/index.html'
|
||||
? `${modernBase}index.html`
|
||||
|
||||
@@ -87,6 +87,20 @@ describe('cli-v1 buildIndexHtml', () => {
|
||||
fs.removeSync(appRoot);
|
||||
});
|
||||
|
||||
test('injects settings-default as a valid client entry mode', () => {
|
||||
const appRoot = createAppPackageRoot();
|
||||
process.argv = ['node', 'nocobase-v1', 'start'];
|
||||
process.env.APP_PACKAGE_ROOT = appRoot;
|
||||
process.env.APP_PUBLIC_PATH = '/';
|
||||
process.env.APP_CLIENT_ENTRY_MODE = 'settings-default';
|
||||
|
||||
buildIndexHtml();
|
||||
|
||||
const html = fs.readFileSync(path.join(appRoot, 'dist/client/index.html'), 'utf-8');
|
||||
expect(html).toContain("window['__nocobase_app_client_entry_mode__'] = 'settings-default';");
|
||||
fs.removeSync(appRoot);
|
||||
});
|
||||
|
||||
test('refreshes cached tpl when new runtime placeholders are missing', () => {
|
||||
const appRoot = createAppPackageRoot();
|
||||
const tplPath = path.join(appRoot, 'dist/client/index.html.tpl');
|
||||
|
||||
@@ -67,6 +67,16 @@ describe('cli-v1 dev command', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveDevRuntimeMode keeps all clients in settings-default', () => {
|
||||
expect(resolveDevRuntimeMode({ appClientEntryMode: 'settings-default' })).toMatchObject({
|
||||
useModernOnlyEntryMode: false,
|
||||
shouldRunClient: true,
|
||||
shouldRunClientV2: true,
|
||||
shouldRunSettings: true,
|
||||
shouldRunServer: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveDevRuntimeMode preserves explicit client-v2-only flag behavior', () => {
|
||||
expect(resolveDevRuntimeMode({ clientV2Only: true, appClientEntryMode: 'legacy-default' })).toMatchObject({
|
||||
useModernOnlyEntryMode: false,
|
||||
|
||||
@@ -433,7 +433,7 @@ const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
||||
exports.DEFAULT_MODERN_CLIENT_PREFIX = DEFAULT_MODERN_CLIENT_PREFIX;
|
||||
|
||||
const DEFAULT_APP_CLIENT_ENTRY_MODE = 'legacy-default';
|
||||
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only']);
|
||||
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only', 'settings-default']);
|
||||
|
||||
exports.DEFAULT_APP_CLIENT_ENTRY_MODE = DEFAULT_APP_CLIENT_ENTRY_MODE;
|
||||
|
||||
|
||||
@@ -279,6 +279,19 @@ test('buildEnvProxyNginxBundle renders app.conf and index HTML with CDN-prefixed
|
||||
expect(bundle.indexSettingsContent).toContain('src="/console/dist/2.1.0-beta.44/settings/assets/runtime.js"');
|
||||
});
|
||||
|
||||
test('buildEnvProxyNginxBundle preserves settings-default in each client entry', async () => {
|
||||
const root = await createTempRoot('nocobase-cli-env-proxy-settings-default-');
|
||||
const runtime = await createLocalRuntime(root, {
|
||||
appClientEntryMode: 'settings-default',
|
||||
});
|
||||
|
||||
const bundle = await buildEnvProxyNginxBundle(runtime);
|
||||
|
||||
expect(bundle.indexV1Content).toContain(`window['__nocobase_app_client_entry_mode__'] = "settings-default";`);
|
||||
expect(bundle.indexV2Content).toContain(`window['__nocobase_app_client_entry_mode__'] = "settings-default";`);
|
||||
expect(bundle.indexSettingsContent).toContain(`window['__nocobase_app_client_entry_mode__'] = "settings-default";`);
|
||||
});
|
||||
|
||||
test('buildEnvProxyNginxBundle omits the root redirect block for root-mounted apps', async () => {
|
||||
const root = await createTempRoot('nocobase-cli-env-proxy-nginx-root-');
|
||||
process.env.NB_CLI_ROOT = root;
|
||||
|
||||
@@ -35,7 +35,7 @@ const DEFAULT_PLUGIN_STATICS_PATH = '/static/plugins/';
|
||||
const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
||||
const SETTINGS_CLIENT_PREFIX = 'settings';
|
||||
const DEFAULT_APP_CLIENT_ENTRY_MODE = 'legacy-default';
|
||||
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only']);
|
||||
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only', 'settings-default']);
|
||||
const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
|
||||
const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
|
||||
const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
|
||||
|
||||
@@ -147,6 +147,19 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
||||
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/');
|
||||
});
|
||||
|
||||
it.each(['/', '/index.html'])('redirects app root entry %s to Settings for settings-default', (pathname) => {
|
||||
const replace = executeBrowserChecker(scriptPath, {
|
||||
pathname,
|
||||
publicPath: '/',
|
||||
modernClientPrefix: 'v',
|
||||
appClientEntryMode: 'settings-default',
|
||||
search: '?from=entry',
|
||||
hash: '#panel',
|
||||
});
|
||||
|
||||
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/settings/?from=entry#panel');
|
||||
});
|
||||
|
||||
it('does not redirect legacy deep links for modern-default', () => {
|
||||
const replace = executeBrowserChecker(scriptPath, {
|
||||
pathname: '/admin',
|
||||
@@ -158,6 +171,17 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
||||
expect(replace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not redirect legacy deep links for settings-default', () => {
|
||||
const replace = executeBrowserChecker(scriptPath, {
|
||||
pathname: '/admin',
|
||||
publicPath: '/',
|
||||
modernClientPrefix: 'v',
|
||||
appClientEntryMode: 'settings-default',
|
||||
});
|
||||
|
||||
expect(replace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rewrites legacy document paths for modern-only', () => {
|
||||
const replace = executeBrowserChecker(scriptPath, {
|
||||
pathname: '/admin/settings/workflow',
|
||||
@@ -180,6 +204,19 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
||||
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/');
|
||||
});
|
||||
|
||||
it('redirects a sub-path app root directly to Settings for settings-default', () => {
|
||||
const replace = executeBrowserChecker(scriptPath, {
|
||||
pathname: '/nocobase/',
|
||||
publicPath: '/nocobase/',
|
||||
modernClientPrefix: 'v',
|
||||
appClientEntryMode: 'settings-default',
|
||||
search: '?from=entry',
|
||||
hash: '#panel',
|
||||
});
|
||||
|
||||
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/settings/?from=entry#panel');
|
||||
});
|
||||
|
||||
it('rewrites sub-app legacy deep links for modern-only without collapsing the sub-app segment', () => {
|
||||
const replace = executeBrowserChecker(scriptPath, {
|
||||
pathname: '/nocobase/apps/a_31itq60q4kg/admin/',
|
||||
|
||||
Reference in New Issue
Block a user