From 93cff43580f5ccb016044445632a09281c2fdc0c Mon Sep 17 00:00:00 2001 From: castastrophe Date: Mon, 24 Feb 2025 18:01:43 -0500 Subject: [PATCH] fix(tokens): support style-dictionary build in windows env --- .gitattributes | 1 + .github/workflows/build.yml | 7 +- .github/workflows/development.yml | 10 +- .storybook/blocks/utilities.js | 4 +- .storybook/package.json | 32 +- nx.json | 9 +- package.json | 9 +- plugins/postcss-add-theming-layer/index.js | 2 +- plugins/stylelint-no-missing-var/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../stylelint-theme-alignment/package.json | 2 +- postcss.config.js | 2 +- tasks/component-builder.js | 32 +- ...ompiled-output.js => component-compare.js} | 344 ++- tasks/component-reporter.js | 9 + tasks/templates/compare-listing.njk | 12 +- tasks/templates/diff-preview.njk | 12 +- tasks/utilities.js | 144 +- tokens/dist/json/tokens.json | 6 + tokens/package.json | 13 +- tokens/postcss.config.js | 8 +- tokens/project.json | 35 +- tokens/style-dictionary.config.js | 135 +- tokens/tasks/token-rollup.js | 45 +- tokens/utilities/attribute-sets-transform.js | 12 + .../utilities/css-font-open-type-transform.js | 46 + tokens/utilities/css-sets-formatter.js | 64 + tokens/utilities/data-json-formatter.js | 26 +- tokens/utilities/index.js | 5 + tokens/utilities/json-sets-formatter.js | 51 + tokens/utilities/name-kebab-transform.js | 8 + tokens/utilities/style-dictionary.utils.js | 89 - tools/bundle/postcss.config.js | 4 +- tools/bundle/project.json | 34 +- tools/bundle/src/index.css | 2 +- tools/bundle/src/index.css.json | 1 - tools/bundle/tasks/bundler.js | 2 +- yarn.lock | 2536 ++++++++++------- 39 files changed, 2300 insertions(+), 1459 deletions(-) rename tasks/{compare-compiled-output.js => component-compare.js} (59%) create mode 100644 tokens/utilities/attribute-sets-transform.js create mode 100644 tokens/utilities/css-font-open-type-transform.js create mode 100644 tokens/utilities/css-sets-formatter.js create mode 100644 tokens/utilities/index.js create mode 100644 tokens/utilities/json-sets-formatter.js create mode 100644 tokens/utilities/name-kebab-transform.js delete mode 100644 tokens/utilities/style-dictionary.utils.js delete mode 100644 tools/bundle/src/index.css.json diff --git a/.gitattributes b/.gitattributes index c9305786367..8b569f7d937 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ +* text=auto eol=lf README.md merge=ours # Treat yarn assets as binaries for diffing diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6fccd0c38f3..b37e2f87880 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -150,10 +150,15 @@ jobs: echo "Changes detected" git status git add . - git diff > changes.diff exit 1 fi + # If there are changes, capture the changes and upload them as an artifact + - name: Capture changes + if: ${{ failure() }} + id: capture-changes + run: git diff --staged > changes.diff + - name: Upload changes if: ${{ failure() }} uses: actions/upload-artifact@v4 diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 6924bb4dfbc..2c138c1a76c 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -52,9 +52,15 @@ jobs: system: - macos-latest - ubuntu-latest - # - windows-latest # todo: debug token style-dictionary failures on windows node-version: - 20 + # experimental: + # - false + # include: + # - system: windows-latest + # experimental: true + # - system: windows-latest + # experimental: true uses: ./.github/workflows/build.yml with: system: ${{ matrix.system }} @@ -190,7 +196,7 @@ jobs: # ------------------------------------------------------------- vrt: name: Testing - if: contains(github.event.pull_request.labels.*.name, 'run_vrt') || ((github.event.pull_request.draft != true || contains(github.event.pull_request.labels.*.name, 'run_ci')) && github.event.pull_request.mergeable == true) + if: ${{ contains(github.event.pull_request.labels.*.name, 'run_vrt') || ((github.event.pull_request.draft != true || contains(github.event.pull_request.labels.*.name, 'run_ci')) && github.event.pull_request.mergeable == true) }} uses: ./.github/workflows/vrt.yml with: skip: ${{ github.base_ref == 'spectrum-two' || contains(github.event.pull_request.labels.*.name, 'skip_vrt') }} diff --git a/.storybook/blocks/utilities.js b/.storybook/blocks/utilities.js index 204cb83a2e6..b5ba302ccd7 100644 --- a/.storybook/blocks/utilities.js +++ b/.storybook/blocks/utilities.js @@ -93,7 +93,7 @@ export function fetchToken(key, fallback = undefined, presets = {}) { // Check if the spectrum data is available if (!tokens || typeof tokens !== "object") return fallback; - return parseData(tokens[key], { color, platform }) ?? fallback; + return parseData(tokens?.[key], { color, platform }) ?? fallback; } /** @@ -110,7 +110,7 @@ export function fetchTokenSet(key, presets = {}) { } // Fetch the theme if it exists; this data exists if wrapped in a ThemeProvider - const { color, platform, tokens } = fetchTheme(presets); + const { color, platform, tokens = {} } = fetchTheme(presets); // Check the token data for a set of tokens matching the provided regex const tokenSet = Object.keys(tokens) diff --git a/.storybook/package.json b/.storybook/package.json index 34e088be049..30ab7f9cddf 100644 --- a/.storybook/package.json +++ b/.storybook/package.json @@ -49,21 +49,21 @@ "@babel/core": "^7.26.0", "@chromatic-com/storybook": "^3.2.3", "@etchteam/storybook-addon-status": "^5.0.0", - "@storybook/addon-a11y": "^8.4.7", - "@storybook/addon-actions": "^8.4.7", - "@storybook/addon-designs": "^8.0.4", - "@storybook/addon-docs": "^8.4.7", - "@storybook/addon-essentials": "^8.4.7", - "@storybook/addon-interactions": "^8.4.7", - "@storybook/blocks": "^8.4.7", - "@storybook/builder-vite": "^8.4.7", - "@storybook/components": "^8.4.7", - "@storybook/core-events": "^8.4.7", - "@storybook/manager-api": "^8.4.7", - "@storybook/preview-api": "^8.4.7", - "@storybook/test-runner": "^0.21.0", - "@storybook/theming": "^8.4.7", - "@storybook/web-components-vite": "^8.4.7", + "@storybook/addon-a11y": "8.4.7", + "@storybook/addon-actions": "8.4.7", + "@storybook/addon-designs": "^8.2.1", + "@storybook/addon-docs": "8.4.7", + "@storybook/addon-essentials": "8.4.7", + "@storybook/addon-interactions": "8.4.7", + "@storybook/blocks": "8.4.7", + "@storybook/builder-vite": "8.4.7", + "@storybook/components": "8.4.7", + "@storybook/core-events": "8.4.7", + "@storybook/manager-api": "8.4.7", + "@storybook/preview-api": "8.4.7", + "@storybook/test-runner": "^0.22.0", + "@storybook/theming": "8.4.7", + "@storybook/web-components-vite": "8.4.7", "@whitespace/storybook-addon-html": "^6.1.1", "chromatic": "^11.22.2", "lit": "^3.2.1", @@ -76,7 +76,7 @@ "react-syntax-highlighter": "^15.6.1", "remark-gfm": "^4.0.0", "rollup-plugin-postcss-lit": "^2.1.0", - "storybook": "^8.4.7", + "storybook": "8.4.7", "vite": "^5.4.11" }, "keywords": [ diff --git a/nx.json b/nx.json index 73cde6cb2af..1e65f08b5bf 100644 --- a/nx.json +++ b/nx.json @@ -89,20 +89,20 @@ "cross-env NODE_OPTIONS=\"--no-warnings\" node -e 'require(\"./tasks/component-builder.js\").default()'" ] }, - "outputs": ["{projectRoot}/dist/*.css", "{projectRoot}/dist/*.map"] + "outputs": ["{projectRoot}/dist/*.css*", "{projectRoot}/dist/**/*.css*"] }, "clean": { "cache": true, "executor": "nx:run-commands", - "inputs": ["{projectRoot}/dist/*.css", "{projectRoot}/dist/*.map", { "externalDependencies": ["rimraf"] }], + "inputs": ["{projectRoot}/dist/*.css*", "{projectRoot}/dist/**/*.css*", { "externalDependencies": ["rimraf"] }], "options": { - "commands": ["rimraf {projectRoot}/dist/*.css {projectRoot}/dist/*.map"] + "commands": ["rimraf {projectRoot}/dist/*.css* {projectRoot}/dist/**/"] }, "outputs": ["{projectRoot}/dist"] }, "compare": { "cache": true, - "dependsOn": [{ "projects": "bundle", "target": "build" }], + "dependsOn": ["build", { "projects": ["bundle"], "target": "build" }], "executor": "nx:run-commands", "inputs": [ "{workspaceRoot}/tasks/compare-compiled-output.js", @@ -217,6 +217,7 @@ } }, "validate": { + "cache": true, "executor": "nx:run-commands", "inputs": [ "{workspaceRoot}/schemas/metadata.schema.json", diff --git a/package.json b/package.json index 199db0c520d..4bcca63f213 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "clean:env": "rm -rf .env .storybook/chromatic.config.json", "clean:preview": "nx clean storybook --skip-nx-cache", "cleaner": "nx run-many --target clean --projects", - "compare": "cross-env NODE_ENV=production node --no-warnings ./tasks/compare-compiled-output.js", + "compare": "cross-env NODE_ENV=production yarn bundle && yarn component:compare", "component:build": "node --no-warnings ./tasks/component-builder.js", "component:compare": "node --no-warnings ./tasks/component-compare.js", "component:report": "node --no-warnings ./tasks/component-reporter.js", @@ -70,7 +70,6 @@ "@changesets/cli": "^2.27.11", "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", - "@csstools/postcss-bundler": "^2.0.6", "@nx/devkit": "^19.8.2", "@spectrum-tools/postcss-add-theming-layer": "1.0.2", "@spectrum-tools/postcss-property-rollup": "0.0.1", @@ -117,9 +116,9 @@ "prettier": "^3.5.3", "rimraf": "^6.0.1", "semver": "^7.7.1", - "stylelint": "^16.15.0", - "stylelint-config-standard": "^36.0.1", - "stylelint-header": "^2.1.0", + "stylelint": "^16.18.0", + "stylelint-config-standard": "^38.0.0", + "stylelint-header": "^3.0.0", "stylelint-high-performance-animation": "^1.11.0", "stylelint-order": "^6.0.4", "stylelint-selector-bem-pattern": "^4.0.1", diff --git a/plugins/postcss-add-theming-layer/index.js b/plugins/postcss-add-theming-layer/index.js index fa9904a5801..38c11bb69f1 100644 --- a/plugins/postcss-add-theming-layer/index.js +++ b/plugins/postcss-add-theming-layer/index.js @@ -17,7 +17,7 @@ const { extractFallbackValue, getVariableName, checkForReplacement -} = require("./utilities"); +} = require("./utilities.js"); /** * @typedef Options diff --git a/plugins/stylelint-no-missing-var/package.json b/plugins/stylelint-no-missing-var/package.json index dff26264bed..d89d35a49a9 100644 --- a/plugins/stylelint-no-missing-var/package.json +++ b/plugins/stylelint-no-missing-var/package.json @@ -35,7 +35,7 @@ "devDependencies": { "ava": "^6.2.0", "c8": "^10.1.3", - "stylelint": "^16.15.0" + "stylelint": "^16.18.0" }, "keywords": [ "css", diff --git a/plugins/stylelint-no-unknown-custom-properties/package.json b/plugins/stylelint-no-unknown-custom-properties/package.json index 23ec67b8c12..a48345f406f 100644 --- a/plugins/stylelint-no-unknown-custom-properties/package.json +++ b/plugins/stylelint-no-unknown-custom-properties/package.json @@ -38,7 +38,7 @@ "devDependencies": { "ava": "^6.2.0", "c8": "^10.1.3", - "stylelint": "^16.15.0" + "stylelint": "^16.18.0" }, "keywords": [ "css", diff --git a/plugins/stylelint-no-unused-custom-properties/package.json b/plugins/stylelint-no-unused-custom-properties/package.json index a7889f144a7..c5289adb889 100644 --- a/plugins/stylelint-no-unused-custom-properties/package.json +++ b/plugins/stylelint-no-unused-custom-properties/package.json @@ -37,7 +37,7 @@ "devDependencies": { "ava": "^6.2.0", "c8": "^10.1.3", - "stylelint": "^16.15.0" + "stylelint": "^16.18.0" }, "keywords": [ "css", diff --git a/plugins/stylelint-theme-alignment/package.json b/plugins/stylelint-theme-alignment/package.json index 38e269695b0..efbc9fe7677 100644 --- a/plugins/stylelint-theme-alignment/package.json +++ b/plugins/stylelint-theme-alignment/package.json @@ -32,7 +32,7 @@ "stylelint": ">=16" }, "devDependencies": { - "stylelint": "^16.15.0" + "stylelint": "^16.18.0" }, "keywords": [ "css", diff --git a/postcss.config.js b/postcss.config.js index d30bb25de04..71b276317e7 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -51,7 +51,7 @@ module.exports = ({ const filePath = packageParts.length > 2 ? packageParts.slice(2).join("/") : "index.css"; if (packageParts[1] === "tokens") { - return join(__dirname, packageParts[1], "dist", "css", filePath); + return join(__dirname, packageParts[1], filePath); } return join(__dirname, "components", packageParts[1], filePath); diff --git a/tasks/component-builder.js b/tasks/component-builder.js index 93afb637693..5a388b0c2fe 100644 --- a/tasks/component-builder.js +++ b/tasks/component-builder.js @@ -17,6 +17,9 @@ const fs = require("fs"); const fsp = fs.promises; const path = require("path"); +const { hideBin } = require("yargs/helpers"); +const yargs = require("yargs"); + const { deflate } = require("zlib"); const { promisify } = require("util"); @@ -37,6 +40,12 @@ const { writeAndReport, } = require("./utilities.js"); +const report = { + failure: (message) => `${"✗".red} ${message}`, + warning: (message) => `${"⚠".yellow} ${message}`, + success: (message) => `${"✓".green} ${message}`, +}; + /** * Process the provided CSS input and write out to a file * @param {string} content @@ -108,7 +117,7 @@ async function processCSS( if (result.warnings().length > 0) { /** @todo, do we want to support a verbose mode that prints out the warnings during the build? */ result.warnings().forEach((warning) => { - logs.push(`${"⚠".yellow} ${warning.text}`); + logs.push(report.warning(warning.text)); }); } @@ -131,13 +140,12 @@ async function processCSS( if (!output) return Promise.resolve(formatted); /* Ensure the directory exists */ - if (!fs.existsSync(path.dirname(output))) { - await fsp.mkdir(path.dirname(output), { recursive: true }).catch((err) => { + const outputDir = path.dirname(output); + if (!fs.existsSync(outputDir)) { + await fsp.mkdir(outputDir, { recursive: true }).catch((err) => { if (!err) return; - logs.push( - `${"✗".red} problem making the ${relativePrint(path.dirname(output), { cwd }).yellow} directory`, - ); + logs.push(report.failure(`problem making the ${relativePrint(outputDir, { cwd })} directory`)); return Promise.reject([...logs, err]); }); } @@ -287,6 +295,12 @@ async function main({ cwd = path.join(dirs.components, componentName); } + if (!fs.existsSync(cwd)) { + return Promise.resolve( + report.failure(`Component directory not found at ${relativePrint(cwd)}`) + ); + } + if (!componentName) { componentName = cwd ? getPackageFromPath(cwd) @@ -345,3 +359,9 @@ async function main({ exports.processCSS = processCSS; exports.fetchContent = fetchContent; exports.default = main; + +let { + _: components, +} = yargs(hideBin(process.argv)).argv; + +Promise.all(components.map((componentName) => main({ componentName }))); diff --git a/tasks/compare-compiled-output.js b/tasks/component-compare.js similarity index 59% rename from tasks/compare-compiled-output.js rename to tasks/component-compare.js index 37b534c4251..213627b2ac5 100644 --- a/tasks/compare-compiled-output.js +++ b/tasks/component-compare.js @@ -1,6 +1,6 @@ -const { existsSync, statSync, readdirSync, mkdirSync } = require("fs"); -const { readFile, writeFile } = require("fs").promises; -const { join, relative, dirname } = require("path"); +const { existsSync, statSync } = require("fs"); +const { readFile } = require("fs").promises; +const { join, relative, dirname, basename, extname } = require("path"); const fg = require("fast-glob"); const tar = require("tar"); @@ -9,7 +9,6 @@ const _ = require("lodash"); const nunjucks = require("nunjucks"); const env = new nunjucks.Environment(); -const { rimrafSync } = require("rimraf"); const npmFetch = require("npm-registry-fetch"); const { hideBin } = require("yargs/helpers"); @@ -18,24 +17,17 @@ const yargs = require("yargs"); const Diff = require("diff"); const Diff2Html = require("diff2html"); -const { default: builder } = require("./component-builder.js"); +const { dirs, log, bytesToSize, copy, writeConsoleTable, cleanAndMkdir, writeAndReport, getAllComponentNames } = require("./utilities.js"); require("colors"); -const pathing = { - root: join(__dirname, "../"), - components: join(__dirname, "../components"), -}; - -const bytesToSize = function (bytes) { - if (bytes === 0) return "0"; - - const sizes = ["bytes", "KB", "MB", "GB", "TB"]; - // Determine the size identifier to use (KB, MB, etc) - const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); - if (i === 0) return (bytes / 1000).toFixed(2) + " " + sizes[1]; - return (bytes / Math.pow(1024, i)).toFixed(2) + " " + sizes[i]; -}; +/** + * A global object to store the paths to the output directories + * this facilitates a shared location for all the tasks to write + * without having to pass the paths around as arguments. + * @type {{ [key: string]: string }} + */ +const pathing = {}; env.addFilter("bytesToSize", bytesToSize); env.addFilter("print", (data) => JSON.stringify(data, null, 2)); @@ -47,39 +39,13 @@ env.addFilter("hasChange", (data) => { }, false); }); -const log = { - error: (err) => process.stderr.write(`${err}\n\n`), - write: (msg) => process.stdout.write(msg), - writeTable: (data, { min = 20, max = 30 } = {}) => { - // This utility function is used to print a table of data to the console - const table = (data = []) => { - return data.map((row, idx) => `${row ?? " "}`.padEnd(idx === 0 ? max : min)).join(""); - }; - - process.stdout.write(`${table(data)}\n`); - }, -}; - -const cleanAndMkdir = (path, clean = true) => { - if (!path) return; - - let isFile = false; - - // If the output directory exists, delete it but don't throw an error if it doesn't - if (clean && existsSync(path)) { - isFile = statSync(path).isFile(); - rimrafSync(path, { preserveRoot: true }); - } - - // Create the output directory fresh - mkdirSync(isFile ? dirname(path) : path, { recursive: true }); -}; - -const allComponents = - readdirSync(pathing.components, { withFileTypes: true }) - ?.filter((file) => file.isDirectory()) - .map((file) => file.name) ?? []; - +/** + * A focused function that handles the template rendering + * @param {string} templateFile + * @param {Object} templateVariables + * @param {string} [outputPath=undefined] + * @returns {Promise} + */ async function renderTemplate( templateFile, templateVariables = {}, @@ -101,16 +67,24 @@ async function renderTemplate( // Generate an HTML summary of the component's compiled assets with links to the HTML diffs of each file const html = env.renderString(template, { - allComponents, + allComponents: getAllComponentNames(false), nav, ...templateVariables, }); if (typeof outputPath === "undefined") return html; - return writeFile(outputPath, html, { encoding: "utf-8" }); + return writeAndReport(html, outputPath); } +/** + * This function generates a visual diff of the compiled assets for a component + * @param {Object} config + * @param {string[]} config.filepaths + * @param {string} config.outputPath + * @param {Map} config.fileMap + * @returns {ReturnType} + */ async function generateDiff({ filepaths, outputPath, @@ -123,9 +97,23 @@ async function generateDiff({ const { local, npm } = data; if (!npm?.content || !local?.content) return; + const extension = extname(npm.path); + const patch = Diff.createPatch(file, npm.content, local.content); - const css = Diff.diffCss(npm.content, local.content); - data.hasDiff = css.length > 1; + + let diff; + switch(extension) { + case ".css": + diff = Diff.diffCss(npm.content, local.content); + break; + case ".json": + diff = Diff.diffJson(npm.content, local.content); + break; + default: + diff = Diff.diffChars(npm.content, local.content); + } + + data.hasDiff = diff.length > 1; // Reflect if the component has a diff to the data fileMap.set(file, data); @@ -150,10 +138,68 @@ async function generateDiff({ ...renderData, html, }, - join(outputPath, `index.html`) + join(outputPath, "index.html") ); } +async function fetchPublishedComponent(packageName, { + cacheLocation, + outputLocation, +}) { + const warnings = []; + let tag; + + // Check if the component exists on npm; do not fail if it isn't found - + // report it and output only the sizes of the local compiled assets + const npmData = + (await npmFetch.json(packageName).catch((err) => { + // @todo: do we need to report on the error messages returned? + warnings.push(err ?? `Failed to fetch ${packageName.yellow} from npm.\n`); + })) ?? {}; + + // If the component exists on npm, fetch the latest release data + // @todo what is the fallback if there isn't a latest tag? + if (npmData["dist-tags"]?.latest) { + tag = npmData["dist-tags"]?.latest; + } + + if (!tag) return; + + // Check locally to see if we have already fetched the tarball + // for this tag; if not, fetch it and extract it + const tarballPath = join(cacheLocation, `${packageName?.split("/")?.[1] ?? packageName}-${tag}.tgz`); + const tarballUrl = npmData.versions?.[tag]?.dist?.tarball; + if (!existsSync(tarballPath) && tarballUrl) { + // Here is where we check the cached packages folder for the tarball + // so we don't have to re-fetch it every time + const tarballFile = await npmFetch(tarballUrl).catch(() => {}); + if ( + !tarballFile || + (tarballFile.status && tarballFile.status !== 200) + ) { + log.error(`Failed to fetch release content for ${packageName}`); + } + else { + await writeAndReport(await tarballFile.buffer(), tarballPath); + } + } + + // The tarball path should exist locally now; if not, something went wrong + if (existsSync(tarballPath)) { + await tar + .extract({ + file: tarballPath, + cwd: outputLocation, + // Only unpack the dist folder + filter: (path) => path.startsWith("package/dist"), + strip: 2, + }) + .catch((err) => warnings.push(err)); + } + + return { tag, warnings }; +} + async function processComponent( component, { @@ -164,12 +210,6 @@ async function processComponent( ) { if (!component) return Promise.reject("No component specified."); - // Build the component fresh - await builder({ - componentName: component, - clean: true, - }); - cleanAndMkdir(join(output, "diffs", component)); cleanAndMkdir(join(pathing.latest, component)); @@ -182,7 +222,6 @@ async function processComponent( cwd = dirname(pkgPath).split("/").slice(0, -1).join("/"); } - let tag; let found = 0; // Using a set, we can remove duplicates from the list of compiled assets @@ -194,77 +233,44 @@ async function processComponent( found++; // Note: component might exist locally but might not contain any compiled output - const files = - (await fg("**/*.css", { cwd: join(cwd, component, "dist") })) ?? []; - files.forEach((file) => filelist.add(file)); - } else { + for (const file of await fg("**/*", { + cwd: join(cwd, component, "dist"), + ignore: ["*.map", "*.min.*", "*.gz"], + })) { + filelist.add(file); + } + } + else { warnings.push( `${ - `${relative(pathing.root, join(cwd, component))}`.yellow + `${relative(dirs.root, join(cwd, component))}`.yellow } not found locally.\n` ); } - if (pkg && pkg.name) { - const printPkgName = pkg.name.yellow; - - // Check if the component exists on npm; do not fail if it isn't found - - // report it and output only the sizes of the local compiled assets - const npmData = - (await npmFetch.json(pkg.name).catch((err) => { - // @todo: do we need to report on the error messages returned? - warnings.push(err ?? `Failed to fetch ${printPkgName} from npm.\n`); - })) ?? {}; - - // If the component exists on npm, fetch the latest release data - // @todo what is the fallback if there isn't a latest tag? - if (npmData["dist-tags"]?.latest) { - tag = npmData["dist-tags"]?.latest; - } + let tag = "latest"; + if (pkg?.name) { + const npmResults = await fetchPublishedComponent(pkg.name, { + cacheLocation, + outputLocation: join(pathing.latest, component), + }); - if (tag) { - // Check locally to see if we have already fetched the tarball - // for this tag; if not, fetch it and extract it - const tarballPath = join(cacheLocation, `${component}-${tag}.tgz`); - const tarballUrl = npmData.versions?.[tag]?.dist?.tarball; - if (!existsSync(tarballPath) && tarballUrl) { - // Here is where we check the cached packages folder for the tarball - // so we don't have to re-fetch it every time - const tarballFile = await npmFetch(tarballUrl).catch(() => {}); - if ( - !tarballFile || - (tarballFile.status && tarballFile.status !== 200) - ) { - log.error(`Failed to fetch release content for ${pkg.name}`); - } else { - await writeFile(tarballPath, await tarballFile.buffer(), { - encoding: "utf-8", - }); - } - } + if (npmResults?.tag) { + tag = npmResults.tag; + } - // The tarball path should exist locally now; if not, something went wrong - if (existsSync(tarballPath)) { - await tar - .extract({ - file: tarballPath, - cwd: join(pathing.latest, component), - // Only unpack the dist folder - filter: (path) => path.startsWith("package/dist"), - strip: 2, - }) - .catch((err) => warnings.push(err)); - } + if (npmResults?.warnings?.length > 0) { + warnings.push(...npmResults.warnings); + } - if (existsSync(join(pathing.latest, component))) { - const files = - (await fg("**/*.css", { - cwd: join(pathing.latest, component), - })) ?? []; + if (existsSync(join(pathing.latest, component))) { + const files = + (await fg("**/*.css", { + cwd: join(pathing.latest, component), + })) ?? []; - if (files.length > 0) found++; - files.forEach((file) => filelist.add(file)); - } + if (files.length > 0) found++; + files.forEach((file) => filelist.add(file)); } } @@ -306,7 +312,6 @@ async function processComponent( } async function processFile(filename, localPath, comparePath) { - const componentName = localPath.split("/")[localPath.split("/").length - 2]; const data = {}; // Look for the file locally @@ -351,10 +356,9 @@ async function main( { skipCache = false } = {} ) { if (!components || components.length === 0) { - components = allComponents; + components = getAllComponentNames(false); } - // Strip out utilities components = components.filter(c => !["actionmenu", "commons"].includes(c)); @@ -371,13 +375,27 @@ async function main( cleanAndMkdir(pathing.latest); + const promises = []; + // Copy the bundled CSS to the output directory + const renderAssets = [ + join(dirs.root, "tools", "bundle", "dist", "index.min.css"), + join(require.resolve("diff2html"), "..", "..", "bundles", "css", "diff2html.min.css"), + join(require.resolve("diff2html"), "..", "..", "bundles", "js", "diff2html.min.js"), + ]; + + renderAssets.forEach((asset) => { + promises.push( + copy(asset, join(pathing.output, basename(asset)), { isDeprecated: false }) + ); + }); + /** * Each component will report on it's file structure locally when compared * against it's latest tag on npm; then a console report will be logged and * a visual diff generated for each file that has changed. */ - const results = await Promise.all( - components.map(async (component) => { + const results = await Promise.all([ + ...(components.map(async (component) => { return processComponent(component, { output: pathing.output, cacheLocation: pathing.cache, @@ -387,8 +405,8 @@ async function main( warnings: [err], }) ); - }) - ).catch((err) => { + })) + ]).catch((err) => { log.error(err); }); @@ -411,6 +429,8 @@ async function main( const componentData = new Map(); + const cliOutput = []; + let hasAnyChange = false; for (const { component, @@ -438,10 +458,10 @@ async function main( }); // This is our report header to indicate the start of a new component's data - log.write(`\n${_.pad(` ${component} `, 20, "-").cyan}\n`); + cliOutput.push(`\n${_.pad(` ${component} `, 20, "-").cyan}\n`); if (warnings.length > 0) { - warnings.forEach((warning) => log.write(`${warning}\n`)); + warnings.forEach((warning) => cliOutput.push(`${warning}\n`)); } const maxColumnWidth = files.reduce((max, file) => { @@ -450,25 +470,29 @@ async function main( }, 30); // Write a table of the file sizes to the console for easy comparison - log.writeTable(["Filename", "Local", `Tag v${tag}`], { min: 15, max: maxColumnWidth + 5}); + cliOutput.push( + writeConsoleTable(["Filename", "Local", `Tag v${tag}`], { min: 15, max: maxColumnWidth + 5}) + ); files.forEach(async (file) => { const { local, npm } = fileMap.get(file); const indicatorColor = (localSize, tagSize = 0) => { - if (localSize < tagSize) return 'green'; - if (localSize > tagSize) return 'red'; - else return 'gray'; + if (localSize < tagSize) return "green"; + if (localSize > tagSize) return "red"; + else return "gray"; }; const localSize = local?.size && `${bytesToSize(local.size)}`[indicatorColor(local.size, npm?.size)]; const tagSize = npm?.size && `${bytesToSize(npm.size)}`.gray; - log.writeTable([ + cliOutput.push( + writeConsoleTable([ `${file}`.green, - localSize ?? `** removed **`.red, - tagSize ?? `** new **`.yellow, - ], { min: 25, max: maxColumnWidth + 15 }); + localSize ?? "** removed **".red, + tagSize ?? "** new **".yellow, + ], { min: 25, max: maxColumnWidth + 15 }) + ); if (local?.size && npm?.size && local.size !== npm.size) { hasComponentChange = true; @@ -478,15 +502,11 @@ async function main( if (hasComponentChange) hasAnyChange = true; } - if (!hasAnyChange) { - log.write(`\n${"✓".green} No changes detected.\n`); - return Promise.resolve(); - } - - await Promise.all( - [...componentData.entries()] - .map(async ([component, { tag, files, }], _, data) => { - return generateDiff({ + await Promise.all([ + ...promises, + ...[...componentData.entries()] + .map(async ([component, { tag, files, }], _, data) => + generateDiff({ filepaths: [...files.keys()], outputPath: join(pathing.output, "diffs", component), fileMap: files, @@ -494,13 +514,18 @@ async function main( component, tag }) - .then(() => true); - }) - ); + ) + ]).then((result) => { + log.write("\n"); + // Print any result strings to the console + [...result].flat(Infinity).filter(Boolean).forEach((r) => log.write(r + "\n")); + }).catch((err) => { + log.error(err); + }); // This is writing a summary of all the components that were compared // to make reviewing the diffs easier to navigate - return renderTemplate( + await renderTemplate( "compare-listing", { title: "Compiled asset comparison", @@ -514,11 +539,18 @@ async function main( if (open) await open(join(output, "index.html")); }) .catch((err) => Promise.reject(err)); + + /** WRITE CONTENT TO CONSOLE */ + cliOutput.forEach(line => log.write(line)); + + if (!hasAnyChange) { + log.write(`\n${"✓".green} No changes detected.\n`); + } } let { _: components, - output = join(pathing.root, ".diff-output"), + output = join(dirs.root, ".diff-output"), cache = true, // @todo allow to run against local main or published versions } = yargs(hideBin(process.argv)).argv; diff --git a/tasks/component-reporter.js b/tasks/component-reporter.js index 444449c833a..90a2da7d1cd 100644 --- a/tasks/component-reporter.js +++ b/tasks/component-reporter.js @@ -15,6 +15,9 @@ const fs = require("fs"); const path = require("path"); +const { hideBin } = require("yargs/helpers"); +const yargs = require("yargs"); + const postcss = require("postcss"); const prettier = require("prettier"); @@ -244,3 +247,9 @@ async function main({ exports.extractModifiers = extractModifiers; exports.default = main; + +let { + _: components, +} = yargs(hideBin(process.argv)).argv; + +Promise.all(components.map((componentName) => main({ componentName }))); diff --git a/tasks/templates/compare-listing.njk b/tasks/templates/compare-listing.njk index 7846fe55449..f47eff8126e 100644 --- a/tasks/templates/compare-listing.njk +++ b/tasks/templates/compare-listing.njk @@ -7,17 +7,7 @@ - - - - - - - - - - - +