diff --git a/.changeset/hungry-phones-bow.md b/.changeset/hungry-phones-bow.md new file mode 100644 index 000000000..80d42e512 --- /dev/null +++ b/.changeset/hungry-phones-bow.md @@ -0,0 +1,5 @@ +--- +'style-dictionary': patch +--- + +Fix `convertToBase64` util to support converting binary files such as fonts, for both Browser and NodeJS. diff --git a/__tests__/__helpers.js b/__tests__/__helpers.js index 751cd0e3b..a3c2a96ad 100644 --- a/__tests__/__helpers.js +++ b/__tests__/__helpers.js @@ -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 @@ -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 () { diff --git a/__tests__/__setup.js b/__tests__/__setup.js index 570a1a79c..8a32fdb99 100644 --- a/__tests__/__setup.js +++ b/__tests__/__setup.js @@ -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 @@ -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(); } diff --git a/__tests__/formats/lessIcons.test.js b/__tests__/formats/lessIcons.test.js index 272c31bf6..a691247a8 100644 --- a/__tests__/formats/lessIcons.test.js +++ b/__tests__/formats/lessIcons.test.js @@ -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/', @@ -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; diff --git a/__tests__/formats/lessVariables.test.js b/__tests__/formats/lessVariables.test.js index f4d64d7fc..ffa9454f0 100644 --- a/__tests__/formats/lessVariables.test.js +++ b/__tests__/formats/lessVariables.test.js @@ -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/', @@ -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; diff --git a/__tests__/register/overwrite.test.js b/__tests__/register/overwrite.test.js index edaaf1f18..5badd68e8 100644 --- a/__tests__/register/overwrite.test.js +++ b/__tests__/register/overwrite.test.js @@ -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 = () => { @@ -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'); diff --git a/__tests__/utils/__snapshots__/convertToBase64.test.snap.js b/__tests__/utils/__snapshots__/convertToBase64.test.snap.js new file mode 100644 index 000000000..45e4c3c64 --- /dev/null +++ b/__tests__/utils/__snapshots__/convertToBase64.test.snap.js @@ -0,0 +1,11 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["utils convertToBase64 should convert binary files 1"] = +`77+9UE5HDQoaCgAAAA1JSERSAAAAIgAAABQIAgAAAGcGeCUAAAAJcEhZcwAACxMAAAsTAQDvv73vv70YAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ce+/vWU8AAAB77+9SURBVHjvv71i77+9Mk1gIBpofH5fee+/vSwD6YCF77+977+977+977+9Ljnvv710cmzvv73vv73vv70v77+977+9Hjw377+9Ozfvv71077+977+977+9ES8P14bvv71H77+977+977+9TXPNjDTvv70kRc6v77+977+977+9YEnvv70177+9QO+/vQAt77+9UO+/vd2w77+977+977+977+977+977+977+977+977+9czfvv73vv70N77+9HTwP77+977+9MO+/ve+/vT5/O33vv70G77+9TQvvv73vv73vv73vv70b77+9EQE+77+9He+/vcud77+9De+/vXzvv73vv73vv71F77+977+9Zm8EWgzvv71u77+9chTvv73vv73vv71bDu+/vQDvv71s77+977+9JTcd77+9yLDvv73vv73vv71P77+977+977+9F9mp77+977+9d++/vQJ6Lu+/ve+/vRpoR3jvv73vv73vv73vv73vv73vv709fwtkAO+/ve+/vT7vv71ANUAF77+9X++/vTrvv73vv70h77+9GgZX77+977+9T++/ve+/ve+/vWd077+9OXB277+977+9w7kl77+977+977+977+9DyTvv71s77+9CFAcKFvvv704Z++/vQ3vv73vv73vv70Ecu+/ve+/vQVAce+/ve+/vSYH77+977+977+9K3YDbQXvv73vv73vv73vv70bKwojN2w5Cu+/vTo7GHbvv70t77+9Su+/vT9977+95r6D77+9BF/vv71s77+977+9QU4SMHfvv70C77+9Ae+/vQPvv71lQD4w77+977+9Ru+/vT1wXlpKGO+/vX3vv73sranvv706UBbvv73MgFxgSiPfmgBvG2Dvv73fuO+/ve+/ve+/ve+/vUtKUhgYJe+/vWrvv73Pnu+/vQFKA00H77+9A2gl77+9Pu+/vcinL++/ve+/vUxp77+9P38D77+9Du+/vRnvv73vv70aQO+/ve+/vTfvv71TA++/ve+/vQjvv70LDDfvv71f77+977+977+914Pvv73vv70a77+977+977+9N3QoBRjvv73JgO+/ve+/ve+/ve+/ve+/vUAg77+9N++/vRlk77+9Ye+/vWjvv73vv71hzafvv73vv73vv71gDRMDXQDvv70CDSDvv70AW00eVs6JSmYAAAAASUVORO+/vUJg77+9`; +/* end snapshot utils convertToBase64 should convert binary files 1 */ + +snapshots["utils convertToBase64 should convert binary files 2"] = +`iVBORw0KGgoAAAANSUhEUgAAACIAAAAUCAIAAABnBnglAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAelJREFUeNpi1DJNYCAaaHx+X3njLAPpgIWXl6u1LjmvdHJshNvnL9/2Hjw3uTs3t3Sys70RLw/Xhq1HgLLVTXPNjDSkJEXOr97O62BJhjWMQN8ALdBQk92w9ai0pIipkfrpczed7A33HTwPlIYwgOKfPn87fe4G0E0LppeT4xugEQE+1h39y53tDYF8oKHlRZHTZm8EWgzkbtxyFMjt7FsOtACobMvEJTcdw8iwhpmDT+3+wxfZqf7bd50Cei7A1xpoR3iww8XL9549fwtkALn+PtZANUAFnF+/OrL8IcMaBle/kk+fvsZndOw5cHb95sO5JZP+//8PJIFsoAhQHChb1Thn6qwNi5bvBHL/kwVAcaOhJgeM58UrdgNtBYbM1NkbKwojN2w5CuQ6Oxh29C3PSvU/ffbmvoPnBF+9bOL4QU4SMHfKAtoBjAOgZUA+MKqBRu89cF5aShjIffrsramxOlAWmMyAXGBKI9+aAG8bYITfuPWYj5dLSlIYGCXqarLPnr8BSgNNB/oDaCXQPqDIpy/fyExpnz9/A5oOzBmmxhpA/umzN7JTA4DZCJkLDDegX4GJ7deDx88a+8nMN3QoBRiByYCB9oCFvEAg2TenGWToYc1ooJFhzaf9x+hgDRMDXQCdAg0gwABbTR5WzolKZgAAAABJRU5ErkJggg==`; +/* end snapshot utils convertToBase64 should convert binary files 2 */ + diff --git a/__tests__/utils/convertToBase64.test.js b/__tests__/utils/convertToBase64.test.js index 796c3e452..962094534 100644 --- a/__tests__/utils/convertToBase64.test.js +++ b/__tests__/utils/convertToBase64.test.js @@ -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; } @@ -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); + } + }); }); }); diff --git a/lib/StyleDictionary.js b/lib/StyleDictionary.js index e21df538d..5ce4f3911 100644 --- a/lib/StyleDictionary.js +++ b/lib/StyleDictionary.js @@ -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 @@ -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; } diff --git a/lib/resolve.js b/lib/resolve.js index 6434e49d0..bdf8bcd57 100644 --- a/lib/resolve.js +++ b/lib/resolve.js @@ -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) @@ -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); diff --git a/lib/utils/combineJSON.js b/lib/utils/combineJSON.js index 334a45bab..dd1d36e14 100644 --- a/lib/utils/combineJSON.js +++ b/lib/utils/combineJSON.js @@ -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 @@ -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(/^\//, '')); @@ -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; } diff --git a/lib/utils/convertToBase64.js b/lib/utils/convertToBase64.js index ca8b1dce6..77e6df438 100644 --- a/lib/utils/convertToBase64.js +++ b/lib/utils/convertToBase64.js @@ -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} + */ +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} */ -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); } diff --git a/lib/utils/isNode.js b/lib/utils/isNode.js new file mode 100644 index 000000000..c6723408d --- /dev/null +++ b/lib/utils/isNode.js @@ -0,0 +1 @@ +export const isNode = typeof window === 'undefined'; diff --git a/snapshot-plugin/chai-wtr-snapshot.js b/snapshot-plugin/chai-wtr-snapshot.js index 15e5b3276..1c45dc3eb 100644 --- a/snapshot-plugin/chai-wtr-snapshot.js +++ b/snapshot-plugin/chai-wtr-snapshot.js @@ -4,6 +4,8 @@ * 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 */ @@ -11,8 +13,6 @@ * @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'); @@ -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 @@ -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 { @@ -148,7 +148,7 @@ export function chaiWtrSnapshot(chai, utils) { }, }; - if (isBrowser) { + if (!isNode) { return; }