@terrazzo/parser
Version:
Parser/validator for the Design Tokens Community Group (DTCG) standard.
109 lines • 4.86 kB
JavaScript
import * as momoa from '@humanwhocodes/momoa';
import { bundle, encodeFragment, parseRef, replaceNode } from '@terrazzo/json-schema-tools';
/** Normalize resolver (assuming it’s been validated) */
export async function normalizeResolver(document, { logger, filename, req, src, yamlToMomoa }) {
// Important note: think about sets, modifiers, and resolutionOrder all
// containing their own partial tokens documents. Now think about JSON $refs
// inside those. Because we want to treat them all as one _eventual_ document,
// we defer resolving $refs until the very last step. In most setups, this has
// no effect on the final result, however, in the scenario where remote
// documents are loaded and they conflict in unexpected ways, resolving too
// early will produce incorrect results.
//
// To prevent this, we bundle ONCE at the very top level, with the `$defs` at
// the top level now containing all partial documents (as opposed to bundling
// every sub document individually). So all that said, we are deciding to
// choose the “all-in-one“ method for closer support with DTCG aliases, but at
// the expense of some edge cases of $refs behaving unexpectedly.
const resolverBundle = await bundle([{ filename, src }], { req, yamlToMomoa });
const resolverSource = momoa.evaluate(resolverBundle.document);
// Resolve $refs, but in a very different way than everywhere else These are
// all _evaluated_, meaning initialized in JS memory. Unlike in the AST, when
// we resolve these they’ll share memory points (which isn’t possible in the
// AST—values must be duplicated). This code is unique because it’s the only
// place where we’re dealing with shared, initialized JS memory.
replaceNode(document, resolverBundle.document); // inject $defs into the root document
for (const set of Object.values(resolverSource.sets ?? {})) {
for (const source of set.sources) {
resolvePartials(source, { resolver: resolverSource, logger });
}
}
for (const modifier of Object.values(resolverSource.modifiers ?? {})) {
for (const context of Object.values(modifier.contexts)) {
for (const source of context) {
resolvePartials(source, { resolver: resolverSource, logger });
}
}
}
for (const item of resolverSource.resolutionOrder ?? []) {
resolvePartials(item, { resolver: resolverSource, logger });
}
return {
name: resolverSource.name,
version: resolverSource.version,
description: resolverSource.description,
sets: resolverSource.sets,
modifiers: resolverSource.modifiers,
resolutionOrder: resolverSource.resolutionOrder,
_source: {
filename,
document,
},
};
}
/** Resolve $refs for already-initialized JS */
function resolvePartials(source, { resolver, logger, }) {
if (!source) {
return;
}
const entry = { group: 'parser', label: 'resolver' };
if (Array.isArray(source)) {
for (const item of source) {
resolvePartials(item, { resolver, logger });
}
}
else if (typeof source === 'object') {
for (const k of Object.keys(source)) {
if (k === '$ref') {
const $ref = source[k];
const { url, subpath = [] } = parseRef($ref);
if (url !== '.' || subpath.length === 0) {
logger.error({ ...entry, message: `Could not load $ref ${JSON.stringify($ref)}` });
}
const found = findObject(resolver, subpath ?? [], logger);
if (subpath[0] === 'sets' || subpath[0] === 'modifiers') {
found.type = subpath[0].replace(/s$/, '');
found.name = subpath[1];
}
if (found) {
for (const k2 of Object.keys(found)) {
source[k2] = found[k2];
}
delete source.$ref;
}
else {
logger.error({ ...entry, message: `Could not find ${JSON.stringify($ref)}` });
}
}
else {
resolvePartials(source[k], { resolver, logger });
}
}
}
}
function findObject(dict, path, logger) {
let node = dict;
for (const idRaw of path) {
const id = idRaw.replaceAll('~', '~0').replaceAll('/', '~1');
if (!(id in node)) {
logger.error({
group: 'parser',
label: 'resolver',
message: `Could not load $ref ${encodeFragment(path)}`,
});
}
node = node[id];
}
return node;
}
//# sourceMappingURL=normalize.js.map