@ossy/app 1.39.7 → 1.40.1
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/README.md +13 -10
- package/cli/build.task.js +17 -8
- package/cli/discover-translation-files.js +139 -0
- package/cli/get-platform-files.task.js +1 -0
- package/cli/manifest-plugin.js +28 -13
- package/cli/merge-translations.task.js +170 -0
- package/cli/start.task.js +2 -1
- package/package.json +13 -10
- package/runtime/page-runtime.js +40 -7
- package/src/manifest/build-manifest-summary.js +6 -1
- package/src/manifest/discover-package-definitions.js +100 -0
- package/src/manifest/serialize-package-definition.js +30 -0
- package/src/shell/App.jsx +22 -14
- package/src/shell/ThemeEditor.jsx +4 -3
package/README.md
CHANGED
|
@@ -113,22 +113,25 @@ Build output: `build/.ossy/tasks.generated.json` — a registry of `[{ type, mod
|
|
|
113
113
|
|
|
114
114
|
## Components (`*.component.jsx`)
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Register injectable UI via `*.component.jsx` in `src/` or feature packages. Components are bundled separately and resolved into slots at runtime.
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
// src/workspace-users.component.jsx
|
|
120
|
-
export const metadata = {
|
|
121
|
-
id: 'workspace/users', // rendered when pageId === 'workspace/users'
|
|
122
|
-
}
|
|
118
|
+
**Shell chrome** — the app assigns placement in `*.layout.jsx`:
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
```jsx
|
|
121
|
+
export const slots = {
|
|
122
|
+
'shell:header': 'app-header',
|
|
123
|
+
'shell:sidebar': 'app-sidebar',
|
|
126
124
|
}
|
|
127
125
|
```
|
|
128
126
|
|
|
129
|
-
|
|
127
|
+
**Resource / input UI** — feature packages use `metadata.id` as the slot key:
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
export const metadata = { id: 'resource:booking/service/form' }
|
|
131
|
+
export default function ServiceForm() { … }
|
|
132
|
+
```
|
|
130
133
|
|
|
131
|
-
See [`docs/component-primitive.md`](./docs/component-primitive.md)
|
|
134
|
+
See [`docs/component-primitive.md`](./docs/component-primitive.md) and [design-system/docs/SLOTS.md](../design-system/docs/SLOTS.md).
|
|
132
135
|
|
|
133
136
|
## Port configuration
|
|
134
137
|
|
package/cli/build.task.js
CHANGED
|
@@ -22,10 +22,12 @@ import getPlatformFiles, {
|
|
|
22
22
|
ACTION_FILE_PATTERN,
|
|
23
23
|
E2E_FILE_PATTERN,
|
|
24
24
|
LAYOUT_FILE_PATTERN,
|
|
25
|
+
TRANSLATIONS_FILE_PATTERN,
|
|
25
26
|
} from './get-platform-files.task.js'
|
|
26
27
|
import { manifestPlugin } from './manifest-plugin.js'
|
|
28
|
+
import { mergeAndEmitTranslations } from './merge-translations.task.js'
|
|
27
29
|
|
|
28
|
-
export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN, COMPONENT_FILE_PATTERN, AGGREGATE_FILE_PATTERN, INTEGRATION_FILE_PATTERN, STARTUP_FILE_PATTERN, EMAIL_FILE_PATTERN, ACTION_FILE_PATTERN, E2E_FILE_PATTERN, LAYOUT_FILE_PATTERN }
|
|
30
|
+
export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN, COMPONENT_FILE_PATTERN, AGGREGATE_FILE_PATTERN, INTEGRATION_FILE_PATTERN, STARTUP_FILE_PATTERN, EMAIL_FILE_PATTERN, ACTION_FILE_PATTERN, E2E_FILE_PATTERN, LAYOUT_FILE_PATTERN, TRANSLATIONS_FILE_PATTERN }
|
|
29
31
|
|
|
30
32
|
// Generated entry stubs live under `build/.ossy/entries/`. Putting them
|
|
31
33
|
// inside `build/` means they're cleaned automatically with every build,
|
|
@@ -185,25 +187,23 @@ function generateStartupStub ({ stubAbs, sourceAbs }) {
|
|
|
185
187
|
].join('\n')
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
// Emails are React components. The stub re-exports `id
|
|
189
|
-
//
|
|
190
|
+
// Emails are React components. The stub re-exports `id` and the default component.
|
|
191
|
+
// Optional `subject` may be resolved from translations at send time instead.
|
|
190
192
|
function generateEmailStub ({ stubAbs, sourceAbs }) {
|
|
191
193
|
const importPath = relImport(stubAbs, sourceAbs)
|
|
192
194
|
return [
|
|
193
195
|
'// Generated by @ossy/app — do not edit',
|
|
194
|
-
`export { id,
|
|
196
|
+
`export { id, default } from '${importPath}'`,
|
|
195
197
|
'',
|
|
196
198
|
].join('\n')
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
// Actions are
|
|
200
|
-
// `run` so the manifest plugin can validate them and the platform server can
|
|
201
|
-
// invoke them at runtime.
|
|
201
|
+
// Actions are intent (metadata only). Implementation lives in a matching `*.task.js`.
|
|
202
202
|
function generateActionStub ({ stubAbs, sourceAbs }) {
|
|
203
203
|
const importPath = relImport(stubAbs, sourceAbs)
|
|
204
204
|
return [
|
|
205
205
|
'// Generated by @ossy/app — do not edit',
|
|
206
|
-
`export {
|
|
206
|
+
`export { metadata } from '${importPath}'`,
|
|
207
207
|
'',
|
|
208
208
|
].join('\n')
|
|
209
209
|
}
|
|
@@ -380,6 +380,7 @@ export async function build (cliArgs = []) {
|
|
|
380
380
|
configValue,
|
|
381
381
|
appPackageName,
|
|
382
382
|
manifestPath: path.join(buildPath, 'manifest.json'),
|
|
383
|
+
projectRoot: cwd,
|
|
383
384
|
}),
|
|
384
385
|
],
|
|
385
386
|
})
|
|
@@ -398,4 +399,12 @@ export async function build (cliArgs = []) {
|
|
|
398
399
|
fs.cpSync(publicSrc, publicOutDir, { recursive: true, force: true })
|
|
399
400
|
}
|
|
400
401
|
|
|
402
|
+
mergeAndEmitTranslations({
|
|
403
|
+
srcDir,
|
|
404
|
+
projectRoot: cwd,
|
|
405
|
+
publicOutDir,
|
|
406
|
+
manifestPath: path.join(buildPath, 'manifest.json'),
|
|
407
|
+
configValue,
|
|
408
|
+
warn: (message) => console.warn(message),
|
|
409
|
+
})
|
|
401
410
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
discoverFilesByPattern,
|
|
6
|
+
TRANSLATIONS_FILE_PATTERN,
|
|
7
|
+
} from './get-platform-files.task.js'
|
|
8
|
+
import {
|
|
9
|
+
isValidLocaleCode,
|
|
10
|
+
parseLocaleFromTranslationFilename,
|
|
11
|
+
} from '@ossy/locale'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} projectRoot
|
|
15
|
+
* @returns {string[]}
|
|
16
|
+
*/
|
|
17
|
+
function collectNodeModulesDirs (projectRoot) {
|
|
18
|
+
const dirs = []
|
|
19
|
+
let current = projectRoot
|
|
20
|
+
while (true) {
|
|
21
|
+
const nm = path.join(current, 'node_modules')
|
|
22
|
+
if (fs.existsSync(nm) && fs.statSync(nm).isDirectory()) dirs.push(nm)
|
|
23
|
+
const parent = path.dirname(current)
|
|
24
|
+
if (parent === current) break
|
|
25
|
+
current = parent
|
|
26
|
+
}
|
|
27
|
+
return dirs
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {import('@ossy/locale').TranslationSourceFile} TranslationSourceFile
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Discover `*.translations.json` from installed packages and app `src/`.
|
|
36
|
+
*
|
|
37
|
+
* @param {{ srcDir: string, projectRoot: string }} options
|
|
38
|
+
* @returns {TranslationSourceFile[]}
|
|
39
|
+
*/
|
|
40
|
+
export function discoverTranslationFiles ({ srcDir, projectRoot }) {
|
|
41
|
+
/** @type {TranslationSourceFile[]} */
|
|
42
|
+
const files = []
|
|
43
|
+
const seenPaths = new Set()
|
|
44
|
+
|
|
45
|
+
const nmDirs = collectNodeModulesDirs(projectRoot)
|
|
46
|
+
/** @type {Array<{ packageName: string, sourcePath: string }>} */
|
|
47
|
+
const packageFiles = []
|
|
48
|
+
|
|
49
|
+
const tryPackageDir = (pkgDir) => {
|
|
50
|
+
const real = fs.realpathSync(pkgDir)
|
|
51
|
+
const pkgJsonPath = path.join(real, 'package.json')
|
|
52
|
+
if (!fs.existsSync(pkgJsonPath)) return
|
|
53
|
+
|
|
54
|
+
let pkg
|
|
55
|
+
try {
|
|
56
|
+
pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
|
57
|
+
} catch {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (!pkg.ossy?.src) return
|
|
61
|
+
|
|
62
|
+
const packageSrcDir = path.resolve(real, pkg.ossy.src)
|
|
63
|
+
const matches = discoverFilesByPattern(packageSrcDir, TRANSLATIONS_FILE_PATTERN)
|
|
64
|
+
for (const sourcePath of matches) {
|
|
65
|
+
const stat = fs.statSync(sourcePath)
|
|
66
|
+
if (!stat.isFile() || stat.size === 0) continue
|
|
67
|
+
packageFiles.push({ packageName: pkg.name, sourcePath })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const nmDir of nmDirs) {
|
|
72
|
+
for (const entry of fs.readdirSync(nmDir, { withFileTypes: true })) {
|
|
73
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
74
|
+
if (entry.name.startsWith('@')) {
|
|
75
|
+
const scopeDir = path.join(nmDir, entry.name)
|
|
76
|
+
if (!fs.existsSync(scopeDir)) continue
|
|
77
|
+
for (const scoped of fs.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
78
|
+
if (scoped.isDirectory() || scoped.isSymbolicLink()) {
|
|
79
|
+
tryPackageDir(path.join(scopeDir, scoped.name))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
tryPackageDir(path.join(nmDir, entry.name))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
packageFiles.sort((a, b) => {
|
|
89
|
+
const byPackage = a.packageName.localeCompare(b.packageName)
|
|
90
|
+
if (byPackage !== 0) return byPackage
|
|
91
|
+
return a.sourcePath.localeCompare(b.sourcePath)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
for (const { packageName, sourcePath } of packageFiles) {
|
|
95
|
+
if (seenPaths.has(sourcePath)) continue
|
|
96
|
+
seenPaths.add(sourcePath)
|
|
97
|
+
const locale = parseLocaleFromTranslationFilename(path.basename(sourcePath))
|
|
98
|
+
if (!locale) continue
|
|
99
|
+
files.push({
|
|
100
|
+
locale,
|
|
101
|
+
sourcePath,
|
|
102
|
+
tier: 'package',
|
|
103
|
+
packageName,
|
|
104
|
+
sortKey: `${packageName}:${sourcePath}`,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const appFiles = discoverFilesByPattern(srcDir, TRANSLATIONS_FILE_PATTERN).sort()
|
|
109
|
+
for (const sourcePath of appFiles) {
|
|
110
|
+
if (seenPaths.has(sourcePath)) continue
|
|
111
|
+
seenPaths.add(sourcePath)
|
|
112
|
+
const stat = fs.statSync(sourcePath)
|
|
113
|
+
if (!stat.isFile() || stat.size === 0) continue
|
|
114
|
+
const locale = parseLocaleFromTranslationFilename(path.basename(sourcePath))
|
|
115
|
+
if (!locale) continue
|
|
116
|
+
files.push({
|
|
117
|
+
locale,
|
|
118
|
+
sourcePath,
|
|
119
|
+
tier: 'app',
|
|
120
|
+
sortKey: sourcePath,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return files
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {TranslationSourceFile[]} files
|
|
129
|
+
*/
|
|
130
|
+
export function validateTranslationLocales (files) {
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
if (!isValidLocaleCode(file.locale)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`[@ossy/app][build] Invalid locale "${file.locale}" in ${file.sourcePath}. ` +
|
|
135
|
+
'Expected ISO 639-1 code (e.g. en, sv).',
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -13,6 +13,7 @@ export const EMAIL_FILE_PATTERN = /\.email\.(jsx?|tsx?)$/
|
|
|
13
13
|
export const ACTION_FILE_PATTERN = /\.action\.(mjs|cjs|js)$/
|
|
14
14
|
export const E2E_FILE_PATTERN = /\.e2e\.(mjs|cjs|js)$/
|
|
15
15
|
export const LAYOUT_FILE_PATTERN = /\.layout\.(jsx?|tsx?)$/
|
|
16
|
+
export const TRANSLATIONS_FILE_PATTERN = /\.translations\.json$/
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action' | 'e2e' | 'layout'} EntryKind
|
package/cli/manifest-plugin.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
5
|
import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.js'
|
|
6
|
+
import { discoverPackageDefinitions } from '../src/manifest/discover-package-definitions.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @typedef {import('./get-platform-files.task.js').PlatformEntry} PlatformEntry
|
|
@@ -29,6 +30,8 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
|
|
|
29
30
|
* the `package` field on manifest entries that live in the app `src/` tree.
|
|
30
31
|
* @property {string} manifestPath
|
|
31
32
|
* Absolute path to write the manifest to (typically `build/manifest.json`).
|
|
33
|
+
* @property {string} projectRoot
|
|
34
|
+
* Absolute path to the consuming app root — used to discover package definitions.
|
|
32
35
|
*
|
|
33
36
|
* @typedef {'page' | 'api' | 'task'} ManifestEntryType
|
|
34
37
|
*
|
|
@@ -91,6 +94,7 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
|
|
|
91
94
|
* @property {E2eManifestEntry[]} e2es Discovered `*.e2e.js` entries from installed packages.
|
|
92
95
|
* @property {LayoutManifestEntry[]} layouts Discovered `*.layout.{jsx,tsx}` entries (includes app `slots` map).
|
|
93
96
|
* @property {object} config Inlined `src/config.js` default export.
|
|
97
|
+
* @property {Record<string, object>} [definitions] Package Definition metadata keyed by slug.
|
|
94
98
|
*/
|
|
95
99
|
|
|
96
100
|
/**
|
|
@@ -131,7 +135,7 @@ function entryPackage (entryInfo, appPackageName) {
|
|
|
131
135
|
return entryInfo.packageName || appPackageName
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configValue, appPackageName = '@ossy/app', manifestPath }) {
|
|
138
|
+
export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configValue, appPackageName = '@ossy/app', manifestPath, projectRoot }) {
|
|
135
139
|
return {
|
|
136
140
|
name: 'ossy-manifest',
|
|
137
141
|
async writeBundle (_, bundle) {
|
|
@@ -264,25 +268,19 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
264
268
|
continue
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
// Actions are
|
|
268
|
-
//
|
|
269
|
-
// `access` ('public' | 'authenticated' | 'workspace', default 'authenticated').
|
|
271
|
+
// Actions are intent: `metadata` only (`{ id, access }`). Matching task
|
|
272
|
+
// with the same id holds `run` (see ADR 0003).
|
|
270
273
|
if (entryInfo.kind === 'action') {
|
|
271
|
-
const actionId = mod
|
|
274
|
+
const actionId = mod?.metadata?.id
|
|
272
275
|
if (typeof actionId !== 'string' || actionId.trim() === '') {
|
|
273
276
|
this.error(
|
|
274
|
-
`[@ossy/app][build] action entry ${entryInfo.sourcePath} must export a non-empty string "id"`,
|
|
275
|
-
)
|
|
276
|
-
}
|
|
277
|
-
if (typeof mod.run !== 'function') {
|
|
278
|
-
this.error(
|
|
279
|
-
`[@ossy/app][build] action entry ${entryInfo.sourcePath} must export a "run" function`,
|
|
277
|
+
`[@ossy/app][build] action entry ${entryInfo.sourcePath} must export a non-empty string "metadata.id"`,
|
|
280
278
|
)
|
|
281
279
|
}
|
|
282
280
|
if (seenActionIds.has(actionId)) {
|
|
283
281
|
this.error(`[@ossy/app][build] Duplicate action id "${actionId}"`)
|
|
284
282
|
}
|
|
285
|
-
const access = mod.access ?? 'authenticated'
|
|
283
|
+
const access = mod.metadata?.access ?? 'authenticated'
|
|
286
284
|
seenActionIds.add(actionId)
|
|
287
285
|
actions.push({ id: actionId, entry: url, access, package: entryPackage(entryInfo, appPackageName) })
|
|
288
286
|
continue
|
|
@@ -483,7 +481,24 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
483
481
|
}
|
|
484
482
|
|
|
485
483
|
/** @type {Manifest} */
|
|
486
|
-
const
|
|
484
|
+
const definitions = projectRoot
|
|
485
|
+
? await discoverPackageDefinitions(projectRoot)
|
|
486
|
+
: {}
|
|
487
|
+
|
|
488
|
+
const manifest = {
|
|
489
|
+
entries,
|
|
490
|
+
components,
|
|
491
|
+
resourceTemplates,
|
|
492
|
+
aggregates,
|
|
493
|
+
integrations,
|
|
494
|
+
startups,
|
|
495
|
+
emails,
|
|
496
|
+
actions,
|
|
497
|
+
e2es,
|
|
498
|
+
layouts,
|
|
499
|
+
definitions,
|
|
500
|
+
config: configValue || {},
|
|
501
|
+
}
|
|
487
502
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
488
503
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
|
489
504
|
},
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { mergeTranslationFiles, pageIdToDocumentTitleKey } from '@ossy/locale'
|
|
5
|
+
import {
|
|
6
|
+
discoverTranslationFiles,
|
|
7
|
+
validateTranslationLocales,
|
|
8
|
+
} from './discover-translation-files.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Array<{ type: string, id: string, path?: string | Record<string, string> }>} entries
|
|
12
|
+
* @param {string[]} supportedLanguages
|
|
13
|
+
* @param {string | undefined} defaultLanguage
|
|
14
|
+
* @param {(message: string) => void} error
|
|
15
|
+
* @param {(message: string) => void} warn
|
|
16
|
+
*/
|
|
17
|
+
export function validateLanguageConfig ({ entries, supportedLanguages, defaultLanguage, error, warn }) {
|
|
18
|
+
if (!Array.isArray(supportedLanguages) || supportedLanguages.length === 0) return
|
|
19
|
+
|
|
20
|
+
if (defaultLanguage && !supportedLanguages.includes(defaultLanguage)) {
|
|
21
|
+
error(
|
|
22
|
+
`[@ossy/app][build] config.defaultLanguage "${defaultLanguage}" is not in supportedLanguages`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const entry of entries.filter((e) => e.type === 'page')) {
|
|
27
|
+
if (typeof entry.path !== 'object' || !entry.path) continue
|
|
28
|
+
|
|
29
|
+
for (const lang of Object.keys(entry.path)) {
|
|
30
|
+
if (!supportedLanguages.includes(lang)) {
|
|
31
|
+
error(
|
|
32
|
+
`[@ossy/app][build] Page "${entry.id}" metadata.path key "${lang}" is not in supportedLanguages`,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const lang of supportedLanguages) {
|
|
38
|
+
if (!(lang in entry.path)) {
|
|
39
|
+
warn(
|
|
40
|
+
`[@ossy/app][build] Page "${entry.id}" metadata.path is missing key for supported language "${lang}"`,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {Array<{ type: string, id: string, title?: string }>} entries
|
|
49
|
+
* @param {Record<string, Record<string, string>>} catalogs
|
|
50
|
+
* @param {string | undefined} defaultLanguage
|
|
51
|
+
* @param {(message: string) => void} warn
|
|
52
|
+
*/
|
|
53
|
+
export function validatePageDocumentTitles ({ entries, catalogs, defaultLanguage, warn }) {
|
|
54
|
+
if (!defaultLanguage) return
|
|
55
|
+
const catalog = catalogs[defaultLanguage] || {}
|
|
56
|
+
|
|
57
|
+
for (const entry of entries.filter((e) => e.type === 'page')) {
|
|
58
|
+
if (entry.title) continue
|
|
59
|
+
const key = pageIdToDocumentTitleKey(entry.id)
|
|
60
|
+
if (!catalog[key]) {
|
|
61
|
+
warn(
|
|
62
|
+
`[@ossy/app][build] Page "${entry.id}" document title key "${key}" missing in default language "${defaultLanguage}" catalog`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {{ actions?: Array<{ id: string }> }} manifest
|
|
70
|
+
* @param {Record<string, Record<string, string>>} catalogs
|
|
71
|
+
* @param {string | undefined} defaultLanguage
|
|
72
|
+
* @param {(message: string) => void} warn
|
|
73
|
+
*/
|
|
74
|
+
export function validateActionTranslationKeys ({ manifest, catalogs, defaultLanguage, warn }) {
|
|
75
|
+
if (!defaultLanguage) return
|
|
76
|
+
const catalog = catalogs[defaultLanguage] || {}
|
|
77
|
+
const actions = Array.isArray(manifest?.actions) ? manifest.actions : []
|
|
78
|
+
|
|
79
|
+
for (const action of actions) {
|
|
80
|
+
const actionId = action.id
|
|
81
|
+
if (!actionId) continue
|
|
82
|
+
if (!catalog[`${actionId}.label`]) {
|
|
83
|
+
warn(
|
|
84
|
+
`[@ossy/app][build] Action "${actionId}" missing "${actionId}.label" in default language catalog`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
if (!catalog[`${actionId}.description`]) {
|
|
88
|
+
warn(
|
|
89
|
+
`[@ossy/app][build] Action "${actionId}" missing "${actionId}.description" in default language catalog`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Discover, merge, validate, and emit per-locale translation catalogs.
|
|
97
|
+
*
|
|
98
|
+
* @param {{
|
|
99
|
+
* srcDir: string
|
|
100
|
+
* projectRoot: string
|
|
101
|
+
* publicOutDir: string
|
|
102
|
+
* manifestPath: string
|
|
103
|
+
* configValue: object
|
|
104
|
+
* log?: (message: string) => void
|
|
105
|
+
* warn?: (message: string) => void
|
|
106
|
+
* }} options
|
|
107
|
+
*/
|
|
108
|
+
export function mergeAndEmitTranslations ({
|
|
109
|
+
srcDir,
|
|
110
|
+
projectRoot,
|
|
111
|
+
publicOutDir,
|
|
112
|
+
manifestPath,
|
|
113
|
+
configValue,
|
|
114
|
+
log = () => {},
|
|
115
|
+
warn = (message) => log(message),
|
|
116
|
+
}) {
|
|
117
|
+
const supportedLanguages = Array.isArray(configValue?.supportedLanguages)
|
|
118
|
+
? configValue.supportedLanguages
|
|
119
|
+
: []
|
|
120
|
+
const defaultLanguage = typeof configValue?.defaultLanguage === 'string'
|
|
121
|
+
? configValue.defaultLanguage
|
|
122
|
+
: undefined
|
|
123
|
+
|
|
124
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
125
|
+
const entries = Array.isArray(manifest.entries) ? manifest.entries : []
|
|
126
|
+
|
|
127
|
+
/** @type {string[]} */
|
|
128
|
+
const errors = []
|
|
129
|
+
validateLanguageConfig({
|
|
130
|
+
entries,
|
|
131
|
+
supportedLanguages,
|
|
132
|
+
defaultLanguage,
|
|
133
|
+
error: (message) => errors.push(message),
|
|
134
|
+
warn,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const discovered = discoverTranslationFiles({ srcDir, projectRoot })
|
|
138
|
+
validateTranslationLocales(discovered)
|
|
139
|
+
|
|
140
|
+
const catalogs = mergeTranslationFiles(discovered, supportedLanguages, {
|
|
141
|
+
readFileSync: (filePath, encoding) => fs.readFileSync(filePath, encoding),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
for (const lang of supportedLanguages) {
|
|
145
|
+
const keys = Object.keys(catalogs[lang] || {})
|
|
146
|
+
if (keys.length === 0) {
|
|
147
|
+
warn(
|
|
148
|
+
`[@ossy/app][build] No translation keys merged for supported language "${lang}"`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
validatePageDocumentTitles({ entries, catalogs, defaultLanguage, warn })
|
|
154
|
+
validateActionTranslationKeys({ manifest, catalogs, defaultLanguage, warn })
|
|
155
|
+
|
|
156
|
+
/** @type {Record<string, string>} */
|
|
157
|
+
const files = {}
|
|
158
|
+
for (const lang of supportedLanguages) {
|
|
159
|
+
const outPath = path.join(publicOutDir, `${lang}.translations.json`)
|
|
160
|
+
fs.writeFileSync(outPath, JSON.stringify(catalogs[lang] || {}, null, 2) + '\n', 'utf8')
|
|
161
|
+
files[lang] = `/${lang}.translations.json`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (errors.length > 0) {
|
|
165
|
+
throw new Error(errors.join('\n'))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
manifest.translations = { files }
|
|
169
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
|
170
|
+
}
|
package/cli/start.task.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import arg from 'arg'
|
|
3
3
|
import { startServer } from '@ossy/platform/server'
|
|
4
|
+
import 'dotenv/config'
|
|
4
5
|
|
|
5
6
|
export async function start (cliArgs = []) {
|
|
6
7
|
const options = arg({
|
|
@@ -13,7 +14,7 @@ export async function start (cliArgs = []) {
|
|
|
13
14
|
const cwd = options['--cwd'] ? path.resolve(options['--cwd']) : process.cwd()
|
|
14
15
|
const buildDir = options['--build-dir'] || 'build'
|
|
15
16
|
const port = options['--port']
|
|
16
|
-
|
|
17
|
+
|
|
17
18
|
const { lifetime } = await startServer({ cwd, buildDir, port })
|
|
18
19
|
await lifetime
|
|
19
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"source": "./src/index.js",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"./runtime/resolve-shell-slots": "./runtime/resolve-shell-slots.js",
|
|
13
13
|
"./runtime/api-runtime": "./runtime/api-runtime.js",
|
|
14
14
|
"./runtime/task-runtime": "./runtime/task-runtime.js",
|
|
15
|
-
"./manifest/build-manifest-summary": "./src/manifest/build-manifest-summary.js"
|
|
15
|
+
"./manifest/build-manifest-summary": "./src/manifest/build-manifest-summary.js",
|
|
16
|
+
"./manifest/discover-package-definitions": "./src/manifest/discover-package-definitions.js",
|
|
17
|
+
"./manifest/serialize-package-definition": "./src/manifest/serialize-package-definition.js"
|
|
16
18
|
},
|
|
17
19
|
"bin": {
|
|
18
20
|
"app": "./cli/index.js"
|
|
@@ -40,14 +42,15 @@
|
|
|
40
42
|
"@babel/eslint-parser": "^7.15.8",
|
|
41
43
|
"@babel/preset-react": "^7.26.3",
|
|
42
44
|
"@babel/register": "^7.25.9",
|
|
43
|
-
"@ossy/design-system": "^1.
|
|
45
|
+
"@ossy/design-system": "^1.40.1",
|
|
46
|
+
"@ossy/locale": "^1.40.1",
|
|
44
47
|
"@ossy/pages": "^1.23.0",
|
|
45
|
-
"@ossy/platform": "^1.
|
|
46
|
-
"@ossy/router": "^1.
|
|
47
|
-
"@ossy/router-react": "^1.
|
|
48
|
-
"@ossy/sdk": "^1.
|
|
49
|
-
"@ossy/sdk-react": "^1.
|
|
50
|
-
"@ossy/themes": "^1.
|
|
48
|
+
"@ossy/platform": "^1.39.1",
|
|
49
|
+
"@ossy/router": "^1.40.1",
|
|
50
|
+
"@ossy/router-react": "^1.40.1",
|
|
51
|
+
"@ossy/sdk": "^1.40.1",
|
|
52
|
+
"@ossy/sdk-react": "^1.40.1",
|
|
53
|
+
"@ossy/themes": "^1.40.1",
|
|
51
54
|
"@rollup/plugin-alias": "^6.0.0",
|
|
52
55
|
"@rollup/plugin-babel": "^7.0.0",
|
|
53
56
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -81,5 +84,5 @@
|
|
|
81
84
|
"README.md",
|
|
82
85
|
"tsconfig.json"
|
|
83
86
|
],
|
|
84
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "c0ba5d90749690634e4dc2705178ff8d89dd3070"
|
|
85
88
|
}
|
package/runtime/page-runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createElement } from 'react'
|
|
2
|
+
import { pageIdToDocumentTitleKey, resolveMessage } from '@ossy/locale'
|
|
2
3
|
import { App } from '../src/shell/App.jsx'
|
|
3
4
|
import { resolvePageSlots } from './resolve-shell-slots.js'
|
|
4
5
|
|
|
@@ -41,16 +42,44 @@ export async function loadLayout (layoutEntry) {
|
|
|
41
42
|
try {
|
|
42
43
|
const mod = await import(layoutEntry)
|
|
43
44
|
return mod?.default ?? null
|
|
44
|
-
} catch {
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
47
|
+
console.error('[@ossy/app][page-runtime] Failed to load layout bundle:', layoutEntry, err)
|
|
48
|
+
}
|
|
45
49
|
return null
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
function resolveDocumentTitle (metadata, props) {
|
|
54
|
+
if (metadata.id && props.messages) {
|
|
55
|
+
const key = pageIdToDocumentTitleKey(metadata.id)
|
|
56
|
+
const hasTranslation =
|
|
57
|
+
props.messages[key] != null || props.fallbackMessages?.[key] != null
|
|
58
|
+
if (hasTranslation) {
|
|
59
|
+
return resolveMessage(props.messages, key, {
|
|
60
|
+
fallbackCatalog: props.fallbackMessages,
|
|
61
|
+
onMissingKey: (missingKey) => {
|
|
62
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
63
|
+
console.warn(
|
|
64
|
+
`[@ossy/app][page-runtime] Missing document title key "${missingKey}" for page "${metadata.id}"`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
71
|
+
console.warn(
|
|
72
|
+
`[@ossy/app][page-runtime] Missing document title key "${key}" for page "${metadata.id}"; falling back to metadata.title`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return metadata.title || props.documentTitle || ''
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
function buildTree ({ Component, Layout, metadata, props }) {
|
|
50
|
-
const lang = props.
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
: createElement(Component, props)
|
|
80
|
+
const lang = props.language || props.defaultLanguage || 'en'
|
|
81
|
+
const pageEl = createElement(Component, props)
|
|
82
|
+
const contentEl = Layout ? createElement(Layout, props, pageEl) : pageEl
|
|
54
83
|
return createElement(
|
|
55
84
|
'html',
|
|
56
85
|
{ lang },
|
|
@@ -58,9 +87,9 @@ function buildTree ({ Component, Layout, metadata, props }) {
|
|
|
58
87
|
'head',
|
|
59
88
|
null,
|
|
60
89
|
createElement('meta', { charSet: 'utf-8' }),
|
|
61
|
-
createElement('title', null, metadata
|
|
90
|
+
createElement('title', null, resolveDocumentTitle(metadata, props)),
|
|
62
91
|
),
|
|
63
|
-
createElement(App, props, contentEl),
|
|
92
|
+
createElement('body', null, createElement(App, props, contentEl)),
|
|
64
93
|
)
|
|
65
94
|
}
|
|
66
95
|
|
|
@@ -152,6 +181,10 @@ export function createPageEntry (pageModule, options = {}) {
|
|
|
152
181
|
pageComponent: Component,
|
|
153
182
|
})
|
|
154
183
|
hydrateRoot(document, buildTree({ Component, Layout, metadata, props: { ...pageProps, components } }))
|
|
184
|
+
}).catch((err) => {
|
|
185
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
186
|
+
console.error('[@ossy/app][page-runtime] Hydration failed:', err)
|
|
187
|
+
}
|
|
155
188
|
})
|
|
156
189
|
}
|
|
157
190
|
|
|
@@ -106,5 +106,10 @@ export function buildManifestSummary (manifest) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
const definitions =
|
|
110
|
+
manifest.definitions && typeof manifest.definitions === 'object' && !Array.isArray(manifest.definitions)
|
|
111
|
+
? manifest.definitions
|
|
112
|
+
: {}
|
|
113
|
+
|
|
114
|
+
return { packages, definitions }
|
|
110
115
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { packageNameToSlug } from './build-manifest-summary.js'
|
|
6
|
+
import { serializePackageDefinition } from './serialize-package-definition.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} projectRoot
|
|
10
|
+
* @returns {string[]}
|
|
11
|
+
*/
|
|
12
|
+
function collectNodeModulesDirs (projectRoot) {
|
|
13
|
+
const dirs = []
|
|
14
|
+
let current = projectRoot
|
|
15
|
+
while (true) {
|
|
16
|
+
const nm = path.join(current, 'node_modules')
|
|
17
|
+
if (fs.existsSync(nm) && fs.statSync(nm).isDirectory()) dirs.push(nm)
|
|
18
|
+
const parent = path.dirname(current)
|
|
19
|
+
if (parent === current) break
|
|
20
|
+
current = parent
|
|
21
|
+
}
|
|
22
|
+
return dirs
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load `Definition.js` from disk only — never import the package main entry,
|
|
27
|
+
* which may pull CLI side effects (e.g. `@ossy/cli` runs on import).
|
|
28
|
+
*
|
|
29
|
+
* @param {string} pkgDir
|
|
30
|
+
* @param {object} pkg
|
|
31
|
+
* @returns {Promise<object | null>}
|
|
32
|
+
*/
|
|
33
|
+
async function loadPackageDefinition (pkgDir, pkg) {
|
|
34
|
+
if (!pkg.ossy?.src) return null
|
|
35
|
+
|
|
36
|
+
const defPath = path.join(pkgDir, pkg.ossy.src, 'Definition.js')
|
|
37
|
+
if (!fs.existsSync(defPath)) return null
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const mod = await import(pathToFileURL(defPath).href + `?t=${Date.now()}`)
|
|
41
|
+
if (mod?.Definition && typeof mod.Definition === 'object') {
|
|
42
|
+
return mod.Definition
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// skip packages without a readable Definition
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Discover `Definition.js` from installed `@ossy/*` packages.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} projectRoot Absolute path to the consuming app root.
|
|
55
|
+
* @returns {Promise<Record<string, object>>} Map keyed by package slug.
|
|
56
|
+
*/
|
|
57
|
+
export async function discoverPackageDefinitions (projectRoot) {
|
|
58
|
+
const nmDirs = collectNodeModulesDirs(projectRoot)
|
|
59
|
+
/** @type {Record<string, object>} */
|
|
60
|
+
const definitions = {}
|
|
61
|
+
const seen = new Set()
|
|
62
|
+
|
|
63
|
+
const tryPackageDir = async (pkgDir) => {
|
|
64
|
+
const real = fs.realpathSync(pkgDir)
|
|
65
|
+
if (seen.has(real)) return
|
|
66
|
+
seen.add(real)
|
|
67
|
+
|
|
68
|
+
const pkgJsonPath = path.join(pkgDir, 'package.json')
|
|
69
|
+
if (!fs.existsSync(pkgJsonPath)) return
|
|
70
|
+
|
|
71
|
+
let pkg
|
|
72
|
+
try {
|
|
73
|
+
pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
|
74
|
+
} catch {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const name = typeof pkg.name === 'string' ? pkg.name.trim() : ''
|
|
79
|
+
if (!name.startsWith('@ossy/')) return
|
|
80
|
+
|
|
81
|
+
const slug = packageNameToSlug(name)
|
|
82
|
+
if (!slug || definitions[slug]) return
|
|
83
|
+
|
|
84
|
+
const raw = await loadPackageDefinition(real, pkg)
|
|
85
|
+
const serialized = serializePackageDefinition(raw)
|
|
86
|
+
if (serialized) definitions[slug] = serialized
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const nmDir of nmDirs) {
|
|
90
|
+
const scopeDir = path.join(nmDir, '@ossy')
|
|
91
|
+
if (!fs.existsSync(scopeDir) || !fs.statSync(scopeDir).isDirectory()) continue
|
|
92
|
+
|
|
93
|
+
for (const entry of fs.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
94
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
95
|
+
await tryPackageDir(path.join(scopeDir, entry.name))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return definitions
|
|
100
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick JSON-serializable Definition fields for manifest / SSR bootstrap.
|
|
3
|
+
* Definition holds module presentation metadata only — capabilities live in manifestSummary.
|
|
4
|
+
*
|
|
5
|
+
* @param {object | null | undefined} definition
|
|
6
|
+
* @returns {object | null}
|
|
7
|
+
*/
|
|
8
|
+
export function serializePackageDefinition (definition) {
|
|
9
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition)) return null
|
|
10
|
+
|
|
11
|
+
/** @type {Record<string, unknown>} */
|
|
12
|
+
const out = {}
|
|
13
|
+
|
|
14
|
+
if (typeof definition.id === 'string' && definition.id.trim()) out.id = definition.id.trim()
|
|
15
|
+
if (typeof definition.title === 'string' && definition.title.trim()) out.title = definition.title.trim()
|
|
16
|
+
if (typeof definition.description === 'string' && definition.description.trim()) {
|
|
17
|
+
out.description = definition.description.trim()
|
|
18
|
+
}
|
|
19
|
+
if (typeof definition.icon === 'string' && definition.icon.trim()) out.icon = definition.icon.trim()
|
|
20
|
+
if (typeof definition.navOrder === 'number' && Number.isFinite(definition.navOrder)) {
|
|
21
|
+
out.navOrder = definition.navOrder
|
|
22
|
+
}
|
|
23
|
+
if (definition.entitlementRequired === false) out.entitlementRequired = false
|
|
24
|
+
if (typeof definition.status === 'string' && definition.status.trim()) out.status = definition.status.trim()
|
|
25
|
+
if (Array.isArray(definition.statuses) && definition.statuses.length) {
|
|
26
|
+
out.statuses = definition.statuses.filter((s) => typeof s === 'string' && s.trim()).map((s) => s.trim())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Object.keys(out).length ? out : null
|
|
30
|
+
}
|
package/src/shell/App.jsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { SDK } from '@ossy/sdk'
|
|
3
3
|
import { WorkspaceProvider } from '@ossy/sdk-react'
|
|
4
|
-
import { Theme, ComponentSlotsProvider } from '@ossy/design-system'
|
|
4
|
+
import { Theme, ComponentSlotsProvider, LocaleProvider } from '@ossy/design-system'
|
|
5
5
|
import { ThemeEditor } from './ThemeEditor.jsx'
|
|
6
6
|
import { defaultAppSettings } from './AppSettings.jsx'
|
|
7
7
|
import { Router } from '@ossy/router-react'
|
|
8
8
|
import { AppContext } from './AppContext.js'
|
|
9
9
|
|
|
10
|
-
export const App = ({ children, ..._appSettings }) => {
|
|
10
|
+
export const App = ({ children, language, messages, fallbackMessages, ..._appSettings }) => {
|
|
11
11
|
const appSettings = { ...defaultAppSettings(), ..._appSettings }
|
|
12
12
|
|
|
13
13
|
// `components` holds resolved ComponentType values — not JSON-serializable,
|
|
@@ -20,17 +20,25 @@ export const App = ({ children, ..._appSettings }) => {
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
<LocaleProvider
|
|
24
|
+
language={language}
|
|
25
|
+
messages={messages}
|
|
26
|
+
fallbackMessages={fallbackMessages}
|
|
27
|
+
defaultLanguage={appSettings.defaultLanguage}
|
|
28
|
+
supportedLanguages={appSettings.supportedLanguages}
|
|
29
|
+
>
|
|
30
|
+
<AppContext.Provider value={contextSettings}>
|
|
31
|
+
<ComponentSlotsProvider slots={components || {}}>
|
|
32
|
+
<Theme theme={appSettings.theme} themes={appSettings.themes}>
|
|
33
|
+
<WorkspaceProvider sdk={sdk}>
|
|
34
|
+
<Router {...appSettings} pages={appSettings.pages || []}>
|
|
35
|
+
{children}
|
|
36
|
+
{appSettings.devMode && <ThemeEditor />}
|
|
37
|
+
</Router>
|
|
38
|
+
</WorkspaceProvider>
|
|
39
|
+
</Theme>
|
|
40
|
+
</ComponentSlotsProvider>
|
|
41
|
+
</AppContext.Provider>
|
|
42
|
+
</LocaleProvider>
|
|
35
43
|
)
|
|
36
44
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useMemo, useCallback, useEffect } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { updateResourceContent } from '@ossy/resources'
|
|
3
|
+
import { useSdk } from '@ossy/sdk-react'
|
|
3
4
|
import { Overlay, Button, useTheme, View, Text } from '@ossy/design-system'
|
|
4
5
|
import { DevPagesPanel } from './DevPagesPanel.jsx'
|
|
5
6
|
|
|
@@ -40,7 +41,7 @@ const ThemeSwitcher = () => {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export const ThemeEditor = () => {
|
|
43
|
-
const
|
|
44
|
+
const sdk = useSdk()
|
|
44
45
|
const [isEditorOpen, setIsEditorOpen] = useState(false)
|
|
45
46
|
const [viewCount, setViewCount] = useState(0)
|
|
46
47
|
// const [theme, temporarilyUpdateTheme] = useTheme()
|
|
@@ -72,7 +73,7 @@ export const ThemeEditor = () => {
|
|
|
72
73
|
|
|
73
74
|
const onSaveTheme = event => {
|
|
74
75
|
event.preventDefault()
|
|
75
|
-
updateResourceContent(theme)
|
|
76
|
+
updateResourceContent(sdk, 'PCX53TaGviq4_8KvK-VOp', theme)
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
useEffect(() => {
|