@salesforce/core 3.31.7 → 3.31.8
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/LICENSE.txt +11 -11
- package/README.md +222 -222
- package/lib/config/aliasesConfig.d.ts +12 -12
- package/lib/config/aliasesConfig.js +27 -27
- package/lib/config/authInfoConfig.d.ts +19 -19
- package/lib/config/authInfoConfig.js +34 -34
- package/lib/config/config.d.ts +311 -311
- package/lib/config/config.js +574 -574
- package/lib/config/configAggregator.d.ts +232 -232
- package/lib/config/configAggregator.js +379 -379
- package/lib/config/configFile.d.ts +199 -199
- package/lib/config/configFile.js +340 -340
- package/lib/config/configGroup.d.ts +141 -141
- package/lib/config/configGroup.js +224 -224
- package/lib/config/configStore.d.ts +241 -241
- package/lib/config/configStore.js +352 -352
- package/lib/config/envVars.d.ts +101 -101
- package/lib/config/envVars.js +456 -456
- package/lib/config/orgUsersConfig.d.ts +31 -31
- package/lib/config/orgUsersConfig.js +41 -41
- package/lib/config/sandboxOrgConfig.d.ts +37 -37
- package/lib/config/sandboxOrgConfig.js +50 -50
- package/lib/config/sandboxProcessCache.d.ts +16 -16
- package/lib/config/sandboxProcessCache.js +37 -37
- package/lib/config/tokensConfig.d.ts +10 -10
- package/lib/config/tokensConfig.js +28 -28
- package/lib/config/ttlConfig.d.ts +34 -34
- package/lib/config/ttlConfig.js +54 -54
- package/lib/crypto/crypto.d.ts +54 -54
- package/lib/crypto/crypto.js +220 -220
- package/lib/crypto/keyChain.d.ts +8 -8
- package/lib/crypto/keyChain.js +61 -61
- package/lib/crypto/keyChainImpl.d.ts +116 -116
- package/lib/crypto/keyChainImpl.js +486 -486
- package/lib/crypto/secureBuffer.d.ts +46 -46
- package/lib/crypto/secureBuffer.js +82 -82
- package/lib/deviceOauthService.d.ts +71 -71
- package/lib/deviceOauthService.js +191 -191
- package/lib/exported.d.ts +38 -38
- package/lib/exported.js +118 -118
- package/lib/global.d.ts +70 -70
- package/lib/global.js +109 -109
- package/lib/lifecycleEvents.d.ts +93 -93
- package/lib/lifecycleEvents.js +188 -188
- package/lib/logger.d.ts +381 -381
- package/lib/logger.js +734 -734
- package/lib/messages.d.ts +291 -291
- package/lib/messages.js +543 -543
- package/lib/org/authInfo.d.ts +344 -344
- package/lib/org/authInfo.js +892 -892
- package/lib/org/authRemover.d.ts +88 -88
- package/lib/org/authRemover.js +182 -182
- package/lib/org/connection.d.ts +197 -197
- package/lib/org/connection.js +395 -395
- package/lib/org/index.d.ts +6 -6
- package/lib/org/index.js +28 -28
- package/lib/org/org.d.ts +558 -558
- package/lib/org/org.js +1267 -1267
- package/lib/org/orgConfigProperties.d.ts +69 -69
- package/lib/org/orgConfigProperties.js +136 -136
- package/lib/org/permissionSetAssignment.d.ts +35 -35
- package/lib/org/permissionSetAssignment.js +125 -125
- package/lib/org/scratchOrgCache.d.ts +20 -20
- package/lib/org/scratchOrgCache.js +32 -32
- package/lib/org/scratchOrgCreate.d.ts +54 -54
- package/lib/org/scratchOrgCreate.js +216 -216
- package/lib/org/scratchOrgErrorCodes.d.ts +10 -10
- package/lib/org/scratchOrgErrorCodes.js +88 -88
- package/lib/org/scratchOrgFeatureDeprecation.d.ts +26 -26
- package/lib/org/scratchOrgFeatureDeprecation.js +109 -109
- package/lib/org/scratchOrgInfoApi.d.ts +68 -68
- package/lib/org/scratchOrgInfoApi.js +413 -413
- package/lib/org/scratchOrgInfoGenerator.d.ts +64 -64
- package/lib/org/scratchOrgInfoGenerator.js +241 -241
- package/lib/org/scratchOrgLifecycleEvents.d.ts +10 -10
- package/lib/org/scratchOrgLifecycleEvents.js +40 -40
- package/lib/org/scratchOrgSettingsGenerator.d.ts +78 -78
- package/lib/org/scratchOrgSettingsGenerator.js +276 -276
- package/lib/org/scratchOrgTypes.d.ts +43 -43
- package/lib/org/scratchOrgTypes.js +8 -8
- package/lib/org/user.d.ts +187 -187
- package/lib/org/user.js +448 -448
- package/lib/schema/printer.d.ts +79 -79
- package/lib/schema/printer.js +260 -260
- package/lib/schema/validator.d.ts +70 -70
- package/lib/schema/validator.js +169 -169
- package/lib/sfError.d.ts +73 -73
- package/lib/sfError.js +136 -136
- package/lib/sfProject.d.ts +357 -357
- package/lib/sfProject.js +671 -671
- package/lib/stateAggregator/accessors/aliasAccessor.d.ts +98 -98
- package/lib/stateAggregator/accessors/aliasAccessor.js +145 -145
- package/lib/stateAggregator/accessors/orgAccessor.d.ts +101 -101
- package/lib/stateAggregator/accessors/orgAccessor.js +240 -240
- package/lib/stateAggregator/accessors/sandboxAccessor.d.ts +8 -8
- package/lib/stateAggregator/accessors/sandboxAccessor.js +27 -27
- package/lib/stateAggregator/accessors/tokenAccessor.d.ts +63 -63
- package/lib/stateAggregator/accessors/tokenAccessor.js +79 -79
- package/lib/stateAggregator/index.d.ts +4 -4
- package/lib/stateAggregator/index.js +26 -26
- package/lib/stateAggregator/stateAggregator.d.ts +25 -25
- package/lib/stateAggregator/stateAggregator.js +45 -45
- package/lib/status/myDomainResolver.d.ts +66 -66
- package/lib/status/myDomainResolver.js +124 -124
- package/lib/status/pollingClient.d.ts +85 -85
- package/lib/status/pollingClient.js +115 -115
- package/lib/status/streamingClient.d.ts +244 -244
- package/lib/status/streamingClient.js +436 -436
- package/lib/status/types.d.ts +89 -89
- package/lib/status/types.js +17 -17
- package/lib/testSetup.d.ts +553 -553
- package/lib/testSetup.js +871 -871
- package/lib/util/cache.d.ts +11 -11
- package/lib/util/cache.js +69 -69
- package/lib/util/checkLightningDomain.d.ts +1 -1
- package/lib/util/checkLightningDomain.js +28 -28
- package/lib/util/directoryWriter.d.ts +12 -12
- package/lib/util/directoryWriter.js +53 -53
- package/lib/util/getJwtAudienceUrl.d.ts +4 -4
- package/lib/util/getJwtAudienceUrl.js +18 -18
- package/lib/util/internal.d.ts +58 -58
- package/lib/util/internal.js +118 -118
- package/lib/util/jsonXmlTools.d.ts +14 -14
- package/lib/util/jsonXmlTools.js +38 -38
- package/lib/util/mapKeys.d.ts +14 -14
- package/lib/util/mapKeys.js +51 -51
- package/lib/util/sfdc.d.ts +52 -52
- package/lib/util/sfdc.js +85 -85
- package/lib/util/sfdcUrl.d.ts +72 -72
- package/lib/util/sfdcUrl.js +215 -215
- package/lib/util/structuredWriter.d.ts +9 -9
- package/lib/util/structuredWriter.js +2 -2
- package/lib/util/zipWriter.d.ts +16 -16
- package/lib/util/zipWriter.js +67 -67
- package/lib/webOAuthServer.d.ts +156 -156
- package/lib/webOAuthServer.js +388 -388
- package/messages/auth.md +37 -37
- package/messages/config.md +156 -156
- package/messages/connection.md +30 -30
- package/messages/core.json +20 -20
- package/messages/core.md +67 -67
- package/messages/encryption.md +85 -85
- package/messages/envVars.md +303 -303
- package/messages/org.md +63 -63
- package/messages/permissionSetAssignment.md +31 -31
- package/messages/scratchOrgCreate.md +23 -23
- package/messages/scratchOrgErrorCodes.md +115 -115
- package/messages/scratchOrgFeatureDeprecation.md +11 -11
- package/messages/scratchOrgInfoApi.md +15 -15
- package/messages/scratchOrgInfoGenerator.md +23 -23
- package/messages/streaming.md +23 -23
- package/messages/user.md +35 -35
- package/package.json +97 -97
package/lib/messages.js
CHANGED
|
@@ -1,544 +1,544 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* Copyright (c) 2020, salesforce.com, inc.
|
|
4
|
-
* All rights reserved.
|
|
5
|
-
* Licensed under the BSD 3-Clause license.
|
|
6
|
-
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
7
|
-
*/
|
|
8
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.Messages = void 0;
|
|
10
|
-
const fs = require("fs");
|
|
11
|
-
const os = require("os");
|
|
12
|
-
const path = require("path");
|
|
13
|
-
const util = require("util");
|
|
14
|
-
const ts_types_1 = require("@salesforce/ts-types");
|
|
15
|
-
const kit_1 = require("@salesforce/kit");
|
|
16
|
-
const sfError_1 = require("./sfError");
|
|
17
|
-
class Key {
|
|
18
|
-
constructor(packageName, bundleName) {
|
|
19
|
-
this.packageName = packageName;
|
|
20
|
-
this.bundleName = bundleName;
|
|
21
|
-
}
|
|
22
|
-
toString() {
|
|
23
|
-
return `${this.packageName}:${this.bundleName}`;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const REGEXP_NO_CONTENT = /^\s*$/g;
|
|
27
|
-
const REGEXP_NO_CONTENT_SECTION = /^#\s*/gm;
|
|
28
|
-
const REGEXP_MD_IS_LIST_ROW = /^[*-]\s+|^ {2}/;
|
|
29
|
-
const REGEXP_MD_LIST_ITEM = /^[*-]\s+/gm;
|
|
30
|
-
const markdownLoader = (filePath, fileContents) => {
|
|
31
|
-
const map = new Map();
|
|
32
|
-
const hasContent = (lineItem) => !REGEXP_NO_CONTENT.exec(lineItem);
|
|
33
|
-
// Filter out sections that don't have content
|
|
34
|
-
const sections = fileContents.split(REGEXP_NO_CONTENT_SECTION).filter(hasContent);
|
|
35
|
-
for (const section of sections) {
|
|
36
|
-
const lines = section.split('\n');
|
|
37
|
-
const firstLine = lines.shift();
|
|
38
|
-
const rest = lines.join('\n').trim();
|
|
39
|
-
if (firstLine && rest.length > 0) {
|
|
40
|
-
const key = firstLine.trim();
|
|
41
|
-
const nonEmptyLines = lines.filter((line) => !!line.trim());
|
|
42
|
-
// If every entry in the value is a list item, then treat this as a list. Indented lines are part of the list.
|
|
43
|
-
if (nonEmptyLines.every((line) => REGEXP_MD_IS_LIST_ROW.exec(line))) {
|
|
44
|
-
const listItems = rest.split(REGEXP_MD_LIST_ITEM).filter(hasContent);
|
|
45
|
-
const values = listItems.map((item) => item
|
|
46
|
-
.split('\n')
|
|
47
|
-
// new lines are ignored in markdown lists
|
|
48
|
-
.filter((line) => !!line.trim())
|
|
49
|
-
// trim off the indentation
|
|
50
|
-
.map((line) => line.trim())
|
|
51
|
-
// put it back together
|
|
52
|
-
.join('\n'));
|
|
53
|
-
map.set(key, values);
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
map.set(key, rest);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
// use error instead of SfError because messages.js should have no internal dependencies.
|
|
61
|
-
throw new Error(`Invalid markdown message file: ${filePath}\nThe line "# <key>" must be immediately followed by the message on a new line.`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return map;
|
|
65
|
-
};
|
|
66
|
-
const jsAndJsonLoader = (filePath, fileContents) => {
|
|
67
|
-
let json;
|
|
68
|
-
try {
|
|
69
|
-
json = JSON.parse(fileContents);
|
|
70
|
-
if (!(0, ts_types_1.isObject)(json)) {
|
|
71
|
-
// Bubble up
|
|
72
|
-
throw new Error(`Unexpected token. Found returned content type '${typeof json}'.`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch (err) {
|
|
76
|
-
// Provide a nicer error message for a common JSON parse error; Unexpected token
|
|
77
|
-
const error = err;
|
|
78
|
-
if (error.message.startsWith('Unexpected token')) {
|
|
79
|
-
const parseError = new Error(`Invalid JSON content in message file: ${filePath}\n${error.message}`);
|
|
80
|
-
parseError.name = error.name;
|
|
81
|
-
throw parseError;
|
|
82
|
-
}
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
return new Map(Object.entries(json));
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* The core message framework manages messages and allows them to be accessible by
|
|
89
|
-
* all plugins and consumers of sfdx-core. It is set up to handle localization down
|
|
90
|
-
* the road at no additional effort to the consumer. Messages can be used for
|
|
91
|
-
* anything from user output (like the console), to error messages, to returned
|
|
92
|
-
* data from a method.
|
|
93
|
-
*
|
|
94
|
-
* Messages are loaded from loader functions. The loader functions will only run
|
|
95
|
-
* when a message is required. This prevents all messages from being loaded into memory at
|
|
96
|
-
* application startup. The functions can load from memory, a file, or a server.
|
|
97
|
-
*
|
|
98
|
-
* In the beginning of your app or file, add the loader functions to be used later. If using
|
|
99
|
-
* json or js files in a root messages directory (`<moduleRoot>/messages`), load the entire directory
|
|
100
|
-
* automatically with {@link Messages.importMessagesDirectory}. Message files must be the following formates.
|
|
101
|
-
*
|
|
102
|
-
* A `.json` file:
|
|
103
|
-
* ```json
|
|
104
|
-
* {
|
|
105
|
-
* "msgKey": "A message displayed in the user",
|
|
106
|
-
* "msgGroup": {
|
|
107
|
-
* "anotherMsgKey": "Another message displayed to the user"
|
|
108
|
-
* },
|
|
109
|
-
* "listOfMessage": ["message1", "message2"]
|
|
110
|
-
* }
|
|
111
|
-
* ```
|
|
112
|
-
*
|
|
113
|
-
* A `.js` file:
|
|
114
|
-
* ```javascript
|
|
115
|
-
* module.exports = {
|
|
116
|
-
* msgKey: 'A message displayed in the user',
|
|
117
|
-
* msgGroup: {
|
|
118
|
-
* anotherMsgKey: 'Another message displayed to the user'
|
|
119
|
-
* },
|
|
120
|
-
* listOfMessage: ['message1', 'message2']
|
|
121
|
-
* }
|
|
122
|
-
* ```
|
|
123
|
-
*
|
|
124
|
-
* A `.md` file:
|
|
125
|
-
* ```markdown
|
|
126
|
-
* # msgKey
|
|
127
|
-
* A message displayed in the user
|
|
128
|
-
*
|
|
129
|
-
* # msgGroup.anotherMsgKey
|
|
130
|
-
* Another message displayed to the user
|
|
131
|
-
*
|
|
132
|
-
* # listOfMessage
|
|
133
|
-
* - message1
|
|
134
|
-
* - message2
|
|
135
|
-
* ```
|
|
136
|
-
*
|
|
137
|
-
* The values support [util.format](https://nodejs.org/api/util.html#util_util_format_format_args) style strings
|
|
138
|
-
* that apply the tokens passed into {@link Message.getMessage}
|
|
139
|
-
*
|
|
140
|
-
* **Note:** When running unit tests individually, you may see errors that the messages aren't found.
|
|
141
|
-
* This is because `index.js` isn't loaded when tests run like they are when the package is required.
|
|
142
|
-
* To allow tests to run, import the message directory in each test (it will only
|
|
143
|
-
* do it once) or load the message file the test depends on individually.
|
|
144
|
-
*
|
|
145
|
-
* ```typescript
|
|
146
|
-
* // Create loader functions for all files in the messages directory
|
|
147
|
-
* Messages.importMessagesDirectory(__dirname);
|
|
148
|
-
*
|
|
149
|
-
* // Now you can use the messages from anywhere in your code or file.
|
|
150
|
-
* // If using importMessageDirectory, the bundle name is the file name.
|
|
151
|
-
* const messages: Messages = Messages.load(packageName, bundleName);
|
|
152
|
-
*
|
|
153
|
-
* // Messages now contains all the message in the bundleName file.
|
|
154
|
-
* messages.getMessage('authInfoCreationError');
|
|
155
|
-
* ```
|
|
156
|
-
*/
|
|
157
|
-
class Messages {
|
|
158
|
-
/**
|
|
159
|
-
* Create a new messages bundle.
|
|
160
|
-
*
|
|
161
|
-
* **Note:** Use {Messages.load} unless you are writing your own loader function.
|
|
162
|
-
*
|
|
163
|
-
* @param bundleName The bundle name.
|
|
164
|
-
* @param locale The locale.
|
|
165
|
-
* @param messages The messages. Can not be modified once created.
|
|
166
|
-
*/
|
|
167
|
-
constructor(bundleName, locale, messages) {
|
|
168
|
-
this.messages = messages;
|
|
169
|
-
this.bundleName = bundleName;
|
|
170
|
-
this.locale = locale;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Get the locale. This will always return 'en_US' but will return the
|
|
174
|
-
* machine's locale in the future.
|
|
175
|
-
*/
|
|
176
|
-
static getLocale() {
|
|
177
|
-
return 'en_US';
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Set a custom loader function for a package and bundle that will be called on {@link Messages.load}.
|
|
181
|
-
*
|
|
182
|
-
* @param packageName The npm package name.
|
|
183
|
-
* @param bundle The name of the bundle.
|
|
184
|
-
* @param loader The loader function.
|
|
185
|
-
*/
|
|
186
|
-
static setLoaderFunction(packageName, bundle, loader) {
|
|
187
|
-
this.loaders.set(new Key(packageName, bundle).toString(), loader);
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Generate a file loading function. Use {@link Messages.importMessageFile} unless
|
|
191
|
-
* overriding the bundleName is required, then manually pass the loader
|
|
192
|
-
* function to {@link Messages.setLoaderFunction}.
|
|
193
|
-
*
|
|
194
|
-
* @param bundleName The name of the bundle.
|
|
195
|
-
* @param filePath The messages file path.
|
|
196
|
-
*/
|
|
197
|
-
static generateFileLoaderFunction(bundleName, filePath) {
|
|
198
|
-
const ext = path.extname(filePath);
|
|
199
|
-
if (!['.json', '.js', '.md'].includes(ext)) {
|
|
200
|
-
throw new Error(`Only json, js and md message files are allowed, not ${ext}: ${filePath}`);
|
|
201
|
-
}
|
|
202
|
-
return (locale) => {
|
|
203
|
-
let fileContents;
|
|
204
|
-
let parser;
|
|
205
|
-
if (ext === '.md') {
|
|
206
|
-
fileContents = fs.readFileSync(filePath, 'utf-8');
|
|
207
|
-
parser = markdownLoader;
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
// Anything can be returned by a js file, so stringify the results to ensure valid json is returned.
|
|
211
|
-
fileContents = JSON.stringify(Messages.readFile(filePath));
|
|
212
|
-
// If the file is empty, JSON.stringify will turn it into "" which will validate on parse.
|
|
213
|
-
if (fileContents === 'null' || fileContents === '""')
|
|
214
|
-
fileContents = '';
|
|
215
|
-
parser = jsAndJsonLoader;
|
|
216
|
-
}
|
|
217
|
-
if (!fileContents || fileContents.trim().length === 0) {
|
|
218
|
-
// messages.js should have no internal dependencies.
|
|
219
|
-
const error = new Error(`Invalid message file: ${filePath}. No content.`);
|
|
220
|
-
error.name = 'SfError';
|
|
221
|
-
throw error;
|
|
222
|
-
}
|
|
223
|
-
const map = parser(filePath, fileContents);
|
|
224
|
-
return new Messages(bundleName, locale, map);
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Add a single message file to the list of loading functions using the file name as the bundle name.
|
|
229
|
-
* The loader will only be added if the bundle name is not already taken.
|
|
230
|
-
*
|
|
231
|
-
* @param packageName The npm package name.
|
|
232
|
-
* @param filePath The path of the file.
|
|
233
|
-
*/
|
|
234
|
-
static importMessageFile(packageName, filePath) {
|
|
235
|
-
const bundleName = path.basename(filePath, path.extname(filePath));
|
|
236
|
-
if (!Messages.isCached(packageName, bundleName)) {
|
|
237
|
-
this.setLoaderFunction(packageName, bundleName, Messages.generateFileLoaderFunction(bundleName, filePath));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Import all json and js files in a messages directory. Use the file name as the bundle key when
|
|
242
|
-
* {@link Messages.load} is called. By default, we're assuming the moduleDirectoryPart is a
|
|
243
|
-
* typescript project and will truncate to root path (where the package.json file is). If your messages
|
|
244
|
-
* directory is in another spot or you are not using typescript, pass in false for truncateToProjectPath.
|
|
245
|
-
*
|
|
246
|
-
* ```
|
|
247
|
-
* // e.g. If your message directory is in the project root, you would do:
|
|
248
|
-
* Messages.importMessagesDirectory(__dirname);
|
|
249
|
-
* ```
|
|
250
|
-
*
|
|
251
|
-
* @param moduleDirectoryPath The path to load the messages folder.
|
|
252
|
-
* @param truncateToProjectPath Will look for the messages directory in the project root (where the package.json file is located).
|
|
253
|
-
* i.e., the module is typescript and the messages folder is in the top level of the module directory.
|
|
254
|
-
* @param packageName The npm package name. Figured out from the root directory's package.json.
|
|
255
|
-
*/
|
|
256
|
-
static importMessagesDirectory(moduleDirectoryPath, truncateToProjectPath = true, packageName) {
|
|
257
|
-
let moduleMessagesDirPath = moduleDirectoryPath;
|
|
258
|
-
let projectRoot = moduleDirectoryPath;
|
|
259
|
-
if (!path.isAbsolute(moduleDirectoryPath)) {
|
|
260
|
-
throw new Error('Invalid module path. Relative URLs are not allowed.');
|
|
261
|
-
}
|
|
262
|
-
while (projectRoot.length >= 0) {
|
|
263
|
-
try {
|
|
264
|
-
fs.statSync(path.join(projectRoot, 'package.json'));
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
if (err.code !== 'ENOENT')
|
|
269
|
-
throw err;
|
|
270
|
-
projectRoot = projectRoot.substring(0, projectRoot.lastIndexOf(path.sep));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
if (truncateToProjectPath) {
|
|
274
|
-
moduleMessagesDirPath = projectRoot;
|
|
275
|
-
}
|
|
276
|
-
if (!packageName) {
|
|
277
|
-
const errMessage = `Invalid or missing package.json file at '${moduleMessagesDirPath}'. If not using a package.json, pass in a packageName.`;
|
|
278
|
-
try {
|
|
279
|
-
packageName = (0, ts_types_1.asString)((0, ts_types_1.ensureJsonMap)(Messages.readFile(path.join(moduleMessagesDirPath, 'package.json'))).name);
|
|
280
|
-
if (!packageName) {
|
|
281
|
-
throw new kit_1.NamedError('MissingPackageName', errMessage);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
catch (err) {
|
|
285
|
-
throw new kit_1.NamedError('MissingPackageName', errMessage, err);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
moduleMessagesDirPath += `${path.sep}messages`;
|
|
289
|
-
for (const file of fs.readdirSync(moduleMessagesDirPath)) {
|
|
290
|
-
const filePath = path.join(moduleMessagesDirPath, file);
|
|
291
|
-
const stat = fs.statSync(filePath);
|
|
292
|
-
if (stat) {
|
|
293
|
-
if (stat.isDirectory()) {
|
|
294
|
-
// When we support other locales, load them from /messages/<local>/<bundleName>.json
|
|
295
|
-
// Change generateFileLoaderFunction to handle loading locales.
|
|
296
|
-
}
|
|
297
|
-
else if (stat.isFile()) {
|
|
298
|
-
this.importMessageFile(packageName, filePath);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Load messages for a given package and bundle. If the bundle is not already cached, use the loader function
|
|
305
|
-
* created from {@link Messages.setLoaderFunction} or {@link Messages.importMessagesDirectory}.
|
|
306
|
-
*
|
|
307
|
-
* **NOTE: Use {@link Messages.load} instead for safe message validation and usage.**
|
|
308
|
-
*
|
|
309
|
-
* ```typescript
|
|
310
|
-
* Messages.importMessagesDirectory(__dirname);
|
|
311
|
-
* const messages = Messages.load('packageName', 'bundleName');
|
|
312
|
-
* ```
|
|
313
|
-
*
|
|
314
|
-
* @param packageName The name of the npm package.
|
|
315
|
-
* @param bundleName The name of the bundle to load.
|
|
316
|
-
*/
|
|
317
|
-
static loadMessages(packageName, bundleName) {
|
|
318
|
-
const key = new Key(packageName, bundleName);
|
|
319
|
-
let messages;
|
|
320
|
-
if (this.isCached(packageName, bundleName)) {
|
|
321
|
-
messages = this.bundles.get(key.toString());
|
|
322
|
-
}
|
|
323
|
-
else if (this.loaders.has(key.toString())) {
|
|
324
|
-
const loader = this.loaders.get(key.toString());
|
|
325
|
-
if (loader) {
|
|
326
|
-
messages = loader(Messages.getLocale());
|
|
327
|
-
this.bundles.set(key.toString(), messages);
|
|
328
|
-
messages = this.bundles.get(key.toString());
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (messages) {
|
|
332
|
-
return messages;
|
|
333
|
-
}
|
|
334
|
-
// Don't use messages inside messages
|
|
335
|
-
throw new kit_1.NamedError('MissingBundleError', `Missing bundle ${key.toString()} for locale ${Messages.getLocale()}.`);
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Load messages for a given package and bundle. If the bundle is not already cached, use the loader function
|
|
339
|
-
* created from {@link Messages.setLoaderFunction} or {@link Messages.importMessagesDirectory}.
|
|
340
|
-
*
|
|
341
|
-
* The message keys that will be used must be passed in for validation. This prevents runtime errors if messages are used but not defined.
|
|
342
|
-
*
|
|
343
|
-
* **NOTE: This should be defined at the top of the file so validation is done at load time rather than runtime.**
|
|
344
|
-
*
|
|
345
|
-
* ```typescript
|
|
346
|
-
* Messages.importMessagesDirectory(__dirname);
|
|
347
|
-
* const messages = Messages.load('packageName', 'bundleName', [
|
|
348
|
-
* 'messageKey1',
|
|
349
|
-
* 'messageKey2',
|
|
350
|
-
* ]);
|
|
351
|
-
* ```
|
|
352
|
-
*
|
|
353
|
-
* @param packageName The name of the npm package.
|
|
354
|
-
* @param bundleName The name of the bundle to load.
|
|
355
|
-
* @param keys The message keys that will be used.
|
|
356
|
-
*/
|
|
357
|
-
static load(packageName, bundleName, keys) {
|
|
358
|
-
const key = new Key(packageName, bundleName);
|
|
359
|
-
let messages;
|
|
360
|
-
if (this.isCached(packageName, bundleName)) {
|
|
361
|
-
messages = this.bundles.get(key.toString());
|
|
362
|
-
}
|
|
363
|
-
else if (this.loaders.has(key.toString())) {
|
|
364
|
-
const loader = this.loaders.get(key.toString());
|
|
365
|
-
if (loader) {
|
|
366
|
-
messages = loader(Messages.getLocale());
|
|
367
|
-
this.bundles.set(key.toString(), messages);
|
|
368
|
-
messages = this.bundles.get(key.toString());
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (messages) {
|
|
372
|
-
// Type guard on key length, but do a runtime check.
|
|
373
|
-
if (!keys || keys.length === 0) {
|
|
374
|
-
throw new kit_1.NamedError('MissingKeysError', 'Can not load messages without providing the message keys that will be used.');
|
|
375
|
-
}
|
|
376
|
-
// Get all messages to validate they are actually present
|
|
377
|
-
for (const messageKey of keys) {
|
|
378
|
-
messages.getMessage(messageKey);
|
|
379
|
-
}
|
|
380
|
-
return messages;
|
|
381
|
-
}
|
|
382
|
-
// Don't use messages inside messages
|
|
383
|
-
throw new kit_1.NamedError('MissingBundleError', `Missing bundle ${key.toString()} for locale ${Messages.getLocale()}.`);
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Check if a bundle already been loaded.
|
|
387
|
-
*
|
|
388
|
-
* @param packageName The npm package name.
|
|
389
|
-
* @param bundleName The bundle name.
|
|
390
|
-
*/
|
|
391
|
-
static isCached(packageName, bundleName) {
|
|
392
|
-
return this.bundles.has(new Key(packageName, bundleName).toString());
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Get a message using a message key and use the tokens as values for tokenization.
|
|
396
|
-
*
|
|
397
|
-
* If the key happens to be an array of messages, it will combine with OS.EOL.
|
|
398
|
-
*
|
|
399
|
-
* @param key The key of the message.
|
|
400
|
-
* @param tokens The values to substitute in the message.
|
|
401
|
-
*
|
|
402
|
-
* **See** https://nodejs.org/api/util.html#util_util_format_format_args
|
|
403
|
-
*/
|
|
404
|
-
getMessage(key, tokens = []) {
|
|
405
|
-
return this.getMessageWithMap(key, tokens, this.messages).join(os.EOL);
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Get messages using a message key and use the tokens as values for tokenization.
|
|
409
|
-
*
|
|
410
|
-
* This will return all messages if the key is an array in the messages file.
|
|
411
|
-
*
|
|
412
|
-
* ```json
|
|
413
|
-
* {
|
|
414
|
-
* "myKey": [ "message1", "message2" ]
|
|
415
|
-
* }
|
|
416
|
-
* ```
|
|
417
|
-
*
|
|
418
|
-
* ```markdown
|
|
419
|
-
* # myKey
|
|
420
|
-
* * message1
|
|
421
|
-
* * message2
|
|
422
|
-
* ```
|
|
423
|
-
*
|
|
424
|
-
* @param key The key of the messages.
|
|
425
|
-
* @param tokens The values to substitute in the message.
|
|
426
|
-
*
|
|
427
|
-
* **See** https://nodejs.org/api/util.html#util_util_format_format_args
|
|
428
|
-
*/
|
|
429
|
-
getMessages(key, tokens = []) {
|
|
430
|
-
return this.getMessageWithMap(key, tokens, this.messages);
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Convenience method to create errors using message labels.
|
|
434
|
-
*
|
|
435
|
-
* `error.name` will be the upper-cased key, remove prefixed `error.` and will always end in Error.
|
|
436
|
-
* `error.actions` will be loaded using `${key}.actions` if available.
|
|
437
|
-
*
|
|
438
|
-
* @param key The key of the error message.
|
|
439
|
-
* @param tokens The error message tokens.
|
|
440
|
-
* @param actionTokens The action messages tokens.
|
|
441
|
-
* @param exitCodeOrCause The exit code which will be used by SfdxCommand or the underlying error that caused this error to be raised.
|
|
442
|
-
* @param cause The underlying error that caused this error to be raised.
|
|
443
|
-
*/
|
|
444
|
-
createError(key, tokens = [], actionTokens = [], exitCodeOrCause, cause) {
|
|
445
|
-
const structuredMessage = this.formatMessageContents('error', key, tokens, actionTokens);
|
|
446
|
-
return new sfError_1.SfError(structuredMessage.message, structuredMessage.name, structuredMessage.actions, exitCodeOrCause, cause);
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Convenience method to create warning using message labels.
|
|
450
|
-
*
|
|
451
|
-
* `warning.name` will be the upper-cased key, remove prefixed `warning.` and will always end in Warning.
|
|
452
|
-
* `warning.actions` will be loaded using `${key}.actions` if available.
|
|
453
|
-
*
|
|
454
|
-
* @param key The key of the warning message.
|
|
455
|
-
* @param tokens The warning message tokens.
|
|
456
|
-
* @param actionTokens The action messages tokens.
|
|
457
|
-
*/
|
|
458
|
-
createWarning(key, tokens = [], actionTokens = []) {
|
|
459
|
-
return this.formatMessageContents('warning', key, tokens, actionTokens);
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Convenience method to create info using message labels.
|
|
463
|
-
*
|
|
464
|
-
* `info.name` will be the upper-cased key, remove prefixed `info.` and will always end in Info.
|
|
465
|
-
* `info.actions` will be loaded using `${key}.actions` if available.
|
|
466
|
-
*
|
|
467
|
-
* @param key The key of the warning message.
|
|
468
|
-
* @param tokens The warning message tokens.
|
|
469
|
-
* @param actionTokens The action messages tokens.
|
|
470
|
-
*/
|
|
471
|
-
createInfo(key, tokens = [], actionTokens = []) {
|
|
472
|
-
return this.formatMessageContents('info', key, tokens, actionTokens);
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Formats message contents given a message type, key, tokens and actions tokens
|
|
476
|
-
*
|
|
477
|
-
* `<type>.name` will be the upper-cased key, remove prefixed `<type>.` and will always end in 'Error | Warning | Info.
|
|
478
|
-
* `<type>.actions` will be loaded using `${key}.actions` if available.
|
|
479
|
-
*
|
|
480
|
-
* @param type The type of the message set must 'error' | 'warning' | 'info'.
|
|
481
|
-
* @param key The key of the warning message.
|
|
482
|
-
* @param tokens The warning message tokens.
|
|
483
|
-
* @param actionTokens The action messages tokens.
|
|
484
|
-
*/
|
|
485
|
-
formatMessageContents(type, key, tokens = [], actionTokens = []) {
|
|
486
|
-
const label = (0, kit_1.upperFirst)(type);
|
|
487
|
-
const labelRegExp = new RegExp(`${label}$`);
|
|
488
|
-
const searchValue = type === 'error' ? /^error.*\./ : new RegExp(`^${type}.`);
|
|
489
|
-
// Convert key to name:
|
|
490
|
-
// 'myMessage' -> `MyMessageWarning`
|
|
491
|
-
// 'myMessageError' -> `MyMessageError`
|
|
492
|
-
// 'warning.myMessage' -> `MyMessageWarning`
|
|
493
|
-
const name = `${(0, kit_1.upperFirst)(key.replace(searchValue, ''))}${labelRegExp.exec(key) ? '' : label}`;
|
|
494
|
-
const message = this.getMessage(key, tokens);
|
|
495
|
-
let actions;
|
|
496
|
-
try {
|
|
497
|
-
actions = this.getMessageWithMap(`${key}.actions`, actionTokens, this.messages);
|
|
498
|
-
}
|
|
499
|
-
catch (e) {
|
|
500
|
-
/* just ignore if actions aren't found */
|
|
501
|
-
}
|
|
502
|
-
return { message, name, actions };
|
|
503
|
-
}
|
|
504
|
-
getMessageWithMap(key, tokens = [], map) {
|
|
505
|
-
// Allow nested keys for better grouping
|
|
506
|
-
const group = RegExp(/([a-zA-Z0-9_-]+)\.(.*)/).exec(key);
|
|
507
|
-
if (group) {
|
|
508
|
-
const parentKey = group[1];
|
|
509
|
-
const childKey = group[2];
|
|
510
|
-
const childObject = map.get(parentKey);
|
|
511
|
-
if (childObject && (0, ts_types_1.isJsonMap)(childObject)) {
|
|
512
|
-
const childMap = new Map(Object.entries(childObject));
|
|
513
|
-
return this.getMessageWithMap(childKey, tokens, childMap);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
if (!map.has(key)) {
|
|
517
|
-
// Don't use messages inside messages
|
|
518
|
-
throw new kit_1.NamedError('MissingMessageError', `Missing message ${this.bundleName}:${key} for locale ${Messages.getLocale()}.`);
|
|
519
|
-
}
|
|
520
|
-
const msg = map.get(key);
|
|
521
|
-
const messages = ((0, ts_types_1.isArray)(msg) ? msg : [msg]);
|
|
522
|
-
return messages.map((message) => {
|
|
523
|
-
(0, ts_types_1.ensureString)(message);
|
|
524
|
-
return util.format(message, ...tokens);
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
exports.Messages = Messages;
|
|
529
|
-
// It would be AWESOME to use Map<Key, Message> but js does an object instance comparison and doesn't let you
|
|
530
|
-
// override valueOf or equals for the === operator, which map uses. So, Use Map<String, Message>
|
|
531
|
-
// A map of loading functions to dynamically load messages when they need to be used
|
|
532
|
-
Messages.loaders = new Map();
|
|
533
|
-
// A map cache of messages bundles that have already been loaded
|
|
534
|
-
Messages.bundles = new Map();
|
|
535
|
-
/**
|
|
536
|
-
* Internal readFile. Exposed for unit testing. Do not use util/fs.readFile as messages.js
|
|
537
|
-
* should have no internal dependencies.
|
|
538
|
-
*
|
|
539
|
-
* @param filePath read file target.
|
|
540
|
-
* @ignore
|
|
541
|
-
*/
|
|
542
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
543
|
-
Messages.readFile = (filePath) => require(filePath);
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2020, salesforce.com, inc.
|
|
4
|
+
* All rights reserved.
|
|
5
|
+
* Licensed under the BSD 3-Clause license.
|
|
6
|
+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.Messages = void 0;
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const os = require("os");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const util = require("util");
|
|
14
|
+
const ts_types_1 = require("@salesforce/ts-types");
|
|
15
|
+
const kit_1 = require("@salesforce/kit");
|
|
16
|
+
const sfError_1 = require("./sfError");
|
|
17
|
+
class Key {
|
|
18
|
+
constructor(packageName, bundleName) {
|
|
19
|
+
this.packageName = packageName;
|
|
20
|
+
this.bundleName = bundleName;
|
|
21
|
+
}
|
|
22
|
+
toString() {
|
|
23
|
+
return `${this.packageName}:${this.bundleName}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const REGEXP_NO_CONTENT = /^\s*$/g;
|
|
27
|
+
const REGEXP_NO_CONTENT_SECTION = /^#\s*/gm;
|
|
28
|
+
const REGEXP_MD_IS_LIST_ROW = /^[*-]\s+|^ {2}/;
|
|
29
|
+
const REGEXP_MD_LIST_ITEM = /^[*-]\s+/gm;
|
|
30
|
+
const markdownLoader = (filePath, fileContents) => {
|
|
31
|
+
const map = new Map();
|
|
32
|
+
const hasContent = (lineItem) => !REGEXP_NO_CONTENT.exec(lineItem);
|
|
33
|
+
// Filter out sections that don't have content
|
|
34
|
+
const sections = fileContents.split(REGEXP_NO_CONTENT_SECTION).filter(hasContent);
|
|
35
|
+
for (const section of sections) {
|
|
36
|
+
const lines = section.split('\n');
|
|
37
|
+
const firstLine = lines.shift();
|
|
38
|
+
const rest = lines.join('\n').trim();
|
|
39
|
+
if (firstLine && rest.length > 0) {
|
|
40
|
+
const key = firstLine.trim();
|
|
41
|
+
const nonEmptyLines = lines.filter((line) => !!line.trim());
|
|
42
|
+
// If every entry in the value is a list item, then treat this as a list. Indented lines are part of the list.
|
|
43
|
+
if (nonEmptyLines.every((line) => REGEXP_MD_IS_LIST_ROW.exec(line))) {
|
|
44
|
+
const listItems = rest.split(REGEXP_MD_LIST_ITEM).filter(hasContent);
|
|
45
|
+
const values = listItems.map((item) => item
|
|
46
|
+
.split('\n')
|
|
47
|
+
// new lines are ignored in markdown lists
|
|
48
|
+
.filter((line) => !!line.trim())
|
|
49
|
+
// trim off the indentation
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
// put it back together
|
|
52
|
+
.join('\n'));
|
|
53
|
+
map.set(key, values);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
map.set(key, rest);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// use error instead of SfError because messages.js should have no internal dependencies.
|
|
61
|
+
throw new Error(`Invalid markdown message file: ${filePath}\nThe line "# <key>" must be immediately followed by the message on a new line.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return map;
|
|
65
|
+
};
|
|
66
|
+
const jsAndJsonLoader = (filePath, fileContents) => {
|
|
67
|
+
let json;
|
|
68
|
+
try {
|
|
69
|
+
json = JSON.parse(fileContents);
|
|
70
|
+
if (!(0, ts_types_1.isObject)(json)) {
|
|
71
|
+
// Bubble up
|
|
72
|
+
throw new Error(`Unexpected token. Found returned content type '${typeof json}'.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
// Provide a nicer error message for a common JSON parse error; Unexpected token
|
|
77
|
+
const error = err;
|
|
78
|
+
if (error.message.startsWith('Unexpected token')) {
|
|
79
|
+
const parseError = new Error(`Invalid JSON content in message file: ${filePath}\n${error.message}`);
|
|
80
|
+
parseError.name = error.name;
|
|
81
|
+
throw parseError;
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
return new Map(Object.entries(json));
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The core message framework manages messages and allows them to be accessible by
|
|
89
|
+
* all plugins and consumers of sfdx-core. It is set up to handle localization down
|
|
90
|
+
* the road at no additional effort to the consumer. Messages can be used for
|
|
91
|
+
* anything from user output (like the console), to error messages, to returned
|
|
92
|
+
* data from a method.
|
|
93
|
+
*
|
|
94
|
+
* Messages are loaded from loader functions. The loader functions will only run
|
|
95
|
+
* when a message is required. This prevents all messages from being loaded into memory at
|
|
96
|
+
* application startup. The functions can load from memory, a file, or a server.
|
|
97
|
+
*
|
|
98
|
+
* In the beginning of your app or file, add the loader functions to be used later. If using
|
|
99
|
+
* json or js files in a root messages directory (`<moduleRoot>/messages`), load the entire directory
|
|
100
|
+
* automatically with {@link Messages.importMessagesDirectory}. Message files must be the following formates.
|
|
101
|
+
*
|
|
102
|
+
* A `.json` file:
|
|
103
|
+
* ```json
|
|
104
|
+
* {
|
|
105
|
+
* "msgKey": "A message displayed in the user",
|
|
106
|
+
* "msgGroup": {
|
|
107
|
+
* "anotherMsgKey": "Another message displayed to the user"
|
|
108
|
+
* },
|
|
109
|
+
* "listOfMessage": ["message1", "message2"]
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* A `.js` file:
|
|
114
|
+
* ```javascript
|
|
115
|
+
* module.exports = {
|
|
116
|
+
* msgKey: 'A message displayed in the user',
|
|
117
|
+
* msgGroup: {
|
|
118
|
+
* anotherMsgKey: 'Another message displayed to the user'
|
|
119
|
+
* },
|
|
120
|
+
* listOfMessage: ['message1', 'message2']
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* A `.md` file:
|
|
125
|
+
* ```markdown
|
|
126
|
+
* # msgKey
|
|
127
|
+
* A message displayed in the user
|
|
128
|
+
*
|
|
129
|
+
* # msgGroup.anotherMsgKey
|
|
130
|
+
* Another message displayed to the user
|
|
131
|
+
*
|
|
132
|
+
* # listOfMessage
|
|
133
|
+
* - message1
|
|
134
|
+
* - message2
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* The values support [util.format](https://nodejs.org/api/util.html#util_util_format_format_args) style strings
|
|
138
|
+
* that apply the tokens passed into {@link Message.getMessage}
|
|
139
|
+
*
|
|
140
|
+
* **Note:** When running unit tests individually, you may see errors that the messages aren't found.
|
|
141
|
+
* This is because `index.js` isn't loaded when tests run like they are when the package is required.
|
|
142
|
+
* To allow tests to run, import the message directory in each test (it will only
|
|
143
|
+
* do it once) or load the message file the test depends on individually.
|
|
144
|
+
*
|
|
145
|
+
* ```typescript
|
|
146
|
+
* // Create loader functions for all files in the messages directory
|
|
147
|
+
* Messages.importMessagesDirectory(__dirname);
|
|
148
|
+
*
|
|
149
|
+
* // Now you can use the messages from anywhere in your code or file.
|
|
150
|
+
* // If using importMessageDirectory, the bundle name is the file name.
|
|
151
|
+
* const messages: Messages = Messages.load(packageName, bundleName);
|
|
152
|
+
*
|
|
153
|
+
* // Messages now contains all the message in the bundleName file.
|
|
154
|
+
* messages.getMessage('authInfoCreationError');
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
class Messages {
|
|
158
|
+
/**
|
|
159
|
+
* Create a new messages bundle.
|
|
160
|
+
*
|
|
161
|
+
* **Note:** Use {Messages.load} unless you are writing your own loader function.
|
|
162
|
+
*
|
|
163
|
+
* @param bundleName The bundle name.
|
|
164
|
+
* @param locale The locale.
|
|
165
|
+
* @param messages The messages. Can not be modified once created.
|
|
166
|
+
*/
|
|
167
|
+
constructor(bundleName, locale, messages) {
|
|
168
|
+
this.messages = messages;
|
|
169
|
+
this.bundleName = bundleName;
|
|
170
|
+
this.locale = locale;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get the locale. This will always return 'en_US' but will return the
|
|
174
|
+
* machine's locale in the future.
|
|
175
|
+
*/
|
|
176
|
+
static getLocale() {
|
|
177
|
+
return 'en_US';
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Set a custom loader function for a package and bundle that will be called on {@link Messages.load}.
|
|
181
|
+
*
|
|
182
|
+
* @param packageName The npm package name.
|
|
183
|
+
* @param bundle The name of the bundle.
|
|
184
|
+
* @param loader The loader function.
|
|
185
|
+
*/
|
|
186
|
+
static setLoaderFunction(packageName, bundle, loader) {
|
|
187
|
+
this.loaders.set(new Key(packageName, bundle).toString(), loader);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Generate a file loading function. Use {@link Messages.importMessageFile} unless
|
|
191
|
+
* overriding the bundleName is required, then manually pass the loader
|
|
192
|
+
* function to {@link Messages.setLoaderFunction}.
|
|
193
|
+
*
|
|
194
|
+
* @param bundleName The name of the bundle.
|
|
195
|
+
* @param filePath The messages file path.
|
|
196
|
+
*/
|
|
197
|
+
static generateFileLoaderFunction(bundleName, filePath) {
|
|
198
|
+
const ext = path.extname(filePath);
|
|
199
|
+
if (!['.json', '.js', '.md'].includes(ext)) {
|
|
200
|
+
throw new Error(`Only json, js and md message files are allowed, not ${ext}: ${filePath}`);
|
|
201
|
+
}
|
|
202
|
+
return (locale) => {
|
|
203
|
+
let fileContents;
|
|
204
|
+
let parser;
|
|
205
|
+
if (ext === '.md') {
|
|
206
|
+
fileContents = fs.readFileSync(filePath, 'utf-8');
|
|
207
|
+
parser = markdownLoader;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Anything can be returned by a js file, so stringify the results to ensure valid json is returned.
|
|
211
|
+
fileContents = JSON.stringify(Messages.readFile(filePath));
|
|
212
|
+
// If the file is empty, JSON.stringify will turn it into "" which will validate on parse.
|
|
213
|
+
if (fileContents === 'null' || fileContents === '""')
|
|
214
|
+
fileContents = '';
|
|
215
|
+
parser = jsAndJsonLoader;
|
|
216
|
+
}
|
|
217
|
+
if (!fileContents || fileContents.trim().length === 0) {
|
|
218
|
+
// messages.js should have no internal dependencies.
|
|
219
|
+
const error = new Error(`Invalid message file: ${filePath}. No content.`);
|
|
220
|
+
error.name = 'SfError';
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
const map = parser(filePath, fileContents);
|
|
224
|
+
return new Messages(bundleName, locale, map);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Add a single message file to the list of loading functions using the file name as the bundle name.
|
|
229
|
+
* The loader will only be added if the bundle name is not already taken.
|
|
230
|
+
*
|
|
231
|
+
* @param packageName The npm package name.
|
|
232
|
+
* @param filePath The path of the file.
|
|
233
|
+
*/
|
|
234
|
+
static importMessageFile(packageName, filePath) {
|
|
235
|
+
const bundleName = path.basename(filePath, path.extname(filePath));
|
|
236
|
+
if (!Messages.isCached(packageName, bundleName)) {
|
|
237
|
+
this.setLoaderFunction(packageName, bundleName, Messages.generateFileLoaderFunction(bundleName, filePath));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Import all json and js files in a messages directory. Use the file name as the bundle key when
|
|
242
|
+
* {@link Messages.load} is called. By default, we're assuming the moduleDirectoryPart is a
|
|
243
|
+
* typescript project and will truncate to root path (where the package.json file is). If your messages
|
|
244
|
+
* directory is in another spot or you are not using typescript, pass in false for truncateToProjectPath.
|
|
245
|
+
*
|
|
246
|
+
* ```
|
|
247
|
+
* // e.g. If your message directory is in the project root, you would do:
|
|
248
|
+
* Messages.importMessagesDirectory(__dirname);
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* @param moduleDirectoryPath The path to load the messages folder.
|
|
252
|
+
* @param truncateToProjectPath Will look for the messages directory in the project root (where the package.json file is located).
|
|
253
|
+
* i.e., the module is typescript and the messages folder is in the top level of the module directory.
|
|
254
|
+
* @param packageName The npm package name. Figured out from the root directory's package.json.
|
|
255
|
+
*/
|
|
256
|
+
static importMessagesDirectory(moduleDirectoryPath, truncateToProjectPath = true, packageName) {
|
|
257
|
+
let moduleMessagesDirPath = moduleDirectoryPath;
|
|
258
|
+
let projectRoot = moduleDirectoryPath;
|
|
259
|
+
if (!path.isAbsolute(moduleDirectoryPath)) {
|
|
260
|
+
throw new Error('Invalid module path. Relative URLs are not allowed.');
|
|
261
|
+
}
|
|
262
|
+
while (projectRoot.length >= 0) {
|
|
263
|
+
try {
|
|
264
|
+
fs.statSync(path.join(projectRoot, 'package.json'));
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
if (err.code !== 'ENOENT')
|
|
269
|
+
throw err;
|
|
270
|
+
projectRoot = projectRoot.substring(0, projectRoot.lastIndexOf(path.sep));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (truncateToProjectPath) {
|
|
274
|
+
moduleMessagesDirPath = projectRoot;
|
|
275
|
+
}
|
|
276
|
+
if (!packageName) {
|
|
277
|
+
const errMessage = `Invalid or missing package.json file at '${moduleMessagesDirPath}'. If not using a package.json, pass in a packageName.`;
|
|
278
|
+
try {
|
|
279
|
+
packageName = (0, ts_types_1.asString)((0, ts_types_1.ensureJsonMap)(Messages.readFile(path.join(moduleMessagesDirPath, 'package.json'))).name);
|
|
280
|
+
if (!packageName) {
|
|
281
|
+
throw new kit_1.NamedError('MissingPackageName', errMessage);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
throw new kit_1.NamedError('MissingPackageName', errMessage, err);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
moduleMessagesDirPath += `${path.sep}messages`;
|
|
289
|
+
for (const file of fs.readdirSync(moduleMessagesDirPath)) {
|
|
290
|
+
const filePath = path.join(moduleMessagesDirPath, file);
|
|
291
|
+
const stat = fs.statSync(filePath);
|
|
292
|
+
if (stat) {
|
|
293
|
+
if (stat.isDirectory()) {
|
|
294
|
+
// When we support other locales, load them from /messages/<local>/<bundleName>.json
|
|
295
|
+
// Change generateFileLoaderFunction to handle loading locales.
|
|
296
|
+
}
|
|
297
|
+
else if (stat.isFile()) {
|
|
298
|
+
this.importMessageFile(packageName, filePath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Load messages for a given package and bundle. If the bundle is not already cached, use the loader function
|
|
305
|
+
* created from {@link Messages.setLoaderFunction} or {@link Messages.importMessagesDirectory}.
|
|
306
|
+
*
|
|
307
|
+
* **NOTE: Use {@link Messages.load} instead for safe message validation and usage.**
|
|
308
|
+
*
|
|
309
|
+
* ```typescript
|
|
310
|
+
* Messages.importMessagesDirectory(__dirname);
|
|
311
|
+
* const messages = Messages.load('packageName', 'bundleName');
|
|
312
|
+
* ```
|
|
313
|
+
*
|
|
314
|
+
* @param packageName The name of the npm package.
|
|
315
|
+
* @param bundleName The name of the bundle to load.
|
|
316
|
+
*/
|
|
317
|
+
static loadMessages(packageName, bundleName) {
|
|
318
|
+
const key = new Key(packageName, bundleName);
|
|
319
|
+
let messages;
|
|
320
|
+
if (this.isCached(packageName, bundleName)) {
|
|
321
|
+
messages = this.bundles.get(key.toString());
|
|
322
|
+
}
|
|
323
|
+
else if (this.loaders.has(key.toString())) {
|
|
324
|
+
const loader = this.loaders.get(key.toString());
|
|
325
|
+
if (loader) {
|
|
326
|
+
messages = loader(Messages.getLocale());
|
|
327
|
+
this.bundles.set(key.toString(), messages);
|
|
328
|
+
messages = this.bundles.get(key.toString());
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (messages) {
|
|
332
|
+
return messages;
|
|
333
|
+
}
|
|
334
|
+
// Don't use messages inside messages
|
|
335
|
+
throw new kit_1.NamedError('MissingBundleError', `Missing bundle ${key.toString()} for locale ${Messages.getLocale()}.`);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Load messages for a given package and bundle. If the bundle is not already cached, use the loader function
|
|
339
|
+
* created from {@link Messages.setLoaderFunction} or {@link Messages.importMessagesDirectory}.
|
|
340
|
+
*
|
|
341
|
+
* The message keys that will be used must be passed in for validation. This prevents runtime errors if messages are used but not defined.
|
|
342
|
+
*
|
|
343
|
+
* **NOTE: This should be defined at the top of the file so validation is done at load time rather than runtime.**
|
|
344
|
+
*
|
|
345
|
+
* ```typescript
|
|
346
|
+
* Messages.importMessagesDirectory(__dirname);
|
|
347
|
+
* const messages = Messages.load('packageName', 'bundleName', [
|
|
348
|
+
* 'messageKey1',
|
|
349
|
+
* 'messageKey2',
|
|
350
|
+
* ]);
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* @param packageName The name of the npm package.
|
|
354
|
+
* @param bundleName The name of the bundle to load.
|
|
355
|
+
* @param keys The message keys that will be used.
|
|
356
|
+
*/
|
|
357
|
+
static load(packageName, bundleName, keys) {
|
|
358
|
+
const key = new Key(packageName, bundleName);
|
|
359
|
+
let messages;
|
|
360
|
+
if (this.isCached(packageName, bundleName)) {
|
|
361
|
+
messages = this.bundles.get(key.toString());
|
|
362
|
+
}
|
|
363
|
+
else if (this.loaders.has(key.toString())) {
|
|
364
|
+
const loader = this.loaders.get(key.toString());
|
|
365
|
+
if (loader) {
|
|
366
|
+
messages = loader(Messages.getLocale());
|
|
367
|
+
this.bundles.set(key.toString(), messages);
|
|
368
|
+
messages = this.bundles.get(key.toString());
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (messages) {
|
|
372
|
+
// Type guard on key length, but do a runtime check.
|
|
373
|
+
if (!keys || keys.length === 0) {
|
|
374
|
+
throw new kit_1.NamedError('MissingKeysError', 'Can not load messages without providing the message keys that will be used.');
|
|
375
|
+
}
|
|
376
|
+
// Get all messages to validate they are actually present
|
|
377
|
+
for (const messageKey of keys) {
|
|
378
|
+
messages.getMessage(messageKey);
|
|
379
|
+
}
|
|
380
|
+
return messages;
|
|
381
|
+
}
|
|
382
|
+
// Don't use messages inside messages
|
|
383
|
+
throw new kit_1.NamedError('MissingBundleError', `Missing bundle ${key.toString()} for locale ${Messages.getLocale()}.`);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Check if a bundle already been loaded.
|
|
387
|
+
*
|
|
388
|
+
* @param packageName The npm package name.
|
|
389
|
+
* @param bundleName The bundle name.
|
|
390
|
+
*/
|
|
391
|
+
static isCached(packageName, bundleName) {
|
|
392
|
+
return this.bundles.has(new Key(packageName, bundleName).toString());
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get a message using a message key and use the tokens as values for tokenization.
|
|
396
|
+
*
|
|
397
|
+
* If the key happens to be an array of messages, it will combine with OS.EOL.
|
|
398
|
+
*
|
|
399
|
+
* @param key The key of the message.
|
|
400
|
+
* @param tokens The values to substitute in the message.
|
|
401
|
+
*
|
|
402
|
+
* **See** https://nodejs.org/api/util.html#util_util_format_format_args
|
|
403
|
+
*/
|
|
404
|
+
getMessage(key, tokens = []) {
|
|
405
|
+
return this.getMessageWithMap(key, tokens, this.messages).join(os.EOL);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Get messages using a message key and use the tokens as values for tokenization.
|
|
409
|
+
*
|
|
410
|
+
* This will return all messages if the key is an array in the messages file.
|
|
411
|
+
*
|
|
412
|
+
* ```json
|
|
413
|
+
* {
|
|
414
|
+
* "myKey": [ "message1", "message2" ]
|
|
415
|
+
* }
|
|
416
|
+
* ```
|
|
417
|
+
*
|
|
418
|
+
* ```markdown
|
|
419
|
+
* # myKey
|
|
420
|
+
* * message1
|
|
421
|
+
* * message2
|
|
422
|
+
* ```
|
|
423
|
+
*
|
|
424
|
+
* @param key The key of the messages.
|
|
425
|
+
* @param tokens The values to substitute in the message.
|
|
426
|
+
*
|
|
427
|
+
* **See** https://nodejs.org/api/util.html#util_util_format_format_args
|
|
428
|
+
*/
|
|
429
|
+
getMessages(key, tokens = []) {
|
|
430
|
+
return this.getMessageWithMap(key, tokens, this.messages);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Convenience method to create errors using message labels.
|
|
434
|
+
*
|
|
435
|
+
* `error.name` will be the upper-cased key, remove prefixed `error.` and will always end in Error.
|
|
436
|
+
* `error.actions` will be loaded using `${key}.actions` if available.
|
|
437
|
+
*
|
|
438
|
+
* @param key The key of the error message.
|
|
439
|
+
* @param tokens The error message tokens.
|
|
440
|
+
* @param actionTokens The action messages tokens.
|
|
441
|
+
* @param exitCodeOrCause The exit code which will be used by SfdxCommand or the underlying error that caused this error to be raised.
|
|
442
|
+
* @param cause The underlying error that caused this error to be raised.
|
|
443
|
+
*/
|
|
444
|
+
createError(key, tokens = [], actionTokens = [], exitCodeOrCause, cause) {
|
|
445
|
+
const structuredMessage = this.formatMessageContents('error', key, tokens, actionTokens);
|
|
446
|
+
return new sfError_1.SfError(structuredMessage.message, structuredMessage.name, structuredMessage.actions, exitCodeOrCause, cause);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Convenience method to create warning using message labels.
|
|
450
|
+
*
|
|
451
|
+
* `warning.name` will be the upper-cased key, remove prefixed `warning.` and will always end in Warning.
|
|
452
|
+
* `warning.actions` will be loaded using `${key}.actions` if available.
|
|
453
|
+
*
|
|
454
|
+
* @param key The key of the warning message.
|
|
455
|
+
* @param tokens The warning message tokens.
|
|
456
|
+
* @param actionTokens The action messages tokens.
|
|
457
|
+
*/
|
|
458
|
+
createWarning(key, tokens = [], actionTokens = []) {
|
|
459
|
+
return this.formatMessageContents('warning', key, tokens, actionTokens);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Convenience method to create info using message labels.
|
|
463
|
+
*
|
|
464
|
+
* `info.name` will be the upper-cased key, remove prefixed `info.` and will always end in Info.
|
|
465
|
+
* `info.actions` will be loaded using `${key}.actions` if available.
|
|
466
|
+
*
|
|
467
|
+
* @param key The key of the warning message.
|
|
468
|
+
* @param tokens The warning message tokens.
|
|
469
|
+
* @param actionTokens The action messages tokens.
|
|
470
|
+
*/
|
|
471
|
+
createInfo(key, tokens = [], actionTokens = []) {
|
|
472
|
+
return this.formatMessageContents('info', key, tokens, actionTokens);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Formats message contents given a message type, key, tokens and actions tokens
|
|
476
|
+
*
|
|
477
|
+
* `<type>.name` will be the upper-cased key, remove prefixed `<type>.` and will always end in 'Error | Warning | Info.
|
|
478
|
+
* `<type>.actions` will be loaded using `${key}.actions` if available.
|
|
479
|
+
*
|
|
480
|
+
* @param type The type of the message set must 'error' | 'warning' | 'info'.
|
|
481
|
+
* @param key The key of the warning message.
|
|
482
|
+
* @param tokens The warning message tokens.
|
|
483
|
+
* @param actionTokens The action messages tokens.
|
|
484
|
+
*/
|
|
485
|
+
formatMessageContents(type, key, tokens = [], actionTokens = []) {
|
|
486
|
+
const label = (0, kit_1.upperFirst)(type);
|
|
487
|
+
const labelRegExp = new RegExp(`${label}$`);
|
|
488
|
+
const searchValue = type === 'error' ? /^error.*\./ : new RegExp(`^${type}.`);
|
|
489
|
+
// Convert key to name:
|
|
490
|
+
// 'myMessage' -> `MyMessageWarning`
|
|
491
|
+
// 'myMessageError' -> `MyMessageError`
|
|
492
|
+
// 'warning.myMessage' -> `MyMessageWarning`
|
|
493
|
+
const name = `${(0, kit_1.upperFirst)(key.replace(searchValue, ''))}${labelRegExp.exec(key) ? '' : label}`;
|
|
494
|
+
const message = this.getMessage(key, tokens);
|
|
495
|
+
let actions;
|
|
496
|
+
try {
|
|
497
|
+
actions = this.getMessageWithMap(`${key}.actions`, actionTokens, this.messages);
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
/* just ignore if actions aren't found */
|
|
501
|
+
}
|
|
502
|
+
return { message, name, actions };
|
|
503
|
+
}
|
|
504
|
+
getMessageWithMap(key, tokens = [], map) {
|
|
505
|
+
// Allow nested keys for better grouping
|
|
506
|
+
const group = RegExp(/([a-zA-Z0-9_-]+)\.(.*)/).exec(key);
|
|
507
|
+
if (group) {
|
|
508
|
+
const parentKey = group[1];
|
|
509
|
+
const childKey = group[2];
|
|
510
|
+
const childObject = map.get(parentKey);
|
|
511
|
+
if (childObject && (0, ts_types_1.isJsonMap)(childObject)) {
|
|
512
|
+
const childMap = new Map(Object.entries(childObject));
|
|
513
|
+
return this.getMessageWithMap(childKey, tokens, childMap);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (!map.has(key)) {
|
|
517
|
+
// Don't use messages inside messages
|
|
518
|
+
throw new kit_1.NamedError('MissingMessageError', `Missing message ${this.bundleName}:${key} for locale ${Messages.getLocale()}.`);
|
|
519
|
+
}
|
|
520
|
+
const msg = map.get(key);
|
|
521
|
+
const messages = ((0, ts_types_1.isArray)(msg) ? msg : [msg]);
|
|
522
|
+
return messages.map((message) => {
|
|
523
|
+
(0, ts_types_1.ensureString)(message);
|
|
524
|
+
return util.format(message, ...tokens);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
exports.Messages = Messages;
|
|
529
|
+
// It would be AWESOME to use Map<Key, Message> but js does an object instance comparison and doesn't let you
|
|
530
|
+
// override valueOf or equals for the === operator, which map uses. So, Use Map<String, Message>
|
|
531
|
+
// A map of loading functions to dynamically load messages when they need to be used
|
|
532
|
+
Messages.loaders = new Map();
|
|
533
|
+
// A map cache of messages bundles that have already been loaded
|
|
534
|
+
Messages.bundles = new Map();
|
|
535
|
+
/**
|
|
536
|
+
* Internal readFile. Exposed for unit testing. Do not use util/fs.readFile as messages.js
|
|
537
|
+
* should have no internal dependencies.
|
|
538
|
+
*
|
|
539
|
+
* @param filePath read file target.
|
|
540
|
+
* @ignore
|
|
541
|
+
*/
|
|
542
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
543
|
+
Messages.readFile = (filePath) => require(filePath);
|
|
544
544
|
//# sourceMappingURL=messages.js.map
|