@factorialco/fcode-cli 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -2
- package/dist/commands/add.js +3 -1
- package/dist/commands/clone.d.ts +1 -0
- package/dist/commands/clone.js +5 -0
- package/dist/commands/i18n/add.d.ts +9 -0
- package/dist/commands/i18n/add.js +13 -0
- package/dist/commands/i18n/pull.d.ts +12 -0
- package/dist/commands/i18n/pull.js +14 -0
- package/dist/commands/i18n/push.d.ts +12 -0
- package/dist/commands/i18n/push.js +14 -0
- package/dist/commands/i18n/remove.d.ts +9 -0
- package/dist/commands/i18n/remove.js +19 -0
- package/dist/commands/i18n/reset.d.ts +9 -0
- package/dist/commands/i18n/reset.js +13 -0
- package/dist/commands/i18n/status.d.ts +11 -0
- package/dist/commands/i18n/status.js +42 -0
- package/dist/commands/pull.js +5 -3
- package/dist/commands/push.js +5 -3
- package/dist/commands/reset.js +3 -1
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +5 -2
- package/dist/commands/status.js +4 -2
- package/dist/commands/team/push.js +3 -0
- package/dist/config/remote.js +1 -1
- package/dist/config/workspace.js +10 -0
- package/dist/core/commands/i18n-command.d.ts +5 -0
- package/dist/core/commands/i18n-command.js +16 -0
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.js +19 -1
- package/dist/core/executors/execution-task.js +4 -0
- package/dist/core/executors/javascript-execution-task.js +26 -1
- package/dist/core/resource-pusher.js +15 -0
- package/dist/core/types.d.ts +29 -1
- package/dist/core/types.js +43 -2
- package/dist/repositories/locale.d.ts +24 -0
- package/dist/repositories/locale.js +102 -0
- package/dist/repositories/resource.js +4 -1
- package/dist/repositories/team.js +5 -2
- package/dist/runtime/libs/python/fcode/__init__.py +1 -0
- package/dist/runtime/libs/python/fcode/context.py +43 -1
- package/dist/services/fcode-graphql-client.d.ts +11 -1
- package/dist/services/fcode-graphql-client.js +111 -0
- package/dist/services/locale-merger.d.ts +23 -0
- package/dist/services/locale-merger.js +111 -0
- package/dist/services/locale-resolver.d.ts +18 -0
- package/dist/services/locale-resolver.js +120 -0
- package/dist/services/process-runner.d.ts +1 -0
- package/dist/services/process-runner.js +2 -2
- package/dist/utils/gitignore.d.ts +1 -1
- package/dist/utils/gitignore.js +4 -1
- package/oclif.manifest.json +194 -1
- package/package.json +2 -1
|
@@ -4,6 +4,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
4
4
|
const fs = require("node:fs");
|
|
5
5
|
const os = require("node:os");
|
|
6
6
|
const path = require("node:path");
|
|
7
|
+
const locale_resolver_1 = require("../../services/locale-resolver");
|
|
7
8
|
const errors_1 = require("../errors");
|
|
8
9
|
const cli_1 = require("../../utils/cli");
|
|
9
10
|
const dependencies_1 = require("../utils/dependencies");
|
|
@@ -114,6 +115,9 @@ class ExecutionTask {
|
|
|
114
115
|
},
|
|
115
116
|
variables: this.getVariablesObject(),
|
|
116
117
|
request: this._options.request ?? 'No request, you are using the CLI',
|
|
118
|
+
// Layered here rather than in the generated runtime: the cloud ships one already-merged
|
|
119
|
+
// flat map, and a local run has to resolve fcode.i18n against the same shape.
|
|
120
|
+
i18n: (0, locale_resolver_1.resolveLocalTranslations)(this._options.locale),
|
|
117
121
|
};
|
|
118
122
|
}
|
|
119
123
|
async prepare() {
|
|
@@ -111,7 +111,12 @@ const main = typeof _hooksModule['${fn}'] === 'function'
|
|
|
111
111
|
};`}
|
|
112
112
|
|
|
113
113
|
global.fcode = {
|
|
114
|
-
|
|
114
|
+
// A version is either a bare string or an options object ({ version: 'v1' }), the
|
|
115
|
+
// form every other versioned helper takes — the cloud reads both, so a local run must too.
|
|
116
|
+
import: (moduleName, tagOrOptions) => {
|
|
117
|
+
const version = typeof tagOrOptions === 'string'
|
|
118
|
+
? tagOrOptions
|
|
119
|
+
: (tagOrOptions && typeof tagOrOptions === 'object' ? tagOrOptions.version : undefined);
|
|
115
120
|
let modulePath = require('path').join('${process.cwd()}', 'modules', moduleName);
|
|
116
121
|
if(version) {
|
|
117
122
|
modulePath = require('path').join(modulePath, 'versions', version)
|
|
@@ -588,6 +593,26 @@ const main = typeof _hooksModule['${fn}'] === 'function'
|
|
|
588
593
|
}
|
|
589
594
|
};
|
|
590
595
|
})(),
|
|
596
|
+
// The third argument is the version options the cloud honours. A workspace directory holds
|
|
597
|
+
// no published snapshots, so a pinned call resolves against the files on disk — the same
|
|
598
|
+
// per-file fallback the cloud applies to a locale that was never published at that tag.
|
|
599
|
+
// Own properties only and no array arguments, exactly like the cloud executors.
|
|
600
|
+
i18n: Object.assign((key, args, _options) => {
|
|
601
|
+
const translations = ${JSON.stringify(metadata.i18n.translations, null, 2)};
|
|
602
|
+
const template = Object.hasOwn(translations, key) ? translations[key] : undefined;
|
|
603
|
+
if (template === undefined) {
|
|
604
|
+
return key;
|
|
605
|
+
}
|
|
606
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
607
|
+
return template;
|
|
608
|
+
}
|
|
609
|
+
// Single pass, so a substituted value containing %{...} is never rescanned.
|
|
610
|
+
return template.replace(/%\{([^}]*)\}/g, (match, name) => {
|
|
611
|
+
const trimmed = name.trim();
|
|
612
|
+
const value = Object.hasOwn(args, trimmed) ? args[trimmed] : undefined;
|
|
613
|
+
return value === undefined || value === null ? match : String(value);
|
|
614
|
+
});
|
|
615
|
+
}, { locale: ${JSON.stringify(metadata.i18n.locale ?? null)} }),
|
|
591
616
|
sendMail: async ({ to, subject, text, html } = {}) => {
|
|
592
617
|
// No manager locally: log the email contents instead of sending, so
|
|
593
618
|
// devs can see exactly what would be delivered when run in the cloud.
|
|
@@ -82,6 +82,16 @@ class ResourcePusher {
|
|
|
82
82
|
const createdVariable = await this._fcodeGraphQLClient.pushVariable(variable);
|
|
83
83
|
return this._repository.save(createdVariable);
|
|
84
84
|
}
|
|
85
|
+
if (this._repository._repositoryName === 'i18n') {
|
|
86
|
+
const locale = resource;
|
|
87
|
+
// The recorded id is only a hint: pushing an identifier a parent workspace owns has to
|
|
88
|
+
// create an override here, so the update path is taken only for a locale this workspace
|
|
89
|
+
// already owns.
|
|
90
|
+
const pushed = locale.id && !locale.resolvingTeamSlug
|
|
91
|
+
? await this._fcodeGraphQLClient.updateLocale(locale)
|
|
92
|
+
: await this._fcodeGraphQLClient.pushLocale(locale);
|
|
93
|
+
return this._repository.save(pushed);
|
|
94
|
+
}
|
|
85
95
|
if (this._repository._repositoryName === 'dependencies') {
|
|
86
96
|
const updatedProgrammingLanguageManifests = await this._fcodeGraphQLClient.pushProgrammingLanguageManifest(resource);
|
|
87
97
|
return this._repository.save(updatedProgrammingLanguageManifests);
|
|
@@ -114,6 +124,11 @@ class ResourcePusher {
|
|
|
114
124
|
if (this._repository._repositoryName === 'variables') {
|
|
115
125
|
await this._fcodeGraphQLClient.removeVariable(resource.id);
|
|
116
126
|
}
|
|
127
|
+
if (this._repository._repositoryName === 'i18n') {
|
|
128
|
+
// By identifier, not slug: an inherited locale is tracked under a qualified slug, and only
|
|
129
|
+
// this workspace's own locales — where the two coincide — ever reach a remote delete.
|
|
130
|
+
await this._fcodeGraphQLClient.removeLocale(resource.locale);
|
|
131
|
+
}
|
|
117
132
|
this._repository.removeMetadata(resource.slug);
|
|
118
133
|
}
|
|
119
134
|
catch (error) {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -41,6 +41,9 @@ export interface TeamMetadata {
|
|
|
41
41
|
variables: {
|
|
42
42
|
[key: string]: Variable;
|
|
43
43
|
};
|
|
44
|
+
i18n: {
|
|
45
|
+
[key: string]: Locale;
|
|
46
|
+
};
|
|
44
47
|
dependencies: {
|
|
45
48
|
[key: string]: ProgrammingLanguageManifest;
|
|
46
49
|
};
|
|
@@ -187,6 +190,27 @@ export declare class ScriptLibrary extends Resource {
|
|
|
187
190
|
constructor(id: string, name: string, sourceCode: string, programmingLanguage: ProgrammingLanguage, resolvingTeamSlug?: string);
|
|
188
191
|
get contentHash(): string;
|
|
189
192
|
}
|
|
193
|
+
export declare class Locale extends Resource {
|
|
194
|
+
static LOCALE_REGEX: RegExp;
|
|
195
|
+
static INHERITED_SUFFIX: string;
|
|
196
|
+
id: string | undefined;
|
|
197
|
+
slug: string;
|
|
198
|
+
locale: string;
|
|
199
|
+
content: string;
|
|
200
|
+
constructor(id: string | undefined, locale: string, content: string, resolvingTeamSlug?: string);
|
|
201
|
+
/**
|
|
202
|
+
* The name a locale is tracked and stored under. Unlike every other resource, the identifier is
|
|
203
|
+
* not unique: a workspace sees its own `en` and each parent workspace's `en`, and all of them are
|
|
204
|
+
* live, because an override is layered over the parent's file key by key rather than replacing
|
|
205
|
+
* it. Naming files after the identifier alone would keep only one of them.
|
|
206
|
+
*
|
|
207
|
+
* A workspace directory therefore holds two files at most per identifier: `en.yaml` for its own
|
|
208
|
+
* and `en.inherited.yaml` for everything it inherits, which `mergeInheritedLocales` collapses
|
|
209
|
+
* into one file. An identifier cannot contain a dot, so the name parses back.
|
|
210
|
+
*/
|
|
211
|
+
static slugFor(locale: string, resolvingTeamSlug?: string): string;
|
|
212
|
+
get contentHash(): string;
|
|
213
|
+
}
|
|
190
214
|
export declare class VersionedScriptLibrary extends Resource {
|
|
191
215
|
id: string;
|
|
192
216
|
slug: string;
|
|
@@ -262,9 +286,10 @@ export declare class Team extends Resource {
|
|
|
262
286
|
errorHandlerConfig?: ErrorHandlerConfig;
|
|
263
287
|
parentTeamSlugs: string[];
|
|
264
288
|
webhookAuth?: WebhookAuth;
|
|
289
|
+
primaryLocale?: string;
|
|
265
290
|
versions?: TeamVersionFileEntry[];
|
|
266
291
|
aliases?: TeamAliasFileEntry[];
|
|
267
|
-
constructor(slug: string, parentTeamSlugs: string[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig, webhookAuth?: WebhookAuth);
|
|
292
|
+
constructor(slug: string, parentTeamSlugs: string[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig, webhookAuth?: WebhookAuth, primaryLocale?: string);
|
|
268
293
|
get contentHash(): string;
|
|
269
294
|
}
|
|
270
295
|
export interface RawErrorHandlerConfig {
|
|
@@ -277,6 +302,7 @@ export interface RawTeamSettings {
|
|
|
277
302
|
errorHandlerConfig?: RawErrorHandlerConfig | null;
|
|
278
303
|
parentTeamSlugs: string[];
|
|
279
304
|
webhookAuth?: WebhookAuth | null;
|
|
305
|
+
primaryLocale?: string | null;
|
|
280
306
|
}
|
|
281
307
|
export interface TeamUpdateInput {
|
|
282
308
|
zoneId?: string;
|
|
@@ -286,6 +312,7 @@ export interface TeamUpdateInput {
|
|
|
286
312
|
headerName?: string | null;
|
|
287
313
|
variableKey?: string | null;
|
|
288
314
|
} | null;
|
|
315
|
+
primaryLocale?: string;
|
|
289
316
|
}
|
|
290
317
|
export type ResourceMetadata = {
|
|
291
318
|
[key: string]: any;
|
|
@@ -317,6 +344,7 @@ export type ExecutionOptions = {
|
|
|
317
344
|
hookFilePath?: string;
|
|
318
345
|
hookFunction?: string;
|
|
319
346
|
silent?: boolean;
|
|
347
|
+
locale?: string;
|
|
320
348
|
};
|
|
321
349
|
export declare const ForceFlag: {
|
|
322
350
|
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
package/dist/core/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ServiceAccount = exports.Dependency = exports.DependenciesFlags = exports.TeamNameArg = exports.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.Team = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedScriptLibrary = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = exports.ResourceStatus = exports.OnConflictResolution = exports.Resource = void 0;
|
|
3
|
+
exports.ServiceAccount = exports.Dependency = exports.DependenciesFlags = exports.TeamNameArg = exports.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.Team = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedScriptLibrary = exports.Locale = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = exports.ResourceStatus = exports.OnConflictResolution = exports.Resource = void 0;
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
const crypto = require("node:crypto");
|
|
6
6
|
const canonical_hash_1 = require("../utils/canonical-hash");
|
|
@@ -103,6 +103,41 @@ class ScriptLibrary extends Resource {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
exports.ScriptLibrary = ScriptLibrary;
|
|
106
|
+
class Locale extends Resource {
|
|
107
|
+
constructor(id, locale, content, resolvingTeamSlug) {
|
|
108
|
+
super();
|
|
109
|
+
this.id = id;
|
|
110
|
+
this.slug = Locale.slugFor(locale, resolvingTeamSlug);
|
|
111
|
+
this.locale = locale;
|
|
112
|
+
this.content = content;
|
|
113
|
+
this.resolvingTeamSlug = resolvingTeamSlug;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The name a locale is tracked and stored under. Unlike every other resource, the identifier is
|
|
117
|
+
* not unique: a workspace sees its own `en` and each parent workspace's `en`, and all of them are
|
|
118
|
+
* live, because an override is layered over the parent's file key by key rather than replacing
|
|
119
|
+
* it. Naming files after the identifier alone would keep only one of them.
|
|
120
|
+
*
|
|
121
|
+
* A workspace directory therefore holds two files at most per identifier: `en.yaml` for its own
|
|
122
|
+
* and `en.inherited.yaml` for everything it inherits, which `mergeInheritedLocales` collapses
|
|
123
|
+
* into one file. An identifier cannot contain a dot, so the name parses back.
|
|
124
|
+
*/
|
|
125
|
+
static slugFor(locale, resolvingTeamSlug) {
|
|
126
|
+
return resolvingTeamSlug ? `${locale}${Locale.INHERITED_SUFFIX}` : locale;
|
|
127
|
+
}
|
|
128
|
+
// Hashed over the raw YAML: the file is the source of truth and round-trips comments and
|
|
129
|
+
// ordering, so any edit — even one that parses to the same translations — is a change to push.
|
|
130
|
+
get contentHash() {
|
|
131
|
+
return crypto.createHash('md5').update(this.content).digest('hex');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.Locale = Locale;
|
|
135
|
+
// Free-form identifier rather than a validated BCP-47 tag, so a workspace can use whatever it
|
|
136
|
+
// already names its translations after. Case is significant: pt-BR and pt-br are different
|
|
137
|
+
// locales, and the platform's unique index is case-sensitive too.
|
|
138
|
+
Locale.LOCALE_REGEX = /^[A-Za-z0-9][\w-]{0,19}$/;
|
|
139
|
+
// Marks a file as owned by a parent workspace, the way `variables.inherited.env` does.
|
|
140
|
+
Locale.INHERITED_SUFFIX = '.inherited';
|
|
106
141
|
class VersionedScriptLibrary extends Resource {
|
|
107
142
|
constructor(id, tag, sourceCode, programmingLanguage) {
|
|
108
143
|
super();
|
|
@@ -166,13 +201,14 @@ class ProgrammingLanguageManifest extends Resource {
|
|
|
166
201
|
}
|
|
167
202
|
exports.ProgrammingLanguageManifest = ProgrammingLanguageManifest;
|
|
168
203
|
class Team extends Resource {
|
|
169
|
-
constructor(slug, parentTeamSlugs, zoneId, errorHandlerConfig, webhookAuth) {
|
|
204
|
+
constructor(slug, parentTeamSlugs, zoneId, errorHandlerConfig, webhookAuth, primaryLocale) {
|
|
170
205
|
super();
|
|
171
206
|
this.slug = slug;
|
|
172
207
|
this.parentTeamSlugs = parentTeamSlugs;
|
|
173
208
|
this.zoneId = zoneId;
|
|
174
209
|
this.errorHandlerConfig = errorHandlerConfig;
|
|
175
210
|
this.webhookAuth = webhookAuth;
|
|
211
|
+
this.primaryLocale = primaryLocale;
|
|
176
212
|
}
|
|
177
213
|
get contentHash() {
|
|
178
214
|
const payload = {
|
|
@@ -186,6 +222,11 @@ class Team extends Resource {
|
|
|
186
222
|
if (this.webhookAuth) {
|
|
187
223
|
payload.webhookAuth = this.webhookAuth;
|
|
188
224
|
}
|
|
225
|
+
// Same insert-only-when-set rule, for the same reason: a team.json written before locales
|
|
226
|
+
// existed must keep the hash already stored in .fcode.
|
|
227
|
+
if (this.primaryLocale) {
|
|
228
|
+
payload.primaryLocale = this.primaryLocale;
|
|
229
|
+
}
|
|
189
230
|
return (0, canonical_hash_1.canonicalJsonHash)(payload);
|
|
190
231
|
}
|
|
191
232
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Locale, ResourceMetadata } from '../core/types';
|
|
2
|
+
import ResourceRepositoryBase from './resource';
|
|
3
|
+
/**
|
|
4
|
+
* Locales live as one YAML file per locale directly under `i18n/`, named after the slug
|
|
5
|
+
* (`i18n/pt-BR.yaml`, `i18n/pt-BR.inherited.yaml`). A file, not a directory: a locale has exactly
|
|
6
|
+
* one artifact, and naming the file after the identifier is what lets `push` work without knowing
|
|
7
|
+
* any ids.
|
|
8
|
+
*
|
|
9
|
+
* A workspace keeps what it inherits next to what it owns, because overriding a locale only layers
|
|
10
|
+
* keys over the inherited file rather than replacing it — see {@link Locale.slugFor}.
|
|
11
|
+
*/
|
|
12
|
+
declare class LocaleRepository extends ResourceRepositoryBase<Locale> {
|
|
13
|
+
constructor();
|
|
14
|
+
find(slug: string): Promise<Locale>;
|
|
15
|
+
findSlugsNotAdded(): Promise<Array<string>>;
|
|
16
|
+
protected getResourcePath(locale: Locale): string;
|
|
17
|
+
protected removeFiles(locale: Locale): Promise<void>;
|
|
18
|
+
protected resourceToMetadata(locale: Locale): ResourceMetadata;
|
|
19
|
+
protected saveFiles(locale: Locale): Promise<void>;
|
|
20
|
+
metadataFromFiles(slug: string): Promise<ResourceMetadata>;
|
|
21
|
+
validSlug(slug: string): boolean;
|
|
22
|
+
slugFormatMessage(): string;
|
|
23
|
+
}
|
|
24
|
+
export default LocaleRepository;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const color_1 = require("@oclif/color");
|
|
6
|
+
const cli_1 = require("../utils/cli");
|
|
7
|
+
const errors_1 = require("../core/errors");
|
|
8
|
+
const types_1 = require("../core/types");
|
|
9
|
+
const file_utils_1 = require("../utils/file-utils");
|
|
10
|
+
const resource_1 = require("./resource");
|
|
11
|
+
const YAML_EXTENSION = '.yaml';
|
|
12
|
+
/**
|
|
13
|
+
* Locales live as one YAML file per locale directly under `i18n/`, named after the slug
|
|
14
|
+
* (`i18n/pt-BR.yaml`, `i18n/pt-BR.inherited.yaml`). A file, not a directory: a locale has exactly
|
|
15
|
+
* one artifact, and naming the file after the identifier is what lets `push` work without knowing
|
|
16
|
+
* any ids.
|
|
17
|
+
*
|
|
18
|
+
* A workspace keeps what it inherits next to what it owns, because overriding a locale only layers
|
|
19
|
+
* keys over the inherited file rather than replacing it — see {@link Locale.slugFor}.
|
|
20
|
+
*/
|
|
21
|
+
class LocaleRepository extends resource_1.default {
|
|
22
|
+
constructor() {
|
|
23
|
+
super({
|
|
24
|
+
name: 'i18n',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async find(slug) {
|
|
28
|
+
const { [this._repositoryName]: locales } = await this.workspaceConfig.getActiveTeam();
|
|
29
|
+
const localeMetadata = locales?.[slug];
|
|
30
|
+
if (!localeMetadata) {
|
|
31
|
+
throw new errors_1.LocalLocaleNotFound(new Error('Locale not found on team metadata'), slug);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(this.getResourcePath(localeMetadata)).toString();
|
|
35
|
+
return new types_1.Locale(localeMetadata.id, localeMetadata.locale ?? slug, content, localeMetadata.resolvingTeamSlug);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new errors_1.LocalLocaleNotFound(error, slug);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async findSlugsNotAdded() {
|
|
42
|
+
const slugsNotAdded = [];
|
|
43
|
+
if (!fs.pathExistsSync(this._repositoryName)) {
|
|
44
|
+
return slugsNotAdded;
|
|
45
|
+
}
|
|
46
|
+
for (const fileName of fs.readdirSync(this._repositoryName)) {
|
|
47
|
+
if (!fileName.endsWith(YAML_EXTENSION)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const slug = path.basename(fileName, YAML_EXTENSION);
|
|
51
|
+
if (await this.workspaceConfig.exists('i18n', slug)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Only `pull` writes an inherited file, and it registers it as it does. One left behind by a
|
|
55
|
+
// workspace that no longer inherits it is stale, not a locale waiting to be tracked.
|
|
56
|
+
if (slug.endsWith(types_1.Locale.INHERITED_SUFFIX)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (this.validSlug(slug)) {
|
|
60
|
+
slugsNotAdded.push(slug);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
cli_1.default.log(color_1.default.yellow(`\n⚠️ local locale file '${fileName}' doesn't have a valid name and will be ignored. ${this.slugFormatMessage()}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return slugsNotAdded;
|
|
67
|
+
}
|
|
68
|
+
getResourcePath(locale) {
|
|
69
|
+
return path.join(this._repositoryName, `${locale.slug ?? locale.locale}${YAML_EXTENSION}`);
|
|
70
|
+
}
|
|
71
|
+
async removeFiles(locale) {
|
|
72
|
+
fs.removeSync(this.getResourcePath(locale));
|
|
73
|
+
this.removeDirectoryIfEmpty(this._repositoryName);
|
|
74
|
+
}
|
|
75
|
+
resourceToMetadata(locale) {
|
|
76
|
+
return {
|
|
77
|
+
id: locale.id,
|
|
78
|
+
locale: locale.locale,
|
|
79
|
+
slug: locale.slug,
|
|
80
|
+
...(locale.resolvingTeamSlug && { resolvingTeamSlug: locale.resolvingTeamSlug }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async saveFiles(locale) {
|
|
84
|
+
// Inherited locales are written read-only, like inherited modules: they belong to a parent
|
|
85
|
+
// workspace, and editing one here would push an override rather than change the parent.
|
|
86
|
+
(0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(locale), locale.content || '', { readOnly: Boolean(locale.resolvingTeamSlug) });
|
|
87
|
+
}
|
|
88
|
+
async metadataFromFiles(slug) {
|
|
89
|
+
return {
|
|
90
|
+
locale: slug,
|
|
91
|
+
slug: slug,
|
|
92
|
+
contentHash: '',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
validSlug(slug) {
|
|
96
|
+
return types_1.Locale.LOCALE_REGEX.test(slug);
|
|
97
|
+
}
|
|
98
|
+
slugFormatMessage() {
|
|
99
|
+
return "Use up to 20 letters, numbers, '-' or '_', starting with a letter or a number.";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.default = LocaleRepository;
|
|
@@ -19,7 +19,10 @@ class ResourceRepositoryBase {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
async findAll() {
|
|
22
|
-
|
|
22
|
+
// Defaulted because a collection added after a workspace was cloned (as `i18n` was) is absent
|
|
23
|
+
// from its sync metadata until the first pull writes it — findAll must see "no resources",
|
|
24
|
+
// not crash every aggregate pull/push/status in every pre-existing workspace.
|
|
25
|
+
const { [this._repositoryName]: resources = {} } = await this.workspaceConfig.getActiveTeam();
|
|
23
26
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
24
27
|
const existResources = await Object.keys(resources).reduce((promise, slug) => promise.then(async (existIds) => {
|
|
25
28
|
const exists = await this.exists(slug);
|
|
@@ -29,7 +29,7 @@ class TeamRepository {
|
|
|
29
29
|
}
|
|
30
30
|
catch { }
|
|
31
31
|
const parentTeamSlugs = Array.isArray(raw.parentTeamSlugs) ? raw.parentTeamSlugs : [];
|
|
32
|
-
const team = new types_1.Team(slug, parentTeamSlugs, raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig), this.parseWebhookAuth(raw.webhookAuth));
|
|
32
|
+
const team = new types_1.Team(slug, parentTeamSlugs, raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig), this.parseWebhookAuth(raw.webhookAuth), typeof raw.primaryLocale === 'string' && raw.primaryLocale.length > 0 ? raw.primaryLocale : undefined);
|
|
33
33
|
// Parsed so a find()+save() round-trip does not drop the pull-only fields.
|
|
34
34
|
if (Array.isArray(raw.versions)) {
|
|
35
35
|
team.versions = raw.versions;
|
|
@@ -51,7 +51,7 @@ class TeamRepository {
|
|
|
51
51
|
tag: raw.errorHandlerConfig.tag ?? null,
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
return new types_1.Team(raw.slug, raw.parentTeamSlugs ?? [], raw.zoneId, errorHandlerConfig, this.parseWebhookAuth(raw.webhookAuth));
|
|
54
|
+
return new types_1.Team(raw.slug, raw.parentTeamSlugs ?? [], raw.zoneId, errorHandlerConfig, this.parseWebhookAuth(raw.webhookAuth), raw.primaryLocale ?? undefined);
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* Writes the cloud's team versions and aliases into the Team as the pull-only team.json
|
|
@@ -111,6 +111,9 @@ class TeamRepository {
|
|
|
111
111
|
if (team.webhookAuth !== undefined) {
|
|
112
112
|
file.webhookAuth = team.webhookAuth;
|
|
113
113
|
}
|
|
114
|
+
if (team.primaryLocale !== undefined) {
|
|
115
|
+
file.primaryLocale = team.primaryLocale;
|
|
116
|
+
}
|
|
114
117
|
// Pull-only global versioning fields, appended after the pushable settings.
|
|
115
118
|
if (team.versions !== undefined) {
|
|
116
119
|
file.versions = team.versions;
|
|
@@ -78,8 +78,42 @@ class Processes(dict):
|
|
|
78
78
|
print(error, file=sys.stderr)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
class I18n:
|
|
82
|
+
"""Local equivalent of the cloud's fcode.i18n.
|
|
83
|
+
|
|
84
|
+
The CLI has already layered the chosen locale over the workspace's primary one, so what
|
|
85
|
+
arrives here is a single flat map. Nothing raises: a key with no translation resolves to the
|
|
86
|
+
key itself, and a placeholder with no argument is left exactly as written.
|
|
87
|
+
|
|
88
|
+
The ``options`` argument (the version a call pins in the cloud) is accepted and ignored: a
|
|
89
|
+
workspace directory holds no published snapshots, so a pinned call resolves against the files
|
|
90
|
+
on disk -- the same per-file fallback the cloud applies to a locale never published at that tag.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, i18n_payload):
|
|
94
|
+
payload = i18n_payload or {}
|
|
95
|
+
self.__translations = payload.get("translations") or {}
|
|
96
|
+
self.locale = payload.get("locale")
|
|
97
|
+
|
|
98
|
+
def __call__(self, key, args=None, options=None):
|
|
99
|
+
template = self.__translations.get(key)
|
|
100
|
+
if template is None:
|
|
101
|
+
return key
|
|
102
|
+
if not isinstance(args, dict):
|
|
103
|
+
return template
|
|
104
|
+
|
|
105
|
+
import re
|
|
106
|
+
|
|
107
|
+
def replace(match):
|
|
108
|
+
value = args.get(match.group(1).strip())
|
|
109
|
+
return match.group(0) if value is None else str(value)
|
|
110
|
+
|
|
111
|
+
# Single pass, so a substituted value containing %{...} is never rescanned.
|
|
112
|
+
return re.sub(r"%\{([^}]*)\}", replace, template)
|
|
113
|
+
|
|
114
|
+
|
|
81
115
|
class FcodeGlobal:
|
|
82
|
-
def __init__(self, context, execution, env, datastore, storage, schedule, variables):
|
|
116
|
+
def __init__(self, context, execution, env, datastore, storage, schedule, variables, i18n=None):
|
|
83
117
|
self.context = context
|
|
84
118
|
self.execution = execution
|
|
85
119
|
self.team = AttrsInfo({"timezone": "UTC"})
|
|
@@ -89,6 +123,7 @@ class FcodeGlobal:
|
|
|
89
123
|
self.schedule = schedule
|
|
90
124
|
self.variables = variables
|
|
91
125
|
self.processes = Processes()
|
|
126
|
+
self.i18n = I18n(i18n)
|
|
92
127
|
|
|
93
128
|
def send_mail(self, to=None, subject=None, text=None, html=None):
|
|
94
129
|
# No manager locally: log the email contents instead of sending, so devs
|
|
@@ -110,6 +145,13 @@ class FcodeGlobal:
|
|
|
110
145
|
return {"messageId": "local-run", "accepted": accepted, "rejected": []}
|
|
111
146
|
|
|
112
147
|
def import_module(self, module_name, version=None):
|
|
148
|
+
"""Import a workspace module, optionally at a version.
|
|
149
|
+
|
|
150
|
+
The version is either a bare string or an options dict ({"version": "v1"}), the form
|
|
151
|
+
every other versioned helper takes — the cloud reads both, so a local run must too.
|
|
152
|
+
"""
|
|
153
|
+
if isinstance(version, dict):
|
|
154
|
+
version = version.get("version")
|
|
113
155
|
module_path = module_name
|
|
114
156
|
if version:
|
|
115
157
|
module_path = os.path.join(module_path, "versions", version)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GraphQLClient } from 'graphql-request';
|
|
2
|
-
import { Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, TeamVersion, TeamVersionAlias, Variable, VersionedProcess, VersionedScriptLibrary, ServiceAccount } from '../core/types';
|
|
2
|
+
import { Locale, Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, TeamVersion, TeamVersionAlias, Variable, VersionedProcess, VersionedScriptLibrary, ServiceAccount } from '../core/types';
|
|
3
3
|
export type FcodeGraphQLClientOptions = {
|
|
4
4
|
url: string;
|
|
5
5
|
accessToken: string;
|
|
@@ -22,6 +22,16 @@ declare class FcodeGraphQLClient {
|
|
|
22
22
|
allVersionedScriptLibraries(scriptLibraryId: string): Promise<Array<VersionedScriptLibrary>>;
|
|
23
23
|
createVersionedScriptLibrary(versionedScriptLibrary: VersionedScriptLibrary): Promise<void>;
|
|
24
24
|
allVariables(): Promise<Array<Variable>>;
|
|
25
|
+
/**
|
|
26
|
+
* Every locale the workspace resolves: its own files, plus one merged file per identifier it
|
|
27
|
+
* inherits. A locale the workspace and a parent both define is listed twice by the platform and
|
|
28
|
+
* kept twice here — an override is layered over the parent's file key by key, so the parent's
|
|
29
|
+
* keys are still what the ones it does not mention resolve to.
|
|
30
|
+
*/
|
|
31
|
+
allLocales(): Promise<Array<Locale>>;
|
|
32
|
+
pushLocale(locale: Locale): Promise<Locale>;
|
|
33
|
+
updateLocale(locale: Locale): Promise<Locale>;
|
|
34
|
+
removeLocale(locale: string | undefined): Promise<void>;
|
|
25
35
|
private toProcessInput;
|
|
26
36
|
pushProcess(process: Process): Promise<Process>;
|
|
27
37
|
removeProcess(id: String | undefined): Promise<Process>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const graphql_request_1 = require("graphql-request");
|
|
4
4
|
const types_1 = require("../core/types");
|
|
5
|
+
const locale_merger_1 = require("./locale-merger");
|
|
5
6
|
const _ = require("lodash");
|
|
6
7
|
// Shared team version selections so every query/mutation returns the same shape.
|
|
7
8
|
const TEAM_VERSION_FIELDS = `
|
|
@@ -302,6 +303,111 @@ class FcodeGraphQLClient {
|
|
|
302
303
|
}
|
|
303
304
|
return variables;
|
|
304
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Every locale the workspace resolves: its own files, plus one merged file per identifier it
|
|
308
|
+
* inherits. A locale the workspace and a parent both define is listed twice by the platform and
|
|
309
|
+
* kept twice here — an override is layered over the parent's file key by key, so the parent's
|
|
310
|
+
* keys are still what the ones it does not mention resolve to.
|
|
311
|
+
*/
|
|
312
|
+
async allLocales() {
|
|
313
|
+
const pageSize = 200;
|
|
314
|
+
const locales = [];
|
|
315
|
+
let after = null;
|
|
316
|
+
try {
|
|
317
|
+
do {
|
|
318
|
+
const { locales: { edges, pageInfo }, } = await this.client.request((0, graphql_request_1.gql) `
|
|
319
|
+
query locales($first: Int!, $after: String) {
|
|
320
|
+
locales(first: $first, after: $after) {
|
|
321
|
+
edges {
|
|
322
|
+
node {
|
|
323
|
+
id
|
|
324
|
+
locale
|
|
325
|
+
content
|
|
326
|
+
resolvingTeamSlug
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
pageInfo {
|
|
330
|
+
hasNextPage
|
|
331
|
+
endCursor
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
`, {
|
|
336
|
+
after,
|
|
337
|
+
first: pageSize,
|
|
338
|
+
});
|
|
339
|
+
for (const { node: { id, locale, content, resolvingTeamSlug }, } of edges) {
|
|
340
|
+
locales.push(new types_1.Locale(id, locale, content, this.inheritedTeam(resolvingTeamSlug)));
|
|
341
|
+
}
|
|
342
|
+
after = pageInfo?.hasNextPage ? pageInfo.endCursor : null;
|
|
343
|
+
} while (after);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
return Promise.reject(this.cleanError(error));
|
|
347
|
+
}
|
|
348
|
+
const merged = (0, locale_merger_1.mergeInheritedLocales)(locales);
|
|
349
|
+
(0, locale_merger_1.assertLocaleFilesCanCoexist)(merged);
|
|
350
|
+
return merged;
|
|
351
|
+
}
|
|
352
|
+
pushLocale(locale) {
|
|
353
|
+
return this.client
|
|
354
|
+
.request((0, graphql_request_1.gql) `
|
|
355
|
+
mutation createLocale($locale: String!, $content: String!) {
|
|
356
|
+
createLocale(locale: $locale, content: $content) {
|
|
357
|
+
id
|
|
358
|
+
locale
|
|
359
|
+
content
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
`, {
|
|
363
|
+
locale: locale.locale,
|
|
364
|
+
content: locale.content,
|
|
365
|
+
})
|
|
366
|
+
.then(({ createLocale: created }) => new types_1.Locale(created.id, created.locale, created.content))
|
|
367
|
+
.catch((error) => {
|
|
368
|
+
return Promise.reject(this.cleanError(error));
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
updateLocale(locale) {
|
|
372
|
+
return this.client
|
|
373
|
+
.request((0, graphql_request_1.gql) `
|
|
374
|
+
mutation updateLocale($locale: String!, $content: String!) {
|
|
375
|
+
updateLocale(locale: $locale, content: $content) {
|
|
376
|
+
id
|
|
377
|
+
locale
|
|
378
|
+
content
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
`, {
|
|
382
|
+
locale: locale.locale,
|
|
383
|
+
content: locale.content,
|
|
384
|
+
})
|
|
385
|
+
.then(({ updateLocale: updated }) => new types_1.Locale(updated.id, updated.locale, updated.content))
|
|
386
|
+
.catch((error) => {
|
|
387
|
+
return Promise.reject(this.cleanError(error));
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
// Addressed by identifier rather than id: a locale is its file, and the delete path is reached
|
|
391
|
+
// from a file that is gone, whose recorded id may predate an override.
|
|
392
|
+
removeLocale(locale) {
|
|
393
|
+
if (!locale) {
|
|
394
|
+
return Promise.reject(new Error('locale is required during remove'));
|
|
395
|
+
}
|
|
396
|
+
return this.client
|
|
397
|
+
.request((0, graphql_request_1.gql) `
|
|
398
|
+
mutation deleteLocale($locale: String!) {
|
|
399
|
+
deleteLocale(locale: $locale) {
|
|
400
|
+
id
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
`, {
|
|
404
|
+
locale,
|
|
405
|
+
})
|
|
406
|
+
.then(() => undefined)
|
|
407
|
+
.catch((error) => {
|
|
408
|
+
return Promise.reject(this.cleanError(error));
|
|
409
|
+
});
|
|
410
|
+
}
|
|
305
411
|
toProcessInput(process, includeCreateOnly) {
|
|
306
412
|
const settings = {
|
|
307
413
|
dependencies: { scopedToProcess: process.settings.dependencies.scopedToProcess },
|
|
@@ -639,6 +745,9 @@ class FcodeGraphQLClient {
|
|
|
639
745
|
settings.webhookAuth.headerName = webhookAuth.headerName;
|
|
640
746
|
}
|
|
641
747
|
}
|
|
748
|
+
if (team.settings?.primaryLocale) {
|
|
749
|
+
settings.primaryLocale = team.settings.primaryLocale;
|
|
750
|
+
}
|
|
642
751
|
return settings;
|
|
643
752
|
}
|
|
644
753
|
mapTeamVersion(teamVersion) {
|
|
@@ -768,6 +877,7 @@ class FcodeGraphQLClient {
|
|
|
768
877
|
headerName
|
|
769
878
|
variableKey
|
|
770
879
|
}
|
|
880
|
+
primaryLocale
|
|
771
881
|
}
|
|
772
882
|
}
|
|
773
883
|
}
|
|
@@ -794,6 +904,7 @@ class FcodeGraphQLClient {
|
|
|
794
904
|
headerName
|
|
795
905
|
variableKey
|
|
796
906
|
}
|
|
907
|
+
primaryLocale
|
|
797
908
|
}
|
|
798
909
|
}
|
|
799
910
|
}
|