@uxf/icons-generator 11.129.0 → 12.1.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.
@@ -0,0 +1,379 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._getFaProPackage = _getFaProPackage;
4
+ exports._getFaProDefaultVersion = _getFaProDefaultVersion;
5
+ exports._resolveFaProSource = _resolveFaProSource;
6
+ exports._locateEntry = _locateEntry;
7
+ exports._prefetchFaProIcons = _prefetchFaProIcons;
8
+ exports._readFaProSvg = _readFaProSvg;
9
+ exports._getFaProIndex = _getFaProIndex;
10
+ /* eslint-disable no-console */
11
+ const fs_1 = require("fs");
12
+ const os_1 = require("os");
13
+ const path_1 = require("path");
14
+ const _npmRegistryConfig_1 = require("./_npmRegistryConfig");
15
+ const _untar_1 = require("./_untar");
16
+ /**
17
+ * Font Awesome kit holding every style the licence covers. A kit is the only
18
+ * distribution channel for the styles FA 7 added on top of classic/sharp/duotone,
19
+ * which is why the per-style npm packages can no longer be the source of truth.
20
+ */
21
+ const DEFAULT_PACKAGE = "@awesome.me/kit-0618a3d496";
22
+ /**
23
+ * Pinned, and pinned *here* rather than in each project's CI, because
24
+ * `src/fa-pro-types.ts` is generated from exactly this version and shipped in
25
+ * the same tarball. Keeping the two in one file is what stops them drifting:
26
+ * a name the union offers is always a name the archive really contains.
27
+ *
28
+ * Upgrading a kit is therefore one commit in this repo — bump this constant,
29
+ * run `npm run icon-types:gen`, release — and consumers pick the matching pair
30
+ * up by bumping `@uxf/icons-generator`. Nobody has to set an env var.
31
+ *
32
+ * `UXF_FA_PRO_VERSION=latest` asks the registry what the newest version is,
33
+ * which is how you find out that there is one.
34
+ */
35
+ const DEFAULT_VERSION = "1.0.2";
36
+ const DEFAULT_CACHE_ROOT = (0, path_1.join)((0, os_1.homedir)(), ".cache", "uxf-icons-generator");
37
+ /**
38
+ * The one directory that holds a plain SVG per icon, grouped into a
39
+ * subdirectory per style. A kit nests it (`package/icons/svgs/<style>/`) while
40
+ * `@fortawesome/fontawesome-pro` does not (`package/svgs/<style>/`), so we
41
+ * anchor on the segment name rather than on its depth.
42
+ *
43
+ * Matching it exactly matters: every package also ships `svgs-full/`, a second
44
+ * copy of all ~85k icons, plus `sprites/`, `sprites-full/` and `webfonts/`,
45
+ * whose `.svg` files are whole sprite sheets and fonts rather than icons.
46
+ */
47
+ const ICON_ROOT = "svgs";
48
+ /**
49
+ * Bumped whenever {@link _locateEntry} changes what it derives, so a cached
50
+ * index written by an older build is discarded instead of silently reused —
51
+ * the package and version alone cannot express "same tarball, read differently".
52
+ */
53
+ const INDEX_SCHEMA = 2;
54
+ /* ------------------------------------------------------------------ paths */
55
+ // `||` rather than `??` throughout: declaring a variable with an empty value
56
+ // is how GitLab CI and shell wrappers spell "unset", and an empty package
57
+ // name, version or cache root would each fail in a confusing way.
58
+ function getCacheRoot() {
59
+ return process.env.UXF_FA_PRO_CACHE_ROOT || DEFAULT_CACHE_ROOT;
60
+ }
61
+ function _getFaProPackage() {
62
+ return process.env.UXF_FA_PRO_PACKAGE || DEFAULT_PACKAGE;
63
+ }
64
+ function _getFaProVersion() {
65
+ return process.env.UXF_FA_PRO_VERSION || DEFAULT_VERSION;
66
+ }
67
+ /** The version `src/fa-pro-types.ts` is expected to have been generated from. */
68
+ function _getFaProDefaultVersion() {
69
+ return DEFAULT_VERSION;
70
+ }
71
+ function slugify(source) {
72
+ return `${source.package}@${source.version}`.replace(/[^a-zA-Z0-9.-]+/g, "_");
73
+ }
74
+ // The schema is part of the path, not just of `index.json`: the cached SVGs
75
+ // were extracted under a particular reading of the archive too, so a bump
76
+ // has to retire them along with the index.
77
+ function sourceDir(source) {
78
+ return (0, path_1.join)(getCacheRoot(), "fa-pro", `v${INDEX_SCHEMA}`, slugify(source));
79
+ }
80
+ function indexFile(source) {
81
+ return (0, path_1.join)(sourceDir(source), "index.json");
82
+ }
83
+ function svgFile(source, namespace, iconName) {
84
+ return (0, path_1.join)(sourceDir(source), "svg", namespace, `${iconName}.svg`);
85
+ }
86
+ function writeFileAtomic(path, content) {
87
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
88
+ // A crashed or concurrent run must never leave a half-written cache entry
89
+ // that later reads would treat as valid.
90
+ const tmp = `${path}.${process.pid}.tmp`;
91
+ (0, fs_1.writeFileSync)(tmp, content);
92
+ (0, fs_1.renameSync)(tmp, path);
93
+ }
94
+ /* --------------------------------------------------------------- registry */
95
+ /**
96
+ * `what` decides how a rejection is explained: the same 404 means "this scope
97
+ * is not proxied" for a package lookup and "no such version" for a version
98
+ * lookup, and sending someone to reconfigure a working Verdaccio uplink over a
99
+ * mistyped version number wastes an afternoon.
100
+ */
101
+ async function registryJson(url, accept, what) {
102
+ const packageName = _getFaProPackage();
103
+ const { authorization, registry } = (0, _npmRegistryConfig_1._resolveRegistryTarget)(packageName);
104
+ const response = await fetch(url, {
105
+ headers: { accept, ...(authorization ? { authorization } : {}) },
106
+ });
107
+ if (response.status === 401 || response.status === 403) {
108
+ throw new Error(`${registry} answered ${response.status} for ${packageName}.\n` +
109
+ `The credential it was given is missing or not accepted. Check the auth for this registry in .npmrc or .yarnrc.yml, and that the uplink's own Font Awesome token is still valid.\n` +
110
+ `Setup steps: docs/recipes/fontawesome-kit-verdaccio.md`);
111
+ }
112
+ if (response.status === 404 && what === "version") {
113
+ // The registry answers the same 404 for an unknown version and for an
114
+ // unknown package, and this endpoint cannot tell them apart.
115
+ throw new Error(`${registry} has no ${packageName}@${_getFaProVersion()}.\n` +
116
+ `Either the version is wrong — run with UXF_FA_PRO_VERSION=latest to see what is published — or the package name is.`);
117
+ }
118
+ if (response.status === 404) {
119
+ const scope = packageName.split("/").at(0);
120
+ throw new Error(`${registry} answered 404 for ${packageName}.\n` +
121
+ `Font Awesome publishes kits under the "${scope}" scope on https://npm.fontawesome.com/, so that scope needs its own uplink on our Verdaccio and a "${scope}:registry=" line in .npmrc (or npmScopes in .yarnrc.yml).\n` +
122
+ `Setup steps: docs/recipes/fontawesome-kit-verdaccio.md`);
123
+ }
124
+ if (!response.ok) {
125
+ throw new Error(`GET ${url} failed with ${response.status} ${response.statusText}.`);
126
+ }
127
+ return response.json();
128
+ }
129
+ let _sourcePromise;
130
+ async function resolveSource() {
131
+ var _a;
132
+ const packageName = _getFaProPackage();
133
+ const requested = _getFaProVersion();
134
+ // Only an explicit "latest" asks the registry; anything else is a version
135
+ // we can use as-is, which keeps the common path free of network I/O.
136
+ if (requested !== "latest") {
137
+ return { package: packageName, version: requested };
138
+ }
139
+ const { registry } = (0, _npmRegistryConfig_1._resolveRegistryTarget)(packageName);
140
+ const packument = (await registryJson(`${registry}${packageName.replace("/", "%2f")}`, "application/vnd.npm.install-v1+json", "package"));
141
+ const version = (_a = packument["dist-tags"]) === null || _a === void 0 ? void 0 : _a.latest;
142
+ if (!version) {
143
+ throw new Error(`Registry ${registry} returned no "latest" dist-tag for ${packageName}.`);
144
+ }
145
+ return { package: packageName, version };
146
+ }
147
+ /**
148
+ * Resolve the package version to read icons from: {@link DEFAULT_VERSION}
149
+ * unless `UXF_FA_PRO_VERSION` overrides it, and only the literal `latest`
150
+ * consults the registry's mutable dist-tag.
151
+ *
152
+ * The promise is memoised so concurrent callers share one round trip, but a
153
+ * rejection is not: a transient network failure must not be sticky for the rest
154
+ * of the process, because the prefetch swallows it and later callers retry.
155
+ */
156
+ function _resolveFaProSource() {
157
+ _sourcePromise !== null && _sourcePromise !== void 0 ? _sourcePromise : (_sourcePromise = resolveSource().catch((error) => {
158
+ _sourcePromise = undefined;
159
+ throw error;
160
+ }));
161
+ return _sourcePromise;
162
+ }
163
+ async function resolveTarballUrl(source) {
164
+ var _a;
165
+ const { registry } = (0, _npmRegistryConfig_1._resolveRegistryTarget)(source.package);
166
+ const manifest = (await registryJson(`${registry}${source.package.replace("/", "%2f")}/${source.version}`, "application/json", "version"));
167
+ const tarball = (_a = manifest.dist) === null || _a === void 0 ? void 0 : _a.tarball;
168
+ if (!tarball) {
169
+ throw new Error(`Registry ${registry} returned no tarball URL for ${source.package}@${source.version}.`);
170
+ }
171
+ return tarball;
172
+ }
173
+ async function openTarballStream(source) {
174
+ const url = await resolveTarballUrl(source);
175
+ const { authorization, registry } = (0, _npmRegistryConfig_1._resolveRegistryTarget)(source.package);
176
+ // `dist.tarball` is whatever the registry chose to put there, so it can
177
+ // point anywhere. The credential was issued for the registry and nobody
178
+ // else — send it only when the tarball is served from the same origin,
179
+ // which is how npm treats its own auth too.
180
+ const headers = new Headers();
181
+ if (authorization && new URL(url).origin === new URL(registry).origin) {
182
+ headers.set("authorization", authorization);
183
+ }
184
+ const response = await fetch(url, { headers });
185
+ if (!response.ok) {
186
+ throw new Error(`GET ${url} failed with ${response.status} ${response.statusText}.`);
187
+ }
188
+ if (!response.body) {
189
+ throw new Error(`GET ${url} returned an empty body.`);
190
+ }
191
+ // The tarball is never written to disk — it is gunzipped and parsed as it
192
+ // arrives, and only the SVGs a project references are kept.
193
+ return response.body.pipeThrough(new DecompressionStream("gzip"));
194
+ }
195
+ /**
196
+ * Derive the namespace an archive entry belongs to, or `null` for entries that
197
+ * are not per-icon SVGs (webfonts, sprites, the `svgs-full` duplicates, …).
198
+ *
199
+ * The namespace is the style directory verbatim, so `svgs/sharp-regular/x.svg`
200
+ * is `sharp-regular.x` — the same string the per-style npm packages produced.
201
+ */
202
+ function _locateEntry(name) {
203
+ if (!name.endsWith(".svg")) {
204
+ return null;
205
+ }
206
+ const segments = name.split("/");
207
+ const root = segments.indexOf(ICON_ROOT);
208
+ // Exactly `<root>/<style>/<icon>.svg`: an icon file sits directly in its
209
+ // style directory, and anything deeper or shallower is not an icon.
210
+ if (root === -1 || segments.length !== root + 3) {
211
+ return null;
212
+ }
213
+ const [namespace, fileName] = segments.slice(root + 1);
214
+ const iconName = fileName.slice(0, -".svg".length);
215
+ if (namespace === "" || iconName === "") {
216
+ return null;
217
+ }
218
+ return { iconName, namespace };
219
+ }
220
+ /* ------------------------------------------------------------------ index */
221
+ function readIndexFromDisk(source) {
222
+ const path = indexFile(source);
223
+ if (!(0, fs_1.existsSync)(path)) {
224
+ return null;
225
+ }
226
+ try {
227
+ const parsed = JSON.parse((0, fs_1.readFileSync)(path, "utf8"));
228
+ const isCurrent = parsed.schema === INDEX_SCHEMA && parsed.version === source.version && parsed.package === source.package;
229
+ return isCurrent ? parsed : null;
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ }
235
+ /**
236
+ * Single streaming pass over the tarball, keeping the contents of `wanted` only.
237
+ *
238
+ * Without a `knownIndex` the pass also records every icon name it sees, which
239
+ * costs a full read of the archive. With one, the exact entry paths are already
240
+ * known, so the stream is cancelled as soon as the last wanted icon shows up.
241
+ */
242
+ async function scanTarball(source, wanted, knownIndex) {
243
+ const seen = {};
244
+ const contents = new Map();
245
+ const stream = await openTarballStream(source);
246
+ const decoder = new TextDecoder();
247
+ for await (const entry of (0, _untar_1._untar)(stream, (name) => {
248
+ var _a;
249
+ var _b;
250
+ const location = _locateEntry(name);
251
+ if (!location) {
252
+ return false;
253
+ }
254
+ if (!knownIndex) {
255
+ ((_a = seen[_b = location.namespace]) !== null && _a !== void 0 ? _a : (seen[_b] = new Set())).add(location.iconName);
256
+ }
257
+ return wanted.has(`${location.namespace}.${location.iconName}`);
258
+ })) {
259
+ const location = _locateEntry(entry.name);
260
+ if (location) {
261
+ contents.set(`${location.namespace}.${location.iconName}`, decoder.decode(entry.content));
262
+ }
263
+ if (knownIndex && contents.size === wanted.size) {
264
+ break;
265
+ }
266
+ }
267
+ if (knownIndex) {
268
+ return { contents, index: null };
269
+ }
270
+ const icons = {};
271
+ for (const [namespace, names] of Object.entries(seen)) {
272
+ icons[namespace] = [...(names !== null && names !== void 0 ? names : [])].sort();
273
+ }
274
+ return {
275
+ contents,
276
+ index: { icons, package: source.package, schema: INDEX_SCHEMA, version: source.version },
277
+ };
278
+ }
279
+ /* ---------------------------------------------------------------- caching */
280
+ function readCachedSvg(source, qualifiedName) {
281
+ const [namespace, ...rest] = qualifiedName.split(".");
282
+ const path = svgFile(source, namespace, rest.join("."));
283
+ return (0, fs_1.existsSync)(path) ? (0, fs_1.readFileSync)(path, "utf8") : null;
284
+ }
285
+ function writeCachedSvg(source, qualifiedName, svg) {
286
+ const [namespace, ...rest] = qualifiedName.split(".");
287
+ writeFileAtomic(svgFile(source, namespace, rest.join(".")), svg);
288
+ }
289
+ /* ------------------------------------------------------------------- API */
290
+ const memory = new Map();
291
+ /**
292
+ * Make sure every requested `"<namespace>.<icon-name>"` is available to
293
+ * {@link _readFaProSvg}, downloading the package tarball at most once.
294
+ *
295
+ * Icons already in the on-disk cache cost nothing but a file read, and since
296
+ * the version is pinned rather than resolved from a dist-tag, such a run makes
297
+ * no request at all.
298
+ */
299
+ async function _prefetchFaProIcons(names) {
300
+ var _a;
301
+ const requested = [...new Set(names)];
302
+ if (requested.length === 0) {
303
+ return;
304
+ }
305
+ const source = await _resolveFaProSource();
306
+ const index = readIndexFromDisk(source);
307
+ const missing = new Set();
308
+ for (const qualifiedName of requested) {
309
+ if (memory.has(qualifiedName)) {
310
+ continue;
311
+ }
312
+ const cached = readCachedSvg(source, qualifiedName);
313
+ if (cached === null) {
314
+ missing.add(qualifiedName);
315
+ }
316
+ else {
317
+ memory.set(qualifiedName, cached);
318
+ }
319
+ }
320
+ if (missing.size === 0) {
321
+ return;
322
+ }
323
+ // With an index in hand we already know which icons the package does not
324
+ // have, so a typo costs nothing instead of a 100+ MB download. Membership
325
+ // is checked against a Set per namespace: the arrays run to ~4 800 names
326
+ // and a project can reference hundreds of icons.
327
+ if (index) {
328
+ const known = new Map();
329
+ for (const qualifiedName of [...missing]) {
330
+ const [namespace, ...rest] = qualifiedName.split(".");
331
+ let namespaceIcons = known.get(namespace);
332
+ if (!namespaceIcons) {
333
+ namespaceIcons = new Set((_a = index.icons[namespace]) !== null && _a !== void 0 ? _a : []);
334
+ known.set(namespace, namespaceIcons);
335
+ }
336
+ if (!namespaceIcons.has(rest.join("."))) {
337
+ missing.delete(qualifiedName);
338
+ }
339
+ }
340
+ if (missing.size === 0) {
341
+ return;
342
+ }
343
+ }
344
+ console.log(`Streaming ${missing.size} icon(s) from ${source.package}@${source.version}…`);
345
+ // Without an index this pass builds one on the way, so a cold cache still
346
+ // costs a single download rather than one for the index and one for the icons.
347
+ const { contents, index: builtIndex } = await scanTarball(source, missing, index !== null && index !== void 0 ? index : undefined);
348
+ if (builtIndex) {
349
+ writeFileAtomic(indexFile(source), JSON.stringify(builtIndex));
350
+ }
351
+ for (const [qualifiedName, svg] of contents) {
352
+ memory.set(qualifiedName, svg);
353
+ writeCachedSvg(source, qualifiedName, svg);
354
+ }
355
+ }
356
+ /** Read a prefetched icon. Returns `null` when the icon is not in the package. */
357
+ function _readFaProSvg(qualifiedName) {
358
+ var _a;
359
+ return (_a = memory.get(qualifiedName)) !== null && _a !== void 0 ? _a : null;
360
+ }
361
+ let _indexPromise;
362
+ async function loadIndex() {
363
+ const source = await _resolveFaProSource();
364
+ const fromDisk = readIndexFromDisk(source);
365
+ if (fromDisk) {
366
+ return fromDisk;
367
+ }
368
+ const { index } = await scanTarball(source, new Set());
369
+ if (!index) {
370
+ throw new Error(`Could not index ${source.package}@${source.version}.`);
371
+ }
372
+ writeFileAtomic(indexFile(source), JSON.stringify(index));
373
+ return index;
374
+ }
375
+ /** Namespaces the resolved package provides, with their icon names. */
376
+ function _getFaProIndex() {
377
+ _indexPromise !== null && _indexPromise !== void 0 ? _indexPromise : (_indexPromise = loadIndex());
378
+ return _indexPromise;
379
+ }
@@ -7,7 +7,9 @@ function _generateAdapterFallback(key, iconName, iconDefinition, config) {
7
7
  (0, _createPath_1._createPath)(config.configFile);
8
8
  (0, _createPath_1._createPath)(config.fallbackFilesDirectory);
9
9
  const filePath = config.fallbackFilesDirectory + key + ".json";
10
- const currentContent = (0, fs_1.readFileSync)(filePath).toString();
10
+ // The snapshot is written by this function and by nothing else, so on a
11
+ // project adopting a provider for the first time it does not exist yet.
12
+ const currentContent = (0, fs_1.existsSync)(filePath) ? (0, fs_1.readFileSync)(filePath).toString() : "";
11
13
  const content = currentContent ? JSON.parse(currentContent) : {};
12
14
  content[iconName] = iconDefinition;
13
15
  (0, fs_1.writeFileSync)(filePath, JSON.stringify(content));
@@ -6,5 +6,9 @@ const _createPath_1 = require("./_createPath");
6
6
  const _createSpriteContent_1 = require("./_createSpriteContent");
7
7
  function _generateSpriteFile(config) {
8
8
  (0, _createPath_1._createPath)(config.configFile);
9
+ // The sprite lands in `generatedDirectory`, which on a project generating
10
+ // icons for the first time does not exist yet — `configFile` above only
11
+ // covers the directory the definition file goes into.
12
+ (0, _createPath_1._createPath)(config.spriteFile);
9
13
  (0, fs_1.writeFileSync)(config.spriteFile, (0, _createSpriteContent_1._createSpriteContent)(config));
10
14
  }
@@ -0,0 +1,13 @@
1
+ export interface RegistryTarget {
2
+ /** Registry base URL, always with a trailing slash. */
3
+ registry: string;
4
+ /** Ready-made `Authorization` header value, or `undefined` for anonymous. */
5
+ authorization?: string;
6
+ }
7
+ /**
8
+ * Work out which registry serves a scoped package and how to authenticate
9
+ * against it, reading the same `.npmrc` / `.yarnrc.yml` files the package
10
+ * manager would. Consumer projects are on Yarn while this monorepo is on npm,
11
+ * so both have to be understood.
12
+ */
13
+ export declare function _resolveRegistryTarget(packageName: string): RegistryTarget;