-
Notifications
You must be signed in to change notification settings - Fork 6
/
metadata.plugin.js
260 lines (234 loc) · 8.31 KB
/
metadata.plugin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/* eslint-disable */
const path = require('path');
const glob = require('glob');
const copyfile = require('cp-file');
const replaceInFile = require('replace-in-file');
const fs = require('fs');
const exclusions = require('./src/exclusions');
const PLUGIN_NAME = 'MetaDataPlugin';
const DEFAULT_METADATA_TEMPLATE = './meta.template.js';
const DEFAULT_LOCALES_DIR = './locales';
const DEFAULT_POSTFIX = '';
const BASE_LOCALE = 'en';
const EXCLUDE_META_KEYWORD = '// @exclude';
/**
* Replace multiple strings in file
* @param {Object} source object with interface`{ [key: string]: string }
* @param {string} filePath path to file for replacing
*/
const replaceWithMultipleSource = (source, filePath) => {
const replaceOptions = Object.entries(source)
.reduce((acc, [key, value]) => ({
from: [...acc.from, `[${key}]`],
to: [...acc.to, value || ''],
}), { from: [], to: [] });
replaceOptions.files = filePath;
replaceInFile.sync(replaceOptions);
};
/**
* Replace string with value in file
* @param {string} from source string, placeholder
* @param {*} to result string
* @param {*} filePath path to file for replacing
*/
const replace = (from, to, filePath) => {
const replaceOptions = {
from,
to,
files: filePath,
};
replaceInFile.sync(replaceOptions);
};
/**
* Grab the language from path to translations file
* @param {string} str path to translations file
* @param {string} localesDir
* @example
* ```
* // './locales/en.json' => 'en'
* // './locales/en/messages.json' => 'en'
* ```
*/
const getLangFromPathString = (str, localesDir) => {
const regexp = new RegExp(`${localesDir}|messages|json|\\.|\\/`, 'gi');
const lang = str.replace(regexp, '');
return lang;
};
/**
* Get value from translation file by key
* @param {string} messageKey the key from translation
* @param {Object} translation
*/
const getMessageValue = (messageKey, translation) => {
if (typeof translation[messageKey] === 'object') {
return translation[messageKey].message;
}
return translation[messageKey];
};
/**
* Building value string from locales
* @param {Object} translation translation object
* @param {Object} fieldOptions object with options for metadata field
* @param {string} localesDir
* @param {string} postfix
* @example
* ```
* // example output: `// description:ru Пример\n// description:en Example\n`
* ```
*/
const getField = (translation, fieldOptions, localesDir, postfix) => {
const json = require(translation);
const { metaName, messageKey, usePostfix } = fieldOptions;
const value = getMessageValue(messageKey, json);
if (!value) {
return '';
}
// define lang for field
const lang = getLangFromPathString(translation, localesDir);
const langTxt = lang === BASE_LOCALE ? '' : `:${lang}`;
const post = usePostfix ? `${postfix}` : '';
// concat current field with already calculated fields
return [lang, `@${metaName}${langTxt} ${value} ${post}`];
};
/**
* Appends exclusions list to metadata
* @param {string} outputPath
*/
const addExclusionsToMetadata = (outputPath) => {
const exclusionsTemplate = exclusions.map((exclusion) =>
`${EXCLUDE_META_KEYWORD} ${exclusion}`).join('\n');
const replaceOptions = {
from: EXCLUDE_META_KEYWORD,
to: exclusionsTemplate,
files: outputPath,
};
replaceInFile.sync(replaceOptions);
}
/**
* Generate metadata file
* @param {string} outputPath
* @param {Function} callback function which should be executed in the end of the plugin's work
* @param {Object} options passed to plugin constructor options
*/
const createMetadata = (outputPath, callback, options) => {
const {
filename,
metadataTemplate = DEFAULT_METADATA_TEMPLATE,
localesDir = DEFAULT_LOCALES_DIR,
postfix = DEFAULT_POSTFIX,
fields = {},
} = options;
// Calculate result metadata file path
const metadataOutputPath = path.join(outputPath, filename);
// Copy template file to output directory
copyfile.sync(metadataTemplate, metadataOutputPath);
addExclusionsToMetadata(metadataOutputPath);
// Separate fields which have multiple translations and simple fields
const multipleFields = {};
const singleFields = {};
Object.entries(fields).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null) {
multipleFields[key] = value;
} else {
singleFields[key] = value;
}
});
// Replace simple fields
replaceWithMultipleSource(singleFields, metadataOutputPath);
// Get all locales paths
const translations = glob.sync(`${localesDir}/**/messages.json`);
// replace fields with multiple values from translations
Object.entries(multipleFields).forEach(([key, fieldOptions]) => {
const fieldsString = translations
.map(translation => getField(translation, fieldOptions, localesDir, postfix))
.filter(str => !!str)
.sort(([lang]) => lang === BASE_LOCALE ? -1 : 1)
.map(([lang, field], i, ar) => {
if (i !== 0) {
field = `// ${field}`;
}
if (i !== ar.length - 1) {
field += '\n';
}
return field;
})
.join('');
// replace needed key with result string
replace(`[${key}]`, fieldsString, metadataOutputPath);
});
callback();
};
/**
* Concats result metadata file with output code
* @param {string} outputPath
* @param {string} userscriptName
* @param {Object} options passed to plugin constructor options
* @param {Function} callback
*/
const concatMetaWithOutput = (outputPath, userscriptName, options, callback) => {
const { filename } = options;
const metadataOutputPath = path.join(outputPath, filename);
const chunkOutputPath = path.resolve(outputPath, userscriptName);
const meta = fs.readFileSync(metadataOutputPath).toString();
const chunk = fs.readFileSync(chunkOutputPath).toString();
fs.writeFileSync(path.resolve(chunkOutputPath), `${meta}${chunk}`);
callback();
};
/**
* MetaDataPlugin
* Creates a copy of metadata template file and replaces placeholders to values
*/
class MetaDataPlugin {
/**
* @param {Object} options
* @property {string} postfix string which will be added to field (need to set `usePostfix` for specifing the fields with postfix)
* @property {string} metadataTemplate Default './meta.template.js' path to template file with meta.
* @property {string} localesDir Default './locales'. path to locales directory.
* @property {Object} fields object with key:value pairs where `key` means placeholder in template file,
* if value is Object, it means that this value should be taken from locales files, in that case
* following properties should be defined in this object:
* `messageKey` - message key from file with translations
* `metaName` - the name of metadata sting
* `usePostfix` - boolean, is should be postfix added for this field
*
* @example
* ```
* postfix: 'Dev',
* metadataTemplate: './some-meta.template.js'
* localesDir: './_locales',
* fields: {
* NAME: {
* messageKey: 'name',
* metaName: 'name',
* usePostfix: true
* },
* DESCRIPTION: {
* messageKey: 'description',
* metaName: 'description'
* },
* DOWNLOAD_URL: 'https://example.com',
* }
* ```
*/
constructor(options) {
if (!options.filename) {
throw new Error('Property "filename" is not defined for metadata file');
}
this.options = {
...options,
filename: `${options.filename}.meta.js`,
};
}
apply(compiler) {
compiler.hooks.emit.tapAsync(PLUGIN_NAME,
(compilation, callback) => createMetadata(compilation.outputOptions.path, callback, this.options));
compiler.hooks.done.tapAsync(PLUGIN_NAME,
({ compilation }, callback) => concatMetaWithOutput(
compilation.outputOptions.path,
this.options.filename.replace('meta.js', 'user.js'),
this.options,
callback,
));
}
}
module.exports = MetaDataPlugin;