@discord/intl-loader-core 0.11.2-rc.3 → 0.11.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discord/intl-loader-core",
3
- "version": "0.11.2-rc.3",
3
+ "version": "0.11.3",
4
4
  "license": "MIT",
5
5
  "description": "Core utilities for writing loaders and transformers using @discord/intl",
6
6
  "author": "Jon Egeland",
@@ -28,8 +28,7 @@
28
28
  "dependencies": {
29
29
  "chokidar": "^3.6.0",
30
30
  "debug": "^4.3.6",
31
- "fast-glob": "^3.3.2",
32
- "@discord/intl-message-database": "0.11.2-rc.3"
31
+ "@discord/intl-message-database": "0.11.3"
33
32
  },
34
33
  "devDependencies": {
35
34
  "@types/debug": "^4.1.12",
package/src/processing.js CHANGED
@@ -54,6 +54,18 @@ function buildTranslationsLocaleMap(sourcePath, sourceFile, translationsPath) {
54
54
  function findAllMessagesFiles(directories, defaultLocale = 'en-US') {
55
55
  return database.findAllMessagesFiles(directories, defaultLocale);
56
56
  }
57
+ /**
58
+ * Given an arbitrary list of `files`, keep only those that can be treated as messages files, either
59
+ * definitions or translations. The returned list is a set of descriptors that can be processed by
60
+ * `processAllMessagesFiles`.
61
+ *
62
+ * @param {string[]} files
63
+ * @param {string} defaultLocale
64
+ * @returns {import('../types').IntlMessagesFileDescriptor[]}
65
+ */
66
+ function filterAllMessagesFiles(files, defaultLocale = 'en-US') {
67
+ return database.filterAllMessagesFiles(files, defaultLocale);
68
+ }
57
69
 
58
70
  /**
59
71
  * Iterate the given `files`, processing each one's content into the database. Processing is done
@@ -217,6 +229,7 @@ function generateTypeDefinitions(sourcePath, outputFile, allowNullability = fals
217
229
 
218
230
  module.exports = {
219
231
  findAllMessagesFiles,
232
+ filterAllMessagesFiles,
220
233
  processAllMessagesFiles,
221
234
  generateTypeDefinitions,
222
235
  precompileFileForLocale,
package/src/watcher.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const path = require('node:path');
2
2
 
3
3
  const chokidar = require('chokidar');
4
- const fg = require('fast-glob');
5
4
  const debug = require('debug')('intl:loader-core:watcher');
6
5
  const {
7
6
  isMessageDefinitionsFile,
@@ -9,93 +8,154 @@ const {
9
8
  } = require('@discord/intl-message-database');
10
9
 
11
10
  const { database } = require('./database');
12
- const { processDefinitionsFile, precompileFileForLocale } = require('./processing');
11
+ const {
12
+ processDefinitionsFile,
13
+ precompileFileForLocale,
14
+ generateTypeDefinitions,
15
+ findAllMessagesFiles,
16
+ filterAllMessagesFiles,
17
+ processAllMessagesFiles,
18
+ } = require('./processing');
13
19
 
14
20
  const ALWAYS_IGNORE_PATTERNS = [
15
21
  // Ignore our own compiled message files, even though they shouldn't have a matching extension.
16
22
  '*.compiled.messages.*',
23
+ '*.d.ts',
24
+ '*.d.ts.map',
17
25
  ];
18
26
  // TODO: This should come from the database extension? Or Utilities? Unsure, but the extensions
19
27
  // should have some centralized location in general
20
- const MESSAGE_DEFINITION_FILE_PATTERNS = ['**/*.messages.js'];
28
+ const MESSAGES_FILE_PATTERNS = ['**/*.messages.*'];
21
29
  const DEFAULT_LOCALE = 'en-US';
22
30
 
23
31
  /**
24
- * @param {string} filePath
25
- * @param {string} assetExtension
26
- * @param {import('./processing').IntlPrecompileOptions} [options]
32
+ * Start a new filesystem watcher to discover and monitor messages files, allowing consumers to
33
+ * react to changes in a single, consistent way.
34
+ *
35
+ * @param {string[]} watchedFolders List of folders to watch recursively.
36
+ * @param {{
37
+ * ignore?: string[],
38
+ * skipInitial?: boolean,
39
+ * persistent?: boolean,
40
+ * once?: boolean,
41
+ * }} options
42
+ * @returns {Promise<{
43
+ * on(event: string, callback: (event: string, path: string) => void): void
44
+ * }>}
27
45
  */
28
- function processFile(filePath, assetExtension, options = {}) {
29
- const { format = IntlCompiledMessageFormat.KeylessJson, bundleSecrets = false } = options;
30
- debug(`Processing file: ${filePath}`);
31
- if (!isMessageDefinitionsFile(filePath)) {
32
- debug(`${filePath} is not a definitions file. Skipping processing`);
33
- return;
46
+ async function watchMessagesFiles(
47
+ watchedFolders,
48
+ { ignore = [], skipInitial = false, once = true } = {},
49
+ ) {
50
+ debug('Pre-initializing database with all discoverable messages files');
51
+ const files = findAllMessagesFiles(watchedFolders);
52
+ processAllMessagesFiles(files);
53
+ if (once) {
54
+ debug('Performing one-shot watch because `once` was true');
55
+ // This is a weird hack around file watching just not working well on
56
+ // containers/CI. `add` events won't fire consistently from chokidar when
57
+ // it's not set to be persistent. So for a `once` run, we can just manually
58
+ // invoke the event for every file that was discovered on the initial scan.
59
+ return {
60
+ on(_, callback) {
61
+ for (const file of files) {
62
+ callback('add', file.filePath);
63
+ }
64
+ },
65
+ };
34
66
  }
35
67
 
36
- try {
37
- // Convert the file name from `.messages.js` to `.compiled.messages.jsona` for output.
38
- const outputPath = filePath.replace(/\.messages\.js$/, `.compiled.messages.${assetExtension}`);
39
- const result = processDefinitionsFile(filePath);
40
- precompileFileForLocale(filePath, result.locale, undefined, {
41
- format,
42
- bundleSecrets,
43
- });
68
+ const ignoredPatterns = ignore.concat(ALWAYS_IGNORE_PATTERNS);
69
+ const globs = watchedFolders.flatMap((folder) =>
70
+ MESSAGES_FILE_PATTERNS.map((pattern) => path.join(folder, pattern)),
71
+ );
44
72
 
45
- database.processDefinitionsFile(filePath);
46
- database.precompile(filePath, DEFAULT_LOCALE, outputPath, {
47
- format: IntlCompiledMessageFormat.KeylessJson,
48
- bundleSecrets,
73
+ debug(`Initializing watch for patterns:\n- ${globs.join('\n- ')}`);
74
+ const watcher = chokidar.watch(globs, {
75
+ ignored: ignoredPatterns,
76
+ ignoreInitial: skipInitial,
77
+ persistent: !once,
78
+ });
79
+ watcher.on('all', (event, path) => {
80
+ processAllMessagesFiles(filterAllMessagesFiles([path]));
81
+ debug(`watcher got event: ${event}, for path ${path}`);
82
+ });
83
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGQUIT']) {
84
+ process.on(signal, async () => {
85
+ debug(`Stopping watch due to`, signal);
86
+ await watcher.close();
87
+ debug('Watch stopped');
49
88
  });
50
- debug(`Wrote definitions to: ${outputPath}`);
51
- } catch (e) {
52
- debug('[INTL Error] Failed to compile messages');
53
- console.error(e);
54
89
  }
90
+
91
+ return watcher;
92
+ }
93
+
94
+ /**
95
+ * Watch the file system for any changes to message files and regenerate their types.
96
+ *
97
+ * @param {chokidar.FSWatcher} watcher
98
+ * @param {{
99
+ * allowNullability?: boolean
100
+ * }} options
101
+ */
102
+ async function generateMessageTypes(watcher, { allowNullability }) {
103
+ watcher.on('all', (_, filePath) => {
104
+ if (!isMessageDefinitionsFile(filePath)) return;
105
+ debug(`Regenerating types for ${filePath}`);
106
+ generateTypeDefinitions(filePath, undefined, allowNullability);
107
+ });
55
108
  }
56
109
 
57
110
  /**
58
- * @param {string[]} watchedFolders
111
+ * Watch the file system for changes to message _definitions_ files, and pre-process them into
112
+ * compiled assets (with the extension `.compiled.messages.${assetExtension}`).
113
+ *
114
+ * @param {chokidar.FSWatcher} watcher
59
115
  * @param {{
60
- * watch?: boolean,
61
116
  * ignore?: string[],
62
117
  * assetExtension?: string
63
118
  * precompileOptions?: import('./processing').IntlPrecompileOptions,
64
119
  * }} options
65
120
  */
66
- async function compileIntlMessageFiles(
67
- watchedFolders,
68
- { watch = true, ignore = [], assetExtension = 'json', precompileOptions = {} } = {},
121
+ async function precompileMessageDefinitionsFiles(
122
+ watcher,
123
+ { assetExtension = 'json', precompileOptions = {} } = {},
69
124
  ) {
70
- const ignoredPatterns = ignore.concat(ALWAYS_IGNORE_PATTERNS);
71
- const globs = watchedFolders.flatMap((folder) =>
72
- MESSAGE_DEFINITION_FILE_PATTERNS.map((pattern) => path.join(folder, pattern)),
73
- );
74
- debug(`Configured message file patterns:\n- ${globs.join('\n- ')}`);
125
+ watcher.on('all', (_, filePath) => {
126
+ const { format = IntlCompiledMessageFormat.KeylessJson, bundleSecrets = false } =
127
+ precompileOptions;
128
+ if (!isMessageDefinitionsFile(filePath)) return;
75
129
 
76
- // Perform one initial scan and compilation to ensure all files exist before Metro might try to
77
- // resolve them.
78
- debug('Scanning for initial messages files');
79
- for await (const filePath of fg.stream(globs, {
80
- ignore: ignoredPatterns,
81
- absolute: true,
82
- onlyFiles: true,
83
- })) {
84
- processFile(filePath.toString(), assetExtension, precompileOptions);
85
- }
86
- debug('Initial message scan completed.');
130
+ debug(`Processing file: ${filePath}`);
131
+ try {
132
+ // Convert the file name from `.messages.js` to `.compiled.messages.jsona` for output.
133
+ const outputPath = filePath.replace(
134
+ /\.messages\.js$/,
135
+ `.compiled.messages.${assetExtension}`,
136
+ );
137
+ const result = processDefinitionsFile(filePath);
138
+ precompileFileForLocale(filePath, result.locale, undefined, {
139
+ format,
140
+ bundleSecrets,
141
+ });
87
142
 
88
- if (watch) {
89
- debug(`Setting up file watching for configured paths`);
90
- chokidar
91
- .watch(globs, { ignored: ignoredPatterns, ignoreInitial: true })
92
- .on('all', (event, filePath) => {
93
- debug(`Got event ${event} for ${filePath}`);
94
- processFile(filePath, assetExtension, precompileOptions);
143
+ database.processDefinitionsFile(filePath);
144
+ database.precompile(filePath, DEFAULT_LOCALE, outputPath, {
145
+ format: IntlCompiledMessageFormat.KeylessJson,
146
+ bundleSecrets,
95
147
  });
96
- } else {
97
- debug('Not watching files because `watch` option was false');
98
- }
148
+ debug(`Wrote definitions to: ${outputPath}`);
149
+ } catch (e) {
150
+ debug('[INTL Error] Failed to compile messages');
151
+ console.error(e);
152
+ }
153
+ });
99
154
  }
100
155
 
101
- module.exports = { compileIntlMessageFiles, ALWAYS_IGNORE_PATTERNS };
156
+ module.exports = {
157
+ watchMessagesFiles,
158
+ precompileMessageDefinitionsFiles,
159
+ generateMessageTypes,
160
+ ALWAYS_IGNORE_PATTERNS,
161
+ };
@@ -11,6 +11,16 @@ export type IntlPrecompileOptions = {
11
11
  * @returns {import('../types').IntlMessagesFileDescriptor[]}
12
12
  */
13
13
  export function findAllMessagesFiles(directories: string[], defaultLocale?: string): import("../types").IntlMessagesFileDescriptor[];
14
+ /**
15
+ * Given an arbitrary list of `files`, keep only those that can be treated as messages files, either
16
+ * definitions or translations. The returned list is a set of descriptors that can be processed by
17
+ * `processAllMessagesFiles`.
18
+ *
19
+ * @param {string[]} files
20
+ * @param {string} defaultLocale
21
+ * @returns {import('../types').IntlMessagesFileDescriptor[]}
22
+ */
23
+ export function filterAllMessagesFiles(files: string[], defaultLocale?: string): import("../types").IntlMessagesFileDescriptor[];
14
24
  /**
15
25
  * Iterate the given `files`, processing each one's content into the database. Processing is done
16
26
  * in parallel using native acceleration, and returns the list of all file names that were used.
@@ -1,16 +1,52 @@
1
1
  /**
2
- * @param {string[]} watchedFolders
2
+ * Start a new filesystem watcher to discover and monitor messages files, allowing consumers to
3
+ * react to changes in a single, consistent way.
4
+ *
5
+ * @param {string[]} watchedFolders List of folders to watch recursively.
6
+ * @param {{
7
+ * ignore?: string[],
8
+ * skipInitial?: boolean,
9
+ * persistent?: boolean,
10
+ * once?: boolean,
11
+ * }} options
12
+ * @returns {Promise<{
13
+ * on(event: string, callback: (event: string, path: string) => void): void
14
+ * }>}
15
+ */
16
+ export function watchMessagesFiles(watchedFolders: string[], { ignore, skipInitial, once }?: {
17
+ ignore?: string[];
18
+ skipInitial?: boolean;
19
+ persistent?: boolean;
20
+ once?: boolean;
21
+ }): Promise<{
22
+ on(event: string, callback: (event: string, path: string) => void): void;
23
+ }>;
24
+ /**
25
+ * Watch the file system for changes to message _definitions_ files, and pre-process them into
26
+ * compiled assets (with the extension `.compiled.messages.${assetExtension}`).
27
+ *
28
+ * @param {chokidar.FSWatcher} watcher
3
29
  * @param {{
4
- * watch?: boolean,
5
30
  * ignore?: string[],
6
31
  * assetExtension?: string
7
32
  * precompileOptions?: import('./processing').IntlPrecompileOptions,
8
33
  * }} options
9
34
  */
10
- export function compileIntlMessageFiles(watchedFolders: string[], { watch, ignore, assetExtension, precompileOptions }?: {
11
- watch?: boolean;
35
+ export function precompileMessageDefinitionsFiles(watcher: chokidar.FSWatcher, { assetExtension, precompileOptions }?: {
12
36
  ignore?: string[];
13
37
  assetExtension?: string;
14
38
  precompileOptions?: import("./processing").IntlPrecompileOptions;
15
39
  }): Promise<void>;
40
+ /**
41
+ * Watch the file system for any changes to message files and regenerate their types.
42
+ *
43
+ * @param {chokidar.FSWatcher} watcher
44
+ * @param {{
45
+ * allowNullability?: boolean
46
+ * }} options
47
+ */
48
+ export function generateMessageTypes(watcher: chokidar.FSWatcher, { allowNullability }: {
49
+ allowNullability?: boolean;
50
+ }): Promise<void>;
16
51
  export const ALWAYS_IGNORE_PATTERNS: string[];
52
+ import chokidar = require("chokidar");