diff --git a/packages/cli/package.json b/packages/cli/package.json index 0143d4d63..a5b603c27 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -62,6 +62,7 @@ "@material/mwc-button": "^0.25.2", "@stencil/core": "^2.12.0", "@types/trusted-types": "^2.0.2", + "geist": "^1.2.0", "lit": "^3.1.0", "lit-redux-router": "~0.20.0", "lodash-es": "^4.17.20", diff --git a/packages/cli/src/plugins/resource/plugin-standard-css.js b/packages/cli/src/plugins/resource/plugin-standard-css.js index f9cfbe664..64c1ba00d 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-css.js +++ b/packages/cli/src/plugins/resource/plugin-standard-css.js @@ -5,10 +5,14 @@ * */ import fs from 'fs'; +import path from 'path'; import { parse, walk } from 'css-tree'; import { ResourceInterface } from '../../lib/resource-interface.js'; +import { normalizePathnameForWindows } from '../../lib/resource-utils.js'; +import { hashString } from '../../lib/hashing-utils.js'; -function bundleCss(body, url, projectDirectory) { +function bundleCss(body, url, compilation) { + const { projectDirectory, outputDir, userWorkspace } = compilation.context; const ast = parse(body, { onParseError(error) { console.log(error.formattedMessage); @@ -29,10 +33,40 @@ function bundleCss(body, url, projectDirectory) { : new URL(value, url); const importContents = fs.readFileSync(resolvedUrl, 'utf-8'); - optimizedCss += bundleCss(importContents, url, projectDirectory); + optimizedCss += bundleCss(importContents, url, compilation); } else { optimizedCss += `@import url('${value}');`; } + } else if (type === 'Url' && this.atrule?.name !== 'import') { + if (value.startsWith('http') || value.startsWith('//')) { + optimizedCss += `url('${value}')`; + return; + } + + const basePath = compilation.config.basePath === '' ? '/' : `${compilation.config.basePath}/`; + let barePath = value.replace(/\.\.\//g, '').replace('./', ''); + + if (barePath.startsWith('/')) { + barePath = barePath.replace('/', ''); + } + + const locationUrl = barePath.startsWith('node_modules') + ? new URL(`./${barePath}`, projectDirectory) + : new URL(`./${barePath}`, userWorkspace); + const hash = hashString(fs.readFileSync(locationUrl, 'utf-8')); + const ext = barePath.split('.').pop(); + const hashedRoot = barePath.replace(`.${ext}`, `.${hash}.${ext}`); + + fs.mkdirSync(normalizePathnameForWindows(new URL(`./${path.dirname(barePath)}/`, outputDir)), { + recursive: true + }); + + fs.promises.copyFile( + locationUrl, + new URL(`./${hashedRoot}`, outputDir) + ); + + optimizedCss += `url('${basePath}${hashedRoot}')`; } else if (type === 'Atrule' && name !== 'import') { optimizedCss += `@${name} `; } else if (type === 'TypeSelector') { @@ -257,7 +291,7 @@ class StandardCssResource extends ResourceInterface { async optimize(url, response) { const body = await response.text(); - const optimizedBody = bundleCss(body, url, this.compilation.context.projectDirectory); + const optimizedBody = bundleCss(body, url, this.compilation); return new Response(optimizedBody); } 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 0b0c9131f..c88c8cc7e 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,10 +14,15 @@ * src/ * components/ * header.js + * images/ + * webcomponents.jpg * pages/ * index.html * styles/ + * main.css * theme.css + * system + * variables.css */ import chai from 'chai'; import fs from 'fs'; @@ -52,9 +57,15 @@ describe('Build Greenwood With: ', function() { `${outputPath}/node_modules/prismjs/themes/` ); + const geistFont = await getDependencyFiles( + `${process.cwd()}/node_modules/geist/dist/fonts/geist-sans/*`, + `${outputPath}/node_modules/geist/dist/fonts/geist-sans/` + ); + runner.setup(outputPath, [ ...getSetupFiles(outputPath), - ...prismCss + ...prismCss, + ...geistFont ]); runner.runCommand(cliPath, 'build'); }); @@ -135,6 +146,56 @@ describe('Build Greenwood With: ', function() { expect(styleTags[0].textContent.replace(/\n/g, '')).to.equal('*{color:red;font-size:blue;}'); }); }); + + describe('bundled URL references in CSS files', function() { + describe('node modules reference', () => { + const fontPath = 'node_modules/geist/dist/fonts/geist-sans'; + let dom; + + before(async function() { + dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html')); + }); + + it('should have the expected @font-face file from node_modules copied into the output directory', async function() { + expect(await glob.promise(path.join(this.context.publicDir, `${fontPath}/*.woff2`))).to.have.lengthOf(1); + }); + + it('should have the expected @font-face file bundle path in the referenced diff --git a/packages/cli/test/cases/build.config.optimization-default/src/styles/main.css b/packages/cli/test/cases/build.config.optimization-default/src/styles/main.css index d2836cee1..947b402f6 100644 --- a/packages/cli/test/cases/build.config.optimization-default/src/styles/main.css +++ b/packages/cli/test/cases/build.config.optimization-default/src/styles/main.css @@ -8,6 +8,7 @@ body { background-color: green; + background-image: url('../images/webcomponents.jpg'); } h1, h2 { diff --git a/packages/cli/test/cases/build.config.optimization-default/src/system/variables.css b/packages/cli/test/cases/build.config.optimization-default/src/system/variables.css index f010eba84..4d89fa110 100644 --- a/packages/cli/test/cases/build.config.optimization-default/src/system/variables.css +++ b/packages/cli/test/cases/build.config.optimization-default/src/system/variables.css @@ -10,7 +10,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('https://www.example.com/assets/fonts/source-sans-pro-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('https://www.example.com/assets/fonts/source-sans-pro-v13-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('https://www.example.com/assets/fonts/source-sans-pro-v13-latin-regular.ttf') format('truetype'); /* Safari, Android, iOS */ } \ No newline at end of file diff --git a/packages/cli/test/cases/serve.config.base-path/serve.config.base-path.spec.js b/packages/cli/test/cases/serve.config.base-path/serve.config.base-path.spec.js index bad6e1dd5..74ed39f32 100644 --- a/packages/cli/test/cases/serve.config.base-path/serve.config.base-path.spec.js +++ b/packages/cli/test/cases/serve.config.base-path/serve.config.base-path.spec.js @@ -49,7 +49,7 @@ describe('Serve Greenwood With: ', function() { const hostname = 'http://127.0.0.1:8080'; const basePath = '/my-path'; const jsHash = '2ce3f02d'; - const cssHash = '1454013616'; + const cssHash = '2106293974'; let runner; before(function() { @@ -229,7 +229,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct response body', function(done) { - expect(body).to.contain('*{color:blue}'); + expect(body).to.contain('*{color:blue;background-image:url(\'/my-path/images/webcomponents.1079385342.jpg\');}'); done(); }); }); diff --git a/packages/cli/test/cases/serve.config.base-path/src/images/webcomponents.jpg b/packages/cli/test/cases/serve.config.base-path/src/images/webcomponents.jpg new file mode 100644 index 000000000..2c5526a95 Binary files /dev/null and b/packages/cli/test/cases/serve.config.base-path/src/images/webcomponents.jpg differ diff --git a/packages/cli/test/cases/serve.config.base-path/src/styles/main.css b/packages/cli/test/cases/serve.config.base-path/src/styles/main.css index 9a7b45f93..acb9cc0f5 100644 --- a/packages/cli/test/cases/serve.config.base-path/src/styles/main.css +++ b/packages/cli/test/cases/serve.config.base-path/src/styles/main.css @@ -1,3 +1,4 @@ * { color: blue; + background-image: url('../images/webcomponents.jpg'); } \ No newline at end of file diff --git a/www/pages/docs/css-and-images.md b/www/pages/docs/css-and-images.md index 6b0d5d310..617699f9e 100644 --- a/www/pages/docs/css-and-images.md +++ b/www/pages/docs/css-and-images.md @@ -25,7 +25,8 @@ Styles can be done in any standards compliant way that will work in a browser. body { font-family: 'Source Sans Pro', sans-serif; - line-height:1.4; + background-image: url('../images/background.webp'); + line-height: 1.4; } @@ -39,17 +40,20 @@ Styles can be done in any standards compliant way that will work in a browser. ``` +> _In the above example, Greenwood will also bundle any `url` references in your CSS automatically._ + ### Assets -For convenience, **Greenwood** does support an "assets" directory wherein anything included in that directory will automatically be copied into the build output directory. This is the recommended location for all your local images, fonts, etc. At this time, anything that is not referenced through an `import`, `@import`, `