@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/platform",
3
- "version": "1.38.6",
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.7.6",
42
- "@ossy/observability": "^1.7.6",
43
- "@ossy/policies": "^1.12.6",
44
- "@ossy/router": "^1.39.6",
45
- "@ossy/sdk": "^1.39.6",
46
- "@ossy/tokens": "^1.12.6",
47
- "@ossy/users": "^1.12.6",
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": "72d150cf20ff7ba25b328bc5b429b065c2840b2b"
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
- module: {
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
  }
@@ -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 — top bar across full width
18
- * sidebar — left column, full height
19
- * toolbar — below header, above content (contextual actions)
20
- * notifications — floating / bottom corner (toasts, badges)
21
- * system-messages — banner above content (maintenance, errors)
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
- pages: manifest.pages.map((page) => ({ id: page.id, path: page.path })),
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
- // Slot loading import each component that declares `export const slot` and
205
- // build a map of slot-name entry URL for the render pipeline. Components
206
- // whose chunk fails to import are silently skipped so a broken slot never
207
- // prevents the server from starting. The slot entries are appended to
208
- // `componentEntries` at render time, keyed by slot name, so that the App's
209
- // ComponentSlotsProvider makes them available to the entire React tree.
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
- pages: manifest.pages.map((page) => ({ id: page.id, path: page.path })),
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
- // Slot component entries are prepended so that layout-level
346
- // ComponentSlotsProviders (which merge on top) can override them.
347
- componentEntries: [...slotComponentEntries, ...manifest.components],
348
+ componentEntries: manifest.components,
349
+ layoutSlots,
350
+ contentSlotName: CONTENT_SLOT_NAME,
348
351
  Layout,
349
352
  layoutEntry,
350
353
  }
@@ -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
+ })