@ossy/platform 1.38.6 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -10
- package/src/Definition.js +1 -7
- package/src/PlatformShell.jsx +11 -11
- package/src/locale.js +90 -0
- package/src/runtime.js +14 -3
- package/src/server.js +28 -25
- package/src/test/jest.setup.js +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
"./definition": "./src/Definition.js",
|
|
23
23
|
"./integrations": "./src/integration.service.js",
|
|
24
24
|
"./test": "./src/test/index.js",
|
|
25
|
+
"./test/jest.setup.js": "./src/test/jest.setup.js",
|
|
25
26
|
"./test/e2e-runner.js": "./src/test/e2e-runner.js",
|
|
26
|
-
"./test/playwright.config.js": "./src/test/playwright.config.js"
|
|
27
|
+
"./test/playwright.config.js": "./src/test/playwright.config.js",
|
|
28
|
+
"./locale": "./src/locale.js"
|
|
27
29
|
},
|
|
28
30
|
"scripts": {
|
|
29
31
|
"start": "PORT=3003 node -e \"import('./src/server.js').then(m => m.startServer())\"",
|
|
@@ -38,13 +40,13 @@
|
|
|
38
40
|
"@aws-sdk/s3-request-presigner": "^3.1057.0",
|
|
39
41
|
"@aws-sdk/util-create-request": "^3.972.26",
|
|
40
42
|
"@aws-sdk/util-format-url": "^3.972.17",
|
|
41
|
-
"@ossy/event-store": "^1.
|
|
42
|
-
"@ossy/
|
|
43
|
-
"@ossy/
|
|
44
|
-
"@ossy/
|
|
45
|
-
"@ossy/sdk": "^1.
|
|
46
|
-
"@ossy/tokens": "^1.
|
|
47
|
-
"@ossy/users": "^1.
|
|
43
|
+
"@ossy/event-store": "^1.8.0",
|
|
44
|
+
"@ossy/locale": "^1.40.0",
|
|
45
|
+
"@ossy/observability": "^1.8.0",
|
|
46
|
+
"@ossy/policies": "^1.13.0",
|
|
47
|
+
"@ossy/sdk": "^1.40.0",
|
|
48
|
+
"@ossy/tokens": "^1.13.0",
|
|
49
|
+
"@ossy/users": "^1.13.0",
|
|
48
50
|
"cookie-parser": "^1.4.7",
|
|
49
51
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
50
52
|
"express": ">=5.0.0 <6.0.0",
|
|
@@ -62,5 +64,5 @@
|
|
|
62
64
|
"src",
|
|
63
65
|
"Dockerfile"
|
|
64
66
|
],
|
|
65
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "d9d31182be64a448d575da432af2830d909ad4b9"
|
|
66
68
|
}
|
package/src/Definition.js
CHANGED
|
@@ -2,12 +2,6 @@ export const Definition = {
|
|
|
2
2
|
id: 'platform',
|
|
3
3
|
title: 'Platform',
|
|
4
4
|
description: 'Deployment platform configuration aligned with deployment-tools S3 platform-config.json.',
|
|
5
|
-
|
|
6
|
-
id: 'platform',
|
|
7
|
-
enabled: true
|
|
8
|
-
},
|
|
5
|
+
icon: 'controller',
|
|
9
6
|
statuses: ['beta'],
|
|
10
|
-
actions: ['resources.create', 'resources.update-content', 'resources.rename', 'resources.remove'],
|
|
11
|
-
views: ['home.page', 'resources.search', 'resources.get'],
|
|
12
|
-
tasks: []
|
|
13
7
|
}
|
package/src/PlatformShell.jsx
CHANGED
|
@@ -13,24 +13,24 @@ import { ComponentSlotsProvider, Slot } from '@ossy/design-system'
|
|
|
13
13
|
* to override or fill these slots with layout-specific components — the inner
|
|
14
14
|
* provider shallow-merges on top.
|
|
15
15
|
*
|
|
16
|
-
* Slot regions rendered here:
|
|
17
|
-
* header
|
|
18
|
-
* sidebar
|
|
19
|
-
* toolbar
|
|
20
|
-
* notifications
|
|
21
|
-
* system-messages
|
|
16
|
+
* Slot regions rendered here (namespaced keys from app `export const slots`):
|
|
17
|
+
* shell:header — top bar across full width
|
|
18
|
+
* shell:sidebar — left column, full height
|
|
19
|
+
* shell:toolbar — below header, above content (contextual actions)
|
|
20
|
+
* shell:notifications — floating / bottom corner (toasts, badges)
|
|
21
|
+
* shell:system-messages — banner above content (maintenance, errors)
|
|
22
22
|
*
|
|
23
23
|
* @param {{ slots?: Record<string, import('react').ComponentType | null>, children: import('react').ReactNode }} props
|
|
24
24
|
*/
|
|
25
25
|
export function PlatformShell({ slots = {}, children }) {
|
|
26
26
|
return (
|
|
27
27
|
<ComponentSlotsProvider slots={slots}>
|
|
28
|
-
<Slot name="system-messages" />
|
|
29
|
-
<Slot name="header" />
|
|
30
|
-
<Slot name="sidebar" />
|
|
31
|
-
<Slot name="toolbar" />
|
|
28
|
+
<Slot name="shell:system-messages" />
|
|
29
|
+
<Slot name="shell:header" />
|
|
30
|
+
<Slot name="shell:sidebar" />
|
|
31
|
+
<Slot name="shell:toolbar" />
|
|
32
32
|
{children}
|
|
33
|
-
<Slot name="notifications" />
|
|
33
|
+
<Slot name="shell:notifications" />
|
|
34
34
|
</ComponentSlotsProvider>
|
|
35
35
|
)
|
|
36
36
|
}
|
package/src/locale.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { createTranslator } from '@ossy/locale'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} buildDir
|
|
8
|
+
* @param {string} language
|
|
9
|
+
* @returns {Record<string, string>}
|
|
10
|
+
*/
|
|
11
|
+
export function loadMessagesForLanguage (buildDir, language) {
|
|
12
|
+
const filePath = path.join(buildDir, 'public', `${language}.translations.json`)
|
|
13
|
+
if (!fs.existsSync(filePath)) return {}
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
16
|
+
const parsed = JSON.parse(raw)
|
|
17
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
|
18
|
+
} catch {
|
|
19
|
+
return {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} buildDir
|
|
25
|
+
* @param {string} language
|
|
26
|
+
* @param {{ defaultLanguage?: string }} [config]
|
|
27
|
+
* @returns {Record<string, string> | undefined}
|
|
28
|
+
*/
|
|
29
|
+
export function loadFallbackMessagesForLanguage (buildDir, language, config = {}) {
|
|
30
|
+
const defaultLanguage = config.defaultLanguage || 'en'
|
|
31
|
+
if (language === defaultLanguage) return undefined
|
|
32
|
+
return loadMessagesForLanguage(buildDir, defaultLanguage)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{ getParamsFromUrl: (url: string) => Record<string, string> }} pageRouter
|
|
37
|
+
* @param {string} requestUrl
|
|
38
|
+
* @param {{ defaultLanguage?: string }} config
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function resolveRequestLanguage (pageRouter, requestUrl, config) {
|
|
42
|
+
const params = pageRouter.getParamsFromUrl(requestUrl)
|
|
43
|
+
return params.language || config.defaultLanguage || 'en'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {{ getParamsFromUrl: (url: string) => Record<string, string> }} pageRouter
|
|
48
|
+
* @param {string} requestUrl
|
|
49
|
+
* @param {string} buildDir
|
|
50
|
+
* @param {{ defaultLanguage?: string }} config
|
|
51
|
+
* @returns {{ language: string, messages: Record<string, string>, fallbackMessages?: Record<string, string> }}
|
|
52
|
+
*/
|
|
53
|
+
export function resolveRequestLocale (pageRouter, requestUrl, buildDir, config) {
|
|
54
|
+
const language = resolveRequestLanguage(pageRouter, requestUrl, config)
|
|
55
|
+
const messages = loadMessagesForLanguage(buildDir, language)
|
|
56
|
+
const fallbackMessages = loadFallbackMessagesForLanguage(buildDir, language, config)
|
|
57
|
+
return {
|
|
58
|
+
language,
|
|
59
|
+
messages,
|
|
60
|
+
...(fallbackMessages ? { fallbackMessages } : {}),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Server-side `t()` for actions, tasks, and email — reads merged catalogs from build output.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} buildDir Absolute path to app `build/`
|
|
68
|
+
* @param {string} [language] Active locale
|
|
69
|
+
* @param {{ defaultLanguage?: string, warnOnMissingKey?: boolean }} [config]
|
|
70
|
+
* @returns {(key: string, params?: Record<string, string | number>) => string}
|
|
71
|
+
*/
|
|
72
|
+
export function createTranslatorForBuild (buildDir, language, config = {}) {
|
|
73
|
+
const defaultLanguage = config.defaultLanguage || 'en'
|
|
74
|
+
const activeLanguage = language || defaultLanguage
|
|
75
|
+
const messages = loadMessagesForLanguage(buildDir, activeLanguage)
|
|
76
|
+
const fallbackCatalog = activeLanguage !== defaultLanguage
|
|
77
|
+
? loadMessagesForLanguage(buildDir, defaultLanguage)
|
|
78
|
+
: undefined
|
|
79
|
+
|
|
80
|
+
const warnOnMissingKey = config.warnOnMissingKey ?? process.env.NODE_ENV !== 'production'
|
|
81
|
+
const onMissingKey = warnOnMissingKey
|
|
82
|
+
? (key) => {
|
|
83
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
84
|
+
console.warn(`[@ossy/locale] Missing translation key: ${key}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
: undefined
|
|
88
|
+
|
|
89
|
+
return createTranslator(messages, { fallbackCatalog, onMissingKey })
|
|
90
|
+
}
|
package/src/runtime.js
CHANGED
|
@@ -5,6 +5,8 @@ import cookieParser from 'cookie-parser'
|
|
|
5
5
|
import morgan from 'morgan'
|
|
6
6
|
import { Router as OssyRouter } from '@ossy/router'
|
|
7
7
|
import { loadManifest, resolveEntryUrl } from './server.js'
|
|
8
|
+
import { resolveRequestLocale } from './locale.js'
|
|
9
|
+
import { buildManifestSummary } from '@ossy/app/manifest/build-manifest-summary'
|
|
8
10
|
import { ProxyInternal } from './proxy-internal.js'
|
|
9
11
|
import { loadSite, invalidateSite } from './site-loader.js'
|
|
10
12
|
import { createLogger } from '@ossy/observability'
|
|
@@ -48,7 +50,7 @@ function buildSiteContext (manifest, buildDir) {
|
|
|
48
50
|
return promise
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
return { pageRouter, apiRouter, manifest, loadEntry }
|
|
53
|
+
return { pageRouter, apiRouter, manifest, manifestSummary: buildManifestSummary(manifest), loadEntry, buildDir }
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
async function getSiteContext (domain) {
|
|
@@ -127,7 +129,7 @@ export async function startRuntime ({ port } = {}) {
|
|
|
127
129
|
return
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
const { pageRouter, apiRouter, manifest, loadEntry } = context
|
|
132
|
+
const { pageRouter, apiRouter, manifest, manifestSummary, loadEntry, buildDir } = context
|
|
131
133
|
|
|
132
134
|
try {
|
|
133
135
|
const apiRoute = apiRouter.getPageByUrl(requestUrl)
|
|
@@ -166,15 +168,24 @@ export async function startRuntime ({ port } = {}) {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
const config = manifest.config
|
|
171
|
+
const { language, messages, fallbackMessages } = resolveRequestLocale(pageRouter, requestUrl, buildDir, config)
|
|
169
172
|
const props = {
|
|
170
173
|
...config,
|
|
171
174
|
...(req.userAppSettings || {}),
|
|
172
175
|
theme: req.userAppSettings?.theme || cloneSerializable(config?.theme),
|
|
173
176
|
themes: cloneSerializable(config?.themes),
|
|
174
177
|
resourceTemplates: cloneSerializable(manifest.resourceTemplates),
|
|
178
|
+
manifestSummary: cloneSerializable(manifestSummary),
|
|
175
179
|
url: requestUrl,
|
|
176
180
|
isAuthenticated: !!req.isAuthenticated,
|
|
177
|
-
|
|
181
|
+
language,
|
|
182
|
+
messages: cloneSerializable(messages),
|
|
183
|
+
...(fallbackMessages ? { fallbackMessages: cloneSerializable(fallbackMessages) } : {}),
|
|
184
|
+
pages: manifest.pages.map((page) => ({
|
|
185
|
+
id: page.id,
|
|
186
|
+
path: page.path,
|
|
187
|
+
...(page.package ? { package: page.package } : {}),
|
|
188
|
+
})),
|
|
178
189
|
pageId: pageRoute.id,
|
|
179
190
|
}
|
|
180
191
|
const html = await mod.render(props)
|
package/src/server.js
CHANGED
|
@@ -10,6 +10,8 @@ import { SDK } from '@ossy/sdk'
|
|
|
10
10
|
import { AggregateRebuild } from '@ossy/event-store'
|
|
11
11
|
import { TaskService } from './tasks/task-service.js'
|
|
12
12
|
import { ChangeStream } from './tasks/change-stream.js'
|
|
13
|
+
import { CONTENT_SLOT_NAME } from '@ossy/app/runtime/resolve-shell-slots'
|
|
14
|
+
import { buildManifestSummary } from '@ossy/app/manifest/build-manifest-summary'
|
|
13
15
|
import { registerResourceTemplate } from './resources/resource-template.registry.js'
|
|
14
16
|
import { IntegrationService } from './integration.service.js'
|
|
15
17
|
import { ActionService } from './actions/action.service.js'
|
|
@@ -17,6 +19,7 @@ import { createLogger } from '@ossy/observability'
|
|
|
17
19
|
import { ConfigService } from './config.service.js'
|
|
18
20
|
import { UsersMiddleware } from './users.middleware.js'
|
|
19
21
|
import { WorkspacesMiddleware } from './workspaces.middleware.js'
|
|
22
|
+
import { resolveRequestLocale } from './locale.js'
|
|
20
23
|
const log = createLogger('@ossy/platform')
|
|
21
24
|
|
|
22
25
|
const DEFAULT_PORT = 3000
|
|
@@ -55,12 +58,12 @@ export function loadManifest (buildDir) {
|
|
|
55
58
|
const apis = entries.filter((e) => e.type === 'api' && validRoutable(e))
|
|
56
59
|
const tasks = entries.filter((e) => e.type === 'task')
|
|
57
60
|
const components = Array.isArray(manifest.components) ? manifest.components : []
|
|
58
|
-
const slots = Array.isArray(manifest.slots) ? manifest.slots : []
|
|
59
61
|
const resourceTemplates = Array.isArray(manifest.resourceTemplates) ? manifest.resourceTemplates : []
|
|
60
62
|
const aggregates = Array.isArray(manifest.aggregates) ? manifest.aggregates : []
|
|
61
63
|
const integrations = Array.isArray(manifest.integrations) ? manifest.integrations : []
|
|
62
64
|
const startups = Array.isArray(manifest.startups) ? manifest.startups : []
|
|
63
65
|
const actions = Array.isArray(manifest.actions) ? manifest.actions : []
|
|
66
|
+
const emails = Array.isArray(manifest.emails) ? manifest.emails : []
|
|
64
67
|
const layouts = Array.isArray(manifest.layouts) ? manifest.layouts : []
|
|
65
68
|
for (const e of entries) { if ((e.type === 'page' || e.type === 'api') && !validRoutable(e)) {
|
|
66
69
|
log.warn(`Skipping ${e.type} "${e.id}" — manifest entry has no path.`)
|
|
@@ -72,14 +75,16 @@ export function loadManifest (buildDir) {
|
|
|
72
75
|
apis,
|
|
73
76
|
tasks,
|
|
74
77
|
components,
|
|
75
|
-
slots,
|
|
76
78
|
resourceTemplates,
|
|
77
79
|
aggregates,
|
|
78
80
|
integrations,
|
|
79
81
|
startups,
|
|
80
82
|
actions,
|
|
83
|
+
emails,
|
|
81
84
|
layouts,
|
|
82
85
|
config: manifest.config || {},
|
|
86
|
+
definitions: manifest.definitions || {},
|
|
87
|
+
translations: manifest.translations || {},
|
|
83
88
|
}
|
|
84
89
|
}
|
|
85
90
|
|
|
@@ -108,6 +113,7 @@ export async function startServer (options = {}) {
|
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
const manifest = loadManifest(buildDir)
|
|
116
|
+
const manifestSummary = buildManifestSummary(manifest)
|
|
111
117
|
const config = manifest.config
|
|
112
118
|
const supportedLanguages = Array.isArray(config.supportedLanguages) ? config.supportedLanguages : []
|
|
113
119
|
const defaultLanguage = config.defaultLanguage
|
|
@@ -201,25 +207,12 @@ export async function startServer (options = {}) {
|
|
|
201
207
|
}
|
|
202
208
|
}
|
|
203
209
|
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Layout-level ComponentSlotsProviders can merge on top to override slots
|
|
211
|
-
// for their subtree (e.g. layout fills `header` with brand-specific chrome).
|
|
212
|
-
/** @type {Array<{ id: string, entry: string }>} */
|
|
213
|
-
const slotComponentEntries = []
|
|
214
|
-
for (const slotEntry of manifest.slots ?? []) {
|
|
215
|
-
try {
|
|
216
|
-
// Verify the chunk is importable at startup so we fail fast on bad builds.
|
|
217
|
-
await import(resolveEntryUrl(slotEntry.entry, buildDir))
|
|
218
|
-
slotComponentEntries.push({ id: slotEntry.slot, entry: slotEntry.entry })
|
|
219
|
-
} catch (err) {
|
|
220
|
-
log.warn(`Failed to load slot component "${slotEntry.slot}"`, undefined, err)
|
|
221
|
-
}
|
|
222
|
-
}
|
|
210
|
+
// App-controlled shell slots: `export const slots` in `*.layout.jsx` is recorded
|
|
211
|
+
// on `manifest.layouts[0].slots` at build time and resolved at render in page-runtime.
|
|
212
|
+
const layoutSlots =
|
|
213
|
+
layoutEntry && typeof layoutEntry.slots === 'object' && !Array.isArray(layoutEntry.slots)
|
|
214
|
+
? layoutEntry.slots
|
|
215
|
+
: {}
|
|
223
216
|
|
|
224
217
|
// Register the SDK so all tasks receive it as `sdk`.
|
|
225
218
|
// Priority: explicit options.sdk → SDK.of() from env vars → null (direct-DB fallback in tasks).
|
|
@@ -332,19 +325,29 @@ export async function startServer (options = {}) {
|
|
|
332
325
|
}
|
|
333
326
|
const Layout = appLayout?.component ?? null
|
|
334
327
|
const layoutEntry = appLayout?.entry ?? null
|
|
328
|
+
const { language, messages, fallbackMessages } = resolveRequestLocale(pageRouter, requestUrl, buildDir, config)
|
|
329
|
+
// Page component → `shell:content` is resolved in page-runtime (SSR + hydrate).
|
|
335
330
|
const props = {
|
|
336
331
|
...config,
|
|
337
332
|
...(req.userAppSettings || {}),
|
|
338
333
|
theme: req.userAppSettings?.theme || cloneSerializable(config?.theme),
|
|
339
334
|
themes: cloneSerializable(config?.themes),
|
|
340
335
|
resourceTemplates: cloneSerializable(manifest.resourceTemplates),
|
|
336
|
+
manifestSummary: cloneSerializable(manifestSummary),
|
|
341
337
|
url: requestUrl,
|
|
342
338
|
isAuthenticated: !!req.isAuthenticated,
|
|
343
|
-
|
|
339
|
+
language,
|
|
340
|
+
messages: cloneSerializable(messages),
|
|
341
|
+
...(fallbackMessages ? { fallbackMessages: cloneSerializable(fallbackMessages) } : {}),
|
|
342
|
+
pages: manifest.pages.map((page) => ({
|
|
343
|
+
id: page.id,
|
|
344
|
+
path: page.path,
|
|
345
|
+
...(page.package ? { package: page.package } : {}),
|
|
346
|
+
})),
|
|
344
347
|
pageId: pageRoute.id,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
+
componentEntries: manifest.components,
|
|
349
|
+
layoutSlots,
|
|
350
|
+
contentSlotName: CONTENT_SLOT_NAME,
|
|
348
351
|
Layout,
|
|
349
352
|
layoutEntry,
|
|
350
353
|
}
|
package/src/test/jest.setup.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TestUtil } from './test.util.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Integration tests run in Node on the host. Mongo in Docker is often a single-node replica set
|
|
3
5
|
* whose persisted config advertises a hostname that only resolves inside Docker (e.g.
|
|
@@ -22,3 +24,7 @@ function jestMongoUrl() {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
process.env.DB_URL = jestMongoUrl()
|
|
27
|
+
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
await TestUtil.CloseDbConnection()
|
|
30
|
+
})
|