Skip to content

Commit

Permalink
feature/issue 1199 url references in CSS bundling (#1211)
Browse files Browse the repository at this point in the history
* bundling of URL referenced assets in CSS files

* add test cases for CSS url reference bundling

* normalize file paths for windows

* base path support

* hashing of bundled CSS filenames

* document CSS bundling behaviors

* filename hashing

* refactoring and console log cleanup
  • Loading branch information
thescientist13 committed May 31, 2024
1 parent cadf3b7 commit b007dff
Show file tree
Hide file tree
Showing 14 changed files with 132 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 37 additions & 3 deletions packages/cli/src/plugins/resource/plugin-standard-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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') {
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
});
Expand Down Expand Up @@ -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 <style> tag in index.html', async function() {
const styleTag = Array.from(dom.window.document.querySelectorAll('head style'));

expect(styleTag[0].textContent).to.contain(`src:url('/${fontPath}/Geist-Regular.965782360.woff2')`);
});
});

describe('user workspace reference', () => {
const imagePath = 'images/webcomponents.1079385342.jpg';

it('should have the expected background image from the user\'s workspace the output directory', async function() {
expect(await glob.promise(path.join(this.context.publicDir, imagePath))).to.have.lengthOf(1);
});

it('should have the expected @font-face file bundle path in the referenced <style> tag in index.html', async function() {
const mainCss = await glob.promise(`${path.join(this.context.publicDir, 'styles')}/main.*.css`);
const contents = await fs.promises.readFile(mainCss[0], 'utf-8');

expect(contents).to.contain(`body{background-color:green;background-image:url('/${imagePath}');}`);
});
});

describe('inline scratch dir workspace reference', () => {
const imagePath = 'images/link.1200825667.png';

it('should have the expected background image from the user\'s workspace the output directory', async function() {
expect(await glob.promise(path.join(this.context.publicDir, imagePath))).to.have.lengthOf(1);
});

it('should have the expected background-image url file bundle path in the referenced <style> tag in index.html', async function() {
const styleTag = Array.from(dom.window.document.querySelectorAll('head style'));

expect(styleTag[0].textContent).to.contain(`html{background-image:url('/${imagePath}')}`);
});
});
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

:root,:host{--primary-color:#16f;--secondary-color:#ff7;}

@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('/assets/fonts/source-sans-pro-v13-latin-regular.woff2')format('woff2'),url('/assets/fonts/source-sans-pro-v13-latin-regular.woff')format('woff'),url('/assets/fonts/source-sans-pro-v13-latin-regular.ttf')format('truetype');}
@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('https://www.example.com/assets/fonts/source-sans-pro-v13-latin-regular.woff2')format('woff2'),url('https://www.example.com/assets/fonts/source-sans-pro-v13-latin-regular.woff')format('woff'),url('https://www.example.com/assets/fonts/source-sans-pro-v13-latin-regular.ttf')format('truetype');}

@import url('https://fonts.googleapis.com/css?family=Raleway&display=swap');

*{margin:0;padding:0;font-family:'Comic Sans',sans-serif;}

body{background-color:green}
body{background-color:green;background-image:url('/images/webcomponents.1079385342.jpg');}

h1,h2{color:var(--primary-color);border:0.5px solid #dddde1;border-left:3px solid var(--color-secondary);border-top:3px solid var(--color-secondary);}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
<link rel="stylesheet" href="/styles/main.css"></link>
<style>
@import url('/node_modules/prismjs/themes/prism-okaidia.css');

@font-face {
font-family: "Geist-Sans";
src: url('../../node_modules/geist/dist/fonts/geist-sans/Geist-Regular.woff2') format("truetype");
}

html {
background-image: url('../images/link.png');
}
</style>
</head>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

body {
background-color: green;
background-image: url('../images/webcomponents.jpg');
}

h1, h2 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
});
});
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
* {
color: blue;
background-image: url('../images/webcomponents.jpg');
}
14 changes: 9 additions & 5 deletions www/pages/docs/css-and-images.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
</style>

Expand All @@ -39,25 +40,28 @@ Styles can be done in any standards compliant way that will work in a browser.
</html>
```

> _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`, `<script>`, `<style>` or `<link>` will not be handled by **Greenwood**.
For convenience, **Greenwood** does support an "assets" directory wherein anything included in that directory will automatically be copied into the build output directory. This can be useful if you have files you want available as part of your build output that are not bundled through CSS or JavaScript as generally anything that is not referenced through an `import`, `@import`, `<script>`, `<style>` or `<link>` will not automatically be handled by **Greenwood** no matter where it is located in your workspace.

#### Example
To use an image in a markdown file, you would reference it as so using standard markdown syntax:

To use an image in a markdown file (which would not be automatically bundled), you would reference it as so using standard markdown syntax:

```md
# This is my page

![my-image](/assets/images/my-image.png)
![my-image](/assets/images/my-image.webp)
```

You can do the same in your HTML

```html
<header>
<h1>Welcome to My Site!</h1>
<img alt="logo" src="/assets/images/logo.png" />
<a href="/assets/download.pdf">Download our product catalog</a>
</header>
```

Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8772,6 +8772,11 @@ gauge@~2.7.3:
strip-ansi "^3.0.1"
wide-align "^1.1.0"

geist@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/geist/-/geist-1.2.2.tgz#6e0c70afb7a99efaa403368b4935c62650619a06"
integrity sha512-uRDrxhvdnPwWJmh+K5+/5LXSKwvJzaYCl9tDXgiBi4hj7hB4K7+n/WLcvJMFs5btvyn0r9OSwCd1s6CmqAsxEw==

genfun@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537"
Expand Down

0 comments on commit b007dff

Please sign in to comment.