From f36c58da81f13d4e0b97d8956aa750e0046d74a6 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sun, 15 Dec 2024 10:26:39 -0500 Subject: [PATCH 1/4] WIP full url transitive deps bundling --- packages/cli/package.json | 1 + .../plugins/resource/plugin-standard-css.js | 50 +++++++++++++------ .../build.config-optimization-default.spec.js | 15 ++++-- .../src/pages/index.html | 2 +- .../build.default.import-node-modules.spec.js | 40 +++++++++++++-- .../src/styles/theme.css | 3 +- www/assets/fonts/source-sans-pro.css | 6 +-- yarn.lock | 12 ++--- 8 files changed, 95 insertions(+), 34 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 6adc61a47..415a8cd08 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -63,6 +63,7 @@ "@spectrum-web-components/action-menu": "^1.0.1", "@spectrum-web-components/styles": "^1.0.1", "@uswds/web-components": "^0.0.1-alpha", + "font-awesome": "^4.6.3", "geist": "^1.2.0", "lit": "^3.1.0", "lit-redux-router": "~0.20.0", diff --git a/packages/cli/src/plugins/resource/plugin-standard-css.js b/packages/cli/src/plugins/resource/plugin-standard-css.js index f5cf8d09b..babe56046 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-css.js +++ b/packages/cli/src/plugins/resource/plugin-standard-css.js @@ -11,9 +11,10 @@ import { ResourceInterface } from '../../lib/resource-interface.js'; import { hashString } from '../../lib/hashing-utils.js'; import { getResolvedHrefFromPathnameShortcut } from '../../lib/node-modules-utils.js'; import { isLocalLink } from '../../lib/resource-utils.js'; +import { derivePackageRoot } from '../../lib/walker-package-ranger.js'; function bundleCss(body, sourceUrl, compilation, workingUrl) { - const { projectDirectory, outputDir, userWorkspace } = compilation.context; + const { projectDirectory, outputDir, userWorkspace, scratchDir } = compilation.context; const ast = parse(body, { onParseError(error) { console.log(error.formattedMessage); @@ -55,34 +56,53 @@ function bundleCss(body, sourceUrl, compilation, workingUrl) { } const { basePath } = compilation.config; - let rootPath = value.replace(/\.\.\//g, '').replace('./', ''); - if (!rootPath.startsWith('/')) { - rootPath = `/${rootPath}`; - } - - const resolvedUrl = rootPath.indexOf('node_modules/') >= 0 - ? new URL(getResolvedHrefFromPathnameShortcut(rootPath, projectDirectory)) - : new URL(`.${rootPath}`, userWorkspace); + // TODO document our resolution strategy here + const resolvedUrl = workingUrl + ? new URL(value, workingUrl) + : value.startsWith('/node_modules/') + ? new URL(getResolvedHrefFromPathnameShortcut(value, projectDirectory)) + : sourceUrl.href.startsWith(scratchDir.href) + ? new URL(`./${value.replace(/\.\.\//g, '').replace('./', '')}`, userWorkspace) + : new URL(value, sourceUrl); + console.log({ value, resolvedUrl, sourceUrl, workingUrl }); if (fs.existsSync(resolvedUrl)) { const isDev = process.env.__GWD_COMMAND__ === 'develop'; // eslint-disable-line no-underscore-dangle - const hash = hashString(fs.readFileSync(resolvedUrl, 'utf-8')); - const ext = rootPath.split('.').pop(); - const hashedRoot = isDev ? rootPath : rootPath.replace(`.${ext}`, `.${hash}.${ext}`); + let finalValue; + + if (resolvedUrl.href.startsWith(userWorkspace.href)) { + finalValue = resolvedUrl.href.replace(userWorkspace.href, '/'); + } else if (value.startsWith('/node_modules/')) { + finalValue = value; + } else if (resolvedUrl.href.indexOf('/node_modules/') >= 0) { + const resolvedRoot = derivePackageRoot(resolvedUrl.href); + const resolvedRootSegments = resolvedRoot.split('/').reverse().filter(segment => segment !== ''); + const specifier = resolvedRootSegments[1].startsWith('@') ? `${resolvedRootSegments[0]}/${resolvedRootSegments[1]}` : resolvedRootSegments[0]; + + // console.log({ resolvedRoot, resolvedRootSegments, specifier }); + finalValue = `/node_modules/${specifier}/${value.replace(/\.\.\//g, '').replace('./', '')}`; + } if (!isDev) { - fs.mkdirSync(new URL(`./${path.dirname(hashedRoot)}/`, outputDir), { + const hash = hashString(fs.readFileSync(resolvedUrl, 'utf-8')); + const ext = resolvedUrl.pathname.split('.').pop(); + + finalValue = finalValue.replace(`.${ext}`, `.${hash}.${ext}`); + + fs.mkdirSync(new URL(`.${path.dirname(finalValue)}/`, outputDir), { recursive: true }); fs.promises.copyFile( resolvedUrl, - new URL(`./${hashedRoot}`, outputDir) + new URL(`.${finalValue}`, outputDir) ); } - optimizedCss += `url('${basePath}${hashedRoot}')`; + // console.log({ finalValue }); + + optimizedCss += `url('${basePath}${finalValue}')`; } else { console.warn(`Unable to locate ${value}. You may need to manually copy this file from its source location to the build output directory.`); optimizedCss += `url('${value}')`; diff --git a/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js b/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js index 45d60ce3b..b60edcc61 100644 --- a/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js +++ b/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js @@ -52,16 +52,21 @@ describe('Build Greenwood With: ', function() { describe(LABEL, function() { before(async function() { - const geistFont = await getDependencyFiles( + // this package has a known issue with import.meta.resolve + // if this gets fixed, we can remove the need for this setup + // https://github.com/vercel/geist-font/issues/150 + const geistPackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/geist/package.json`, + `${outputPath}/node_modules/geist/` + ); + const geistFonts = await getDependencyFiles( `${process.cwd()}/node_modules/geist/dist/fonts/geist-sans/*`, `${outputPath}/node_modules/geist/dist/fonts/geist-sans/` ); runner.setup(outputPath, [ - // this package has a known issue with import.meta.resolve - // if this gets fixed, we can remove the need for this setup - // https://github.com/vercel/geist-font/issues/150 - ...geistFont + ...geistPackageJson, + ...geistFonts ]); runner.runCommand(cliPath, 'build'); }); diff --git a/packages/cli/test/cases/build.config.optimization-default/src/pages/index.html b/packages/cli/test/cases/build.config.optimization-default/src/pages/index.html index 0f1a170f5..3c07b0659 100644 --- a/packages/cli/test/cases/build.config.optimization-default/src/pages/index.html +++ b/packages/cli/test/cases/build.config.optimization-default/src/pages/index.html @@ -9,7 +9,7 @@ @font-face { font-family: "Geist-Sans"; - src: url('../../node_modules/geist/dist/fonts/geist-sans/Geist-Regular.woff2') format("truetype"); + src: url('/node_modules/geist/dist/fonts/geist-sans/Geist-Regular.woff2') format("truetype"); } html { diff --git a/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js b/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js index e9bf7058d..3d6cad024 100644 --- a/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js +++ b/packages/cli/test/cases/build.default.import-node-modules/build.default.import-node-modules.spec.js @@ -26,7 +26,7 @@ import fs from 'fs'; import glob from 'glob-promise'; import { JSDOM } from 'jsdom'; import path from 'path'; -import { getOutputTeardownFiles } from '../../../../../test/utils.js'; +import { getOutputTeardownFiles, getDependencyFiles } from '../../../../../test/utils.js'; import { Runner } from 'gallinago'; import { fileURLToPath, URL } from 'url'; @@ -49,7 +49,29 @@ describe('Build Greenwood With: ', function() { let dom; before(async function() { - runner.setup(outputPath); + // this package has a known issue with import.meta.resolve + // in that it has no main, module, or exports so it has to be hoisted + // at least for this current version + // https://unpkg.com/browse/font-awesome@4.7.0/package.json + // https://github.com/FortAwesome/Font-Awesome/pull/19041 + const fontAwesomePackageJson = await getDependencyFiles( + `${process.cwd()}/node_modules/font-awesome/package.json`, + `${outputPath}/node_modules/font-awesome/` + ); + const fontAwesomeCssFiles = await getDependencyFiles( + `${process.cwd()}/node_modules/font-awesome/css/*`, + `${outputPath}/node_modules/font-awesome/css/` + ); + const fontAwesomeFontFiles = await getDependencyFiles( + `${process.cwd()}/node_modules/font-awesome/fonts/*`, + `${outputPath}/node_modules/font-awesome/fonts/` + ); + + runner.setup(outputPath, [ + ...fontAwesomePackageJson, + ...fontAwesomeCssFiles, + ...fontAwesomeFontFiles + ]); runner.runCommand(cliPath, 'build'); dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html')); @@ -141,10 +163,22 @@ describe('Build Greenwood With: ', function() { expect(contents.indexOf(':root,:host{--spectrum-global-animation-linear:cubic-bezier(0, 0, 1, 1);')).to.equal(0); }); }); + + describe(' with reference to transient relative node_modules url(...) references', function() { + it('should have the expected number of font files referenced in vendor CSS file in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, 'node_modules/font-awesome/fonts/*'))).to.have.lengthOf(5); + }); + + it('should have the expected url link for the bundled font-awesome file', async function() { + const themeFile = await glob.promise(path.join(this.context.publicDir, 'styles/theme.*.css')); + const contents = fs.readFileSync(themeFile[0], 'utf-8'); + + expect(contents.indexOf('@font-face {font-family:\'FontAwesome\';src:url(\'/node_modules/font-awesome/fonts/fontawesome-webfont.139345087.eot?v=4.7.0\');') > 0).to.equal(true); + }); + }); }); after(function() { runner.teardown(getOutputTeardownFiles(outputPath)); }); - }); \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.import-node-modules/src/styles/theme.css b/packages/cli/test/cases/build.default.import-node-modules/src/styles/theme.css index 13ce1b4c1..d9e356ee1 100644 --- a/packages/cli/test/cases/build.default.import-node-modules/src/styles/theme.css +++ b/packages/cli/test/cases/build.default.import-node-modules/src/styles/theme.css @@ -1 +1,2 @@ -@import url('/node_modules/@spectrum-web-components/styles/all-large-dark.css'); \ No newline at end of file +@import url('/node_modules/@spectrum-web-components/styles/all-large-dark.css'); +@import url('/node_modules/font-awesome/css/font-awesome.css'); \ No newline at end of file diff --git a/www/assets/fonts/source-sans-pro.css b/www/assets/fonts/source-sans-pro.css index d7a4d6ace..693430235 100644 --- a/www/assets/fonts/source-sans-pro.css +++ b/www/assets/fonts/source-sans-pro.css @@ -5,7 +5,7 @@ font-weight: 400; font-display: swap; src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), - url('/assets/fonts/source-sans-pro-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ - url('/assets/fonts/source-sans-pro-v13-latin-regular.woff') format('woff'), /* Modern Browsers */ - url('/assets/fonts/source-sans-pro-v13-latin-regular.ttf') format('truetype'); /* Safari, Android, iOS */ + url('./source-sans-pro-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('./source-sans-pro-v13-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('./source-sans-pro-v13-latin-regular.ttf') format('truetype'); /* Safari, Android, iOS */ } diff --git a/yarn.lock b/yarn.lock index e92a0ac7c..b3e814571 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8976,6 +8976,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +font-awesome@^4.6.3: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + integrity sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -16022,7 +16027,7 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" -"source-map-js@>=0.6.2 <2.0.0": +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -16032,11 +16037,6 @@ source-map-js@^1.0.1, source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" From 7b598617de1a9e320e0033da4c77b4fa78ffd082 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sun, 15 Dec 2024 11:01:28 -0500 Subject: [PATCH 2/4] support and testing for local resources absolute escape hatch --- .../plugins/resource/plugin-standard-css.js | 20 +++++++++---------- .../build.config-optimization-default.spec.js | 10 ++++++++++ .../fixtures/expected.css | 2 +- .../src/foo/bar.baz | 1 + www/assets/fonts/source-sans-pro.css | 4 ++-- 5 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 packages/cli/test/cases/build.config.optimization-default/src/foo/bar.baz diff --git a/packages/cli/src/plugins/resource/plugin-standard-css.js b/packages/cli/src/plugins/resource/plugin-standard-css.js index babe56046..bef4c94cf 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-css.js +++ b/packages/cli/src/plugins/resource/plugin-standard-css.js @@ -58,15 +58,16 @@ function bundleCss(body, sourceUrl, compilation, workingUrl) { const { basePath } = compilation.config; // TODO document our resolution strategy here - const resolvedUrl = workingUrl - ? new URL(value, workingUrl) - : value.startsWith('/node_modules/') - ? new URL(getResolvedHrefFromPathnameShortcut(value, projectDirectory)) - : sourceUrl.href.startsWith(scratchDir.href) - ? new URL(`./${value.replace(/\.\.\//g, '').replace('./', '')}`, userWorkspace) - : new URL(value, sourceUrl); + const resolvedUrl = value.startsWith('/node_modules/') + ? new URL(getResolvedHrefFromPathnameShortcut(value, projectDirectory)) + : value.startsWith('/') + ? new URL(`.${value}`, userWorkspace) + : workingUrl + ? new URL(value, workingUrl) + : sourceUrl.href.startsWith(scratchDir.href) + ? new URL(`./${value.replace(/\.\.\//g, '').replace('./', '')}`, userWorkspace) + : new URL(value, sourceUrl); - console.log({ value, resolvedUrl, sourceUrl, workingUrl }); if (fs.existsSync(resolvedUrl)) { const isDev = process.env.__GWD_COMMAND__ === 'develop'; // eslint-disable-line no-underscore-dangle let finalValue; @@ -80,7 +81,6 @@ function bundleCss(body, sourceUrl, compilation, workingUrl) { const resolvedRootSegments = resolvedRoot.split('/').reverse().filter(segment => segment !== ''); const specifier = resolvedRootSegments[1].startsWith('@') ? `${resolvedRootSegments[0]}/${resolvedRootSegments[1]}` : resolvedRootSegments[0]; - // console.log({ resolvedRoot, resolvedRootSegments, specifier }); finalValue = `/node_modules/${specifier}/${value.replace(/\.\.\//g, '').replace('./', '')}`; } @@ -100,8 +100,6 @@ function bundleCss(body, sourceUrl, compilation, workingUrl) { ); } - // console.log({ finalValue }); - optimizedCss += `url('${basePath}${finalValue}')`; } else { console.warn(`Unable to locate ${value}. You may need to manually copy this file from its source location to the build output directory.`); diff --git a/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js b/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js index b60edcc61..391177eb8 100644 --- a/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js +++ b/packages/cli/test/cases/build.config.optimization-default/build.config-optimization-default.spec.js @@ -14,6 +14,8 @@ * src/ * components/ * header.js + * foo/ + * bar.baz * images/ * webcomponents.jpg * pages/ @@ -196,6 +198,14 @@ describe('Build Greenwood With: ', function() { expect(styleTag[0].textContent).to.contain(`html{background-image:url('/${imagePath}')}`); }); }); + + describe('absolute user workspace reference', () => { + const resourcePath = 'foo/bar.642520792.baz'; + + it('should have the expected resource reference from the user\'s workspace in the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, resourcePath))).to.have.lengthOf(1); + }); + }); }); }); }); diff --git a/packages/cli/test/cases/build.config.optimization-default/fixtures/expected.css b/packages/cli/test/cases/build.config.optimization-default/fixtures/expected.css index c311a63c3..d35b28600 100644 --- a/packages/cli/test/cases/build.config.optimization-default/fixtures/expected.css +++ b/packages/cli/test/cases/build.config.optimization-default/fixtures/expected.css @@ -62,7 +62,7 @@ h1:has(+h2){margin:0 0 0.25rem 0} .snippet{margin:var(--size-4) 0;padding:0 var(--size-4);} -h1{background-image:url('/foo/bar.baz')} +h1{background-image:url('/foo/bar.642520792.baz')} .has-success{background-image:url('data:image/svg+xml;...')} diff --git a/packages/cli/test/cases/build.config.optimization-default/src/foo/bar.baz b/packages/cli/test/cases/build.config.optimization-default/src/foo/bar.baz new file mode 100644 index 000000000..088de9967 --- /dev/null +++ b/packages/cli/test/cases/build.config.optimization-default/src/foo/bar.baz @@ -0,0 +1 @@ +some file \ No newline at end of file diff --git a/www/assets/fonts/source-sans-pro.css b/www/assets/fonts/source-sans-pro.css index 693430235..3294bd15d 100644 --- a/www/assets/fonts/source-sans-pro.css +++ b/www/assets/fonts/source-sans-pro.css @@ -1,11 +1,11 @@ -/* source-sans-pro-regular - latin */ +/* intentionally mixing paths for CSS bundling testing */ @font-face { font-family: 'Source Sans Pro'; font-style: normal; font-weight: 400; font-display: swap; src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), - url('./source-sans-pro-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('/assets/fonts/source-sans-pro-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ url('./source-sans-pro-v13-latin-regular.woff') format('woff'), /* Modern Browsers */ url('./source-sans-pro-v13-latin-regular.ttf') format('truetype'); /* Safari, Android, iOS */ } From 45dc08264e5c94c70787bf51edb3f8ef7be46774 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sun, 15 Dec 2024 11:19:29 -0500 Subject: [PATCH 3/4] comment CSS resolution logic --- .../src/plugins/resource/plugin-standard-css.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/plugins/resource/plugin-standard-css.js b/packages/cli/src/plugins/resource/plugin-standard-css.js index bef4c94cf..cf2161c1c 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-css.js +++ b/packages/cli/src/plugins/resource/plugin-standard-css.js @@ -57,7 +57,14 @@ function bundleCss(body, sourceUrl, compilation, workingUrl) { const { basePath } = compilation.config; - // TODO document our resolution strategy here + /* + * Our resolution algorithm works as follows: + * 1. First, check if it is a shortcut alias to node_modules, in which we use Node's resolution algorithm + * 2. Next, check if it is an absolute path "escape" hatch based path and just resolve to the user's workspace + * 3. If there is a workingUrl, then just join the current value with the current working file we're processing + * 4. If the starting file is in the scratch directory, likely means it is just an extracted inline