@researchdatabox/redbox-dev-tools 0.0.1-beta.10213 → 0.0.1-beta.10760
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 +25 -3
- package/dist/cli.js +6 -2
- package/dist/commands/form-config.js +8 -11
- package/dist/commands/sync-translation.d.ts +12 -0
- package/dist/commands/sync-translation.js +160 -0
- package/dist/templates/hook-archetype.js +4 -3
- package/package.json +3 -3
- package/templates/hook-archetype/standard/.gitignore.hbs +1 -0
- package/templates/hook-archetype/standard/.npmignore.hbs +2 -1
- package/templates/hook-archetype/standard/AGENTS.md.hbs +18 -1
- package/templates/hook-archetype/standard/Dockerfile.hbs +12 -11
- package/templates/hook-archetype/standard/README.md.hbs +28 -10
- package/templates/hook-archetype/standard/assets/default/.gitkeep +1 -0
- package/templates/hook-archetype/standard/assets/images/.gitkeep +1 -0
- package/templates/hook-archetype/standard/assets/styles/.gitkeep +1 -0
- package/templates/hook-archetype/standard/language-defaults/README.md.hbs +10 -0
- package/templates/hook-archetype/standard/language-defaults/en/translation.json.hbs +3 -0
- package/templates/hook-archetype/standard/language-defaults/meta.json.hbs +6 -0
- package/templates/hook-archetype/standard/package.json.hbs +10 -6
- package/templates/hook-archetype/standard/src/index.ts.hbs +11 -11
- package/templates/hook-archetype/standard/src/migrations/index.ts.hbs +19 -0
- package/templates/hook-archetype/standard/support/bootstrap-data/namedqueries/.gitkeep +1 -0
- package/templates/hook-archetype/standard/support/bootstrap-data/records/.gitkeep +1 -0
- package/templates/hook-archetype/standard/support/bootstrap-data/reports/.gitkeep +1 -0
- package/templates/hook-archetype/standard/support/bootstrap-data/vocabularies/.gitkeep +1 -0
- package/templates/hook-archetype/standard/support/development/core-mount.env.hbs +3 -0
- package/templates/hook-archetype/standard/support/development/docker-compose.core-mount.yml.hbs +37 -0
- package/templates/hook-archetype/standard/support/development/docker-compose.yml.hbs +53 -20
- package/templates/hook-archetype/standard/support/development/prepare-core-mount.sh.hbs +61 -0
- package/templates/hook-archetype/standard/support/integration-testing/docker-compose.mocha.yml.hbs +45 -11
- package/templates/hook-archetype/standard/support/integration-testing/run-mocha-hook.sh.hbs +5 -0
- package/templates/hook-archetype/standard/support/integration-testing/run-mocha-redbox.sh.hbs +100 -0
- package/templates/hook-archetype/standard/support/prepare-minio.sh.hbs +74 -0
- package/templates/hook-archetype/standard/test/integration/hook-registration.test.ts.hbs +3 -1
- package/templates/hook-archetype/standard/test/unit/index.test.ts.hbs +11 -5
- package/templates/hook-archetype/standard/views/default/.gitkeep +1 -0
package/README.md
CHANGED
|
@@ -31,11 +31,11 @@ npx @researchdatabox/redbox-dev-tools init redbox-hook-my-client
|
|
|
31
31
|
This will:
|
|
32
32
|
|
|
33
33
|
- Generate the standard hook archetype in the current directory
|
|
34
|
-
- Create `src/`, `test/`, `support/`, `assets/`, `views/`, and `
|
|
34
|
+
- Create `src/`, `test/`, `support/`, `assets/`, `views/`, `language-defaults/`, and `support/bootstrap-data/`
|
|
35
35
|
- Create a `package.json` with the shared Redbox dependency contract
|
|
36
|
-
- Generate starter controller, service, form-config, config, Docker, and test files
|
|
36
|
+
- Generate starter controller, service, form-config, config, migration registry, Docker, development, and test files
|
|
37
37
|
|
|
38
|
-
The standard archetype is based on the current hook structure used for `redbox-hook-jcu`, but stripped back to generic placeholders so a new client hook can start from the same shape without inheriting client-specific logic.
|
|
38
|
+
The standard archetype is based on the current hook structure used for migrated client hooks such as `redbox-hook-jcu` and `redbox-hook-cqu`, but stripped back to generic placeholders so a new client hook can start from the same shape without inheriting client-specific logic.
|
|
39
39
|
|
|
40
40
|
The archetype is now backed by file-based Handlebars templates under [templates/hook-archetype](templates/hook-archetype), which gives us a clean path for future variants such as branding-heavy hooks, lighter hooks, or alternate TypeScript setups.
|
|
41
41
|
|
|
@@ -102,6 +102,7 @@ module.exports = hook;
|
|
|
102
102
|
Hooks should declare only:
|
|
103
103
|
|
|
104
104
|
- `peerDependencies.@researchdatabox/redbox-core`
|
|
105
|
+
- `devDependencies.@researchdatabox/redbox-core`
|
|
105
106
|
- `devDependencies.@researchdatabox/redbox-dev-tools`
|
|
106
107
|
- direct `dependencies` for hook-owned runtime libraries only
|
|
107
108
|
|
|
@@ -142,6 +143,19 @@ Template variants are selected with `--template`:
|
|
|
142
143
|
npx @researchdatabox/redbox-dev-tools init redbox-hook-my-client --template standard
|
|
143
144
|
```
|
|
144
145
|
|
|
146
|
+
The generated project includes a typed migration registry under `src/migrations`, client customisation slots under `support/bootstrap-data`, `assets`, `views`, and `language-defaults`, and Docker-backed local development scripts:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm run dev:run:build
|
|
150
|
+
npm run dev:run
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
To mount a local `redbox-portal` checkout while migrating a client hook, set `REDBOX_PORTAL_PATH` in `support/development/core-mount.env` and run:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npm run dev:run:core
|
|
157
|
+
```
|
|
158
|
+
|
|
145
159
|
### `check`
|
|
146
160
|
|
|
147
161
|
Validate that the current hook package does not directly pin shared Redbox runtime or toolchain dependencies.
|
|
@@ -209,6 +223,14 @@ Optional Angular service scaffold:
|
|
|
209
223
|
npx @researchdatabox/redbox-dev-tools generate form-component my-widget --app form --with-service
|
|
210
224
|
```
|
|
211
225
|
|
|
226
|
+
### `sync-translation`
|
|
227
|
+
|
|
228
|
+
Read translations from API and write to language defaults files.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npx @researchdatabox/redbox-dev-tools sync-translation -a "https://example.com" -l ~/redbox-portal/language-defaults
|
|
232
|
+
```
|
|
233
|
+
|
|
212
234
|
### `help`
|
|
213
235
|
|
|
214
236
|
Show CLI help and usage information.
|
package/dist/cli.js
CHANGED
|
@@ -42,12 +42,14 @@ const hook_archetype_1 = require("./templates/hook-archetype");
|
|
|
42
42
|
const form_config_1 = require("./commands/form-config");
|
|
43
43
|
const completion_1 = require("./completion");
|
|
44
44
|
const cli_parsers_1 = require("./cli-parsers");
|
|
45
|
+
const sync_translation_1 = require("./commands/sync-translation");
|
|
45
46
|
const program = new commander_1.Command();
|
|
47
|
+
const packageJson = readPackageJson(path.resolve(__dirname, '..', 'package.json'));
|
|
46
48
|
// List of dependencies that are common in our legacy hooks but should not be in modern hook packages as they are supplied by redbox-core
|
|
47
49
|
const forbiddenHookDeps = ['axios', 'rxjs', 'lodash', 'mocha', 'chai', 'ts-node', 'typescript'];
|
|
48
50
|
const allowedSharedHookDeps = {
|
|
49
51
|
dependencies: new Set(),
|
|
50
|
-
devDependencies: new Set(['@researchdatabox/redbox-dev-tools']),
|
|
52
|
+
devDependencies: new Set(['@researchdatabox/redbox-core', '@researchdatabox/redbox-dev-tools']),
|
|
51
53
|
peerDependencies: new Set(['@researchdatabox/redbox-core']),
|
|
52
54
|
};
|
|
53
55
|
function readPackageJson(pkgPath) {
|
|
@@ -80,6 +82,7 @@ function migrateHookDependencyContract(pkg) {
|
|
|
80
82
|
delete nextPkg.peerDependencies[depName];
|
|
81
83
|
}
|
|
82
84
|
nextPkg.peerDependencies['@researchdatabox/redbox-core'] = nextPkg.peerDependencies['@researchdatabox/redbox-core'] ?? '*';
|
|
85
|
+
nextPkg.devDependencies['@researchdatabox/redbox-core'] = nextPkg.devDependencies['@researchdatabox/redbox-core'] ?? nextPkg.peerDependencies['@researchdatabox/redbox-core'];
|
|
83
86
|
nextPkg.devDependencies['@researchdatabox/redbox-dev-tools'] = nextPkg.devDependencies['@researchdatabox/redbox-dev-tools'] ?? '*';
|
|
84
87
|
return nextPkg;
|
|
85
88
|
}
|
|
@@ -104,7 +107,7 @@ function findForbiddenHookDeps(pkg) {
|
|
|
104
107
|
program
|
|
105
108
|
.name('redbox-dev-tools')
|
|
106
109
|
.description('CLI for ReDBox hook development and code generation')
|
|
107
|
-
.version('
|
|
110
|
+
.version(packageJson.version ?? '0.0.0')
|
|
108
111
|
.option('--root <path>', 'ReDBox repo root directory (required for generators)')
|
|
109
112
|
.option('--core-types-root <path>', 'Override core-types package root')
|
|
110
113
|
.option('--angular-root <path>', 'Override angular projects root')
|
|
@@ -131,6 +134,7 @@ program
|
|
|
131
134
|
(0, form_config_1.registerMigrateFigshareConfigCommand)(program);
|
|
132
135
|
(0, form_config_1.registerClientFormConfigCommand)(program);
|
|
133
136
|
(0, form_config_1.registerQuestionTreeDiagramCommand)(program);
|
|
137
|
+
(0, sync_translation_1.registerSyncTranslationCommand)(program);
|
|
134
138
|
program
|
|
135
139
|
.command('init [name]')
|
|
136
140
|
.description('Initialize a new ReDBox hook project from the standard archetype')
|
|
@@ -267,7 +267,6 @@ function createMigratedFigsharePublishingConfig(brand, figshareApi, figshareApiE
|
|
|
267
267
|
},
|
|
268
268
|
record: {
|
|
269
269
|
articleIdPath: mapping.recordFigArticleId ?? 'metadata.figshare_article_id',
|
|
270
|
-
articleUrlPaths: Array.isArray(mapping.recordFigArticleURL) ? mapping.recordFigArticleURL : [mapping.recordFigArticleURL ?? 'metadata.figshare_article_location'],
|
|
271
270
|
dataLocationsPath: mapping.recordDataLocations ?? 'metadata.dataLocations',
|
|
272
271
|
statusPath: 'metadata.figshareStatus',
|
|
273
272
|
errorPath: 'metadata.figshareError',
|
|
@@ -280,7 +279,6 @@ function createMigratedFigsharePublishingConfig(brand, figshareApi, figshareApiE
|
|
|
280
279
|
selectedFlagPath: 'selected'
|
|
281
280
|
},
|
|
282
281
|
authors: {
|
|
283
|
-
source: 'defaultRedboxContributors',
|
|
284
282
|
uniqueBy: mapping.recordAuthorUniqueBy ?? 'email',
|
|
285
283
|
externalNameField: mapping.recordAuthorExternalName ?? 'text_full_name',
|
|
286
284
|
maxInlineAuthors: 50,
|
|
@@ -293,11 +291,10 @@ function createMigratedFigsharePublishingConfig(brand, figshareApi, figshareApiE
|
|
|
293
291
|
keywords: { kind: 'path', path: 'metadata.finalKeywords', defaultValue: [] },
|
|
294
292
|
funding: { kind: 'path', path: 'metadata.funder' },
|
|
295
293
|
license: { source: { kind: 'path', path: mapping.recordLicensePath ?? 'metadata.license' }, matchBy: 'urlContains', required: true },
|
|
296
|
-
categories: { source: { kind: 'path', path: mapping.recordCategoryPath ?? 'metadata.forCodes', defaultValue: [] }
|
|
294
|
+
categories: { source: { kind: 'path', path: mapping.recordCategoryPath ?? 'metadata.forCodes', defaultValue: [] } },
|
|
297
295
|
customFields: []
|
|
298
296
|
},
|
|
299
297
|
categories: {
|
|
300
|
-
strategy: 'for2020Mapping',
|
|
301
298
|
mappingTable: ((figshareForMapping.FORMapping ?? []).map((entry) => ({
|
|
302
299
|
sourceCode: String(entry.redboxCode ?? entry.sourceCode ?? ''),
|
|
303
300
|
figshareCategoryId: Number(entry.figshareCategoryId ?? entry.figCode ?? 0)
|
|
@@ -307,11 +304,8 @@ function createMigratedFigsharePublishingConfig(brand, figshareApi, figshareApiE
|
|
|
307
304
|
assets: {
|
|
308
305
|
enableHostedFiles: true,
|
|
309
306
|
enableLinkFiles: true,
|
|
310
|
-
dedupeStrategy: 'sourceId',
|
|
311
307
|
staging: {
|
|
312
|
-
|
|
313
|
-
cleanupPolicy: 'deleteAfterSuccess',
|
|
314
|
-
diskSpaceThresholdBytes: 1073741824
|
|
308
|
+
cleanupPolicy: 'deleteAfterSuccess'
|
|
315
309
|
}
|
|
316
310
|
},
|
|
317
311
|
embargo: {
|
|
@@ -320,7 +314,6 @@ function createMigratedFigsharePublishingConfig(brand, figshareApi, figshareApiE
|
|
|
320
314
|
accessRights: {
|
|
321
315
|
accessRights: { kind: 'path', path: 'metadata.accessRights' },
|
|
322
316
|
fullEmbargoUntil: { kind: 'path', path: 'metadata.embargoUntil' },
|
|
323
|
-
fileEmbargoUntil: { kind: 'path', path: 'metadata.embargoUntil' },
|
|
324
317
|
reason: { kind: 'path', path: 'metadata.embargoReason' }
|
|
325
318
|
}
|
|
326
319
|
},
|
|
@@ -329,7 +322,6 @@ function createMigratedFigsharePublishingConfig(brand, figshareApi, figshareApiE
|
|
|
329
322
|
uploadedFilesCleanupDelay: typeof mapping.scheduleUploadedFilesCleanupJob === 'string' ? mapping.scheduleUploadedFilesCleanupJob : 'in 5 minutes'
|
|
330
323
|
},
|
|
331
324
|
workflow: {
|
|
332
|
-
transitionRules: [],
|
|
333
325
|
transitionJob: {
|
|
334
326
|
enabled: String(mapping.figshareScheduledTransitionRecordWorkflowFromArticlePropertiesJob?.enabled ?? 'false') === 'true',
|
|
335
327
|
namedQuery: String(mapping.figshareScheduledTransitionRecordWorkflowFromArticlePropertiesJob?.namedQuery ?? ''),
|
|
@@ -346,7 +338,12 @@ function createMigratedFigsharePublishingConfig(brand, figshareApi, figshareApiE
|
|
|
346
338
|
},
|
|
347
339
|
writeBack: {
|
|
348
340
|
articleId: 'metadata.figshare_article_id',
|
|
349
|
-
|
|
341
|
+
// The legacy article URL paths are write targets, so they belong to writeBack.
|
|
342
|
+
articleUrls: Array.isArray(mapping.recordFigArticleURL)
|
|
343
|
+
? mapping.recordFigArticleURL
|
|
344
|
+
: [typeof mapping.recordFigArticleURL === 'string' && mapping.recordFigArticleURL.trim() !== ''
|
|
345
|
+
? mapping.recordFigArticleURL
|
|
346
|
+
: 'metadata.figshare_article_location'],
|
|
350
347
|
extraFields: []
|
|
351
348
|
},
|
|
352
349
|
migrationReport: {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
type MetaEntryKey = string;
|
|
3
|
+
declare const contentFormats: readonly ["plain", "html"];
|
|
4
|
+
type ContentFormats = typeof contentFormats[number];
|
|
5
|
+
type MetaEntryValue = {
|
|
6
|
+
category?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
contentFormat?: ContentFormats;
|
|
9
|
+
};
|
|
10
|
+
export type MetaEntries = Record<MetaEntryKey, MetaEntryValue>;
|
|
11
|
+
export declare function registerSyncTranslationCommand(program: Command): void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerSyncTranslationCommand = registerSyncTranslationCommand;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const contentFormats = ["plain", "html"];
|
|
10
|
+
function isContentFormat(value) {
|
|
11
|
+
return contentFormats.some(contentFormat => contentFormat === value);
|
|
12
|
+
}
|
|
13
|
+
function firstNonEmptyString(...value) {
|
|
14
|
+
for (const item of value) {
|
|
15
|
+
if (typeof item === 'string' && item.length > 0) {
|
|
16
|
+
return item;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function registerSyncTranslationCommand(program) {
|
|
22
|
+
program
|
|
23
|
+
.command('sync-translation')
|
|
24
|
+
.description('Read translations from API and write to language defaults files.')
|
|
25
|
+
.requiredOption('-l, --language-defaults <path>', 'Path to the language-defaults directory containing locales and meta json files.')
|
|
26
|
+
.requiredOption('-a, --api-base <url>', 'Base url for the API to obtain translations.')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
try {
|
|
29
|
+
const globalOptions = program.opts();
|
|
30
|
+
const langDefaultsPath = node_path_1.default.resolve(options.languageDefaults);
|
|
31
|
+
const apiBaseUrl = options.apiBase;
|
|
32
|
+
if (!langDefaultsPath) {
|
|
33
|
+
throw new Error(`Must provide language-defaults path.`);
|
|
34
|
+
}
|
|
35
|
+
if (!apiBaseUrl) {
|
|
36
|
+
throw new Error(`Must provide api-base url.`);
|
|
37
|
+
}
|
|
38
|
+
// Read files
|
|
39
|
+
const langDefaultsMetaPath = node_path_1.default.resolve(langDefaultsPath, 'meta.json');
|
|
40
|
+
console.log(`Load translation meta file ${langDefaultsMetaPath}`);
|
|
41
|
+
const langDefaultsMetaData = JSON.parse(await promises_1.default.readFile(langDefaultsMetaPath, { encoding: 'utf8' }));
|
|
42
|
+
const langDefaultsLocales = (await promises_1.default.readdir(langDefaultsPath, { withFileTypes: true }))
|
|
43
|
+
.filter(dirent => dirent.isDirectory())
|
|
44
|
+
.map(dirent => dirent.name);
|
|
45
|
+
const langDefaultsLocaleTranslationData = [];
|
|
46
|
+
for (const locale of langDefaultsLocales) {
|
|
47
|
+
const p = node_path_1.default.resolve(langDefaultsPath, locale, 'translation.json');
|
|
48
|
+
const data = JSON.parse(await promises_1.default.readFile(p, { encoding: 'utf8' }));
|
|
49
|
+
console.log(`Load translation locale file ${p}`);
|
|
50
|
+
langDefaultsLocaleTranslationData.push({ locale, data });
|
|
51
|
+
}
|
|
52
|
+
// Read urls
|
|
53
|
+
const translationUrl = new URL('/default/rdmp/locales/__locale__/translation.json', apiBaseUrl);
|
|
54
|
+
const entriesUrl = new URL('/default/rdmp/app/i18n/entries', apiBaseUrl);
|
|
55
|
+
console.log(`Load translation entries url ${entriesUrl}`);
|
|
56
|
+
const entriesResponse = await fetch(entriesUrl, { signal: AbortSignal.timeout(5000) });
|
|
57
|
+
if (!entriesResponse.ok) {
|
|
58
|
+
throw new Error(`Could not fetch translation entries: status ${entriesResponse.status} body ${await entriesResponse.text()}`);
|
|
59
|
+
}
|
|
60
|
+
const entriesData = await entriesResponse.json();
|
|
61
|
+
const translationData = [];
|
|
62
|
+
for (const langDefaultsLocale of langDefaultsLocales) {
|
|
63
|
+
const translationLocaleUrl = translationUrl.toString().replace('/__locale__/', `/${langDefaultsLocale}/`);
|
|
64
|
+
console.log(`Load translation locale url ${translationLocaleUrl}`);
|
|
65
|
+
const translationResponse = await fetch(translationLocaleUrl, { signal: AbortSignal.timeout(5000) });
|
|
66
|
+
if (!translationResponse.ok) {
|
|
67
|
+
throw new Error(`Could not fetch translation locale: status ${translationResponse.status} body ${await translationResponse.text()}`);
|
|
68
|
+
}
|
|
69
|
+
translationData.push({
|
|
70
|
+
locale: langDefaultsLocale,
|
|
71
|
+
data: await translationResponse.json()
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Merge locale data
|
|
75
|
+
// Maintain _meta in translation.json file, but don't update it from remote meta.
|
|
76
|
+
const translationMerged = {};
|
|
77
|
+
for (const { locale, data } of langDefaultsLocaleTranslationData) {
|
|
78
|
+
if (locale in translationMerged) {
|
|
79
|
+
throw new Error(`Duplicate locale ${locale}`);
|
|
80
|
+
}
|
|
81
|
+
const translationDataFromUrl = translationData.find(i => i.locale === locale) ?? { locale, data: {} };
|
|
82
|
+
const dataOnlyStrings = {};
|
|
83
|
+
for (const [key, value] of Object.entries(translationDataFromUrl.data)) {
|
|
84
|
+
if (typeof value === 'string') {
|
|
85
|
+
dataOnlyStrings[key] = value;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(`Dropped key '${key}' with non-string value ${JSON.stringify(value)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const newData = { ...data, ...dataOnlyStrings };
|
|
92
|
+
if (globalOptions.dryRun) {
|
|
93
|
+
const changes = Object.entries(newData)
|
|
94
|
+
.filter(([key, value]) => data[key] !== value)
|
|
95
|
+
.map(([key, value]) => [key, { 'original': data[key], 'new': value }]);
|
|
96
|
+
if (changes.length > 0) {
|
|
97
|
+
console.log({ locale, changes: Object.fromEntries(changes) });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
translationMerged[locale] = newData;
|
|
101
|
+
}
|
|
102
|
+
if (globalOptions.dryRun) {
|
|
103
|
+
console.log('[dry-run] Translation data merged; no file written.');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Write merged data back to locale file
|
|
107
|
+
for (const [locale, data] of Object.entries(translationMerged)) {
|
|
108
|
+
const p = node_path_1.default.resolve(langDefaultsPath, locale, 'translation.json');
|
|
109
|
+
console.log(`Writing updated translation data to ${p}`);
|
|
110
|
+
await promises_1.default.writeFile(p, JSON.stringify(data, null, 2));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Merge meta, in highest to lowest priority order:
|
|
114
|
+
// remote meta, _meta key from the translation, existing meta entry.
|
|
115
|
+
// Use a calculated value for description and category if they are not provided.
|
|
116
|
+
const metaMerged = {};
|
|
117
|
+
for (const [locale, localeTranslation] of Object.entries(translationMerged)) {
|
|
118
|
+
const entriesLocale = entriesData.filter(e => e.locale === locale);
|
|
119
|
+
for (const key of Object.keys(localeTranslation)) {
|
|
120
|
+
if (key === '_meta' || key.startsWith('_meta.')) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const entriesItems = entriesLocale.filter(e => e.key === key);
|
|
124
|
+
const entriesItem = entriesItems.length > 0 ? entriesItems[0] : undefined;
|
|
125
|
+
const translationItem = localeTranslation[key] ?? "";
|
|
126
|
+
const translationMetaItem = ('_meta' in localeTranslation && localeTranslation['_meta'] !== null && typeof localeTranslation['_meta'] === 'object') ? localeTranslation['_meta'][key] : undefined;
|
|
127
|
+
const metaItem = langDefaultsMetaData[key];
|
|
128
|
+
// Populate the meta data.
|
|
129
|
+
const contentFormatRaw = firstNonEmptyString(entriesItem?.contentFormat, translationMetaItem?.contentFormat, metaItem?.contentFormat);
|
|
130
|
+
let contentFormat = isContentFormat(contentFormatRaw) ? contentFormatRaw : undefined;
|
|
131
|
+
// Don't guess whether the translation value is html or plain, leave it as-is.
|
|
132
|
+
const description = firstNonEmptyString(entriesItem?.description, translationMetaItem?.description, metaItem?.description, `Translation for ${key}`);
|
|
133
|
+
const keySplit = key.split(/[_\-:.@]+/).filter(i => !!i);
|
|
134
|
+
const category = firstNonEmptyString(entriesItem?.category, translationMetaItem?.category, metaItem?.category, keySplit.length > 0 ? keySplit[0] : undefined);
|
|
135
|
+
const newItem = { category, description, contentFormat };
|
|
136
|
+
if (globalOptions.dryRun && JSON.stringify(metaItem) !== JSON.stringify(newItem)) {
|
|
137
|
+
console.log({ key, metaItem, translationItem, entriesItem, newItem });
|
|
138
|
+
}
|
|
139
|
+
if (Object.keys(newItem).length > 0 && Object.values(newItem).some(v => !!v)) {
|
|
140
|
+
metaMerged[key] = newItem;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (globalOptions.dryRun) {
|
|
145
|
+
console.log('[dry-run] Meta merged; no file written. Changed entries printed above.');
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// Write merged meta back to file
|
|
149
|
+
console.log(`Writing updated meta to ${langDefaultsMetaPath}`);
|
|
150
|
+
await promises_1.default.writeFile(langDefaultsMetaPath, JSON.stringify(metaMerged, null, 2));
|
|
151
|
+
}
|
|
152
|
+
console.log(`\n🛠️ Ran sync-translation`);
|
|
153
|
+
console.log('\n✅ Done!\n');
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error(`\n❌ Error: `, error);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -40,7 +40,8 @@ exports.generateHookArchetype = generateHookArchetype;
|
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const handlebars_1 = __importDefault(require("handlebars"));
|
|
43
|
-
handlebars_1.default.
|
|
43
|
+
const handlebars = handlebars_1.default.create();
|
|
44
|
+
handlebars.registerHelper('json', (value) => JSON.stringify(value));
|
|
44
45
|
function toPascalCase(value) {
|
|
45
46
|
return value
|
|
46
47
|
.split(/[^A-Za-z0-9]+/)
|
|
@@ -74,7 +75,7 @@ function walkTemplateFiles(dir) {
|
|
|
74
75
|
}
|
|
75
76
|
function renderTemplate(sourcePath, context) {
|
|
76
77
|
const templateSource = fs.readFileSync(sourcePath, 'utf8');
|
|
77
|
-
const template =
|
|
78
|
+
const template = handlebars.compile(templateSource, { noEscape: true });
|
|
78
79
|
return template(context);
|
|
79
80
|
}
|
|
80
81
|
function toOutputRelativePath(templateRoot, filePath) {
|
|
@@ -146,7 +147,7 @@ function buildTemplateContext(options) {
|
|
|
146
147
|
hookShortName,
|
|
147
148
|
hookPascalName,
|
|
148
149
|
description: options.description ?? `ReDBox hook for ${hookPascalName}`,
|
|
149
|
-
integrationDirName: hookShortName
|
|
150
|
+
integrationDirName: `hook-${hookShortName}`,
|
|
150
151
|
redboxCoreVersion: readDependencyPackageVersion(packageRoot, 'redbox-core'),
|
|
151
152
|
redboxDevToolsVersion: readOwnPackageVersion(packageRoot),
|
|
152
153
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@researchdatabox/redbox-dev-tools",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.10760",
|
|
4
4
|
"description": "ReDBox customisation dev tools - shared configuration, generators, and migration tooling",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"homepage": "https://github.com/redbox-mint/redbox-portal/tree/v4.5.1#readme",
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@researchdatabox/redbox-core": "0.0.1-beta.
|
|
48
|
-
"@researchdatabox/sails-ng-common": "0.0.1-beta.
|
|
47
|
+
"@researchdatabox/redbox-core": "0.0.1-beta.10760",
|
|
48
|
+
"@researchdatabox/sails-ng-common": "0.0.1-beta.10760",
|
|
49
49
|
"@tsconfig/node24": "24.0.4",
|
|
50
50
|
"@types/chai": "5.2.3",
|
|
51
51
|
"@types/lodash": "4.17.24",
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# {{packageName}} Agent Guide
|
|
2
2
|
|
|
3
|
-
This hook follows the
|
|
3
|
+
This hook follows the richer ReDBox client hook archetype generated by `@researchdatabox/redbox-dev-tools init`.
|
|
4
4
|
|
|
5
5
|
Keep these rules in place unless the user explicitly asks to change the hook model.
|
|
6
6
|
|
|
7
|
+
## Project Shape
|
|
8
|
+
|
|
9
|
+
- `src/index.ts` is the hook entry point and compiles to `dist/index.js`.
|
|
10
|
+
- `src/config/` contains config returned from `registerRedboxConfig()`.
|
|
11
|
+
- `src/form-config/` contains typed form definitions returned from `registerRedboxFormConfigs()`.
|
|
12
|
+
- `src/migrations/` contains the synchronous hook migration registry.
|
|
13
|
+
- `support/bootstrap-data/`, `assets/`, `views/`, and `language-defaults/` hold client material used by the Docker harness and hook runtime.
|
|
14
|
+
- `support/development/` and `support/integration-testing/` contain the local portal and Mocha harnesses.
|
|
15
|
+
|
|
7
16
|
## Dependency Contract
|
|
8
17
|
|
|
9
18
|
- `@researchdatabox/redbox-core` is the runtime compatibility anchor.
|
|
@@ -27,8 +36,16 @@ Use the existing scripts in `package.json`.
|
|
|
27
36
|
|
|
28
37
|
Do not replace the custom `run-hook-tsc` or `run-hook-mocha` entrypoints with plain `tsc` or `mocha`.
|
|
29
38
|
|
|
39
|
+
## Config And Migrations
|
|
40
|
+
|
|
41
|
+
Prefer returning config from `registerRedboxConfig()` instead of mutating `sails.config` in `configure()`.
|
|
42
|
+
|
|
43
|
+
Keep `registerRedboxMigrations()` synchronous. Add new migration modules to `src/migrations/index.ts`, and give each migration a unique hook-prefixed, sortable name such as `{{packageName}}:20260703T000000-seed-client-defaults`.
|
|
44
|
+
|
|
30
45
|
## Docker Runtime
|
|
31
46
|
|
|
32
47
|
The docker compose files intentionally set `NODE_PATH` so a mounted hook can resolve host-provided runtime dependencies from the Redbox portal dependency tree.
|
|
33
48
|
|
|
34
49
|
Do not remove that unless the runtime install model changes deliberately.
|
|
50
|
+
|
|
51
|
+
For mounted-core development, update `support/development/core-mount.env` or pass `REDBOX_PORTAL_PATH` in the environment, then use `npm run dev:run:core`. Do not hand-edit the generated `support/development/.tmp/core-mount.env` file.
|
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
ARG
|
|
1
|
+
ARG REDBOX_PORTAL_IMAGE=qcifengineering/redbox-portal:develop-pdfgen
|
|
2
2
|
|
|
3
|
-
FROM ${
|
|
3
|
+
FROM ${REDBOX_PORTAL_IMAGE} AS builder
|
|
4
4
|
|
|
5
5
|
USER root
|
|
6
6
|
|
|
7
|
-
COPY
|
|
8
|
-
COPY redbox-portal/packages/redbox-core /opt/redbox-portal/packages/redbox-core
|
|
9
|
-
COPY redbox-portal/packages/sails-ng-common /opt/redbox-portal/packages/sails-ng-common
|
|
10
|
-
COPY redbox-portal/packages/redbox-dev-tools /opt/redbox-portal/packages/redbox-dev-tools
|
|
7
|
+
COPY . /opt/{{packageName}}
|
|
11
8
|
|
|
12
9
|
RUN --mount=type=cache,target=/root/.npm \
|
|
13
10
|
cd /opt/{{packageName}} \
|
|
14
|
-
&& npm install --include=dev --ignore-scripts --
|
|
11
|
+
&& npm install --include=dev --ignore-scripts --strict-peer-deps \
|
|
15
12
|
&& npm run compile \
|
|
13
|
+
&& HOOK_TARBALL="$(npm pack --pack-destination /tmp --silent)" \
|
|
16
14
|
&& cd /opt/redbox-portal \
|
|
17
|
-
&& npm install --legacy-peer-deps --ignore-scripts /
|
|
18
|
-
&&
|
|
15
|
+
&& npm install --legacy-peer-deps --ignore-scripts "/tmp/${HOOK_TARBALL}" \
|
|
16
|
+
&& rm -f "/tmp/${HOOK_TARBALL}" \
|
|
17
|
+
&& mkdir -p /opt/redbox-portal/views /opt/redbox-portal/assets /opt/redbox-portal/.tmp/public /opt/redbox-portal/language-defaults /opt/redbox-portal/bootstrap-data \
|
|
19
18
|
&& if [ -d /opt/{{packageName}}/views ]; then cp -a /opt/{{packageName}}/views/. /opt/redbox-portal/views/; fi \
|
|
20
19
|
&& if [ -d /opt/{{packageName}}/assets ]; then cp -a /opt/{{packageName}}/assets/. /opt/redbox-portal/assets/; fi \
|
|
21
|
-
&& if [ -d /opt/{{packageName}}/
|
|
20
|
+
&& if [ -d /opt/{{packageName}}/assets ]; then cp -a /opt/{{packageName}}/assets/. /opt/redbox-portal/.tmp/public/; fi \
|
|
21
|
+
&& if [ -d /opt/{{packageName}}/language-defaults ]; then cp -a /opt/{{packageName}}/language-defaults/. /opt/redbox-portal/language-defaults/; fi \
|
|
22
|
+
&& if [ -d /opt/{{packageName}}/support/bootstrap-data ]; then cp -a /opt/{{packageName}}/support/bootstrap-data/. /opt/redbox-portal/bootstrap-data/; fi
|
|
22
23
|
|
|
23
|
-
FROM ${
|
|
24
|
+
FROM ${REDBOX_PORTAL_IMAGE} AS {{packageName}}
|
|
24
25
|
USER root
|
|
25
26
|
|
|
26
27
|
COPY --from=builder --chown=node:node --chmod='u=rwx,g=rx,o=rx' /opt/redbox-portal /opt/redbox-portal
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# {{packageName}}
|
|
2
2
|
|
|
3
|
-
A generated ReDBox hook archetype.
|
|
3
|
+
A generated ReDBox client hook archetype.
|
|
4
4
|
|
|
5
|
-
This project
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- direct `dependencies` are reserved for hook-owned runtime libraries only
|
|
10
|
-
- the generated package versions assume `redbox-core` and `redbox-dev-tools` are installed from npm, not from a sibling `redbox-portal` checkout
|
|
5
|
+
This project is shaped for migrating v4 client customisation hooks to the
|
|
6
|
+
current ReDBox hook contract. It includes TypeScript registrations for config,
|
|
7
|
+
form configs, services, controllers, and migrations, plus Docker-backed
|
|
8
|
+
development and Mocha integration harnesses.
|
|
11
9
|
|
|
12
10
|
## Development
|
|
13
11
|
|
|
@@ -19,21 +17,41 @@ npm run test:unit
|
|
|
19
17
|
|
|
20
18
|
Local installs intentionally allow lifecycle scripts so native modules and other postinstall hooks can run. Use `npm install --ignore-scripts` only in controlled CI or container contexts where that trade-off is deliberate.
|
|
21
19
|
|
|
22
|
-
For the
|
|
20
|
+
For the image-backed portal harness:
|
|
23
21
|
|
|
24
22
|
```bash
|
|
25
23
|
npm run dev:run:build
|
|
26
24
|
npm run dev:run
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
To test against a local `redbox-portal` checkout, set `REDBOX_PORTAL_PATH` in
|
|
28
|
+
`support/development/core-mount.env` and run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm run dev:run:core
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The helper script resolves relative paths from this hook root, validates the
|
|
35
|
+
portal checkout, installs missing dependencies, compiles the hook, compiles the
|
|
36
|
+
portal server packages, and writes the resolved path to
|
|
37
|
+
`support/development/.tmp/core-mount.env` for Docker Compose.
|
|
38
|
+
|
|
39
|
+
## Dependency Contract
|
|
40
|
+
|
|
41
|
+
- `@researchdatabox/redbox-core` is the runtime compatibility contract
|
|
42
|
+
- `@researchdatabox/redbox-dev-tools` provides the shared compile and unit-test toolchain
|
|
43
|
+
- direct `dependencies` are reserved for hook-owned runtime libraries only
|
|
44
|
+
- shared packages such as `axios`, `rxjs`, `lodash`, `mocha`, `chai`, `ts-node`, and `typescript` should not be pinned directly here
|
|
30
45
|
|
|
31
46
|
## Structure
|
|
32
47
|
|
|
33
48
|
- `src/index.ts`: hook entrypoint using `defineRedboxHook()`
|
|
34
49
|
- `src/api/controllers`: hook controllers
|
|
35
50
|
- `src/api/services`: hook services
|
|
36
|
-
- `src/config`: configuration and reusable form definition helpers
|
|
51
|
+
- `src/config`: configuration and reusable form definition helpers registered through `registerRedboxConfig()`
|
|
37
52
|
- `src/form-config`: form config exports
|
|
53
|
+
- `src/migrations`: synchronous hook migration registry
|
|
54
|
+
- `support/bootstrap-data`: bootstrap data copied into the portal image/harness
|
|
55
|
+
- `assets`, `views`, and `language-defaults`: client customisation material copied or resolved by the hook runtime
|
|
38
56
|
- `test/unit`: fast hook-local TypeScript tests
|
|
39
57
|
- `test/integration`: portal integration tests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
This folder contains hook-provided i18n defaults that can seed the database
|
|
2
|
+
and serve as runtime fallbacks when the database has no matching value.
|
|
3
|
+
|
|
4
|
+
Expected structure:
|
|
5
|
+
|
|
6
|
+
- `language-defaults/<lng>/translation.json`
|
|
7
|
+
- `language-defaults/meta.json` for optional category/description metadata
|
|
8
|
+
|
|
9
|
+
Keep generated client locale assets out of this folder; those belong under
|
|
10
|
+
the portal asset build output, not in the hook source.
|
|
@@ -9,18 +9,22 @@
|
|
|
9
9
|
"hasControllers": true,
|
|
10
10
|
"hasServices": true,
|
|
11
11
|
"hasConfig": true,
|
|
12
|
-
"hasFormConfigs": true
|
|
12
|
+
"hasFormConfigs": true,
|
|
13
|
+
"hasMigrations": true
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"compile": "node ./node_modules/@researchdatabox/redbox-dev-tools/bin/run-hook-tsc.js -p tsconfig.json",
|
|
16
17
|
"test": "npm run test:unit && npm run test:integration:mocha",
|
|
17
|
-
"test:unit": "TS_NODE_PROJECT=./test/tsconfig.json npm run compile && node ./node_modules/@researchdatabox/redbox-dev-tools/bin/run-hook-mocha.js --config ./test/unit/.mocharc.cjs ./test/unit",
|
|
18
|
-
"test:integration:mocha": "npm run compile &&docker compose -f ./support/integration-testing/docker-compose.mocha.yml up --build --menu=false --abort-on-container-exit --exit-code-from redboxportal",
|
|
18
|
+
"test:unit": "TS_NODE_PROJECT=./test/tsconfig.json npm run compile && node ./node_modules/@researchdatabox/redbox-dev-tools/bin/run-hook-mocha.js --config ./test/unit/.mocharc.cjs \"./test/unit/**/*.test.ts\"",
|
|
19
|
+
"test:integration:mocha": "npm run compile && docker compose -f ./support/integration-testing/docker-compose.mocha.yml up --build --menu=false --abort-on-container-exit --exit-code-from redboxportal",
|
|
19
20
|
"test:integration:mocha:clean": "docker compose -f ./support/integration-testing/docker-compose.mocha.yml down -v --remove-orphans --timeout 180",
|
|
20
|
-
"dev:run": "
|
|
21
|
-
"dev:run:
|
|
21
|
+
"dev:run:prepare": "mkdir -p ./support/development/.tmp/attachments/staging ./support/development/.tmp/mongo ./support/development/.tmp/minio/data",
|
|
22
|
+
"dev:run": "npm run dev:run:prepare && docker compose -f ./support/development/docker-compose.yml up --menu=false --abort-on-container-exit --exit-code-from rbportal",
|
|
23
|
+
"dev:run:build": "docker compose -f ./support/development/docker-compose.yml build --pull",
|
|
22
24
|
"dev:run:clean": "docker compose -f ./support/development/docker-compose.yml down -v --remove-orphans --timeout 180",
|
|
23
|
-
"dev:run:prepare": "
|
|
25
|
+
"dev:run:core:prepare": "bash ./support/development/prepare-core-mount.sh",
|
|
26
|
+
"dev:run:core": "npm run dev:run:core:prepare && docker compose --env-file ./support/development/.tmp/core-mount.env -f ./support/development/docker-compose.yml -f ./support/development/docker-compose.core-mount.yml up --menu=false --abort-on-container-exit --exit-code-from rbportal",
|
|
27
|
+
"dev:run:core:clean": "REDBOX_PORTAL_PATH=/tmp/unused docker compose -f ./support/development/docker-compose.yml -f ./support/development/docker-compose.core-mount.yml down -v --remove-orphans --timeout 180"
|
|
24
28
|
},
|
|
25
29
|
"keywords": [
|
|
26
30
|
"sails",
|