Skip to content

Commit

Permalink
feat: add convertJSONToDTCG & convertZIPToDTCG utils
Browse files Browse the repository at this point in the history
  • Loading branch information
jorenbroekema committed May 20, 2024
1 parent 3678c9c commit 6bd674c
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 26 deletions.
8 changes: 6 additions & 2 deletions docs/src/components/sd-playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { bundle } from '../utils/rollup-bundle.ts';
import { changeLang, init, monaco } from '../monaco/monaco.ts';
import { analyzeDependencies } from '../utils/analyzeDependencies.ts';
import type SlRadioGroup from '@shoelace-style/shoelace/dist/components/radio-group/radio-group.js';
import { downloadZIP } from '../utils/downloadZIP.ts';
import { downloadZIP } from '../../../lib/utils/downloadFile.js';

const { Volume } = memfs;

Expand Down Expand Up @@ -459,7 +459,11 @@ node build-tokens.${scriptLang}
\`\`\`
`;

await downloadZIP(files);
const today = new Date(Date.now());
const filename = `sd-output_${today.getFullYear()}-${today.getMonth()}-${(
'0' + today.getDate()
).slice(-2)}.zip`;
await downloadZIP(files, filename);
}
}

Expand Down
23 changes: 0 additions & 23 deletions docs/src/utils/downloadZIP.ts

This file was deleted.

2 changes: 1 addition & 1 deletion lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import memfs from '@bundled-es-modules/memfs';
/**
* Allow to be overridden by setter, set default to memfs for browser env, node:fs for node env
*/
export let fs = /** @type {Volume} */ memfs;
export let fs = /** @type {Volume} */ (memfs);

/**
* since ES modules exports are read-only, use a setter
Expand Down
115 changes: 115 additions & 0 deletions lib/utils/convertToDTCG.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import isPlainObject from 'is-plain-obj';
import { posix as path } from 'path-unified';
import {
BlobReader,
TextWriter,
ZipReader,
ZipWriter,
BlobWriter,
TextReader,
} from '@zip.js/zip.js';
import { fs } from 'style-dictionary/fs';

/**
* @typedef {import('@zip.js/zip.js').Entry} Entry
* @typedef {import('../../types/DesignToken.d.ts').DesignToken} DesignToken
* @typedef {import('../../types/DesignToken.d.ts').DesignTokens} DesignTokens
*/
Expand All @@ -10,13 +21,18 @@ import isPlainObject from 'is-plain-obj';
* @param {{applyTypesToGroup?: boolean}} [opts]
*/
function recurse(slice, opts) {
// we use a Set to avoid duplicate values
/** @type {Set<string>} */
let types = new Set();

// this slice within the dictionary is a design token
if (Object.hasOwn(slice, 'value')) {
const token = /** @type {DesignToken} */ (slice);
// convert to $ prefixed properties
Object.keys(token).forEach((key) => {
switch (key) {
case 'type':
// track the encountered types for this layer
types.add(/** @type {string} */ (token[key]));
// eslint-disable-next-line no-fallthrough
case 'value':
Expand All @@ -28,11 +44,18 @@ function recurse(slice, opts) {
});
return types;
} else {
// token group, not a token
// go through all props and call itself recursively for object-value props
Object.keys(slice).forEach((key) => {
if (isPlainObject(slice[key])) {
// call Set again to dedupe the accumulation of the two sets
types = new Set([...types, ...recurse(slice[key], opts)]);
}
});

// Now that we've checked every property, let's see how many types we found
// If it's only 1 type, we know we can apply the type on the ancestor group
// and remove it from the children
if (types.size === 1 && opts?.applyTypesToGroup !== false) {
slice.$type = [...types][0];
Object.keys(slice).forEach((key) => {
Expand All @@ -48,7 +71,99 @@ function recurse(slice, opts) {
* @param {{applyTypesToGroup?: boolean}} [opts]
*/
export function convertToDTCG(dictionary, opts) {
// making a copy, so we don't mutate the original input
// this makes for more predictable API (input -> output)
const copy = structuredClone(dictionary);
recurse(copy, opts);
return copy;
}

/**
* @param {Entry} entry
*/
async function resolveZIPEntryData(entry) {
let data;
if (entry.getData) {
data = await entry.getData(new TextWriter('utf-8'));
}
return [entry.filename, data];
}

/**
* @param {Blob|string} blobOrPath
*/
async function blobify(blobOrPath) {
if (typeof blobOrPath === 'string') {
const buf = await fs.promises.readFile(path.resolve(blobOrPath));
return new Blob([buf]);
}
return blobOrPath;
}

/**
* @param {Blob} blob
* @param {string} type
* @param {string} [path]
*/
function validateBlobType(blob, type, path) {
if (blob.type.includes(type)) {
throw new Error(
`File ${path ?? '(Blob)'} is of type ${blob.type}, but a ${type} type blob was expected.`,
);
}
}

/**
* @param {Blob|string} blobOrPath
*/
export async function convertJSONToDTCG(blobOrPath) {
const jsonBlob = await blobify(blobOrPath);
validateBlobType(jsonBlob, 'json', typeof blobOrPath === 'string' ? blobOrPath : undefined);

const reader = new FileReader(); // no arguments
reader.readAsText(jsonBlob);
const fileContent = await new Promise((resolve) => {
reader.addEventListener('load', () => {
resolve(reader.result);
});
});
const converted = JSON.stringify(convertToDTCG(JSON.parse(fileContent)), null, 2);
return new Blob([converted], {
type: 'application/json',
});
}

/**
* @param {Blob|string} blobOrPath
*/
export async function convertZIPToDTCG(blobOrPath) {
const zipBlob = await blobify(blobOrPath);
validateBlobType(zipBlob, 'zip', typeof blobOrPath === 'string' ? blobOrPath : undefined);

const zipReader = new ZipReader(new BlobReader(zipBlob));
const zipEntries = await zipReader.getEntries({
filenameEncoding: 'utf-8',
});
const zipEntriesWithData = /** @type {string[][]} */ (
(
await Promise.all(
zipEntries.filter((entry) => !entry.directory).map((entry) => resolveZIPEntryData(entry)),
)
).filter((entry) => !!entry[1])
);

const convertedZip = Object.fromEntries(
zipEntriesWithData.map(([fileName, data]) => [
fileName,
JSON.stringify(convertToDTCG(JSON.parse(data)), null, 2),
]),
);

const zipWriter = new ZipWriter(new BlobWriter('application/zip'));
await Promise.all(
Object.entries(convertedZip).map(([key, value]) => zipWriter.add(key, new TextReader(value))),
);

// Close zip and return Blob
return zipWriter.close();
}
62 changes: 62 additions & 0 deletions lib/utils/downloadFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ZipWriter, BlobWriter, TextReader } from '@zip.js/zip.js';

/**
* Caution: browser-only utilities
* Would be weird to support in NodeJS since end-user = developer
* so the question would be: where to store the file, if we don't know
* where the blob/files object came from to begin with
*/

/**
* @param {Blob} blob
* @param {string} filename
*/
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);

// Auto-download the ZIP through anchor
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(url);
}

/**
* @param {string | Blob} stringOrBlob
* @param {string} filename
*/
export function downloadJSON(stringOrBlob, filename = 'output.json') {
/** @type {Blob} */
let jsonBlob;
// check if it's a Blob.., instanceof is too strict e.g. Blob polyfills
if (stringOrBlob.constructor.name === 'Blob') {
jsonBlob = /** @type {Blob} */ (stringOrBlob);
} else {
jsonBlob = new Blob([stringOrBlob], { type: 'application/json' });
}
downloadBlob(jsonBlob, filename);
}

/**
* @param {Record<string, string> | Blob} filesOrBlob
* @param {string} filename
*/
export async function downloadZIP(filesOrBlob, filename = 'output.zip') {
/** @type {Blob} */
let zipBlob;
// check if it's a Blob.., instanceof is too strict e.g. Blob polyfills
if (filesOrBlob.constructor.name === 'Blob') {
zipBlob = /** @type {Blob} */ (filesOrBlob);
} else {
const zipWriter = new ZipWriter(new BlobWriter('application/zip'));

await Promise.all(
Object.entries(filesOrBlob).map(([key, value]) => zipWriter.add(key, new TextReader(value))),
);

// Close zip and make into URL
zipBlob = await zipWriter.close();
}
downloadBlob(zipBlob, filename);
}

0 comments on commit 6bd674c

Please sign in to comment.