Skip to content

Commit ca45a3c

Browse files
committed
feat: option to inject css onto html head
close #1
1 parent 0648845 commit ca45a3c

6 files changed

Lines changed: 399 additions & 5 deletions

File tree

ACKNOWLEDGEMENTS.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
This software used some code from [requirejs](https://github.com/requirejs/requirejs), [aurelia-pal](https://github.com/aurelia/pal), and [style-loader](https://github.com/webpack-contrib/style-loader).
2+
3+
Below are the required notices from them.
4+
5+
====
6+
7+
Copyright jQuery Foundation and other contributors, https://jquery.org/
8+
9+
This software consists of voluntary contributions made by many
10+
individuals. For exact contribution history, see the revision history
11+
available at https://github.com/requirejs/requirejs
12+
13+
The following license applies to all parts of this software except as
14+
documented below:
15+
16+
====
17+
18+
Permission is hereby granted, free of charge, to any person obtaining
19+
a copy of this software and associated documentation files (the
20+
"Software"), to deal in the Software without restriction, including
21+
without limitation the rights to use, copy, modify, merge, publish,
22+
distribute, sublicense, and/or sell copies of the Software, and to
23+
permit persons to whom the Software is furnished to do so, subject to
24+
the following conditions:
25+
26+
The above copyright notice and this permission notice shall be
27+
included in all copies or substantial portions of the Software.
28+
29+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
31+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
32+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
33+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
34+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
35+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
36+
37+
====
38+
39+
Copyright and related rights for sample code are waived via CC0. Sample
40+
code is defined as all source code displayed within the prose of the
41+
documentation.
42+
43+
CC0: http://creativecommons.org/publicdomain/zero/1.0/
44+
45+
====
46+
47+
Files located in the node_modules directory, and certain utilities used
48+
to build or test the software in the test and dist directories, are
49+
externally maintained libraries used by this software which have their own
50+
licenses; we recommend you read them, as their terms may differ from the
51+
terms above.
52+
53+
----
54+
55+
The MIT License (MIT)
56+
57+
Copyright (c) 2010 - 2018 Blue Spire Inc.
58+
59+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
60+
61+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
62+
63+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
64+
65+
----
66+
67+
Copyright JS Foundation and other contributors
68+
69+
Permission is hereby granted, free of charge, to any person obtaining
70+
a copy of this software and associated documentation files (the
71+
'Software'), to deal in the Software without restriction, including
72+
without limitation the rights to use, copy, modify, merge, publish,
73+
distribute, sublicense, and/or sell copies of the Software, and to
74+
permit persons to whom the Software is furnished to do so, subject to
75+
the following conditions:
76+
77+
The above copyright notice and this permission notice shall be
78+
included in all copies or substantial portions of the Software.
79+
80+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
81+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
82+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
83+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
84+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
85+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
86+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

spec/all-browser-spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ require('./transformers/defines.spec');
1414
require('./transformers/text.spec');
1515
require('./shared.spec');
1616
require('./stub-module.spec');
17+
require('./inject-css.spec');

spec/bundler.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import test from 'tape';
22
import Bundler from '../src/index';
3+
import trace from '../src/trace';
34
import {contentOrFile} from '../src/shared';
45
import {mockResolve, buildReadFile, mockLocator} from './mock';
56

@@ -8,6 +9,11 @@ function mockTrace(unit) {
89
const moduleId = unit.moduleId;
910
const shim = unit.shim;
1011

12+
if (unit.moduleId === 'ext:css') {
13+
// don't mock ext:css trace
14+
return trace(unit);
15+
}
16+
1117
if (unit.packageName === 'jquery') {
1218
return Promise.resolve({
1319
path: unit.path,
@@ -732,3 +738,43 @@ test('Bundler traces files, split bundles, continuously update bundles in watch
732738
)
733739
.then(t.end);
734740
});
741+
742+
test('Bundler supports inject css', t => {
743+
const fakeFs = {
744+
'node_modules/dumber-module-loader/dist/index.js': 'dumber-module-loader',
745+
'node_modules/dumber/package.json': JSON.stringify({name: 'dumber', main: './dist/index'}),
746+
'node_modules/dumber/dist/inject-css.js': '',
747+
};
748+
const bundler = createBundler(fakeFs, {
749+
injectCss: true
750+
});
751+
752+
Promise.resolve()
753+
.then(() => bundler.capture({path: 'src/app.js', contents: '', moduleId: 'app'}))
754+
.then(() => bundler.resolve())
755+
.then(() => bundler.bundle())
756+
.then(
757+
bundleMap => {
758+
t.deepEqual(bundleMap, {
759+
'entry-bundle': {
760+
files: [
761+
{contents: 'dumber-module-loader;'},
762+
{contents: 'define.switchToUserSpace();'},
763+
{path: 'src/app.js', contents: "define('app',[],1);", sourceMap: undefined},
764+
{path: '__stub__/ext-css.js', contents: "define('ext:css',['dumber/dist/inject-css'],function(m){return m;});", sourceMap: undefined},
765+
{contents: 'define.switchToPackageSpace();'},
766+
{path: 'node_modules/dumber/dist/inject-css.js', contents: "define('dumber/dist/inject-css',[],1);", sourceMap: undefined},
767+
{contents: 'define.switchToUserSpace();'},
768+
],
769+
config: {
770+
baseUrl: '/dist',
771+
bundles: {}
772+
}
773+
}
774+
})
775+
},
776+
err => t.fail(err.stack)
777+
)
778+
.then(t.end);
779+
});
780+

spec/inject-css.spec.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import test from 'tape';
2+
import {fixupCSSUrls} from '../src/inject-css';
3+
4+
// tests partly copied from
5+
// https://github.com/webpack-contrib/style-loader/blob/master/test/fixUrls.test.js
6+
test('fixupCSSUrls throws on null/undefined', t => {
7+
t.throws(() => fixupCSSUrls('foo/bar', null));
8+
t.throws(() => fixupCSSUrls('foo/bar', undefined));
9+
t.end();
10+
});
11+
12+
test('fixupCSSUrls: Blank css is not modified', t => {
13+
const css = '';
14+
t.equal(fixupCSSUrls('foo/bar', css), css);
15+
t.end();
16+
});
17+
18+
test('fixupCSSUrls: No url is not modified', t => {
19+
const css = 'body { }';
20+
t.equal(fixupCSSUrls('foo/bar', css), css);
21+
t.end();
22+
});
23+
24+
test("fixupCSSUrls: Full url isn't changed (no quotes)", t => {
25+
const css = 'body { background-image:url ( http://example.com/bg.jpg ); }';
26+
t.equal(fixupCSSUrls('foo/bar', css), css);
27+
t.end();
28+
});
29+
30+
test("fixupCSSUrls: Full url isn't changed (no quotes, spaces)", t => {
31+
const css = 'body { background-image:url ( http://example.com/bg.jpg ); }';
32+
t.equal(fixupCSSUrls('foo/bar', css), css);
33+
t.end();
34+
});
35+
36+
test("fixupCSSUrls: Full url isn't changed (double quotes)", t => {
37+
const css = 'body { background-image:url("http://example.com/bg.jpg"); }';
38+
t.equal(fixupCSSUrls('foo/bar', css), css);
39+
t.end();
40+
});
41+
42+
test("fixupCSSUrls: Full url isn't changed (double quotes, spaces)", t => {
43+
const css = 'body { background-image:url ( "http://example.com/bg.jpg" ); }';
44+
t.equal(fixupCSSUrls('foo/bar', css), css);
45+
t.end();
46+
});
47+
48+
test("fixupCSSUrls: Full url isn't changed (single quotes)", t => {
49+
const css = 'body { background-image:url(\'http://example.com/bg.jpg\'); }';
50+
t.equal(fixupCSSUrls('foo/bar', css), css);
51+
t.end();
52+
});
53+
54+
test("fixupCSSUrls: Full url isn't changed (single quotes, spaces)", t => {
55+
const css = 'body { background-image:url ( \'http://example.com/bg.jpg\' ); }';
56+
t.equal(fixupCSSUrls('foo/bar', css), css);
57+
t.end();
58+
});
59+
60+
test('fixupCSSUrls: Multiple full urls are not changed', t => {
61+
const css = "body { background-image:url(http://example.com/bg.jpg); }\ndiv.main { background-image:url ( 'https://www.anothersite.com/another.png' ); }";
62+
t.equal(fixupCSSUrls('foo/bar', css), css);
63+
t.end();
64+
});
65+
66+
test("fixupCSSUrls: Http url isn't changed", t => {
67+
const css = 'body { background-image:url(http://example.com/bg.jpg); }';
68+
t.equal(fixupCSSUrls('foo/bar', css), css);
69+
t.end();
70+
});
71+
72+
test("fixupCSSUrls: Https url isn't changed", t => {
73+
const css = 'body { background-image:url(https://example.com/bg.jpg); }';
74+
t.equal(fixupCSSUrls('foo/bar', css), css);
75+
t.end();
76+
});
77+
78+
test("fixupCSSUrls: HTTPS url isn't changed", t => {
79+
const css = 'body { background-image:url(HTTPS://example.com/bg.jpg); }';
80+
t.equal(fixupCSSUrls('foo/bar', css), css);
81+
t.end();
82+
});
83+
84+
test("fixupCSSUrls: File url isn't changed", t => {
85+
const css = 'body { background-image:url(file:///example.com/bg.jpg); }';
86+
t.equal(fixupCSSUrls('foo/bar', css), css);
87+
t.end();
88+
});
89+
90+
test("fixupCSSUrls: Double slash url isn't changed", t => {
91+
const css = 'body { background-image:url(//example.com/bg.jpg); }';
92+
t.equal(fixupCSSUrls('foo/bar', css), css);
93+
t.end();
94+
});
95+
96+
test("fixupCSSUrls: Image data uri url isn't changed", t => {
97+
const css = 'body { background-image:url(data:image/png;base64,qsrwABYuwNkimqm3gAAAABJRU5ErkJggg==); }';
98+
t.equal(fixupCSSUrls('foo/bar', css), css);
99+
t.end();
100+
});
101+
102+
test("fixupCSSUrls: Font data uri url isn't changed", t => {
103+
const css = 'body { background-image:url(data:application/x-font-woff;charset=utf-8;base64,qsrwABYuwNkimqm3gAAAABJRU5ErkJggg); }';
104+
t.equal(fixupCSSUrls('foo/bar', css), css);
105+
t.end();
106+
});
107+
108+
test('fixupCSSUrls: Relative url with dot slash', t => {
109+
const css = 'body { background-image:url(./c/d/bg.jpg); }';
110+
const expected = "body { background-image:url('foo/c/d/bg.jpg'); }";
111+
t.equal(fixupCSSUrls('foo/bar', css), expected);
112+
t.end();
113+
});
114+
115+
test('fixupCSSUrls: Multiple relative urls', t => {
116+
const css = 'body { background-image:URL ( "./bg.jpg" ); }\ndiv.main { background-image:url(../c/d/bg.jpg); }';
117+
const expected = "body { background-image:url('foo/bg.jpg'); }\ndiv.main { background-image:url('c/d/bg.jpg'); }";
118+
t.equal(fixupCSSUrls('foo/bar', css), expected);
119+
t.end();
120+
});
121+
122+
test("fixupCSSUrls: url with hash isn't changed", t => {
123+
const css = 'body { background-image:url(#bg.jpg); }';
124+
t.equal(fixupCSSUrls('foo/bar', css), css);
125+
t.end();
126+
});
127+
128+
test('fixupCSSUrls: Empty url should be skipped', t => {
129+
let css = 'body { background-image:url(); }';
130+
t.equal(fixupCSSUrls('foo/bar', css), css);
131+
css = 'body { background-image:url( ); }';
132+
t.equal(fixupCSSUrls('foo/bar', css), css);
133+
css = 'body { background-image:url(\n); }';
134+
t.equal(fixupCSSUrls('foo/bar', css), css);
135+
css = 'body { background-image:url(\'\'); }';
136+
t.equal(fixupCSSUrls('foo/bar', css), css);
137+
css = 'body { background-image:url(\' \'); }';
138+
t.equal(fixupCSSUrls('foo/bar', css), css);
139+
css = 'body { background-image:url(""); }';
140+
t.equal(fixupCSSUrls('foo/bar', css), css);
141+
css = 'body { background-image:url(" "); }';
142+
t.equal(fixupCSSUrls('foo/bar', css), css);
143+
t.end();
144+
});
145+
146+
test("fixupCSSUrls: Rooted url isn't changed", t => {
147+
let css = 'body { background-image:url(/bg.jpg); }';
148+
t.equal(fixupCSSUrls('foo/bar', css), css);
149+
css = 'body { background-image:url(/a/b/bg.jpg); }';
150+
t.equal(fixupCSSUrls('foo/bar', css), css);
151+
t.end();
152+
});
153+
154+
test("fixupCSSUrls doesn't break inline SVG", t => {
155+
const css = "body { background-image:url('data:image/svg+xml;charset=utf-8,<svg><feFlood flood-color=\"rgba(0,0,0,0.5)\" /></svg>'); }";
156+
t.equal(fixupCSSUrls('foo/bar', css), css);
157+
t.end();
158+
});
159+
160+
test("fixupCSSUrls doesn't break inline SVG with HTML comment", t => {
161+
const css = "body { background-image:url('data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3C!--%20Comment%20--%3E%0A%3Csvg%3E%3C%2Fsvg%3E%0A'); }";
162+
t.equal(fixupCSSUrls('foo/bar', css), css);
163+
t.end();
164+
});

src/index.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export default class Bundler {
3939
this._cache = cache;
4040
}
4141

42+
// turn on injection of css (inject onto html head)
43+
if (opts.injectCss || opts.injectCSS) this._injectCss = true;
44+
4245
this._unitsMap = {};
4346
this._moduleId_done = new Set();
4447
this._moduleIds_todo = new Set();
@@ -146,8 +149,8 @@ export default class Bundler {
146149
}
147150

148151
_resolveExplicitDepsIfNeeded() {
149-
if (this.isExplicitDepsResolved) return Promise.resolve();
150-
this.isExplicitDepsResolved = true;
152+
if (this._isExplicitDepsResolved) return Promise.resolve();
153+
this._isExplicitDepsResolved = true;
151154

152155
let p = Promise.resolve();
153156

@@ -170,8 +173,8 @@ export default class Bundler {
170173
}
171174

172175
_resolvePrependsAndAppends() {
173-
if (this.isPrependsAndAppendsResolved) return Promise.resolve();
174-
this.isPrependsAndAppendsResolved = true;
176+
if (this._isPrependsAndAppendsResolved) return Promise.resolve();
177+
this._isPrependsAndAppendsResolved = true;
175178

176179
let {_prepends, _appends} = this;
177180
let prepends = new Array(_prepends.length);
@@ -213,9 +216,21 @@ export default class Bundler {
213216
}
214217
}
215218

219+
_supportInjectCssIfNeeded() {
220+
if (!this._injectCss || this._isInjectCssTurnedOn) return Promise.resolve();
221+
this._isInjectCssTurnedOn = true;
222+
223+
return this.capture({
224+
path:'__stub__/ext-css.js',
225+
contents: "define(['dumber/dist/inject-css'],function(m){return m;});",
226+
moduleId: 'ext:css'
227+
});
228+
}
229+
216230
resolve() {
217231
let todo = [];
218-
return this._resolvePrependsAndAppends()
232+
return this._supportInjectCssIfNeeded()
233+
.then(() => this._resolvePrependsAndAppends())
219234
.then(() => this._resolveExplicitDepsIfNeeded())
220235
.then(() => {
221236
const consults = [];

0 commit comments

Comments
 (0)