@researchdatabox/redbox-dev-tools 0.0.1 → 5.0.0-RC1
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 +259 -14
- package/bin/cli.js +3 -0
- package/bin/run-hook-mocha.js +3 -0
- package/bin/run-hook-tsc.js +3 -0
- package/bin/wrapper-webpack.js +168 -0
- package/config/tsconfig.base.json +16 -0
- package/dist/bin/run-hook-mocha.d.ts +1 -0
- package/dist/bin/run-hook-mocha.js +49 -0
- package/dist/bin/run-hook-tsc.d.ts +1 -0
- package/dist/bin/run-hook-tsc.js +19 -0
- package/dist/cli-parsers.d.ts +9 -0
- package/dist/cli-parsers.js +131 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +495 -0
- package/dist/commands/form-config.d.ts +6 -0
- package/dist/commands/form-config.js +401 -0
- package/dist/commands/sync-translation.d.ts +23 -0
- package/dist/commands/sync-translation.js +348 -0
- package/dist/completion/index.d.ts +1 -0
- package/dist/completion/index.js +218 -0
- package/dist/generators/add-method.d.ts +27 -0
- package/dist/generators/add-method.js +235 -0
- package/dist/generators/angular-app.d.ts +24 -0
- package/dist/generators/angular-app.js +342 -0
- package/dist/generators/angular-service.d.ts +21 -0
- package/dist/generators/angular-service.js +154 -0
- package/dist/generators/controller.d.ts +40 -0
- package/dist/generators/controller.js +195 -0
- package/dist/generators/form-component.d.ts +46 -0
- package/dist/generators/form-component.js +906 -0
- package/dist/generators/form-field.d.ts +15 -0
- package/dist/generators/form-field.js +160 -0
- package/dist/generators/migration.d.ts +15 -0
- package/dist/generators/migration.js +100 -0
- package/dist/generators/model.d.ts +42 -0
- package/dist/generators/model.js +206 -0
- package/dist/generators/service.d.ts +16 -0
- package/dist/generators/service.js +111 -0
- package/dist/runtime-resolver.d.ts +3 -0
- package/dist/runtime-resolver.js +27 -0
- package/dist/templates/hook-archetype.d.ts +12 -0
- package/dist/templates/hook-archetype.js +187 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +53 -0
- package/dist/utils/config-helper.d.ts +71 -0
- package/dist/utils/config-helper.js +574 -0
- package/dist/utils/generator.d.ts +10 -0
- package/dist/utils/generator.js +68 -0
- package/dist/utils/hook-tsconfig.d.ts +9 -0
- package/dist/utils/hook-tsconfig.js +104 -0
- package/dist/utils/paths.d.ts +10 -0
- package/dist/utils/paths.js +60 -0
- package/index.js +4 -0
- package/package.json +76 -19
- package/runtime-resolver.d.ts +1 -0
- package/runtime-resolver.js +3 -0
- package/skills/redbox-angular-apps/SKILL.md +42 -0
- package/skills/redbox-angular-services/SKILL.md +29 -0
- package/skills/redbox-controllers/SKILL.md +65 -0
- package/skills/redbox-feature-design-planner/SKILL.md +169 -0
- package/skills/redbox-feature-implementation-review/SKILL.md +117 -0
- package/skills/redbox-form-components/SKILL.md +114 -0
- package/skills/redbox-form-config/SKILL.md +34 -0
- package/skills/redbox-hook-development/SKILL.md +66 -0
- package/skills/redbox-services/SKILL.md +54 -0
- package/skills/redbox-test-verification/SKILL.md +38 -0
- package/skills/redbox-testing/SKILL.md +64 -0
- package/skills/redbox-web-verification/SKILL.md +71 -0
- package/skills/redbox-web-verification/redbox-angular-apps/SKILL.md +48 -0
- package/skills/redbox-web-verification/redbox-angular-services/SKILL.md +30 -0
- package/skills/redbox-web-verification/redbox-controllers/SKILL.md +56 -0
- package/skills/redbox-web-verification/redbox-form-config/SKILL.md +29 -0
- package/skills/redbox-web-verification/redbox-hook-development/SKILL.md +61 -0
- package/skills/redbox-web-verification/redbox-services/SKILL.md +45 -0
- package/skills/redbox-web-verification/redbox-testing/SKILL.md +53 -0
- package/skills/redbox-web-verification/redbox-web-verification/SKILL.md +47 -0
- package/templates/hook-archetype/standard/.gitignore.hbs +5 -0
- package/templates/hook-archetype/standard/.npmignore.hbs +4 -0
- package/templates/hook-archetype/standard/.npmrc.hbs +6 -0
- package/templates/hook-archetype/standard/.nvmrc.hbs +1 -0
- package/templates/hook-archetype/standard/AGENTS.md.hbs +51 -0
- package/templates/hook-archetype/standard/Dockerfile.hbs +29 -0
- package/templates/hook-archetype/standard/README.md.hbs +57 -0
- package/templates/hook-archetype/standard/assets/default/.gitkeep +1 -0
- package/templates/hook-archetype/standard/assets/fonts/.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/.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 +44 -0
- package/templates/hook-archetype/standard/src/api/controllers/ExampleController.ts.hbs +21 -0
- package/templates/hook-archetype/standard/src/api/controllers/index.ts.hbs +23 -0
- package/templates/hook-archetype/standard/src/api/services/ExampleService.ts.hbs +22 -0
- package/templates/hook-archetype/standard/src/api/services/index.ts.hbs +23 -0
- package/templates/hook-archetype/standard/src/config/brandingConfigurationDefaults.ts.hbs +9 -0
- package/templates/hook-archetype/standard/src/config/reusableFormDefinitions.ts.hbs +6 -0
- package/templates/hook-archetype/standard/src/form-config/default-1.0-draft.ts.hbs +20 -0
- package/templates/hook-archetype/standard/src/form-config/index.ts.hbs +8 -0
- package/templates/hook-archetype/standard/src/index.ts.hbs +55 -0
- package/templates/hook-archetype/standard/src/migrations/index.ts.hbs +19 -0
- package/templates/hook-archetype/standard/support/bootstrap-data/.gitkeep +1 -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 +108 -0
- 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 +118 -0
- package/templates/hook-archetype/standard/support/integration-testing/run-mocha-hook.sh.hbs +14 -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 +15 -0
- package/templates/hook-archetype/standard/test/support/globals.ts.hbs +40 -0
- package/templates/hook-archetype/standard/test/tsconfig.json.hbs +8 -0
- package/templates/hook-archetype/standard/test/unit/.mocharc.cjs.hbs +9 -0
- package/templates/hook-archetype/standard/test/unit/index.test.ts.hbs +46 -0
- package/templates/hook-archetype/standard/tsconfig.json.hbs +15 -0
- package/templates/hook-archetype/standard/views/.gitkeep +1 -0
- package/templates/hook-archetype/standard/views/default/.gitkeep +1 -0
- package/testing.d.ts +1 -0
- package/testing.js +3 -0
- package/.travis.yml +0 -23
- package/commands/initProject.js +0 -129
- package/redbox-dev-tools +0 -70
- package/yarn.lock +0 -1061
|
@@ -0,0 +1,348 @@
|
|
|
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 commander_1 = require("commander");
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const contentFormats = ["plain", "html"];
|
|
11
|
+
/**
|
|
12
|
+
* Use a type predicate to narrow a string to a content format.
|
|
13
|
+
* @param value The value to check.
|
|
14
|
+
*/
|
|
15
|
+
function isContentFormat(value) {
|
|
16
|
+
return contentFormats.some(contentFormat => contentFormat === value);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build a path from a base path and additional parts.
|
|
20
|
+
* @param basePath The base path.
|
|
21
|
+
* @param parts The additional path parts.
|
|
22
|
+
*/
|
|
23
|
+
function buildPath(basePath, ...parts) {
|
|
24
|
+
if (!basePath.trim()) {
|
|
25
|
+
throw new Error(`Invalid base path ${basePath}`);
|
|
26
|
+
}
|
|
27
|
+
const basePathResolved = node_path_1.default.resolve(basePath);
|
|
28
|
+
const resolved = node_path_1.default.resolve(...[basePathResolved, ...parts]);
|
|
29
|
+
// Ensure resolved path is inside basePath
|
|
30
|
+
if (!resolved.startsWith(basePathResolved + node_path_1.default.sep) && resolved !== basePathResolved) {
|
|
31
|
+
throw new Error(`Invalid path ${resolved}`);
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the last non-empty string.
|
|
37
|
+
* @param value The strings.
|
|
38
|
+
*/
|
|
39
|
+
function lastNonEmptyString(...value) {
|
|
40
|
+
for (let index = (value.length - 1); index >= 0; index--) {
|
|
41
|
+
const item = value[index];
|
|
42
|
+
if (typeof item === 'string' && item.length > 0) {
|
|
43
|
+
return item;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read the translation json files.
|
|
50
|
+
* @param filesPath The path to the top directory containing the meta.json file.
|
|
51
|
+
*/
|
|
52
|
+
async function readTranslationFiles(filesPath) {
|
|
53
|
+
// Read files
|
|
54
|
+
const metaPath = buildPath(filesPath, 'meta.json');
|
|
55
|
+
console.log(`Load translation meta file ${metaPath}`);
|
|
56
|
+
const metaData = JSON.parse(await promises_1.default.readFile(metaPath, { encoding: 'utf8' }));
|
|
57
|
+
const localeNames = (await promises_1.default.readdir(filesPath, { withFileTypes: true }))
|
|
58
|
+
.filter(dirent => dirent.isDirectory())
|
|
59
|
+
.map(dirent => dirent.name);
|
|
60
|
+
const localeTranslationData = [];
|
|
61
|
+
for (const locale of localeNames) {
|
|
62
|
+
let p;
|
|
63
|
+
try {
|
|
64
|
+
p = buildPath(filesPath, locale, 'translation.json');
|
|
65
|
+
const translationStat = await promises_1.default.stat(p);
|
|
66
|
+
if (!translationStat.isFile()) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
72
|
+
// Nested folders such as language-defaults/demo are documentation
|
|
73
|
+
// or sample data, not locale roots.
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
if (!p) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const data = JSON.parse(await promises_1.default.readFile(p, { encoding: 'utf8' }));
|
|
82
|
+
console.log(`Load translation locale file ${p}`);
|
|
83
|
+
localeTranslationData.push({ locale, source: p, data });
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
meta: {
|
|
87
|
+
source: metaPath,
|
|
88
|
+
data: metaData,
|
|
89
|
+
},
|
|
90
|
+
locales: localeTranslationData,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Write updated translation json files.
|
|
95
|
+
* @param opts The options.
|
|
96
|
+
* @param opts.meta The meta details.
|
|
97
|
+
* @param opts.locales The translation locale data.
|
|
98
|
+
* @param opts.dryRun Whether this is a dry run.
|
|
99
|
+
* @param opts.writePath The top-level path to write the output.
|
|
100
|
+
*/
|
|
101
|
+
async function writeTranslationFiles(opts) {
|
|
102
|
+
const metaPath = buildPath(opts?.writePath, 'meta.json');
|
|
103
|
+
if (opts.dryRun) {
|
|
104
|
+
console.log(`[dry-run] Not writing updated meta to ${metaPath}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Write merged meta back to file
|
|
108
|
+
console.log(`Writing updated meta to ${metaPath}`);
|
|
109
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(metaPath), { recursive: true });
|
|
110
|
+
await promises_1.default.writeFile(metaPath, JSON.stringify(opts.meta, null, 2));
|
|
111
|
+
}
|
|
112
|
+
// Write translation files
|
|
113
|
+
for (const [locale, data] of Object.entries(opts.locales)) {
|
|
114
|
+
const translationPath = buildPath(opts?.writePath, locale, 'translation.json');
|
|
115
|
+
if (opts.dryRun) {
|
|
116
|
+
console.log(`[dry-run] Not writing updated translation data to ${translationPath}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(`Writing updated translation data to ${translationPath}`);
|
|
120
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(translationPath), { recursive: true });
|
|
121
|
+
await promises_1.default.writeFile(translationPath, JSON.stringify(data, null, 2));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Read translation data from ReDBox API.
|
|
127
|
+
* @param apiBaseUrl The base url for the API calls.
|
|
128
|
+
*/
|
|
129
|
+
async function readTranslationApi(apiBaseUrl) {
|
|
130
|
+
if (!apiBaseUrl.includes('://')) {
|
|
131
|
+
throw new Error(`Api base url is not a valid url '${apiBaseUrl}'`);
|
|
132
|
+
}
|
|
133
|
+
if (!apiBaseUrl.endsWith('/')) {
|
|
134
|
+
apiBaseUrl += '/';
|
|
135
|
+
}
|
|
136
|
+
const translationUrl = new URL('default/rdmp/locales/__locale__/translation.json', apiBaseUrl);
|
|
137
|
+
const entriesUrl = new URL('default/rdmp/app/i18n/entries', apiBaseUrl);
|
|
138
|
+
console.log(`Load translation entries url ${entriesUrl}`);
|
|
139
|
+
const entriesResponse = await fetch(entriesUrl, { signal: AbortSignal.timeout(5000) });
|
|
140
|
+
if (!entriesResponse.ok) {
|
|
141
|
+
throw new Error(`Could not fetch translation entries: status ${entriesResponse.status} body ${await entriesResponse.text()}`);
|
|
142
|
+
}
|
|
143
|
+
const entriesData = await entriesResponse.json();
|
|
144
|
+
const metaData = Object.create(null);
|
|
145
|
+
const localeTranslationData = [];
|
|
146
|
+
const localeNames = Array.from(new Set(entriesData.map(e => e.locale).filter(l => typeof l === 'string'))).sort();
|
|
147
|
+
for (const localeName of localeNames) {
|
|
148
|
+
if (!localeName || !localeName.trim() || localeName.includes('.') || localeName.includes('/')) {
|
|
149
|
+
console.warn(`Ignoring invalid locale name '${localeName}'.`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const translationLocaleUrl = new URL(translationUrl.toString().replace('/__locale__/', `/${encodeURIComponent(localeName)}/`));
|
|
153
|
+
console.log(`Load translation locale url ${translationLocaleUrl}`);
|
|
154
|
+
const translationResponse = await fetch(translationLocaleUrl, { signal: AbortSignal.timeout(5000) });
|
|
155
|
+
if (!translationResponse.ok) {
|
|
156
|
+
throw new Error(`Could not fetch translation locale: status ${translationResponse.status} body ${await translationResponse.text()}`);
|
|
157
|
+
}
|
|
158
|
+
localeTranslationData.push({
|
|
159
|
+
locale: localeName,
|
|
160
|
+
source: translationLocaleUrl.toString(),
|
|
161
|
+
data: await translationResponse.json()
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const entryData of entriesData) {
|
|
165
|
+
if (!entryData.key) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!(entryData.key in metaData)) {
|
|
169
|
+
metaData[entryData.key] = Object.create(null);
|
|
170
|
+
}
|
|
171
|
+
if (entryData.category) {
|
|
172
|
+
metaData[entryData.key].category = entryData.category;
|
|
173
|
+
}
|
|
174
|
+
if (entryData.contentFormat) {
|
|
175
|
+
metaData[entryData.key].contentFormat = entryData.contentFormat;
|
|
176
|
+
}
|
|
177
|
+
if (entryData.description) {
|
|
178
|
+
metaData[entryData.key].description = entryData.description;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
meta: {
|
|
183
|
+
source: entriesUrl.toString(),
|
|
184
|
+
data: metaData,
|
|
185
|
+
},
|
|
186
|
+
locales: localeTranslationData,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Merge two or more meta entries.
|
|
191
|
+
* @param opts The merge options.
|
|
192
|
+
* @param opts.key The translation key.
|
|
193
|
+
* @param opts.value The translation value.
|
|
194
|
+
* @param opts.items The meta entries.
|
|
195
|
+
* @param opts.guessContentFormat Whether to guess the content format from the translation value.
|
|
196
|
+
*/
|
|
197
|
+
function mergeMetaItems(opts) {
|
|
198
|
+
// content format
|
|
199
|
+
const contentFormatRaw = lastNonEmptyString(...opts.items?.map(i => i?.contentFormat));
|
|
200
|
+
let contentFormat = isContentFormat(contentFormatRaw) ? contentFormatRaw : undefined;
|
|
201
|
+
const isHtml = opts.guessContentFormat === true &&
|
|
202
|
+
opts.value !== undefined &&
|
|
203
|
+
contentFormat === undefined &&
|
|
204
|
+
(typeof opts.value === 'string' && (opts.value?.includes('<') &&
|
|
205
|
+
opts.value?.includes('</') &&
|
|
206
|
+
opts.value?.includes('>')));
|
|
207
|
+
if (isHtml) {
|
|
208
|
+
contentFormat = "html";
|
|
209
|
+
}
|
|
210
|
+
// description
|
|
211
|
+
const description = lastNonEmptyString(...[
|
|
212
|
+
opts?.key === undefined ? "" : `Translation for ${opts.key}`,
|
|
213
|
+
...opts.items?.map(i => i?.description),
|
|
214
|
+
]);
|
|
215
|
+
// category
|
|
216
|
+
const keySplit = typeof opts.key === 'string' ? opts.key.split(/[_\-:.@]+/).filter(i => !!i) : [];
|
|
217
|
+
const category = lastNonEmptyString(...[
|
|
218
|
+
keySplit.length > 0 ? keySplit[0] : undefined,
|
|
219
|
+
...opts.items?.map(i => i?.category),
|
|
220
|
+
]);
|
|
221
|
+
const result = Object.create(null);
|
|
222
|
+
result.category = category;
|
|
223
|
+
result.description = description;
|
|
224
|
+
result.contentFormat = contentFormat;
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
function registerSyncTranslationCommand(program) {
|
|
228
|
+
const sourcesCollected = [];
|
|
229
|
+
program
|
|
230
|
+
.command('sync-translation')
|
|
231
|
+
.description('Read translations from API and/or json files, and write to language translation files.')
|
|
232
|
+
.option('-l, --language-defaults <path>', 'Path to read the language-defaults directory containing locales and meta json files. Can be provided multiple times.', (value, previous) => {
|
|
233
|
+
previous.push({ type: 'language-defaults', value });
|
|
234
|
+
return previous;
|
|
235
|
+
}, sourcesCollected)
|
|
236
|
+
.option('-a, --api-base <url>', 'Base url for the API to read translation data. Can be provided multiple times.', (value, previous) => {
|
|
237
|
+
previous.push({ type: 'api-base', value });
|
|
238
|
+
return previous;
|
|
239
|
+
}, sourcesCollected)
|
|
240
|
+
.requiredOption('-o, --output <path>', 'Path to the output directory or file.')
|
|
241
|
+
.addOption(new commander_1.Option('-f, --format <format>', 'The output format.')
|
|
242
|
+
.choices(['language-defaults'])
|
|
243
|
+
.default('language-defaults'))
|
|
244
|
+
.action(async (options) => {
|
|
245
|
+
try {
|
|
246
|
+
const globalOptions = program.opts();
|
|
247
|
+
const outputPath = options.output;
|
|
248
|
+
const outputFormat = options.format;
|
|
249
|
+
const sources = sourcesCollected.splice(0);
|
|
250
|
+
if (sources.length === 0) {
|
|
251
|
+
throw new Error(`Must provide at least one of language-defaults path or api-base url.`);
|
|
252
|
+
}
|
|
253
|
+
// Get existing translation data
|
|
254
|
+
const sourceMeta = [];
|
|
255
|
+
const sourceTranslations = [];
|
|
256
|
+
// Precedence order: later items overwrite earlier items.
|
|
257
|
+
// Populate source meta and translations in order provided in command line.
|
|
258
|
+
for (const { type, value } of sources) {
|
|
259
|
+
if (type === 'language-defaults') {
|
|
260
|
+
const { meta, locales } = await readTranslationFiles(value);
|
|
261
|
+
sourceMeta.push(meta);
|
|
262
|
+
sourceTranslations.push(...locales);
|
|
263
|
+
}
|
|
264
|
+
else if (type === 'api-base') {
|
|
265
|
+
const { meta, locales } = await readTranslationApi(value);
|
|
266
|
+
sourceMeta.push(meta);
|
|
267
|
+
sourceTranslations.push(...locales);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
throw new Error(`Unknown source type '${type}'.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// The keys for locales are the source of truth,
|
|
274
|
+
// so any keys in meta that are not in any locale data will be dropped.
|
|
275
|
+
const keys = new Set(sourceTranslations.flatMap(i => Object.keys(i.data)));
|
|
276
|
+
// Cache translations by locale so each source is grouped only once.
|
|
277
|
+
const sourceTranslationsByLocale = new Map();
|
|
278
|
+
for (const sourceTranslation of sourceTranslations) {
|
|
279
|
+
const localeTranslations = sourceTranslationsByLocale.get(sourceTranslation.locale) ?? [];
|
|
280
|
+
localeTranslations.push(sourceTranslation);
|
|
281
|
+
sourceTranslationsByLocale.set(sourceTranslation.locale, localeTranslations);
|
|
282
|
+
}
|
|
283
|
+
// The unique locale names.
|
|
284
|
+
const localeNames = Array.from(sourceTranslationsByLocale.keys()).sort();
|
|
285
|
+
// Gather translation and meta entries.
|
|
286
|
+
const meta = Object.create(null);
|
|
287
|
+
const locales = Object.create(null);
|
|
288
|
+
for (const key of keys) {
|
|
289
|
+
if (!key) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
for (const localeName of localeNames) {
|
|
293
|
+
if (!localeName) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (!(localeName in locales)) {
|
|
297
|
+
locales[localeName] = Object.create(null);
|
|
298
|
+
}
|
|
299
|
+
const localeTranslationData = sourceTranslationsByLocale.get(localeName) ?? [];
|
|
300
|
+
// Translation value for a locale key.
|
|
301
|
+
const values = localeTranslationData
|
|
302
|
+
.map(i => i.data?.[key])
|
|
303
|
+
.filter(i => typeof i === 'string')
|
|
304
|
+
.filter(i => i?.trim().length > 0);
|
|
305
|
+
if (new Set(values).size > 1) {
|
|
306
|
+
console.warn(`Translation '${localeName}' key '${key}' has more than one value: ${JSON.stringify(values)}`);
|
|
307
|
+
}
|
|
308
|
+
const valuesNonString = localeTranslationData
|
|
309
|
+
.map(i => i.data?.[key])
|
|
310
|
+
.filter(i => typeof i !== 'string' && i !== undefined);
|
|
311
|
+
if (valuesNonString.length > 0) {
|
|
312
|
+
console.warn(`Translation '${localeName}' key '${key}' has non-string values that were ignored:`, valuesNonString);
|
|
313
|
+
}
|
|
314
|
+
const value = lastNonEmptyString(...values);
|
|
315
|
+
locales[localeName][key] = value ?? "";
|
|
316
|
+
}
|
|
317
|
+
// Use the first populated locale in sorted order when inferring meta.
|
|
318
|
+
const valueForMeta = localeNames
|
|
319
|
+
.map(localeName => locales[localeName]?.[key])
|
|
320
|
+
.find(value => typeof value === 'string' && value.trim().length > 0);
|
|
321
|
+
meta[key] = mergeMetaItems({
|
|
322
|
+
key,
|
|
323
|
+
value: valueForMeta,
|
|
324
|
+
items: sourceMeta.map(i => i.data?.[key]),
|
|
325
|
+
guessContentFormat: true,
|
|
326
|
+
}) ?? Object.create(null);
|
|
327
|
+
}
|
|
328
|
+
// Write output
|
|
329
|
+
if (outputFormat === "language-defaults") {
|
|
330
|
+
await writeTranslationFiles({
|
|
331
|
+
meta,
|
|
332
|
+
locales,
|
|
333
|
+
writePath: outputPath,
|
|
334
|
+
dryRun: globalOptions.dryRun,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
throw new Error(`Unknown output format '${outputFormat}'`);
|
|
339
|
+
}
|
|
340
|
+
console.log(`\n🛠️ Ran sync-translation`);
|
|
341
|
+
console.log('\n✅ Done!\n');
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
console.error(`\n❌ Error: `, error);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateCompletionScript(shell: string): string;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateCompletionScript = generateCompletionScript;
|
|
4
|
+
function getCompletionSpec() {
|
|
5
|
+
const globalOptions = ['--root', '--core-types-root', '--angular-root', '--dry-run', '-h', '--help', '-V', '--version'];
|
|
6
|
+
const topLevelCommands = ['init', 'check', 'migrate-hook-dependencies', 'generate', 'g', 'install-skills', 'skills', 'migrate-form-config', 'completion', 'help'];
|
|
7
|
+
const generateCommands = ['controller', 'service', 'method', 'angular-app', 'angular-service', 'form-component', 'form-field', 'migration', 'model'];
|
|
8
|
+
const commandOptions = {
|
|
9
|
+
'migrate-form-config': ['-i', '--input', '-o', '--output'],
|
|
10
|
+
'check': [],
|
|
11
|
+
'completion': [],
|
|
12
|
+
'init': [],
|
|
13
|
+
'install-skills': [],
|
|
14
|
+
'migrate-hook-dependencies': [],
|
|
15
|
+
'skills': [],
|
|
16
|
+
'generate controller': ['--actions', '--webservice', '--class-name', '--route', '--routes', '--nav', '--lang', '--auth'],
|
|
17
|
+
'generate service': ['--methods'],
|
|
18
|
+
'generate method': ['--file', '--method', '--route', '--http', '--auth', '--nav', '--lang'],
|
|
19
|
+
'generate angular-app': ['--ejs-view', '--auth'],
|
|
20
|
+
'generate angular-service': ['--app', '--methods'],
|
|
21
|
+
'generate form-component': ['--app', '--with-service'],
|
|
22
|
+
'generate form-field': ['--type'],
|
|
23
|
+
'generate migration': [],
|
|
24
|
+
'generate model': ['--identity', '--attrs', '--belongs-to', '--has-many']
|
|
25
|
+
};
|
|
26
|
+
const fileValueOptions = ['--root', '--core-types-root', '--angular-root', '--input', '--output', '--file'];
|
|
27
|
+
return { globalOptions, topLevelCommands, generateCommands, commandOptions, fileValueOptions };
|
|
28
|
+
}
|
|
29
|
+
function generateBashCompletion() {
|
|
30
|
+
const spec = getCompletionSpec();
|
|
31
|
+
const top = spec.topLevelCommands.join(' ');
|
|
32
|
+
const generateSubs = spec.generateCommands.join(' ');
|
|
33
|
+
const globalOpts = spec.globalOptions.join(' ');
|
|
34
|
+
const fileOpts = spec.fileValueOptions.join(' ');
|
|
35
|
+
const commandOptionEntries = Object.entries(spec.commandOptions)
|
|
36
|
+
.map(([key, value]) => `"${key}") opts="${value.join(' ')}";;`)
|
|
37
|
+
.join('\n ');
|
|
38
|
+
return `# bash completion for redbox-dev-tools
|
|
39
|
+
_redbox_dev_tools_completion() {
|
|
40
|
+
local cur prev words cword
|
|
41
|
+
_init_completion || return
|
|
42
|
+
|
|
43
|
+
local top_commands="${top}"
|
|
44
|
+
local generate_subcommands="${generateSubs}"
|
|
45
|
+
local global_opts="${globalOpts}"
|
|
46
|
+
local file_value_opts="${fileOpts}"
|
|
47
|
+
|
|
48
|
+
case "$prev" in
|
|
49
|
+
$file_value_opts)
|
|
50
|
+
COMPREPLY=($(compgen -f -- "$cur"))
|
|
51
|
+
return
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
if [[ $cword -le 1 ]]; then
|
|
56
|
+
COMPREPLY=($(compgen -W "$top_commands $global_opts" -- "$cur"))
|
|
57
|
+
return
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [[ "\${words[1]}" == "generate" || "\${words[1]}" == "g" ]]; then
|
|
61
|
+
if [[ $cword -eq 2 ]]; then
|
|
62
|
+
COMPREPLY=($(compgen -W "$generate_subcommands" -- "$cur"))
|
|
63
|
+
return
|
|
64
|
+
fi
|
|
65
|
+
local key="generate \${words[2]}"
|
|
66
|
+
local opts=""
|
|
67
|
+
case "$key" in
|
|
68
|
+
${commandOptionEntries}
|
|
69
|
+
*) opts="";;
|
|
70
|
+
esac
|
|
71
|
+
COMPREPLY=($(compgen -W "$opts $global_opts" -- "$cur"))
|
|
72
|
+
return
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
local opts=""
|
|
76
|
+
case "\${words[1]}" in
|
|
77
|
+
"migrate-form-config") opts="-i --input -o --output" ;;
|
|
78
|
+
"check") opts="" ;;
|
|
79
|
+
"completion") opts="" ;;
|
|
80
|
+
"init") opts="" ;;
|
|
81
|
+
"install-skills") opts="" ;;
|
|
82
|
+
"migrate-hook-dependencies") opts="" ;;
|
|
83
|
+
"skills") opts="" ;;
|
|
84
|
+
*) opts="" ;;
|
|
85
|
+
esac
|
|
86
|
+
|
|
87
|
+
COMPREPLY=($(compgen -W "$opts $global_opts" -- "$cur"))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
complete -F _redbox_dev_tools_completion redbox-dev-tools
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
function generateZshCompletion() {
|
|
94
|
+
const bashScript = generateBashCompletion();
|
|
95
|
+
return `#compdef redbox-dev-tools
|
|
96
|
+
autoload -U +X bashcompinit && bashcompinit
|
|
97
|
+
${bashScript}`;
|
|
98
|
+
}
|
|
99
|
+
function generateFishCompletion() {
|
|
100
|
+
const spec = getCompletionSpec();
|
|
101
|
+
const lines = [
|
|
102
|
+
'# fish completion for redbox-dev-tools',
|
|
103
|
+
'set -l __rbhk_cmd redbox-dev-tools',
|
|
104
|
+
'complete -c $__rbhk_cmd -f'
|
|
105
|
+
];
|
|
106
|
+
for (const cmd of spec.topLevelCommands) {
|
|
107
|
+
lines.push(`complete -c $__rbhk_cmd -n "__fish_use_subcommand" -a "${cmd}"`);
|
|
108
|
+
}
|
|
109
|
+
for (const option of spec.globalOptions) {
|
|
110
|
+
if (option.startsWith('--')) {
|
|
111
|
+
const name = option.replace(/^--/, '');
|
|
112
|
+
const requiresValue = ['root', 'core-types-root', 'angular-root'].includes(name);
|
|
113
|
+
lines.push(`complete -c $__rbhk_cmd -l ${name}${requiresValue ? ' -r' : ''}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const sub of spec.generateCommands) {
|
|
117
|
+
lines.push(`complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate g; and __fish_use_subcommand" -a "${sub}"`);
|
|
118
|
+
}
|
|
119
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from migrate-form-config" -s i -l input -r');
|
|
120
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from migrate-form-config" -s o -l output -r');
|
|
121
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from completion" -a "bash zsh fish powershell pwsh"');
|
|
122
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from controller" -l actions -r');
|
|
123
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from controller" -l webservice');
|
|
124
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from controller" -l class-name -r');
|
|
125
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from controller" -l route -r');
|
|
126
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from controller" -l routes -r');
|
|
127
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from controller" -l nav -r');
|
|
128
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from controller" -l lang -r');
|
|
129
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from controller" -l auth -r');
|
|
130
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from service" -l methods -r');
|
|
131
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from method" -l file -r');
|
|
132
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from method" -l method -r');
|
|
133
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from method" -l route -r');
|
|
134
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from method" -l http -r');
|
|
135
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from method" -l auth -r');
|
|
136
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from method" -l nav -r');
|
|
137
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from method" -l lang -r');
|
|
138
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from angular-app" -l ejs-view -r');
|
|
139
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from angular-app" -l auth -r');
|
|
140
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from angular-service" -l app -r');
|
|
141
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from angular-service" -l methods -r');
|
|
142
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from form-component" -l app -r');
|
|
143
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from form-component" -l with-service');
|
|
144
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from form-field" -l type -r');
|
|
145
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from model" -l identity -r');
|
|
146
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from model" -l attrs -r');
|
|
147
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from model" -l belongs-to -r');
|
|
148
|
+
lines.push('complete -c $__rbhk_cmd -n "__fish_seen_subcommand_from generate; and __fish_seen_subcommand_from model" -l has-many -r');
|
|
149
|
+
return `${lines.join('\n')}\n`;
|
|
150
|
+
}
|
|
151
|
+
function generatePowerShellCompletion() {
|
|
152
|
+
return `# PowerShell completion for redbox-dev-tools
|
|
153
|
+
Register-ArgumentCompleter -Native -CommandName redbox-dev-tools -ScriptBlock {
|
|
154
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
155
|
+
$tokens = $commandAst.CommandElements | ForEach-Object { $_.Extent.Text }
|
|
156
|
+
$global = @('--root','--core-types-root','--angular-root','--dry-run','-h','--help','-V','--version')
|
|
157
|
+
$top = @('init','check','migrate-hook-dependencies','generate','g','install-skills','skills','migrate-form-config','completion','help')
|
|
158
|
+
$generateSub = @('controller','service','method','angular-app','angular-service','form-component','form-field','migration','model')
|
|
159
|
+
|
|
160
|
+
if ($tokens.Count -le 2) {
|
|
161
|
+
@($top + $global) | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
162
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
163
|
+
}
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if ($tokens[1] -eq 'generate' -or $tokens[1] -eq 'g') {
|
|
168
|
+
if ($tokens.Count -le 3) {
|
|
169
|
+
$generateSub | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
170
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
171
|
+
}
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
$sub = $tokens[2]
|
|
175
|
+
$opts = switch ($sub) {
|
|
176
|
+
'controller' { @('--actions','--webservice','--class-name','--route','--routes','--nav','--lang','--auth') }
|
|
177
|
+
'service' { @('--methods') }
|
|
178
|
+
'method' { @('--file','--method','--route','--http','--auth','--nav','--lang') }
|
|
179
|
+
'angular-app' { @('--ejs-view','--auth') }
|
|
180
|
+
'angular-service' { @('--app','--methods') }
|
|
181
|
+
'form-component' { @('--app','--with-service') }
|
|
182
|
+
'form-field' { @('--type') }
|
|
183
|
+
'model' { @('--identity','--attrs','--belongs-to','--has-many') }
|
|
184
|
+
default { @() }
|
|
185
|
+
}
|
|
186
|
+
@($opts + $global) | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
187
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
|
|
188
|
+
}
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
$opts = switch ($tokens[1]) {
|
|
193
|
+
'migrate-form-config' { @('-i','--input','-o','--output') }
|
|
194
|
+
'completion' { @('bash','zsh','fish','powershell','pwsh') }
|
|
195
|
+
default { @() }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@($opts + $global) | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
199
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
function generateCompletionScript(shell) {
|
|
205
|
+
switch (shell) {
|
|
206
|
+
case 'bash':
|
|
207
|
+
return generateBashCompletion();
|
|
208
|
+
case 'zsh':
|
|
209
|
+
return generateZshCompletion();
|
|
210
|
+
case 'fish':
|
|
211
|
+
return generateFishCompletion();
|
|
212
|
+
case 'powershell':
|
|
213
|
+
case 'pwsh':
|
|
214
|
+
return generatePowerShellCompletion();
|
|
215
|
+
default:
|
|
216
|
+
throw new Error(`Unsupported shell '${shell}'. Expected one of: bash, zsh, fish, powershell`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Generator, GeneratorOptions } from '../utils/generator';
|
|
2
|
+
import { RedboxPaths } from '../utils/paths';
|
|
3
|
+
import { NavigationMapping, LanguageDefaultEntry } from '../utils/config-helper';
|
|
4
|
+
export interface AddMethodGeneratorOptions extends GeneratorOptions {
|
|
5
|
+
file: string;
|
|
6
|
+
method: string;
|
|
7
|
+
route?: string;
|
|
8
|
+
http?: string;
|
|
9
|
+
auth?: string[];
|
|
10
|
+
navigation?: NavigationMapping[];
|
|
11
|
+
languageDefaults?: LanguageDefaultEntry[];
|
|
12
|
+
paths: RedboxPaths;
|
|
13
|
+
}
|
|
14
|
+
export declare class AddMethodGenerator extends Generator {
|
|
15
|
+
private filePath;
|
|
16
|
+
private method;
|
|
17
|
+
private route?;
|
|
18
|
+
private http?;
|
|
19
|
+
private auth?;
|
|
20
|
+
private navigation;
|
|
21
|
+
private languageDefaults;
|
|
22
|
+
private paths;
|
|
23
|
+
private project;
|
|
24
|
+
constructor(options: AddMethodGeneratorOptions);
|
|
25
|
+
generate(): Promise<void>;
|
|
26
|
+
private updateClass;
|
|
27
|
+
}
|