Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support convert binary files to b64 #1364

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hungry-phones-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'style-dictionary': patch
---

Fix `convertToBase64` util to support converting binary files such as fonts, for both Browser and NodeJS.
3 changes: 2 additions & 1 deletion __tests__/__helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { expect } from 'chai';
import { fs } from 'style-dictionary/fs';
import { resolve } from '../lib/resolve.js';
import isPlainObject from 'is-plain-obj';
import { isNode } from '../lib/utils/isNode.js';

export const cleanConsoleOutput = (str) => {
const arr = str
Expand Down Expand Up @@ -70,7 +71,7 @@ export const dirExists = (dirPath, _fs = fs) => {
export function fixDate() {
const constantDate = new Date('2000-01-01');
// eslint-disable-next-line no-undef
const __global = typeof window === 'object' ? window : globalThis;
const __global = isNode ? globalThis : window;

// eslint-disable-next-line no-undef
__global.Date = function () {
Expand Down
3 changes: 2 additions & 1 deletion __tests__/__setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fs } from 'style-dictionary/fs';
import { chaiWtrSnapshot } from '../snapshot-plugin/chai-wtr-snapshot.js';
import { fixDate } from './__helpers.js';
import { writeZIP } from '../lib/utils/convertToDTCG.js';
import { isNode } from '../lib/utils/isNode.js';

/**
* We have a bunch of files that we use a mock data for our tests
Expand All @@ -23,7 +24,7 @@ export const hasInitialized = new Promise((resolve) => {
hasInitializedResolve = resolve;
});
// in case of Node env, we can resolve it immediately since we don't do this setup stuff
if (typeof window !== 'object') {
if (isNode) {
hasInitializedResolve();
}

Expand Down
3 changes: 2 additions & 1 deletion __tests__/formats/lessIcons.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { expect } from 'chai';
import formats from '../../lib/common/formats.js';
import createFormatArgs from '../../lib/utils/createFormatArgs.js';
import flattenTokens from '../../lib/utils/flattenTokens.js';
import { isNode } from '../../lib/utils/isNode.js';

const file = {
destination: '__output/',
Expand Down Expand Up @@ -67,7 +68,7 @@ describe('formats', () => {
);

let _less;
if (typeof window === 'object') {
if (!isNode) {
await import('less/dist/less.js');
// eslint-disable-next-line no-undef
_less = less;
Expand Down
3 changes: 2 additions & 1 deletion __tests__/formats/lessVariables.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { expect } from 'chai';
import formats from '../../lib/common/formats.js';
import createFormatArgs from '../../lib/utils/createFormatArgs.js';
import flattenTokens from '../../lib/utils/flattenTokens.js';
import { isNode } from '../../lib/utils/isNode.js';

const file = {
destination: '__output/',
Expand Down Expand Up @@ -56,7 +57,7 @@ describe('formats', () => {
file,
);
let _less;
if (typeof window === 'object') {
if (!isNode) {
await import('less/dist/less.js');
// eslint-disable-next-line no-undef
_less = less;
Expand Down
3 changes: 2 additions & 1 deletion __tests__/register/overwrite.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import StyleDictionary from 'style-dictionary';
import { expect } from 'chai';
import transformBuiltins from '../../lib/common/transforms.js';
import { isNode } from '../../lib/utils/isNode.js';

describe('Register overwrites', () => {
const reset = () => {
Expand Down Expand Up @@ -31,7 +32,7 @@ describe('Register overwrites', () => {
// Unfortunately, Mocha Node runs this in parallel with other test files and these tests
// fail purely due to multiple test files writing stuff to the Register class
// TODO: In the future we may be able to run mocha test files in parallel processes
if (typeof window !== 'undefined') {
if (!isNode) {
expect(sd1.hooks.transforms[builtInHookName].type).to.equal('value');
expect(sd2.hooks.transforms[builtInHookName].type).to.equal('name');
expect(sd3.hooks.transforms[builtInHookName].type).to.equal('name');
Expand Down
11 changes: 11 additions & 0 deletions __tests__/utils/__snapshots__/convertToBase64.test.snap.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 32 additions & 10 deletions __tests__/utils/convertToBase64.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@
*/
import { expect } from 'chai';
import convertToBase64 from '../../lib/utils/convertToBase64.js';
import { isNode } from '../../lib/utils/isNode.js';

describe('utils', () => {
describe('convertToBase64', () => {
it('should error if filePath isnt a string', () => {
expect(convertToBase64.bind(null)).to.throw('filePath name must be a string');
expect(convertToBase64.bind(null, [])).to.throw('filePath name must be a string');
expect(convertToBase64.bind(null, {})).to.throw('filePath name must be a string');
it('should error if filePath isnt a string', async () => {
const err = 'filePath name must be a string';
await expect(convertToBase64()).to.eventually.be.rejectedWith(err);
await expect(convertToBase64([])).to.eventually.be.rejectedWith(err);
await expect(convertToBase64({})).to.eventually.be.rejectedWith(err);
});

it('should error if filePath isnt a file', () => {
it('should error if filePath isnt a file', async () => {
let errMessage;
try {
convertToBase64('foo');
await convertToBase64('foo');
} catch (e) {
errMessage = e.message;
}
Expand All @@ -35,14 +37,34 @@ describe('utils', () => {
);
});

it('should return a string', () => {
expect(typeof convertToBase64('__tests__/__configs/test.json')).to.equal('string');
it('should return a string', async () => {
expect(typeof (await convertToBase64('__tests__/__configs/test.json'))).to.equal('string');
});

it('should be a valid base64 string', () => {
expect(convertToBase64('__tests__/__json_files/simple.json')).to.equal(
it('should be a valid base64 string', async () => {
expect(await convertToBase64('__tests__/__json_files/simple.json')).to.equal(
'ewogICJmb28iOiAiYmFyIiwKICAiYmFyIjogIntmb299Igp9',
);
});

it('should convert binary files', async () => {
/**
* The base64 is different between these environment because of
* the fact that the browser uses the FileReaderAPI as a round-about way
* to turn the binary file into a base64 string, so the data representation
* is different in browsers than when doing buffer.toString in Node.
* Using memfs in browser to read/store a binary file is also not really supported I think..
* which may also cause a different base64 string result, but the utility is mostly for Node users anyways
*/
if (!isNode) {
await expect(
await convertToBase64('__tests__/__assets/images/mdpi/flag_us_small.png'),
).to.matchSnapshot(1);
} else {
await expect(
await convertToBase64('__tests__/__assets/images/mdpi/flag_us_small.png'),
).to.matchSnapshot(2);
}
});
});
});
3 changes: 2 additions & 1 deletion lib/StyleDictionary.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import filterTokens from './filterTokens.js';
import cleanFiles from './cleanFiles.js';
import cleanDirs from './cleanDirs.js';
import cleanActions from './cleanActions.js';
import { isNode } from './utils/isNode.js';

/**
* @typedef {import('../types/Volume.ts').Volume} Volume
Expand Down Expand Up @@ -227,7 +228,7 @@ export default class StyleDictionary extends Register {
);
} else {
let _filePath = cfgFilePath;
if (typeof window !== 'object' && process?.platform === 'win32') {
if (isNode && process?.platform === 'win32') {
// Windows FS compatibility. If in browser, we use an FS shim which doesn't require this Windows workaround
_filePath = new URL(`file:///${_filePath}`).href;
}
Expand Down
3 changes: 2 additions & 1 deletion lib/resolve.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { posix, win32 } from 'path-unified';
import { isNode } from './utils/isNode.js';

/**
* If we're on Windows AND we're not in browser context, use win32 resolve (with \'s)
Expand All @@ -11,7 +12,7 @@ export const resolve = (path, customVolumeUsed = false) => {
if (customVolumeUsed) {
return path;
}
if (process?.platform === 'win32' && typeof window !== 'object') {
if (isNode && process?.platform === 'win32') {
return win32.resolve(path);
}
return posix.resolve(path);
Expand Down
5 changes: 3 additions & 2 deletions lib/utils/combineJSON.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { fs } from 'style-dictionary/fs';
import { resolve } from '../resolve.js';
import deepExtend from './deepExtend.js';
import { detectDtcgSyntax } from './detectDtcgSyntax.js';
import { isNode } from './isNode.js';

/**
* @typedef {import('../../types/Volume.ts').Volume} Volume
Expand Down Expand Up @@ -76,7 +77,7 @@ export default async function combineJSON(
files = files.concat(new_files);
}

if (typeof window === 'object') {
if (!isNode) {
// adjust for browser env glob results have leading slash
// make sure we dont remove these in Node, that would break absolute paths!!
files = files.map((f) => f.replace(/^\//, ''));
Expand All @@ -101,7 +102,7 @@ export default async function combineJSON(
if (['.js', '.mjs'].includes(extname(filePath))) {
let resolvedPath = resolve(filePath, vol?.__custom_fs__);
// eslint-disable-next-line no-undef
if (typeof window !== 'object' && process?.platform === 'win32') {
if (isNode && process?.platform === 'win32') {
// Windows FS compatibility. If in browser, we use an FS shim which doesn't require this Windows workaround
resolvedPath = new URL(`file:///${resolvedPath}`).href;
}
Expand Down
39 changes: 31 additions & 8 deletions lib/utils/convertToBase64.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,43 @@

import { fs } from 'style-dictionary/fs';
import { resolve } from '../resolve.js';
import { isNode } from './isNode.js';

/**
* @param {Buffer} buffer
* @returns {string|Promise<string>}
*/
function toBase64(buffer) {
if (isNode) {
// Node.js environment
return buffer.toString('base64');
} else {
// Browser environment
return new Promise((resolve, reject) => {
const blob = new Blob([buffer], { type: 'application/octet-stream' });
const reader = new FileReader();
reader.onloadend = () => {
const base64String = /** @type {string } */ (reader.result).split(',')[1];
resolve(base64String);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
}

/**
* @typedef {import('../../types/Volume.ts').Volume} Volume
* Takes a file and converts it to a base64 string.
* @private
* @param {String} filePath - Path to the file you want base64'd
* @param {string} filePath - Path to the file you want base64'd
* @param {Volume} [vol]
* @returns {String}
* @returns {Promise<string>}
*/
export default function convertToBase64(filePath, vol = fs) {
export default async function convertToBase64(filePath, vol = fs) {
if (typeof filePath !== 'string') throw new Error('Token filePath name must be a string');

const body = /** @type {string} */ (
vol.readFileSync(resolve(filePath, vol.__custom_fs__), 'utf-8')
);
return btoa(body);
// typecast to Buffer because we know that without specifying encoding, this returns a Buffer
// @ts-expect-error requires encoding options, this is a mistake in memfs types definition
const body = /** @type {Buffer} */ (vol.readFileSync(resolve(filePath, vol.__custom_fs__)));
return toBase64(body);
}
1 change: 1 addition & 0 deletions lib/utils/isNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isNode = typeof window === 'undefined';
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions snapshot-plugin/chai-wtr-snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
* e.g. with https://www.npmjs.com/package/pretty-format (right now we only escape tildes)
*/

import { isNode } from '../lib/utils/isNode.js';

// Exclude from code coverage since this is just a devtool
/* c8 ignore start */

/**
* @typedef {import('@types/chai')} Chai
*/

const isBrowser = typeof window === 'object';

async function blobToDataUrl(blob) {
let buffer = Buffer.from(await blob.text());
return 'data:' + blob.type + ';base64,' + buffer.toString('base64');
Expand Down Expand Up @@ -75,7 +75,7 @@ export function chaiWtrSnapshot(chai, utils) {
let updateSnapshots = false;
let currentSnapshot;
let name;
if (isBrowser) {
if (!isNode) {
// WTR ENV
const { getSnapshot, getSnapshotConfig } = await import('@web/test-runner-commands');
// eslint-disable-next-line no-undef
Expand Down Expand Up @@ -110,7 +110,7 @@ export function chaiWtrSnapshot(chai, utils) {
);
}
} else if (currentSnapshot !== snapshot) {
if (isBrowser) {
if (!isNode) {
const { saveSnapshot: saveSnapshotWTR } = await import('@web/test-runner-commands');
await saveSnapshotWTR({ name, content: snapshot });
} else {
Expand Down Expand Up @@ -148,7 +148,7 @@ export function chaiWtrSnapshot(chai, utils) {
},
};

if (isBrowser) {
if (!isNode) {
return;
}

Expand Down