@factorialco/fcode-cli 2.0.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 +248 -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/setup-debug.js +2 -2
- package/dist/commands/status.js +4 -2
- package/dist/commands/team/aliases/delete.d.ts +9 -0
- package/dist/commands/team/aliases/delete.js +28 -0
- package/dist/commands/team/aliases/list.d.ts +6 -0
- package/dist/commands/team/aliases/list.js +23 -0
- package/dist/commands/team/aliases/set.d.ts +10 -0
- package/dist/commands/team/aliases/set.js +25 -0
- package/dist/commands/team/pull.js +8 -1
- package/dist/commands/team/push.js +6 -0
- package/dist/commands/team/versions/create.d.ts +13 -0
- package/dist/commands/team/versions/create.js +42 -0
- package/dist/commands/team/versions/delete.d.ts +12 -0
- package/dist/commands/team/versions/delete.js +32 -0
- package/dist/commands/team/versions/list.d.ts +6 -0
- package/dist/commands/team/versions/list.js +25 -0
- package/dist/commands/variables/status.js +11 -0
- package/dist/config/remote.js +1 -1
- package/dist/config/settings.d.ts +1 -0
- package/dist/config/settings.js +6 -0
- 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 +66 -2
- package/dist/core/types.js +45 -3
- package/dist/env.d.ts +2 -2
- package/dist/env.js +1 -1
- 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.d.ts +7 -1
- package/dist/repositories/team.js +39 -2
- package/dist/repositories/variable.d.ts +24 -0
- package/dist/repositories/variable.js +70 -22
- package/dist/runtime/executor.js +2 -2
- package/dist/runtime/executor.py +2 -2
- package/dist/runtime/libs/python/fcode/__init__.py +3 -2
- package/dist/runtime/libs/python/fcode/context.py +43 -1
- package/dist/services/fcode-graphql-client.d.ts +20 -1
- package/dist/services/fcode-graphql-client.js +276 -17
- 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 +2 -0
- package/dist/utils/gitignore.js +9 -0
- package/oclif.manifest.json +511 -127
- package/package.json +11 -2
|
@@ -2,7 +2,33 @@
|
|
|
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");
|
|
7
|
+
// Shared team version selections so every query/mutation returns the same shape.
|
|
8
|
+
const TEAM_VERSION_FIELDS = `
|
|
9
|
+
id
|
|
10
|
+
tag
|
|
11
|
+
comment
|
|
12
|
+
createdAt
|
|
13
|
+
createdBy {
|
|
14
|
+
id
|
|
15
|
+
name
|
|
16
|
+
}
|
|
17
|
+
manifest {
|
|
18
|
+
resourceType
|
|
19
|
+
resourceId
|
|
20
|
+
slug
|
|
21
|
+
outcome
|
|
22
|
+
error
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
const TEAM_VERSION_ALIAS_FIELDS = `
|
|
26
|
+
id
|
|
27
|
+
name
|
|
28
|
+
tag
|
|
29
|
+
createdAt
|
|
30
|
+
updatedAt
|
|
31
|
+
`;
|
|
6
32
|
// Shared process selection so pull queries and push mutations return the same
|
|
7
33
|
// fields, keeping mapProcess (and therefore contentHash) consistent.
|
|
8
34
|
const PROCESS_FIELDS = `
|
|
@@ -236,30 +262,148 @@ class FcodeGraphQLClient {
|
|
|
236
262
|
return Promise.reject(this.cleanError(error));
|
|
237
263
|
});
|
|
238
264
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
265
|
+
// Pages through every variable: a workspace lists its own plus the ones inherited from
|
|
266
|
+
// its parent workspaces, so a single page is not enough to be sure nothing is missed.
|
|
267
|
+
async allVariables() {
|
|
268
|
+
const pageSize = 200;
|
|
269
|
+
const variables = [];
|
|
270
|
+
let after = null;
|
|
271
|
+
try {
|
|
272
|
+
do {
|
|
273
|
+
const { teamVariables: { edges, pageInfo }, } = await this.client.request((0, graphql_request_1.gql) `
|
|
274
|
+
query teamVariables($first: Int!, $after: String) {
|
|
275
|
+
teamVariables(first: $first, after: $after) {
|
|
276
|
+
edges {
|
|
277
|
+
node {
|
|
278
|
+
id
|
|
279
|
+
key
|
|
280
|
+
value
|
|
281
|
+
isSensitive
|
|
282
|
+
resolvingTeamSlug
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
pageInfo {
|
|
286
|
+
hasNextPage
|
|
287
|
+
endCursor
|
|
250
288
|
}
|
|
251
289
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
290
|
+
}
|
|
291
|
+
`, {
|
|
292
|
+
after,
|
|
293
|
+
first: pageSize,
|
|
294
|
+
});
|
|
295
|
+
for (const { node: { id, isSensitive, key, value, resolvingTeamSlug }, } of edges) {
|
|
296
|
+
variables.push(new types_1.Variable(id, key, value, isSensitive, this.inheritedTeam(resolvingTeamSlug)));
|
|
297
|
+
}
|
|
298
|
+
after = pageInfo?.hasNextPage ? pageInfo.endCursor : null;
|
|
299
|
+
} while (after);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
return Promise.reject(this.cleanError(error));
|
|
303
|
+
}
|
|
304
|
+
return variables;
|
|
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
|
+
}
|
|
255
333
|
}
|
|
256
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
|
+
}
|
|
257
361
|
}
|
|
258
362
|
`, {
|
|
259
|
-
|
|
260
|
-
|
|
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,
|
|
261
384
|
})
|
|
262
|
-
.then(({
|
|
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)
|
|
263
407
|
.catch((error) => {
|
|
264
408
|
return Promise.reject(this.cleanError(error));
|
|
265
409
|
});
|
|
@@ -601,8 +745,121 @@ class FcodeGraphQLClient {
|
|
|
601
745
|
settings.webhookAuth.headerName = webhookAuth.headerName;
|
|
602
746
|
}
|
|
603
747
|
}
|
|
748
|
+
if (team.settings?.primaryLocale) {
|
|
749
|
+
settings.primaryLocale = team.settings.primaryLocale;
|
|
750
|
+
}
|
|
604
751
|
return settings;
|
|
605
752
|
}
|
|
753
|
+
mapTeamVersion(teamVersion) {
|
|
754
|
+
return {
|
|
755
|
+
id: teamVersion.id,
|
|
756
|
+
tag: teamVersion.tag,
|
|
757
|
+
comment: teamVersion.comment ?? null,
|
|
758
|
+
manifest: (teamVersion.manifest ?? []).map((entry) => ({
|
|
759
|
+
resourceType: entry.resourceType,
|
|
760
|
+
resourceId: entry.resourceId,
|
|
761
|
+
slug: entry.slug,
|
|
762
|
+
outcome: entry.outcome,
|
|
763
|
+
error: entry.error ?? null,
|
|
764
|
+
})),
|
|
765
|
+
createdAt: teamVersion.createdAt,
|
|
766
|
+
createdBy: teamVersion.createdBy ?? null,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
mapTeamVersionAlias(alias) {
|
|
770
|
+
return {
|
|
771
|
+
id: alias.id,
|
|
772
|
+
name: alias.name,
|
|
773
|
+
tag: alias.tag,
|
|
774
|
+
createdAt: alias.createdAt,
|
|
775
|
+
updatedAt: alias.updatedAt ?? null,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
allTeamVersions() {
|
|
779
|
+
return this.client
|
|
780
|
+
.request((0, graphql_request_1.gql) `
|
|
781
|
+
query allTeamVersions {
|
|
782
|
+
allTeamVersions {
|
|
783
|
+
${TEAM_VERSION_FIELDS}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
`)
|
|
787
|
+
.then(({ allTeamVersions }) => allTeamVersions.map((teamVersion) => this.mapTeamVersion(teamVersion)))
|
|
788
|
+
.catch((error) => {
|
|
789
|
+
return Promise.reject(this.cleanError(error));
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
createTeamVersion(tag, comment) {
|
|
793
|
+
return this.client
|
|
794
|
+
.request((0, graphql_request_1.gql) `
|
|
795
|
+
mutation createTeamVersion($tag: String!, $comment: String) {
|
|
796
|
+
createTeamVersion(tag: $tag, comment: $comment) {
|
|
797
|
+
${TEAM_VERSION_FIELDS}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
`, { tag, comment: comment ?? null })
|
|
801
|
+
.then(({ createTeamVersion }) => this.mapTeamVersion(createTeamVersion))
|
|
802
|
+
.catch((error) => {
|
|
803
|
+
return Promise.reject(this.cleanError(error));
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
deleteTeamVersion(tag) {
|
|
807
|
+
return this.client
|
|
808
|
+
.request((0, graphql_request_1.gql) `
|
|
809
|
+
mutation deleteTeamVersion($tag: String!) {
|
|
810
|
+
deleteTeamVersion(tag: $tag) {
|
|
811
|
+
${TEAM_VERSION_FIELDS}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
`, { tag })
|
|
815
|
+
.then(({ deleteTeamVersion }) => this.mapTeamVersion(deleteTeamVersion))
|
|
816
|
+
.catch((error) => {
|
|
817
|
+
return Promise.reject(this.cleanError(error));
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
allTeamVersionAliases() {
|
|
821
|
+
return this.client
|
|
822
|
+
.request((0, graphql_request_1.gql) `
|
|
823
|
+
query allTeamVersionAliases {
|
|
824
|
+
allTeamVersionAliases {
|
|
825
|
+
${TEAM_VERSION_ALIAS_FIELDS}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
`)
|
|
829
|
+
.then(({ allTeamVersionAliases }) => allTeamVersionAliases.map((alias) => this.mapTeamVersionAlias(alias)))
|
|
830
|
+
.catch((error) => {
|
|
831
|
+
return Promise.reject(this.cleanError(error));
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
/** The API upserts: an existing alias with the same name is re-pointed to the tag. */
|
|
835
|
+
createTeamVersionAlias(name, tag) {
|
|
836
|
+
return this.client
|
|
837
|
+
.request((0, graphql_request_1.gql) `
|
|
838
|
+
mutation createTeamVersionAlias($input: TeamVersionAliasInput!) {
|
|
839
|
+
createTeamVersionAlias(input: $input) {
|
|
840
|
+
${TEAM_VERSION_ALIAS_FIELDS}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
`, { input: { name, tag } })
|
|
844
|
+
.then(({ createTeamVersionAlias }) => this.mapTeamVersionAlias(createTeamVersionAlias))
|
|
845
|
+
.catch((error) => {
|
|
846
|
+
return Promise.reject(this.cleanError(error));
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
deleteTeamVersionAlias(id) {
|
|
850
|
+
return this.client
|
|
851
|
+
.request((0, graphql_request_1.gql) `
|
|
852
|
+
mutation deleteTeamVersionAlias($id: ID!) {
|
|
853
|
+
deleteTeamVersionAlias(id: $id) {
|
|
854
|
+
${TEAM_VERSION_ALIAS_FIELDS}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
`, { id })
|
|
858
|
+
.then(({ deleteTeamVersionAlias }) => this.mapTeamVersionAlias(deleteTeamVersionAlias))
|
|
859
|
+
.catch((error) => {
|
|
860
|
+
return Promise.reject(this.cleanError(error));
|
|
861
|
+
});
|
|
862
|
+
}
|
|
606
863
|
team() {
|
|
607
864
|
return this.client
|
|
608
865
|
.request((0, graphql_request_1.gql) `
|
|
@@ -620,6 +877,7 @@ class FcodeGraphQLClient {
|
|
|
620
877
|
headerName
|
|
621
878
|
variableKey
|
|
622
879
|
}
|
|
880
|
+
primaryLocale
|
|
623
881
|
}
|
|
624
882
|
}
|
|
625
883
|
}
|
|
@@ -646,6 +904,7 @@ class FcodeGraphQLClient {
|
|
|
646
904
|
headerName
|
|
647
905
|
variableKey
|
|
648
906
|
}
|
|
907
|
+
primaryLocale
|
|
649
908
|
}
|
|
650
909
|
}
|
|
651
910
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Locale } from '../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Collapses the locale files a workspace inherits into one file per identifier.
|
|
4
|
+
*
|
|
5
|
+
* The cloud lists a locale once per workspace that defines it, and every one of them is live: an
|
|
6
|
+
* override is layered over its parent key by key, so a locale several workspaces define is several
|
|
7
|
+
* files. A workspace directory keeps one inherited file per identifier instead — one file cannot
|
|
8
|
+
* hold two names — so the parents' files are merged here, in the order the cloud resolves them.
|
|
9
|
+
*
|
|
10
|
+
* A single parent's file is kept verbatim, comments and nesting included. Only a locale that
|
|
11
|
+
* several parents define is rewritten, as a flat mapping of dotted keys, which is the shape the
|
|
12
|
+
* lookup uses anyway and the one shape where a nested and a dotted key cannot disagree.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeInheritedLocales(locales: Array<Locale>): Array<Locale>;
|
|
15
|
+
/**
|
|
16
|
+
* Locale identifiers are case-sensitive on the platform (pt-BR and pt-br are different locales),
|
|
17
|
+
* but the filesystems macOS and Windows usually run on are not — both identifiers would collapse
|
|
18
|
+
* into one file, so a pull would write one locale over the other, `status` would report a phantom
|
|
19
|
+
* change forever, and the next push would silently overwrite one locale's cloud content with the
|
|
20
|
+
* other's. Refused outright before anything is written; on a case-sensitive filesystem the files
|
|
21
|
+
* live side by side and nothing is refused.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertLocaleFilesCanCoexist(locales: Array<Locale>, filesystemKeepsCase?: boolean): void;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeInheritedLocales = mergeInheritedLocales;
|
|
4
|
+
exports.assertLocaleFilesCanCoexist = assertLocaleFilesCanCoexist;
|
|
5
|
+
const fs = require("fs-extra");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const yaml_1 = require("yaml");
|
|
9
|
+
const color_1 = require("@oclif/color");
|
|
10
|
+
const cli_1 = require("../utils/cli");
|
|
11
|
+
const types_1 = require("../core/types");
|
|
12
|
+
const locale_resolver_1 = require("./locale-resolver");
|
|
13
|
+
/**
|
|
14
|
+
* Collapses the locale files a workspace inherits into one file per identifier.
|
|
15
|
+
*
|
|
16
|
+
* The cloud lists a locale once per workspace that defines it, and every one of them is live: an
|
|
17
|
+
* override is layered over its parent key by key, so a locale several workspaces define is several
|
|
18
|
+
* files. A workspace directory keeps one inherited file per identifier instead — one file cannot
|
|
19
|
+
* hold two names — so the parents' files are merged here, in the order the cloud resolves them.
|
|
20
|
+
*
|
|
21
|
+
* A single parent's file is kept verbatim, comments and nesting included. Only a locale that
|
|
22
|
+
* several parents define is rewritten, as a flat mapping of dotted keys, which is the shape the
|
|
23
|
+
* lookup uses anyway and the one shape where a nested and a dotted key cannot disagree.
|
|
24
|
+
*/
|
|
25
|
+
function mergeInheritedLocales(locales) {
|
|
26
|
+
const inheritedByIdentifier = new Map();
|
|
27
|
+
for (const locale of locales.filter((locale) => locale.resolvingTeamSlug)) {
|
|
28
|
+
inheritedByIdentifier.set(locale.locale, [...(inheritedByIdentifier.get(locale.locale) ?? []), locale]);
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
...locales.filter((locale) => !locale.resolvingTeamSlug),
|
|
32
|
+
...[...inheritedByIdentifier.values()].map((files) => (files.length === 1 ? files[0] : mergeLocaleFiles(files))),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The platform walks parents sorted by their slug — `Team.parentTeamIdsInResolutionOrder` — and
|
|
37
|
+
* the first defining a key wins. Deriving that order here, rather than trusting the order the API
|
|
38
|
+
* happened to list the rows in (which follows the listing's `orderBy`, `updatedAt` by default),
|
|
39
|
+
* is what keeps a local run and the cloud resolving the same text.
|
|
40
|
+
*/
|
|
41
|
+
function inResolutionOrder(files) {
|
|
42
|
+
return [...files].sort((a, b) => {
|
|
43
|
+
const [aSlug, bSlug] = [a.resolvingTeamSlug ?? '', b.resolvingTeamSlug ?? ''];
|
|
44
|
+
return aSlug < bSlug ? -1 : (aSlug > bSlug ? 1 : 0);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @param files the same locale as every parent workspace defines it, in any order
|
|
49
|
+
* @returns one locale carrying their merged translations, attributed to the winning parent
|
|
50
|
+
*/
|
|
51
|
+
function mergeLocaleFiles(files) {
|
|
52
|
+
const ordered = inResolutionOrder(files);
|
|
53
|
+
const [winner] = ordered;
|
|
54
|
+
const translations = {};
|
|
55
|
+
// Lowest precedence first, so the first parent in resolution order is the text that survives —
|
|
56
|
+
// the same layering the platform applies.
|
|
57
|
+
for (const file of [...ordered].reverse()) {
|
|
58
|
+
Object.assign(translations, translationsOf(file));
|
|
59
|
+
}
|
|
60
|
+
return new types_1.Locale(winner.id, winner.locale, `${mergeHeader(ordered)}${(0, yaml_1.stringify)(translations)}`, winner.resolvingTeamSlug);
|
|
61
|
+
}
|
|
62
|
+
function translationsOf(file) {
|
|
63
|
+
try {
|
|
64
|
+
return (0, locale_resolver_1.flattenTranslations)((0, yaml_1.parse)(file.content));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// Skipped rather than fatal: a workspace cannot fix a parent's file, and the cloud ignores an
|
|
68
|
+
// unparseable one too.
|
|
69
|
+
cli_1.default.log(color_1.default.yellow(`\n⚠️ locale '${file.locale}' from workspace '${file.resolvingTeamSlug}' could not be parsed and will be ignored: ${error.message}`));
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Locale identifiers are case-sensitive on the platform (pt-BR and pt-br are different locales),
|
|
75
|
+
* but the filesystems macOS and Windows usually run on are not — both identifiers would collapse
|
|
76
|
+
* into one file, so a pull would write one locale over the other, `status` would report a phantom
|
|
77
|
+
* change forever, and the next push would silently overwrite one locale's cloud content with the
|
|
78
|
+
* other's. Refused outright before anything is written; on a case-sensitive filesystem the files
|
|
79
|
+
* live side by side and nothing is refused.
|
|
80
|
+
*/
|
|
81
|
+
function assertLocaleFilesCanCoexist(locales, filesystemKeepsCase = filesystemKeepsCaseApart()) {
|
|
82
|
+
const slugsByFoldedSlug = new Map();
|
|
83
|
+
for (const locale of locales) {
|
|
84
|
+
const slug = locale.slug ?? locale.locale;
|
|
85
|
+
const folded = slug.toLowerCase();
|
|
86
|
+
slugsByFoldedSlug.set(folded, [...(slugsByFoldedSlug.get(folded) ?? []), slug]);
|
|
87
|
+
}
|
|
88
|
+
const collisions = [...slugsByFoldedSlug.values()].filter((slugs) => slugs.length > 1);
|
|
89
|
+
if (collisions.length === 0 || filesystemKeepsCase) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const described = collisions.map((slugs) => `'${slugs.join("' / '")}'`).join(', ');
|
|
93
|
+
throw new Error(`locales differing only in case cannot both be stored on this filesystem: ${described}. ` +
|
|
94
|
+
'Rename or remove one of them in the cloud and pull again.');
|
|
95
|
+
}
|
|
96
|
+
function filesystemKeepsCaseApart() {
|
|
97
|
+
const probe = fs.mkdtempSync(path.join(os.tmpdir(), 'fcode-case-'));
|
|
98
|
+
try {
|
|
99
|
+
fs.outputFileSync(path.join(probe, 'a'), '');
|
|
100
|
+
return !fs.existsSync(path.join(probe, 'A'));
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
fs.removeSync(probe);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function mergeHeader(files) {
|
|
107
|
+
const workspaces = files.map((file) => file.resolvingTeamSlug).join(', ');
|
|
108
|
+
return (`# Generated by fcode: the '${files[0].locale}' translations inherited from ${workspaces},\n` +
|
|
109
|
+
`# merged in the platform's resolution order (the first wins where they disagree).\n` +
|
|
110
|
+
`# Edit i18n/${files[0].locale}.yaml to override any of these keys.\n`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the translations a local run resolves `fcode.i18n` against, so `fcode run` behaves like
|
|
3
|
+
* the cloud does.
|
|
4
|
+
*
|
|
5
|
+
* The cloud does this work in the engine: it merges the workspace's locale over its parents', then
|
|
6
|
+
* layers the chosen locale over the primary one and ships a single flat map. Locally there is no
|
|
7
|
+
* engine, so the same layering happens here over the files in `i18n/` — including the inherited
|
|
8
|
+
* ones, which `pull` writes into the same directory as `<locale>.inherited.yaml`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function flattenTranslations(document: unknown, prefix?: string, target?: Record<string, string>): Record<string, string>;
|
|
11
|
+
/**
|
|
12
|
+
* @param requestedLocale the locale to run with, or undefined to use the workspace's primary
|
|
13
|
+
* @returns the locale actually used and its flat translations, with the primary locale underneath
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveLocalTranslations(requestedLocale?: string, workspaceDir?: string): {
|
|
16
|
+
locale?: string;
|
|
17
|
+
translations: Record<string, string>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.flattenTranslations = flattenTranslations;
|
|
4
|
+
exports.resolveLocalTranslations = resolveLocalTranslations;
|
|
5
|
+
const fs = require("fs-extra");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const yaml_1 = require("yaml");
|
|
8
|
+
const cli_1 = require("../utils/cli");
|
|
9
|
+
const color_1 = require("@oclif/color");
|
|
10
|
+
const types_1 = require("../core/types");
|
|
11
|
+
const I18N_DIRECTORY = 'i18n';
|
|
12
|
+
const YAML_EXTENSION = '.yaml';
|
|
13
|
+
const KEY_SEPARATOR = '.';
|
|
14
|
+
/**
|
|
15
|
+
* Builds the translations a local run resolves `fcode.i18n` against, so `fcode run` behaves like
|
|
16
|
+
* the cloud does.
|
|
17
|
+
*
|
|
18
|
+
* The cloud does this work in the engine: it merges the workspace's locale over its parents', then
|
|
19
|
+
* layers the chosen locale over the primary one and ships a single flat map. Locally there is no
|
|
20
|
+
* engine, so the same layering happens here over the files in `i18n/` — including the inherited
|
|
21
|
+
* ones, which `pull` writes into the same directory as `<locale>.inherited.yaml`.
|
|
22
|
+
*/
|
|
23
|
+
function flattenTranslations(document, prefix = '', target = {}) {
|
|
24
|
+
if (document === null || document === undefined || typeof document !== 'object' || Array.isArray(document)) {
|
|
25
|
+
return target;
|
|
26
|
+
}
|
|
27
|
+
for (const [key, value] of Object.entries(document)) {
|
|
28
|
+
const keyPath = prefix ? `${prefix}${KEY_SEPARATOR}${key}` : key;
|
|
29
|
+
if (value === null || value === undefined) {
|
|
30
|
+
// A key written without a value is a placeholder: it counts as not translated, so lookups
|
|
31
|
+
// fall through to the fallback locale instead of resolving to an empty string.
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
35
|
+
flattenTranslations(value, keyPath, target);
|
|
36
|
+
}
|
|
37
|
+
else if (!Array.isArray(value)) {
|
|
38
|
+
target[keyPath] = String(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return target;
|
|
42
|
+
}
|
|
43
|
+
function readLocaleFile(workspaceDir, slug) {
|
|
44
|
+
const filePath = path.join(workspaceDir, I18N_DIRECTORY, `${slug}${YAML_EXTENSION}`);
|
|
45
|
+
if (!fs.pathExistsSync(filePath)) {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return flattenTranslations((0, yaml_1.parse)(fs.readFileSync(filePath).toString()));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// A broken translation file must never stop a run, exactly as in the cloud.
|
|
53
|
+
cli_1.default.log(color_1.default.yellow(`\n⚠️ locale '${slug}' could not be parsed and will be ignored: ${error.message}`));
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The identifier a locale file contributes keys to, whether the workspace owns the file or not. */
|
|
58
|
+
function identifierOf(slug) {
|
|
59
|
+
return slug.endsWith(types_1.Locale.INHERITED_SUFFIX) ? slug.slice(0, -types_1.Locale.INHERITED_SUFFIX.length) : slug;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One locale as this workspace resolves it: what it inherits, with its own file layered on top key
|
|
63
|
+
* by key, so a key it does not translate itself still resolves to the parent's text. `pull` has
|
|
64
|
+
* already merged the parents' files into the inherited one.
|
|
65
|
+
*/
|
|
66
|
+
function readLocale(workspaceDir, locale) {
|
|
67
|
+
return {
|
|
68
|
+
...readLocaleFile(workspaceDir, `${locale}${types_1.Locale.INHERITED_SUFFIX}`),
|
|
69
|
+
...readLocaleFile(workspaceDir, locale),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function chosenPrimaryLocaleOf(workspaceDir) {
|
|
73
|
+
try {
|
|
74
|
+
const team = fs.readJsonSync(path.join(workspaceDir, 'team.json'));
|
|
75
|
+
return typeof team?.primaryLocale === 'string' && team.primaryLocale.length > 0 ? team.primaryLocale : undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Every locale file in the workspace, own and inherited: `pull` writes them all to `i18n/`. */
|
|
82
|
+
function localeFileSlugs(workspaceDir) {
|
|
83
|
+
const directory = path.join(workspaceDir, I18N_DIRECTORY);
|
|
84
|
+
if (!fs.pathExistsSync(directory)) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return fs
|
|
88
|
+
.readdirSync(directory)
|
|
89
|
+
.filter((fileName) => fileName.endsWith(YAML_EXTENSION))
|
|
90
|
+
.map((fileName) => path.basename(fileName, YAML_EXTENSION))
|
|
91
|
+
.sort();
|
|
92
|
+
}
|
|
93
|
+
/** Every locale the workspace resolves, counting a locale defined in several workspaces once. */
|
|
94
|
+
function availableLocales(workspaceDir) {
|
|
95
|
+
return [...new Set(localeFileSlugs(workspaceDir).map((slug) => identifierOf(slug)))].sort();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The workspace's primary locale: the one chosen in `team.json`, or its first locale when none is
|
|
99
|
+
* chosen — or when the chosen one has no file anymore, which would otherwise resolve zero keys.
|
|
100
|
+
* Mirrors the platform's `PrimaryLocale.resolve`, so a local run and the cloud agree on which
|
|
101
|
+
* locale a process without an explicit one resolves against.
|
|
102
|
+
*/
|
|
103
|
+
function primaryLocaleOf(workspaceDir) {
|
|
104
|
+
const available = availableLocales(workspaceDir);
|
|
105
|
+
const chosen = chosenPrimaryLocaleOf(workspaceDir);
|
|
106
|
+
return chosen && available.includes(chosen) ? chosen : available[0];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* @param requestedLocale the locale to run with, or undefined to use the workspace's primary
|
|
110
|
+
* @returns the locale actually used and its flat translations, with the primary locale underneath
|
|
111
|
+
*/
|
|
112
|
+
function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd()) {
|
|
113
|
+
const primary = primaryLocaleOf(workspaceDir);
|
|
114
|
+
const locale = requestedLocale || primary;
|
|
115
|
+
if (!locale) {
|
|
116
|
+
return { locale: undefined, translations: {} };
|
|
117
|
+
}
|
|
118
|
+
const fallback = primary && primary !== locale ? readLocale(workspaceDir, primary) : {};
|
|
119
|
+
return { locale, translations: { ...fallback, ...readLocale(workspaceDir, locale) } };
|
|
120
|
+
}
|
|
@@ -34,7 +34,7 @@ function mergeVariables(base, ...overrides) {
|
|
|
34
34
|
return [...map.values()];
|
|
35
35
|
}
|
|
36
36
|
async function runProcess(options) {
|
|
37
|
-
const { slug, version, logLevel = 'DEBUG', request, jsonLogs = false, suppressElapsedLog = false } = options;
|
|
37
|
+
const { slug, version, logLevel = 'DEBUG', request, jsonLogs = false, suppressElapsedLog = false, locale } = options;
|
|
38
38
|
const processRepository = new process_1.default();
|
|
39
39
|
const variableRepository = new variable_1.default();
|
|
40
40
|
const fcodeProcess = await processRepository.find(slug, version);
|
|
@@ -50,7 +50,7 @@ async function runProcess(options) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
const ExecutionTask = fcodeProcess.script.programmingLanguage === 'PYTHON' ? python_execution_task_1.default : javascript_execution_task_1.default;
|
|
53
|
-
const executor = new ExecutionTask(fcodeProcess.script.programmingLanguage, fcodeProcess, { version, parameters, variables, logLevel, request, jsonLogs });
|
|
53
|
+
const executor = new ExecutionTask(fcodeProcess.script.programmingLanguage, fcodeProcess, { version, parameters, variables, logLevel, request, jsonLogs, locale });
|
|
54
54
|
const start = Date.now();
|
|
55
55
|
try {
|
|
56
56
|
await executor.prepare();
|
|
@@ -13,5 +13,7 @@ export declare function renderGitignoreWithInherited(existing: string, inherited
|
|
|
13
13
|
* Refreshes the workspace .gitignore so inherited processes/modules (owned by a
|
|
14
14
|
* parent team and pulled read-only) are not tracked by git. Regenerated from the
|
|
15
15
|
* current workspace metadata, so resources that stop being inherited drop out.
|
|
16
|
+
* Inherited variable values live in one file, which is ignored as soon as the
|
|
17
|
+
* workspace inherits any variable; inherited locales are one file each, like modules.
|
|
16
18
|
*/
|
|
17
19
|
export declare function syncInheritedGitignore(workspaceDir?: string): Promise<void>;
|
package/dist/utils/gitignore.js
CHANGED
|
@@ -6,6 +6,8 @@ exports.syncInheritedGitignore = syncInheritedGitignore;
|
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
const fs = require("fs-extra");
|
|
8
8
|
const workspace_1 = require("../config/workspace");
|
|
9
|
+
const global_1 = require("../config/global");
|
|
10
|
+
const settings_1 = require("../config/settings");
|
|
9
11
|
const filenamify_1 = require("./filenamify");
|
|
10
12
|
const file_utils_1 = require("./file-utils");
|
|
11
13
|
exports.INHERITED_BLOCK_START = '# BEGIN fcode inherited resources (auto-generated, do not edit)';
|
|
@@ -43,9 +45,12 @@ function stripManagedBlock(content) {
|
|
|
43
45
|
* Refreshes the workspace .gitignore so inherited processes/modules (owned by a
|
|
44
46
|
* parent team and pulled read-only) are not tracked by git. Regenerated from the
|
|
45
47
|
* current workspace metadata, so resources that stop being inherited drop out.
|
|
48
|
+
* Inherited variable values live in one file, which is ignored as soon as the
|
|
49
|
+
* workspace inherits any variable; inherited locales are one file each, like modules.
|
|
46
50
|
*/
|
|
47
51
|
async function syncInheritedGitignore(workspaceDir = process.cwd()) {
|
|
48
52
|
const team = await new workspace_1.default(workspaceDir).getActiveTeam();
|
|
53
|
+
const inheritedVariablesFileName = await new global_1.default().getSetting(settings_1.SettingKeys.INHERITED_VARIABLES_FILE_NAME);
|
|
49
54
|
const inheritedPaths = [
|
|
50
55
|
...Object.values(team.processes)
|
|
51
56
|
.filter((process) => process.resolvingTeamSlug)
|
|
@@ -53,6 +58,10 @@ async function syncInheritedGitignore(workspaceDir = process.cwd()) {
|
|
|
53
58
|
...Object.values(team.modules)
|
|
54
59
|
.filter((module) => module.resolvingTeamSlug)
|
|
55
60
|
.map((module) => `modules/${(0, filenamify_1.default)(module.name)}/`),
|
|
61
|
+
...(Object.values(team.variables ?? {}).some((variable) => variable.resolvingTeamSlug) ? [inheritedVariablesFileName] : []),
|
|
62
|
+
...Object.values(team.i18n ?? {})
|
|
63
|
+
.filter((locale) => locale.resolvingTeamSlug)
|
|
64
|
+
.map((locale) => `i18n/${(0, filenamify_1.default)(locale.slug)}.yaml`),
|
|
56
65
|
];
|
|
57
66
|
const gitignorePath = path.join(workspaceDir, '.gitignore');
|
|
58
67
|
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|