@researchdatabox/redbox-dev-tools 0.0.1-beta.10300 → 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.
Files changed (36) hide show
  1. package/README.md +25 -3
  2. package/dist/cli.js +6 -2
  3. package/dist/commands/form-config.js +8 -11
  4. package/dist/commands/sync-translation.d.ts +12 -0
  5. package/dist/commands/sync-translation.js +160 -0
  6. package/dist/templates/hook-archetype.js +8 -3
  7. package/package.json +3 -3
  8. package/templates/hook-archetype/standard/.gitignore.hbs +1 -0
  9. package/templates/hook-archetype/standard/.npmignore.hbs +2 -1
  10. package/templates/hook-archetype/standard/AGENTS.md.hbs +18 -1
  11. package/templates/hook-archetype/standard/Dockerfile.hbs +14 -11
  12. package/templates/hook-archetype/standard/README.md.hbs +28 -10
  13. package/templates/hook-archetype/standard/assets/default/.gitkeep +1 -0
  14. package/templates/hook-archetype/standard/assets/images/.gitkeep +1 -0
  15. package/templates/hook-archetype/standard/assets/styles/.gitkeep +1 -0
  16. package/templates/hook-archetype/standard/language-defaults/README.md.hbs +10 -0
  17. package/templates/hook-archetype/standard/language-defaults/en/translation.json.hbs +3 -0
  18. package/templates/hook-archetype/standard/language-defaults/meta.json.hbs +6 -0
  19. package/templates/hook-archetype/standard/package.json.hbs +10 -6
  20. package/templates/hook-archetype/standard/src/index.ts.hbs +11 -11
  21. package/templates/hook-archetype/standard/src/migrations/index.ts.hbs +19 -0
  22. package/templates/hook-archetype/standard/support/bootstrap-data/namedqueries/.gitkeep +1 -0
  23. package/templates/hook-archetype/standard/support/bootstrap-data/records/.gitkeep +1 -0
  24. package/templates/hook-archetype/standard/support/bootstrap-data/reports/.gitkeep +1 -0
  25. package/templates/hook-archetype/standard/support/bootstrap-data/vocabularies/.gitkeep +1 -0
  26. package/templates/hook-archetype/standard/support/development/core-mount.env.hbs +3 -0
  27. package/templates/hook-archetype/standard/support/development/docker-compose.core-mount.yml.hbs +37 -0
  28. package/templates/hook-archetype/standard/support/development/docker-compose.yml.hbs +53 -20
  29. package/templates/hook-archetype/standard/support/development/prepare-core-mount.sh.hbs +61 -0
  30. package/templates/hook-archetype/standard/support/integration-testing/docker-compose.mocha.yml.hbs +45 -11
  31. package/templates/hook-archetype/standard/support/integration-testing/run-mocha-hook.sh.hbs +5 -0
  32. package/templates/hook-archetype/standard/support/integration-testing/run-mocha-redbox.sh.hbs +100 -0
  33. package/templates/hook-archetype/standard/support/prepare-minio.sh.hbs +74 -0
  34. package/templates/hook-archetype/standard/test/integration/hook-registration.test.ts.hbs +3 -1
  35. package/templates/hook-archetype/standard/test/unit/index.test.ts.hbs +11 -5
  36. 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 `language-defaults/`
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('1.0.0')
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: [] }, mappingStrategy: 'for2020Mapping' },
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
- tempDir: String(figshareApi.attachmentsFigshareTempDir ?? ''),
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
- articleUrls: ['metadata.figshare_article_location'],
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
+ }
@@ -32,11 +32,16 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.generateHookArchetype = generateHookArchetype;
37
40
  const fs = __importStar(require("fs"));
38
41
  const path = __importStar(require("path"));
39
- const sails_ng_common_1 = require("@researchdatabox/sails-ng-common");
42
+ const handlebars_1 = __importDefault(require("handlebars"));
43
+ const handlebars = handlebars_1.default.create();
44
+ handlebars.registerHelper('json', (value) => JSON.stringify(value));
40
45
  function toPascalCase(value) {
41
46
  return value
42
47
  .split(/[^A-Za-z0-9]+/)
@@ -70,7 +75,7 @@ function walkTemplateFiles(dir) {
70
75
  }
71
76
  function renderTemplate(sourcePath, context) {
72
77
  const templateSource = fs.readFileSync(sourcePath, 'utf8');
73
- const template = (0, sails_ng_common_1.handlebarsCompile)(templateSource, { noEscape: true });
78
+ const template = handlebars.compile(templateSource, { noEscape: true });
74
79
  return template(context);
75
80
  }
76
81
  function toOutputRelativePath(templateRoot, filePath) {
@@ -142,7 +147,7 @@ function buildTemplateContext(options) {
142
147
  hookShortName,
143
148
  hookPascalName,
144
149
  description: options.description ?? `ReDBox hook for ${hookPascalName}`,
145
- integrationDirName: hookShortName,
150
+ integrationDirName: `hook-${hookShortName}`,
146
151
  redboxCoreVersion: readDependencyPackageVersion(packageRoot, 'redbox-core'),
147
152
  redboxDevToolsVersion: readOwnPackageVersion(packageRoot),
148
153
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@researchdatabox/redbox-dev-tools",
3
- "version": "0.0.1-beta.10300",
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.10300",
48
- "@researchdatabox/sails-ng-common": "0.0.1-beta.10300",
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",
@@ -2,3 +2,4 @@ dist
2
2
  node_modules
3
3
  .tmp
4
4
  coverage
5
+ support/development/.tmp
@@ -1,3 +1,4 @@
1
1
  src
2
2
  test
3
- support
3
+ support/*
4
+ !support/prepare-minio.sh
@@ -1,9 +1,18 @@
1
1
  # {{packageName}} Agent Guide
2
2
 
3
- This hook follows the standard ReDBox hook archetype generated by `@researchdatabox/redbox-dev-tools init`.
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,24 +1,27 @@
1
- ARG REDBOX_BASE_IMAGE=qcifengineering/redbox-portal:develop-pdfgen
1
+ ARG REDBOX_PORTAL_IMAGE=qcifengineering/redbox-portal:develop-pdfgen
2
2
 
3
- FROM ${REDBOX_BASE_IMAGE} AS builder
3
+ FROM ${REDBOX_PORTAL_IMAGE} AS builder
4
4
 
5
5
  USER root
6
6
 
7
- COPY {{packageName}} /opt/{{packageName}}
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 --legacy-peer-deps \
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 /opt/{{packageName}} \
18
- && mkdir -p /opt/redbox-portal/language-defaults \
19
- && if [ -d /opt/{{packageName}}/language-defaults ]; then cp -a /opt/{{packageName}}/language-defaults/. /opt/redbox-portal/language-defaults/; fi
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 \
18
+ && if [ -d /opt/{{packageName}}/views ]; then cp -a /opt/{{packageName}}/views/. /opt/redbox-portal/views/; fi \
19
+ && if [ -d /opt/{{packageName}}/assets ]; then cp -a /opt/{{packageName}}/assets/. /opt/redbox-portal/assets/; fi \
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
20
23
 
21
- FROM ${REDBOX_BASE_IMAGE} AS {{packageName}}
24
+ FROM ${REDBOX_PORTAL_IMAGE} AS {{packageName}}
22
25
  USER root
23
26
 
24
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 keeps its dependency declarations intentionally small:
6
-
7
- - `@researchdatabox/redbox-core` is the ReDBox runtime compatibility contract
8
- - `@researchdatabox/redbox-dev-tools` provides the shared compile and unit-test toolchain
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 docker-backed portal harness:
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
- The development compose stack expects a locally trusted CA certificate at `support/development/certs/dev-ca.pem` and mounts it with `NODE_EXTRA_CA_CERTS`. Generate that certificate with your local CA tooling, for example `mkcert -CAROOT`, before starting the stack.
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,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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "@{{hookShortName}}-hook-status": "{{hookPascalName}} hook ready"
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "{{hookShortName}}-hook-status": {
3
+ "category": "{{hookShortName}}",
4
+ "description": "Example translation seeded by {{packageName}}"
5
+ }
6
+ }
@@ -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": "docker compose -f ./support/development/docker-compose.yml up --menu=false --abort-on-container-exit --exit-code-from rbportal",
21
- "dev:run:build": "docker compose -f ./support/development/docker-compose.yml build",
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": "mkdir -p ./support/development/.tmp/attachments/staging"
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",