@tokens-studio/sdk
Version:
The official SDK for Tokens Studio
199 lines (197 loc) • 7.15 kB
JavaScript
import { parse } from 'acorn';
import { asyncWalk } from 'estree-walker';
import * as prettier from 'prettier/standalone';
import * as parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
async function analyzeDependencies(code) {
const dependencies = [];
const ast = parse(code, {
allowImportExportEverywhere: true,
ecmaVersion: 'latest',
});
// @ts-expect-error mismatch between acorn and estree type for Program
await asyncWalk(ast, {
enter: async (node) => {
if (node.type === 'ImportDeclaration') {
const source = `${node.source.value}`;
dependencies.push({
source: source,
specifiers: node.specifiers.map((spec) => ({
name: spec.local.name,
default: spec.type === 'ImportDefaultSpecifier',
})),
package: source
.split('/')
.slice(0, source.startsWith('@') ? 2 : 1)
.join('/'),
});
}
},
});
return dependencies;
}
function getImportsStrMap(dependencies, hasThemes) {
const addToDeps = (dependencies, { package: pkg, source, specifiers }) => {
const foundSource = dependencies.find((dep) => dep.source === source);
if (foundSource) {
// add to specifiers and dedupe
foundSource.specifiers = [
...new Set([...foundSource.specifiers, ...specifiers]),
];
}
else {
dependencies.push({
source,
package: pkg,
specifiers,
});
}
};
// if there's theming, we will import permutateThemes, so add to dependencies
if (hasThemes) {
addToDeps(dependencies, {
package: '@tokens-studio/sd-transforms',
source: '@tokens-studio/sd-transforms',
specifiers: [
{
name: 'permutateThemes',
default: false,
},
],
});
addToDeps(dependencies, {
package: 'node:fs',
source: 'node:fs',
specifiers: [
{
name: 'readFileSync',
default: false,
},
],
});
addToDeps(dependencies, {
package: 'node:path',
source: 'node:path',
specifiers: [
{
name: 'path',
default: true,
},
],
});
}
const importsStrMap = new Map();
// convert our dependencies array to a ESM imports string
dependencies.forEach((dep) => {
importsStrMap.set(dep.source, {
namedImportsStr: dep.specifiers
.filter((spec) => !spec.default)
.map((spec) => spec.name)
.join(', '),
defaultImportStr: dep.specifiers.find((spec) => spec.default)?.name,
});
});
return importsStrMap;
}
function getDepsESMString(dependencies, hasThemes) {
const importsStrMap = getImportsStrMap(dependencies, hasThemes);
return Array.from(importsStrMap)
.map(([source, data]) => {
const { defaultImportStr, namedImportsStr } = data;
// named, default, or both:
// import foo, { named } from 'bar';
// import foo from 'bar';
// import { named } from 'bar';
return `import ${defaultImportStr ? `${namedImportsStr ? `${defaultImportStr}, ` : defaultImportStr}` : ''}${namedImportsStr ? `{ ${namedImportsStr} }` : ''} from '${source}';`;
})
.join('\n');
}
// replace {theme} placeholder with ${name}
// handle "" to become ``, otherwise ${} doesn't work
function handleThemePlaceholder(str) {
const reg = /(:\s*?)"(.*){theme}(.*)"/g;
return str.replace(reg, (_match, colonPlusSpace, w1, w2) => {
return `${colonPlusSpace}\`${w1}\${name}${w2}\``;
});
}
// make it relative to the script's module location
function handleBuildPath(cfg) {
if (cfg.platforms) {
Object.entries(cfg.platforms).forEach(([key, platform]) => {
if (platform.buildPath) {
cfg.platforms[key].buildPath =
`\`\${path.resolve(import.meta.dirname, '${platform.buildPath}')}/\``;
}
});
}
}
function handleBuildPathString(cfg) {
// remove wrapping "" around the buildPath values, as a consequence of JSON stringifying the object
const matches = [...cfg.matchAll(/"buildPath": ("`.+?`")/g)];
matches.forEach((match) => {
if (match[1]) {
cfg = cfg.replace(match[1], match[1].slice(1, match[1].length - 1));
}
});
return cfg;
}
function handleConfig(cfg, hasThemes = false) {
if (hasThemes) {
delete cfg.source;
}
handleBuildPath(cfg);
let str = handleBuildPathString(JSON.stringify(cfg, null, '\t'));
if (hasThemes) {
str = str.replace('{', `{ source: tokensets.map(tokenset => path.resolve(import.meta.dirname, 'tokens', \`\${tokenset}.json\`)),`);
str = str.replace(`"filter": "enabled-sets-from-themes",`, '');
str = handleThemePlaceholder(str);
}
return str;
}
export async function combineFunctionsConfig(configsArtifact) {
const hasThemes = !!configsArtifact.themeOptions;
const dependencies = await analyzeDependencies(configsArtifact.functions);
// regex that covers imports
const reg = /import[\S\s]+?(from)\s+?['"].+?['"];?/g;
const functionsWithoutImports = configsArtifact.functions.replace(reg, '');
const cfg = handleConfig(configsArtifact.config, hasThemes);
const functionsContent = `// Note: make sure you install the dependencies used in these imports
${getDepsESMString(dependencies, hasThemes)}
${functionsWithoutImports.trim()}`;
let newFileContent = `/**
* DO NOT EDIT. This file was automatically generated, and any changes
* you make here will probably be overwritten.
* Feel free to copy this to another location for you to maintain yourself.
*/
${functionsContent}
`;
const buildString = `// optionally, cleanup files first..
await sd.cleanAllPlatforms();
await sd.buildAllPlatforms();`;
if (hasThemes) {
newFileContent += `
const $themes = JSON.parse(
readFileSync(path.resolve(import.meta.dirname, '$themes.json'), 'utf-8')
);
const themes = permutateThemes($themes, { separator: '_' });
const configs = Object.entries(themes).map(([name, tokensets]) => (${cfg}));
for (const cfg of configs) {
const sd = new StyleDictionary(cfg);
${buildString}
}
`;
}
else {
newFileContent += `
const sd = new StyleDictionary(${cfg});
${buildString}
`;
}
const formattedCode = await prettier.format(newFileContent, {
parser: 'babel',
plugins: [prettierPluginEstree, parserBabel],
singleQuote: true,
});
return formattedCode;
}
//# sourceMappingURL=combine-functions-config.js.map