@saykit/config 0.6.1 → 0.8.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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  const require_storage = require("../storage-DqhzA5H8.cjs");
3
- const require_loader = require("../loader-A1uS7qkK.cjs");
3
+ const require_loader = require("../loader-BdCeoj3-.cjs");
4
4
  let _commander_js_extra_typings = require("@commander-js/extra-typings");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as mergeExtractedMessages, i as expandBucketOutputPath, n as writeCatalogueMessages, o as pruneLocaleMessages, t as readCatalogueMessages } from "../storage-B6mn0s3V.mjs";
3
- import { t as resolveConfig } from "../loader-DLyuZzF2.mjs";
3
+ import { t as resolveConfig } from "../loader-DwF1lM3f.mjs";
4
4
  import { Command, program } from "@commander-js/extra-typings";
5
5
  import { access, glob, readFile, stat, watch } from "node:fs/promises";
6
6
  import { join, relative } from "node:path";
@@ -1,3 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_loader = require("../../loader-A1uS7qkK.cjs");
2
+ const require_loader = require("../../loader-BdCeoj3-.cjs");
3
3
  exports.resolveConfig = require_loader.resolveConfig;
4
+ exports.resolveConfigFile = require_loader.resolveConfigFile;
@@ -1,5 +1,11 @@
1
1
  import { n as Config } from "../../shapes-CRlXtss0.cjs";
2
2
  //#region src/features/loader/resolve.d.ts
3
+ /**
4
+ * The config file {@link resolveConfig} would load, for callers that need the
5
+ * path itself rather than its contents — salting a bundler's cache key with it,
6
+ * for one, since what a catalogue assembles into depends on the config.
7
+ */
8
+ declare function resolveConfigFile(name?: string): string;
3
9
  declare function resolveConfig(name?: string): Config;
4
10
  //#endregion
5
- export { resolveConfig };
11
+ export { resolveConfig, resolveConfigFile };
@@ -1,5 +1,11 @@
1
1
  import { n as Config } from "../../shapes-CRlXtss0.mjs";
2
2
  //#region src/features/loader/resolve.d.ts
3
+ /**
4
+ * The config file {@link resolveConfig} would load, for callers that need the
5
+ * path itself rather than its contents — salting a bundler's cache key with it,
6
+ * for one, since what a catalogue assembles into depends on the config.
7
+ */
8
+ declare function resolveConfigFile(name?: string): string;
3
9
  declare function resolveConfig(name?: string): Config;
4
10
  //#endregion
5
- export { resolveConfig };
11
+ export { resolveConfig, resolveConfigFile };
@@ -1,2 +1,2 @@
1
- import { t as resolveConfig } from "../../loader-DLyuZzF2.mjs";
2
- export { resolveConfig };
1
+ import { n as resolveConfigFile, t as resolveConfig } from "../../loader-DwF1lM3f.mjs";
2
+ export { resolveConfig, resolveConfigFile };
@@ -1,5 +1,33 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_hash = require("../../hash-51ce8YiG.cjs");
3
+ //#region src/features/messages/escape.ts
4
+ /**
5
+ * Quoting for text that has to survive a message format unchanged.
6
+ *
7
+ * This lives beside the converter rather than in a transform because it belongs
8
+ * to the format being written, not to the syntax being read. A literal is the
9
+ * same literal whether it came from a template literal or from JSX, and the
10
+ * moment there is a second target format its reserved characters — and the way
11
+ * it spells an escape — are its own.
12
+ */
13
+ /**
14
+ * Escape literal text so ICU MessageFormat 1 reads it as the text it is.
15
+ *
16
+ * MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
17
+ * doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
18
+ * only opens a quoted run when a character that needs quoting follows it —
19
+ * anything else and the apostrophe is itself literal. So `'#'` is a real escape
20
+ * inside a plural and the three characters `'#'` anywhere else, which is why
21
+ * `hash` has to be told rather than assumed.
22
+ *
23
+ * @param text Literal text as the author wrote it
24
+ * @param hash Whether `#` stands for a number here, i.e. whether some `plural`
25
+ * or `ordinal` encloses this text
26
+ */
27
+ function escapeIcuLiteral(text, hash = false) {
28
+ return text.replace(/'/gu, "''").replace(hash ? /[{}#]+/gu : /[{}]+/gu, (run) => `'${run}'`);
29
+ }
30
+ //#endregion
3
31
  //#region src/features/messages/types.ts
4
32
  var Base = class {
5
33
  toICUString() {
@@ -20,10 +48,12 @@ var LiteralMessage = class extends Base {
20
48
  var ArgumentMessage = class extends Base {
21
49
  identifier;
22
50
  expression;
23
- constructor(identifier, expression) {
51
+ format;
52
+ constructor(identifier, expression, format) {
24
53
  super();
25
54
  this.identifier = identifier;
26
55
  this.expression = expression;
56
+ this.format = format;
27
57
  }
28
58
  };
29
59
  var ElementMessage = class extends Base {
@@ -42,12 +72,14 @@ var ChoiceMessage = class extends Base {
42
72
  identifier;
43
73
  branches;
44
74
  expression;
45
- constructor(kind, identifier, branches, expression) {
75
+ offset;
76
+ constructor(kind, identifier, branches, expression, offset) {
46
77
  super();
47
78
  this.kind = kind;
48
79
  this.identifier = identifier;
49
80
  this.branches = branches;
50
81
  this.expression = expression;
82
+ this.offset = offset;
51
83
  }
52
84
  };
53
85
  var CompositeMessage = class extends Base {
@@ -76,8 +108,13 @@ const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
76
108
  */
77
109
  const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
78
110
  /**
79
- * The ICU case a branch is written as. A number names an exact value rather
80
- * than a key, and only `plural` and `ordinal` are allowed to select on one.
111
+ * The ICU case a branch is written as.
112
+ *
113
+ * Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
114
+ * so is distinct from the CLDR category that would otherwise match it. `select`
115
+ * has no such syntax — its cases are literal string matches, and `=0` there is
116
+ * a parse error — so a numeric key stays bare, where it matches both `0` and
117
+ * `'0'`.
81
118
  *
82
119
  * Digits are read literally rather than coerced, because everything JavaScript
83
120
  * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
@@ -85,8 +122,9 @@ const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$
85
122
  * author's own key out of the catalogue. Anything else stays a key, where the
86
123
  * whitespace or punctuation that made it numeric-looking is caught.
87
124
  */
88
- function getBranchCase(identifier) {
125
+ function getBranchCase(kind, identifier) {
89
126
  const key = String(identifier);
127
+ if (kind === "select") return key;
90
128
  return /^\d+$/u.test(key) ? `=${+key}` : key;
91
129
  }
92
130
  /**
@@ -100,12 +138,12 @@ function getBranchCase(identifier) {
100
138
  */
101
139
  function validateBranchIdentifier(kind, identifier) {
102
140
  if (typeof identifier !== "string") return;
103
- const branch = getBranchCase(identifier);
141
+ const branch = getBranchCase(kind, identifier);
104
142
  if (!BRANCH_PATTERN.test(branch)) {
105
143
  const suggestion = suggestBranchIdentifier(identifier);
106
144
  throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
107
145
  }
108
- if (kind === "select" && branch.startsWith("=")) throw new Error(`Invalid select branch key '${identifier}', a number selects an exact value, which only 'plural' and 'ordinal' accept`);
146
+ if (kind === "select" && /^=\d+$/u.test(branch)) throw new Error(`Invalid select branch key '${identifier}', an exact value is only meaningful to 'plural' and 'ordinal', write it as '${branch.slice(1)}'`);
109
147
  }
110
148
  /**
111
149
  * The nearest identifier-safe form of a key, so the error names the fix as well
@@ -125,9 +163,28 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
125
163
  while (reserved.has(identifier)) identifier = `${sequence.current++}`;
126
164
  return identifier;
127
165
  }
166
+ const numbered = {
167
+ tags: [],
168
+ values: []
169
+ };
170
+ /**
171
+ * The number this expression already has, or a fresh one. An anonymous
172
+ * placeholder is named by its position, so the same value written twice would
173
+ * otherwise arrive as two names — `{0} x {1}` for one length, which asks a
174
+ * translator to keep two holes in step and the caller to supply one value
175
+ * under two props. This is the rule explicit names already follow, where a
176
+ * repeat is allowed precisely when nothing distinguishes it.
177
+ */
178
+ function number(assigned, expression) {
179
+ const seen = assigned.find(([e]) => equivalent(e, expression));
180
+ if (seen) return seen[1];
181
+ const identifier = next();
182
+ assigned.push([expression, identifier]);
183
+ return identifier;
184
+ }
128
185
  function walk(message) {
129
186
  if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
130
- if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
187
+ if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = number(message instanceof ElementMessage ? numbered.tags : numbered.values, message.expression);
131
188
  }
132
189
  if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
133
190
  if (message instanceof ChoiceMessage) for (const branch of message.branches) {
@@ -167,30 +224,105 @@ function collectAssignedIdentifiers(message, equivalent) {
167
224
  //#endregion
168
225
  //#region src/features/messages/convert.ts
169
226
  function convertMessageToIcu(message) {
170
- function internalConvertMessageToIcu(message) {
227
+ function internalConvertMessageToIcu(message, hash) {
171
228
  switch (true) {
172
- case message instanceof LiteralMessage: return String(message.text);
173
- case message instanceof ArgumentMessage: return `{${String(message.identifier)}}`;
229
+ case message instanceof LiteralMessage: return escapeIcuLiteral(String(message.text), hash !== void 0);
230
+ case message instanceof ArgumentMessage: {
231
+ if (!message.format && hash !== void 0 && String(message.identifier) === hash) return "#";
232
+ const parts = [String(message.identifier)];
233
+ if (message.format) parts.push(message.format.type);
234
+ if (message.format?.style) parts.push(message.format.style);
235
+ return `{${parts.join(", ")}}`;
236
+ }
174
237
  case message instanceof ElementMessage: {
175
238
  if (message.children.length === 0) return `<${String(message.identifier)}/>`;
176
- const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
239
+ const children = message.children.map((m) => internalConvertMessageToIcu(m, hash)).join("");
177
240
  return `<${String(message.identifier)}>${children}</${String(message.identifier)}>`;
178
241
  }
179
242
  case message instanceof ChoiceMessage: {
243
+ const scope = message.kind === "select" ? hash : String(message.identifier);
180
244
  const branches = message.branches.map(({ identifier, value }) => ({
181
- identifier: getBranchCase(identifier),
182
- value: internalConvertMessageToIcu(value)
245
+ identifier: getBranchCase(message.kind, identifier),
246
+ value: internalConvertMessageToIcu(value, scope)
183
247
  })).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
184
248
  const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
185
- return `{${String(message.identifier)}, ${format},\n${branches}}`;
249
+ const offset = message.offset === void 0 || message.kind === "select" ? "" : ` offset:${message.offset}`;
250
+ return `{${String(message.identifier)}, ${format},${offset}\n${branches}}`;
186
251
  }
187
- case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
252
+ case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m, hash)).join("");
188
253
  default: throw new Error("Unknown message type", { cause: message });
189
254
  }
190
255
  }
191
- return internalConvertMessageToIcu(message).trim();
256
+ return internalConvertMessageToIcu(message, void 0).trim();
257
+ }
258
+ //#endregion
259
+ //#region src/features/messages/format.ts
260
+ /**
261
+ * The ICU argument types a macro can author, and the named styles each accepts.
262
+ *
263
+ * `currency` is deliberately absent from `number`. MF1 has nowhere to write the
264
+ * currency code — it comes from the formatter's configuration, not the message
265
+ * — so `{price, number, currency}` formats as a literal `{$price}` at runtime
266
+ * rather than an amount. Currency belongs to number skeletons, which the
267
+ * formatter does not accept yet either.
268
+ *
269
+ * `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
270
+ * oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
271
+ * equivalent, and the third is deprecated in ICU itself in favour of `plural`.
272
+ */
273
+ const ARGUMENT_STYLES = {
274
+ number: ["integer", "percent"],
275
+ date: [
276
+ "short",
277
+ "medium",
278
+ "long",
279
+ "full"
280
+ ],
281
+ time: [
282
+ "short",
283
+ "medium",
284
+ "long",
285
+ "full"
286
+ ]
287
+ };
288
+ const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
289
+ function isArgumentType(kind) {
290
+ return Object.hasOwn(ARGUMENT_STYLES, kind);
291
+ }
292
+ /**
293
+ * A literal `NumberFormat` pattern, e.g. `#,##0.00`.
294
+ *
295
+ * A pattern has to carry a digit placeholder — `#` or `0` — because that is
296
+ * what makes it a pattern rather than a word. Without that requirement any
297
+ * brace-free string qualifies, which quietly readmits the named styles this
298
+ * module exists to reject: `currency` would sail through as a "pattern" and
299
+ * extract to the `{price, number, currency}` the formatter cannot honour, and
300
+ * so would a plain typo.
301
+ *
302
+ * Braces are excluded separately: ICU reserves them for its own pattern syntax,
303
+ * so a style carrying one would close the argument early and take the rest of
304
+ * the message with it.
305
+ */
306
+ const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
307
+ /**
308
+ * Reject an argument style the formatter cannot honour, while the style is
309
+ * still attached to a file and a line.
310
+ *
311
+ * A style is a bare string in the source and a bare string in the catalogue, so
312
+ * nothing between here and the runtime has an opinion about it. Left unchecked,
313
+ * a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
314
+ * perfectly normal and only misformats once it reaches a user.
315
+ */
316
+ function validateArgumentStyle(type, style) {
317
+ const named = ARGUMENT_STYLES[type];
318
+ if (named.includes(style)) return;
319
+ if (type === "number" && LITERAL_STYLE_PATTERN.test(style)) return;
320
+ const expected = named.map((s) => `'${s}'`).join(", ");
321
+ throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
192
322
  }
193
323
  //#endregion
324
+ exports.ARGUMENT_STYLES = ARGUMENT_STYLES;
325
+ exports.ARGUMENT_TYPES = ARGUMENT_TYPES;
194
326
  exports.AUTO_INCREMENT_IDENTIFIER = AUTO_INCREMENT_IDENTIFIER;
195
327
  exports.ArgumentMessage = ArgumentMessage;
196
328
  exports.ChoiceMessage = ChoiceMessage;
@@ -199,6 +331,9 @@ exports.ElementMessage = ElementMessage;
199
331
  exports.LiteralMessage = LiteralMessage;
200
332
  exports.assignSequenceIdentifiers = assignSequenceIdentifiers;
201
333
  exports.convertMessageToIcu = convertMessageToIcu;
334
+ exports.escapeIcuLiteral = escapeIcuLiteral;
202
335
  exports.generateHash = require_hash.generateHash;
203
336
  exports.getBranchCase = getBranchCase;
337
+ exports.isArgumentType = isArgumentType;
338
+ exports.validateArgumentStyle = validateArgumentStyle;
204
339
  exports.validateBranchIdentifier = validateBranchIdentifier;
@@ -1,8 +1,46 @@
1
+ //#region src/features/messages/format.d.ts
2
+ /**
3
+ * The ICU argument types a macro can author, and the named styles each accepts.
4
+ *
5
+ * `currency` is deliberately absent from `number`. MF1 has nowhere to write the
6
+ * currency code — it comes from the formatter's configuration, not the message
7
+ * — so `{price, number, currency}` formats as a literal `{$price}` at runtime
8
+ * rather than an amount. Currency belongs to number skeletons, which the
9
+ * formatter does not accept yet either.
10
+ *
11
+ * `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
12
+ * oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
13
+ * equivalent, and the third is deprecated in ICU itself in favour of `plural`.
14
+ */
15
+ declare const ARGUMENT_STYLES: {
16
+ readonly number: readonly ["integer", "percent"];
17
+ readonly date: readonly ["short", "medium", "long", "full"];
18
+ readonly time: readonly ["short", "medium", "long", "full"];
19
+ };
20
+ type ArgumentType = keyof typeof ARGUMENT_STYLES;
21
+ declare const ARGUMENT_TYPES: ArgumentType[];
22
+ declare function isArgumentType(kind: string): kind is ArgumentType;
23
+ /**
24
+ * Reject an argument style the formatter cannot honour, while the style is
25
+ * still attached to a file and a line.
26
+ *
27
+ * A style is a bare string in the source and a bare string in the catalogue, so
28
+ * nothing between here and the runtime has an opinion about it. Left unchecked,
29
+ * a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
30
+ * perfectly normal and only misformats once it reaches a user.
31
+ */
32
+ declare function validateArgumentStyle(type: ArgumentType, style: string): void;
33
+ //#endregion
1
34
  //#region src/features/messages/identifier.d.ts
2
35
  declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
3
36
  /**
4
- * The ICU case a branch is written as. A number names an exact value rather
5
- * than a key, and only `plural` and `ordinal` are allowed to select on one.
37
+ * The ICU case a branch is written as.
38
+ *
39
+ * Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
40
+ * so is distinct from the CLDR category that would otherwise match it. `select`
41
+ * has no such syntax — its cases are literal string matches, and `=0` there is
42
+ * a parse error — so a numeric key stays bare, where it matches both `0` and
43
+ * `'0'`.
6
44
  *
7
45
  * Digits are read literally rather than coerced, because everything JavaScript
8
46
  * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
@@ -10,7 +48,7 @@ declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
10
48
  * author's own key out of the catalogue. Anything else stays a key, where the
11
49
  * whitespace or punctuation that made it numeric-looking is caught.
12
50
  */
13
- declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
51
+ declare function getBranchCase(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
14
52
  /**
15
53
  * Reject a branch key ICU cannot express, while the key is still attached to a
16
54
  * file and a line.
@@ -40,10 +78,23 @@ declare class LiteralMessage extends Base {
40
78
  readonly text: string;
41
79
  constructor(text: string);
42
80
  }
81
+ /**
82
+ * An ICU argument type and style, e.g. `{n, number, percent}`. A style is
83
+ * optional — `{n, number}` is the type's default formatting.
84
+ *
85
+ * Both are kept as the ICU strings they are written as, rather than as `Intl`
86
+ * options, because the catalogue is the source of truth and only the ICU
87
+ * spelling round-trips back out of it.
88
+ */
89
+ interface ArgumentFormat {
90
+ type: ArgumentType;
91
+ style?: string;
92
+ }
43
93
  declare class ArgumentMessage extends Base {
44
94
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
45
95
  readonly expression: any;
46
- constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any);
96
+ readonly format?: ArgumentFormat | undefined;
97
+ constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any, format?: ArgumentFormat | undefined);
47
98
  }
48
99
  declare class ElementMessage extends Base {
49
100
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
@@ -59,10 +110,22 @@ declare class ChoiceMessage extends Base {
59
110
  readonly value: Message;
60
111
  }[];
61
112
  readonly expression: any;
113
+ /**
114
+ * Subtracted from the selector before `#` is formatted, so "You and 2
115
+ * others" can select on a total of three. Only `plural` and `ordinal`
116
+ * accept one; `select` has no number to offset.
117
+ */
118
+ readonly offset?: number | undefined;
62
119
  constructor(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, branches: {
63
120
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
64
121
  readonly value: Message;
65
- }[], expression: any);
122
+ }[], expression: any,
123
+ /**
124
+ * Subtracted from the selector before `#` is formatted, so "You and 2
125
+ * others" can select on a total of three. Only `plural` and `ordinal`
126
+ * accept one; `select` has no number to offset.
127
+ */
128
+ offset?: number | undefined);
66
129
  }
67
130
  declare class CompositeMessage extends Base {
68
131
  readonly descriptor: {
@@ -84,7 +147,33 @@ type Message = LiteralMessage | ArgumentMessage | ElementMessage | ChoiceMessage
84
147
  //#region src/features/messages/convert.d.ts
85
148
  declare function convertMessageToIcu(message: Message): string;
86
149
  //#endregion
150
+ //#region src/features/messages/escape.d.ts
151
+ /**
152
+ * Quoting for text that has to survive a message format unchanged.
153
+ *
154
+ * This lives beside the converter rather than in a transform because it belongs
155
+ * to the format being written, not to the syntax being read. A literal is the
156
+ * same literal whether it came from a template literal or from JSX, and the
157
+ * moment there is a second target format its reserved characters — and the way
158
+ * it spells an escape — are its own.
159
+ */
160
+ /**
161
+ * Escape literal text so ICU MessageFormat 1 reads it as the text it is.
162
+ *
163
+ * MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
164
+ * doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
165
+ * only opens a quoted run when a character that needs quoting follows it —
166
+ * anything else and the apostrophe is itself literal. So `'#'` is a real escape
167
+ * inside a plural and the three characters `'#'` anywhere else, which is why
168
+ * `hash` has to be told rather than assumed.
169
+ *
170
+ * @param text Literal text as the author wrote it
171
+ * @param hash Whether `#` stands for a number here, i.e. whether some `plural`
172
+ * or `ordinal` encloses this text
173
+ */
174
+ declare function escapeIcuLiteral(text: string, hash?: boolean): string;
175
+ //#endregion
87
176
  //#region src/features/messages/hash.d.ts
88
177
  declare function generateHash(input: string, context?: string): string;
89
178
  //#endregion
90
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
179
+ export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
@@ -1,8 +1,46 @@
1
+ //#region src/features/messages/format.d.ts
2
+ /**
3
+ * The ICU argument types a macro can author, and the named styles each accepts.
4
+ *
5
+ * `currency` is deliberately absent from `number`. MF1 has nowhere to write the
6
+ * currency code — it comes from the formatter's configuration, not the message
7
+ * — so `{price, number, currency}` formats as a literal `{$price}` at runtime
8
+ * rather than an amount. Currency belongs to number skeletons, which the
9
+ * formatter does not accept yet either.
10
+ *
11
+ * `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
12
+ * oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
13
+ * equivalent, and the third is deprecated in ICU itself in favour of `plural`.
14
+ */
15
+ declare const ARGUMENT_STYLES: {
16
+ readonly number: readonly ["integer", "percent"];
17
+ readonly date: readonly ["short", "medium", "long", "full"];
18
+ readonly time: readonly ["short", "medium", "long", "full"];
19
+ };
20
+ type ArgumentType = keyof typeof ARGUMENT_STYLES;
21
+ declare const ARGUMENT_TYPES: ArgumentType[];
22
+ declare function isArgumentType(kind: string): kind is ArgumentType;
23
+ /**
24
+ * Reject an argument style the formatter cannot honour, while the style is
25
+ * still attached to a file and a line.
26
+ *
27
+ * A style is a bare string in the source and a bare string in the catalogue, so
28
+ * nothing between here and the runtime has an opinion about it. Left unchecked,
29
+ * a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
30
+ * perfectly normal and only misformats once it reaches a user.
31
+ */
32
+ declare function validateArgumentStyle(type: ArgumentType, style: string): void;
33
+ //#endregion
1
34
  //#region src/features/messages/identifier.d.ts
2
35
  declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
3
36
  /**
4
- * The ICU case a branch is written as. A number names an exact value rather
5
- * than a key, and only `plural` and `ordinal` are allowed to select on one.
37
+ * The ICU case a branch is written as.
38
+ *
39
+ * Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
40
+ * so is distinct from the CLDR category that would otherwise match it. `select`
41
+ * has no such syntax — its cases are literal string matches, and `=0` there is
42
+ * a parse error — so a numeric key stays bare, where it matches both `0` and
43
+ * `'0'`.
6
44
  *
7
45
  * Digits are read literally rather than coerced, because everything JavaScript
8
46
  * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
@@ -10,7 +48,7 @@ declare const AUTO_INCREMENT_IDENTIFIER: unique symbol;
10
48
  * author's own key out of the catalogue. Anything else stays a key, where the
11
49
  * whitespace or punctuation that made it numeric-looking is caught.
12
50
  */
13
- declare function getBranchCase(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
51
+ declare function getBranchCase(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER): string;
14
52
  /**
15
53
  * Reject a branch key ICU cannot express, while the key is still attached to a
16
54
  * file and a line.
@@ -40,10 +78,23 @@ declare class LiteralMessage extends Base {
40
78
  readonly text: string;
41
79
  constructor(text: string);
42
80
  }
81
+ /**
82
+ * An ICU argument type and style, e.g. `{n, number, percent}`. A style is
83
+ * optional — `{n, number}` is the type's default formatting.
84
+ *
85
+ * Both are kept as the ICU strings they are written as, rather than as `Intl`
86
+ * options, because the catalogue is the source of truth and only the ICU
87
+ * spelling round-trips back out of it.
88
+ */
89
+ interface ArgumentFormat {
90
+ type: ArgumentType;
91
+ style?: string;
92
+ }
43
93
  declare class ArgumentMessage extends Base {
44
94
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
45
95
  readonly expression: any;
46
- constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any);
96
+ readonly format?: ArgumentFormat | undefined;
97
+ constructor(identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, expression: any, format?: ArgumentFormat | undefined);
47
98
  }
48
99
  declare class ElementMessage extends Base {
49
100
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
@@ -59,10 +110,22 @@ declare class ChoiceMessage extends Base {
59
110
  readonly value: Message;
60
111
  }[];
61
112
  readonly expression: any;
113
+ /**
114
+ * Subtracted from the selector before `#` is formatted, so "You and 2
115
+ * others" can select on a total of three. Only `plural` and `ordinal`
116
+ * accept one; `select` has no number to offset.
117
+ */
118
+ readonly offset?: number | undefined;
62
119
  constructor(kind: string, identifier: string | typeof AUTO_INCREMENT_IDENTIFIER, branches: {
63
120
  identifier: string | typeof AUTO_INCREMENT_IDENTIFIER;
64
121
  readonly value: Message;
65
- }[], expression: any);
122
+ }[], expression: any,
123
+ /**
124
+ * Subtracted from the selector before `#` is formatted, so "You and 2
125
+ * others" can select on a total of three. Only `plural` and `ordinal`
126
+ * accept one; `select` has no number to offset.
127
+ */
128
+ offset?: number | undefined);
66
129
  }
67
130
  declare class CompositeMessage extends Base {
68
131
  readonly descriptor: {
@@ -84,7 +147,33 @@ type Message = LiteralMessage | ArgumentMessage | ElementMessage | ChoiceMessage
84
147
  //#region src/features/messages/convert.d.ts
85
148
  declare function convertMessageToIcu(message: Message): string;
86
149
  //#endregion
150
+ //#region src/features/messages/escape.d.ts
151
+ /**
152
+ * Quoting for text that has to survive a message format unchanged.
153
+ *
154
+ * This lives beside the converter rather than in a transform because it belongs
155
+ * to the format being written, not to the syntax being read. A literal is the
156
+ * same literal whether it came from a template literal or from JSX, and the
157
+ * moment there is a second target format its reserved characters — and the way
158
+ * it spells an escape — are its own.
159
+ */
160
+ /**
161
+ * Escape literal text so ICU MessageFormat 1 reads it as the text it is.
162
+ *
163
+ * MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
164
+ * doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
165
+ * only opens a quoted run when a character that needs quoting follows it —
166
+ * anything else and the apostrophe is itself literal. So `'#'` is a real escape
167
+ * inside a plural and the three characters `'#'` anywhere else, which is why
168
+ * `hash` has to be told rather than assumed.
169
+ *
170
+ * @param text Literal text as the author wrote it
171
+ * @param hash Whether `#` stands for a number here, i.e. whether some `plural`
172
+ * or `ordinal` encloses this text
173
+ */
174
+ declare function escapeIcuLiteral(text: string, hash?: boolean): string;
175
+ //#endregion
87
176
  //#region src/features/messages/hash.d.ts
88
177
  declare function generateHash(input: string, context?: string): string;
89
178
  //#endregion
90
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
179
+ export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentFormat, ArgumentMessage, ArgumentType, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, Message, PlaceholderEquivalence, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
@@ -1,4 +1,32 @@
1
1
  import { t as generateHash } from "../../hash-DvzpieJD.mjs";
2
+ //#region src/features/messages/escape.ts
3
+ /**
4
+ * Quoting for text that has to survive a message format unchanged.
5
+ *
6
+ * This lives beside the converter rather than in a transform because it belongs
7
+ * to the format being written, not to the syntax being read. A literal is the
8
+ * same literal whether it came from a template literal or from JSX, and the
9
+ * moment there is a second target format its reserved characters — and the way
10
+ * it spells an escape — are its own.
11
+ */
12
+ /**
13
+ * Escape literal text so ICU MessageFormat 1 reads it as the text it is.
14
+ *
15
+ * MF1 escapes with an apostrophe: a run wrapped in one is taken literally, and a
16
+ * doubled apostrophe is a single apostrophe. The subtlety is that an apostrophe
17
+ * only opens a quoted run when a character that needs quoting follows it —
18
+ * anything else and the apostrophe is itself literal. So `'#'` is a real escape
19
+ * inside a plural and the three characters `'#'` anywhere else, which is why
20
+ * `hash` has to be told rather than assumed.
21
+ *
22
+ * @param text Literal text as the author wrote it
23
+ * @param hash Whether `#` stands for a number here, i.e. whether some `plural`
24
+ * or `ordinal` encloses this text
25
+ */
26
+ function escapeIcuLiteral(text, hash = false) {
27
+ return text.replace(/'/gu, "''").replace(hash ? /[{}#]+/gu : /[{}]+/gu, (run) => `'${run}'`);
28
+ }
29
+ //#endregion
2
30
  //#region src/features/messages/types.ts
3
31
  var Base = class {
4
32
  toICUString() {
@@ -19,10 +47,12 @@ var LiteralMessage = class extends Base {
19
47
  var ArgumentMessage = class extends Base {
20
48
  identifier;
21
49
  expression;
22
- constructor(identifier, expression) {
50
+ format;
51
+ constructor(identifier, expression, format) {
23
52
  super();
24
53
  this.identifier = identifier;
25
54
  this.expression = expression;
55
+ this.format = format;
26
56
  }
27
57
  };
28
58
  var ElementMessage = class extends Base {
@@ -41,12 +71,14 @@ var ChoiceMessage = class extends Base {
41
71
  identifier;
42
72
  branches;
43
73
  expression;
44
- constructor(kind, identifier, branches, expression) {
74
+ offset;
75
+ constructor(kind, identifier, branches, expression, offset) {
45
76
  super();
46
77
  this.kind = kind;
47
78
  this.identifier = identifier;
48
79
  this.branches = branches;
49
80
  this.expression = expression;
81
+ this.offset = offset;
50
82
  }
51
83
  };
52
84
  var CompositeMessage = class extends Base {
@@ -75,8 +107,13 @@ const AUTO_INCREMENT_IDENTIFIER = Symbol("auto-increment");
75
107
  */
76
108
  const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$/u;
77
109
  /**
78
- * The ICU case a branch is written as. A number names an exact value rather
79
- * than a key, and only `plural` and `ordinal` are allowed to select on one.
110
+ * The ICU case a branch is written as.
111
+ *
112
+ * Under `plural` and `ordinal` a number names an exact value, spelled `=0`, and
113
+ * so is distinct from the CLDR category that would otherwise match it. `select`
114
+ * has no such syntax — its cases are literal string matches, and `=0` there is
115
+ * a parse error — so a numeric key stays bare, where it matches both `0` and
116
+ * `'0'`.
80
117
  *
81
118
  * Digits are read literally rather than coerced, because everything JavaScript
82
119
  * is willing to call a number is not: `''`, `' '`, and `'+0'` all coerce to
@@ -84,8 +121,9 @@ const BRANCH_PATTERN = /^(?:=\d+|[^\p{Pattern_Syntax}\p{Pattern_White_Space}]+)$
84
121
  * author's own key out of the catalogue. Anything else stays a key, where the
85
122
  * whitespace or punctuation that made it numeric-looking is caught.
86
123
  */
87
- function getBranchCase(identifier) {
124
+ function getBranchCase(kind, identifier) {
88
125
  const key = String(identifier);
126
+ if (kind === "select") return key;
89
127
  return /^\d+$/u.test(key) ? `=${+key}` : key;
90
128
  }
91
129
  /**
@@ -99,12 +137,12 @@ function getBranchCase(identifier) {
99
137
  */
100
138
  function validateBranchIdentifier(kind, identifier) {
101
139
  if (typeof identifier !== "string") return;
102
- const branch = getBranchCase(identifier);
140
+ const branch = getBranchCase(kind, identifier);
103
141
  if (!BRANCH_PATTERN.test(branch)) {
104
142
  const suggestion = suggestBranchIdentifier(identifier);
105
143
  throw new Error(`Invalid ${kind} branch key '${identifier}', an ICU key cannot contain punctuation or whitespace` + (suggestion ? `, try '${suggestion}'` : ""));
106
144
  }
107
- if (kind === "select" && branch.startsWith("=")) throw new Error(`Invalid select branch key '${identifier}', a number selects an exact value, which only 'plural' and 'ordinal' accept`);
145
+ if (kind === "select" && /^=\d+$/u.test(branch)) throw new Error(`Invalid select branch key '${identifier}', an exact value is only meaningful to 'plural' and 'ordinal', write it as '${branch.slice(1)}'`);
108
146
  }
109
147
  /**
110
148
  * The nearest identifier-safe form of a key, so the error names the fix as well
@@ -124,9 +162,28 @@ function assignSequenceIdentifiers(message, sequence = { current: 0 }, equivalen
124
162
  while (reserved.has(identifier)) identifier = `${sequence.current++}`;
125
163
  return identifier;
126
164
  }
165
+ const numbered = {
166
+ tags: [],
167
+ values: []
168
+ };
169
+ /**
170
+ * The number this expression already has, or a fresh one. An anonymous
171
+ * placeholder is named by its position, so the same value written twice would
172
+ * otherwise arrive as two names — `{0} x {1}` for one length, which asks a
173
+ * translator to keep two holes in step and the caller to supply one value
174
+ * under two props. This is the rule explicit names already follow, where a
175
+ * repeat is allowed precisely when nothing distinguishes it.
176
+ */
177
+ function number(assigned, expression) {
178
+ const seen = assigned.find(([e]) => equivalent(e, expression));
179
+ if (seen) return seen[1];
180
+ const identifier = next();
181
+ assigned.push([expression, identifier]);
182
+ return identifier;
183
+ }
127
184
  function walk(message) {
128
185
  if (message instanceof ArgumentMessage || message instanceof ElementMessage || message instanceof ChoiceMessage) {
129
- if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = next();
186
+ if (message.identifier === AUTO_INCREMENT_IDENTIFIER) message.identifier = number(message instanceof ElementMessage ? numbered.tags : numbered.values, message.expression);
130
187
  }
131
188
  if (message instanceof CompositeMessage || message instanceof ElementMessage) for (const child of message.children) walk(child);
132
189
  if (message instanceof ChoiceMessage) for (const branch of message.branches) {
@@ -166,28 +223,101 @@ function collectAssignedIdentifiers(message, equivalent) {
166
223
  //#endregion
167
224
  //#region src/features/messages/convert.ts
168
225
  function convertMessageToIcu(message) {
169
- function internalConvertMessageToIcu(message) {
226
+ function internalConvertMessageToIcu(message, hash) {
170
227
  switch (true) {
171
- case message instanceof LiteralMessage: return String(message.text);
172
- case message instanceof ArgumentMessage: return `{${String(message.identifier)}}`;
228
+ case message instanceof LiteralMessage: return escapeIcuLiteral(String(message.text), hash !== void 0);
229
+ case message instanceof ArgumentMessage: {
230
+ if (!message.format && hash !== void 0 && String(message.identifier) === hash) return "#";
231
+ const parts = [String(message.identifier)];
232
+ if (message.format) parts.push(message.format.type);
233
+ if (message.format?.style) parts.push(message.format.style);
234
+ return `{${parts.join(", ")}}`;
235
+ }
173
236
  case message instanceof ElementMessage: {
174
237
  if (message.children.length === 0) return `<${String(message.identifier)}/>`;
175
- const children = message.children.map((m) => internalConvertMessageToIcu(m)).join("");
238
+ const children = message.children.map((m) => internalConvertMessageToIcu(m, hash)).join("");
176
239
  return `<${String(message.identifier)}>${children}</${String(message.identifier)}>`;
177
240
  }
178
241
  case message instanceof ChoiceMessage: {
242
+ const scope = message.kind === "select" ? hash : String(message.identifier);
179
243
  const branches = message.branches.map(({ identifier, value }) => ({
180
- identifier: getBranchCase(identifier),
181
- value: internalConvertMessageToIcu(value)
244
+ identifier: getBranchCase(message.kind, identifier),
245
+ value: internalConvertMessageToIcu(value, scope)
182
246
  })).map(({ identifier, value }) => ` ${identifier} {${value}}\n`).join("");
183
247
  const format = message.kind === "ordinal" ? "selectordinal" : message.kind;
184
- return `{${String(message.identifier)}, ${format},\n${branches}}`;
248
+ const offset = message.offset === void 0 || message.kind === "select" ? "" : ` offset:${message.offset}`;
249
+ return `{${String(message.identifier)}, ${format},${offset}\n${branches}}`;
185
250
  }
186
- case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m)).join("");
251
+ case message instanceof CompositeMessage: return Object.entries(message.children).map(([, m]) => internalConvertMessageToIcu(m, hash)).join("");
187
252
  default: throw new Error("Unknown message type", { cause: message });
188
253
  }
189
254
  }
190
- return internalConvertMessageToIcu(message).trim();
255
+ return internalConvertMessageToIcu(message, void 0).trim();
256
+ }
257
+ //#endregion
258
+ //#region src/features/messages/format.ts
259
+ /**
260
+ * The ICU argument types a macro can author, and the named styles each accepts.
261
+ *
262
+ * `currency` is deliberately absent from `number`. MF1 has nowhere to write the
263
+ * currency code — it comes from the formatter's configuration, not the message
264
+ * — so `{price, number, currency}` formats as a literal `{$price}` at runtime
265
+ * rather than an amount. Currency belongs to number skeletons, which the
266
+ * formatter does not accept yet either.
267
+ *
268
+ * `spellout`, RBNF `ordinal`, and `choice` are absent by decision rather than
269
+ * oversight: the first two are ICU4J/ICU4C rule-based formats with no `Intl`
270
+ * equivalent, and the third is deprecated in ICU itself in favour of `plural`.
271
+ */
272
+ const ARGUMENT_STYLES = {
273
+ number: ["integer", "percent"],
274
+ date: [
275
+ "short",
276
+ "medium",
277
+ "long",
278
+ "full"
279
+ ],
280
+ time: [
281
+ "short",
282
+ "medium",
283
+ "long",
284
+ "full"
285
+ ]
286
+ };
287
+ const ARGUMENT_TYPES = Object.keys(ARGUMENT_STYLES);
288
+ function isArgumentType(kind) {
289
+ return Object.hasOwn(ARGUMENT_STYLES, kind);
290
+ }
291
+ /**
292
+ * A literal `NumberFormat` pattern, e.g. `#,##0.00`.
293
+ *
294
+ * A pattern has to carry a digit placeholder — `#` or `0` — because that is
295
+ * what makes it a pattern rather than a word. Without that requirement any
296
+ * brace-free string qualifies, which quietly readmits the named styles this
297
+ * module exists to reject: `currency` would sail through as a "pattern" and
298
+ * extract to the `{price, number, currency}` the formatter cannot honour, and
299
+ * so would a plain typo.
300
+ *
301
+ * Braces are excluded separately: ICU reserves them for its own pattern syntax,
302
+ * so a style carrying one would close the argument early and take the rest of
303
+ * the message with it.
304
+ */
305
+ const LITERAL_STYLE_PATTERN = /^[^{}\r\n]*[#0][^{}\r\n]*$/;
306
+ /**
307
+ * Reject an argument style the formatter cannot honour, while the style is
308
+ * still attached to a file and a line.
309
+ *
310
+ * A style is a bare string in the source and a bare string in the catalogue, so
311
+ * nothing between here and the runtime has an opinion about it. Left unchecked,
312
+ * a typo like `{d, date, meduim}` extracts to a catalogue entry that looks
313
+ * perfectly normal and only misformats once it reaches a user.
314
+ */
315
+ function validateArgumentStyle(type, style) {
316
+ const named = ARGUMENT_STYLES[type];
317
+ if (named.includes(style)) return;
318
+ if (type === "number" && LITERAL_STYLE_PATTERN.test(style)) return;
319
+ const expected = named.map((s) => `'${s}'`).join(", ");
320
+ throw new Error(`Invalid ${type} style '${style}', expected ${expected}` + (type === "number" ? ", or a literal number pattern such as #,##0.00" : ""));
191
321
  }
192
322
  //#endregion
193
- export { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, generateHash, getBranchCase, validateBranchIdentifier };
323
+ export { ARGUMENT_STYLES, ARGUMENT_TYPES, AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage, assignSequenceIdentifiers, convertMessageToIcu, escapeIcuLiteral, generateHash, getBranchCase, isArgumentType, validateArgumentStyle, validateBranchIdentifier };
@@ -73,13 +73,22 @@ const configLoaders = Object.freeze({
73
73
  });
74
74
  //#endregion
75
75
  //#region src/features/loader/resolve.ts
76
- function resolveConfig(name = "saykit") {
76
+ /**
77
+ * The config file {@link resolveConfig} would load, for callers that need the
78
+ * path itself rather than its contents — salting a bundler's cache key with it,
79
+ * for one, since what a catalogue assembles into depends on the config.
80
+ */
81
+ function resolveConfigFile(name = "saykit") {
77
82
  const file = findConfigFile(name, process.cwd());
78
83
  if (!file) throw new Error(`Could not find config file for "${name}"`);
79
- const ext = (0, node_path.extname)(file.id).toLowerCase();
84
+ return file.id;
85
+ }
86
+ function resolveConfig(name = "saykit") {
87
+ const id = resolveConfigFile(name);
88
+ const ext = (0, node_path.extname)(id).toLowerCase();
80
89
  const load = ext in configLoaders ? configLoaders[ext] : null;
81
90
  if (!load) throw new Error(`Unsupported config file type "${ext}" for "${name}"`);
82
- const config = load(file.id);
91
+ const config = load(id);
83
92
  if (!config || typeof config !== "object") throw new Error(`Invalid config file for "${name}"`);
84
93
  return config;
85
94
  }
@@ -90,3 +99,9 @@ Object.defineProperty(exports, "resolveConfig", {
90
99
  return resolveConfig;
91
100
  }
92
101
  });
102
+ Object.defineProperty(exports, "resolveConfigFile", {
103
+ enumerable: true,
104
+ get: function() {
105
+ return resolveConfigFile;
106
+ }
107
+ });
@@ -73,15 +73,24 @@ const configLoaders = Object.freeze({
73
73
  });
74
74
  //#endregion
75
75
  //#region src/features/loader/resolve.ts
76
- function resolveConfig(name = "saykit") {
76
+ /**
77
+ * The config file {@link resolveConfig} would load, for callers that need the
78
+ * path itself rather than its contents — salting a bundler's cache key with it,
79
+ * for one, since what a catalogue assembles into depends on the config.
80
+ */
81
+ function resolveConfigFile(name = "saykit") {
77
82
  const file = findConfigFile(name, process.cwd());
78
83
  if (!file) throw new Error(`Could not find config file for "${name}"`);
79
- const ext = extname(file.id).toLowerCase();
84
+ return file.id;
85
+ }
86
+ function resolveConfig(name = "saykit") {
87
+ const id = resolveConfigFile(name);
88
+ const ext = extname(id).toLowerCase();
80
89
  const load = ext in configLoaders ? configLoaders[ext] : null;
81
90
  if (!load) throw new Error(`Unsupported config file type "${ext}" for "${name}"`);
82
- const config = load(file.id);
91
+ const config = load(id);
83
92
  if (!config || typeof config !== "object") throw new Error(`Invalid config file for "${name}"`);
84
93
  return config;
85
94
  }
86
95
  //#endregion
87
- export { resolveConfig as t };
96
+ export { resolveConfigFile as n, resolveConfig as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/config",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "CLI and configuration tooling for saykit",
5
5
  "keywords": [
6
6
  "cli",