Skip to content

Commit

Permalink
Process inline styles and scripts (#1456)
Browse files Browse the repository at this point in the history
  • Loading branch information
DeMoorJasper authored and devongovett committed Jul 21, 2018
1 parent 4316ff5 commit 7423541
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 20 deletions.
6 changes: 6 additions & 0 deletions src/Pipeline.js
Expand Up @@ -65,6 +65,12 @@ class Pipeline {
subAsset.cacheData = Object.assign(asset.cacheData, subAsset.cacheData);

let processed = await this.processAsset(subAsset);
if (rendition.meta) {
for (let res of processed) {
res.meta = rendition.meta;
}
}

generated = generated.concat(processed);
asset.hash = md5(asset.hash + subAsset.hash);
} else {
Expand Down
27 changes: 18 additions & 9 deletions src/assets/CSSAsset.js
Expand Up @@ -29,19 +29,19 @@ class CSSAsset extends Asset {

collectDependencies() {
this.ast.root.walkAtRules('import', rule => {
let params = valueParser(rule.params).nodes;
let [name, ...media] = params;
let params = valueParser(rule.params);
let [name, ...media] = params.nodes;
let dep;
if (name.type === 'string') {
dep = name.value;
} else if (
if (
name.type === 'function' &&
name.value === 'url' &&
name.nodes.length
) {
dep = name.nodes[0].value;
name = name.nodes[0];
}

dep = name.value;

if (!dep) {
throw new Error('Could not find import name for ' + rule);
}
Expand All @@ -50,10 +50,19 @@ class CSSAsset extends Asset {
return;
}

media = valueParser.stringify(media).trim();
this.addDependency(dep, {media, loc: rule.source.start});
// If this came from an inline <style> tag, don't inline the imported file. Replace with the correct URL instead.
// TODO: run CSSPackager on inline style tags.
let inlineHTML =
this.options.rendition && this.options.rendition.inlineHTML;
if (inlineHTML) {
name.value = this.addURLDependency(dep, {loc: rule.source.start});
rule.params = params.toString();
} else {
media = valueParser.stringify(media).trim();
this.addDependency(dep, {media, loc: rule.source.start});
rule.remove();
}

rule.remove();
this.ast.dirty = true;
});

Expand Down
95 changes: 93 additions & 2 deletions src/assets/HTMLAsset.js
Expand Up @@ -61,6 +61,12 @@ const META = {
]
};

const SCRIPT_TYPES = {
'application/javascript': 'js',
'text/javascript': 'js',
'application/json': false
};

// Options to be passed to `addURLDependency` for certain tags + attributes
const OPTIONS = {
a: {
Expand Down Expand Up @@ -172,8 +178,93 @@ class HTMLAsset extends Asset {
}
}

generate() {
return this.isAstDirty ? render(this.ast) : this.contents;
async generate() {
// Extract inline <script> and <style> tags for processing.
let parts = [];
this.ast.walk(node => {
if (node.tag === 'script' || node.tag === 'style') {
let value = node.content && node.content.join('').trim();
if (value) {
let type;

if (node.tag === 'style') {
if (node.attrs && node.attrs.type) {
type = node.attrs.type.split('/')[1];
} else {
type = 'css';
}
} else if (node.attrs && node.attrs.type) {
// Skip JSON
if (SCRIPT_TYPES[node.attrs.type] === false) {
return node;
}

if (SCRIPT_TYPES[node.attrs.type]) {
type = SCRIPT_TYPES[node.attrs.type];
} else {
type = node.attrs.type.split('/')[1];
}
} else {
type = 'js';
}

parts.push({
type,
value,
inlineHTML: true,
meta: {
type: 'tag',
node
}
});
}
}

// Process inline style attributes.
if (node.attrs && node.attrs.style) {
parts.push({
type: 'css',
value: node.attrs.style,
meta: {
type: 'attr',
node
}
});
}

return node;
});

return parts;
}

async postProcess(generated) {
// Replace inline scripts and styles with processed results.
for (let rendition of generated) {
let {type, node} = rendition.meta;
if (type === 'attr' && rendition.type === 'css') {
node.attrs.style = rendition.value;
} else if (type === 'tag') {
if (
(rendition.type === 'js' && node.tag === 'script') ||
(rendition.type === 'css' && node.tag === 'style')
) {
node.content = rendition.value;
}

// Delete "type" attribute, since CSS and JS are the defaults.
if (node.attrs) {
delete node.attrs.type;
}
}
}

return [
{
type: 'html',
value: render(this.ast)
}
];
}
}

Expand Down
13 changes: 10 additions & 3 deletions src/transforms/htmlnano.js
Expand Up @@ -4,10 +4,17 @@ const htmlnano = require('htmlnano');
module.exports = async function(asset) {
await asset.parseIfNeeded();

let htmlNanoConfig = await asset.getConfig(
['.htmlnanorc', '.htmlnanorc.js'],
{packageKey: 'htmlnano'}
let htmlNanoConfig = Object.assign(
{},
await asset.getConfig(['.htmlnanorc', '.htmlnanorc.js'], {
packageKey: 'htmlnano'
}),
{
minifyCss: false,
minifyJs: false
}
);

let res = await posthtml([htmlnano(htmlNanoConfig)]).process(asset.ast, {
skipParse: true
});
Expand Down
12 changes: 12 additions & 0 deletions src/visitors/dependencies.js
Expand Up @@ -156,6 +156,18 @@ function addDependency(asset, node, opts = {}) {
return;
}

// If this came from an inline <script> tag, throw an error.
// TODO: run JSPackager on inline script tags.
let inlineHTML =
asset.options.rendition && asset.options.rendition.inlineHTML;
if (inlineHTML) {
let err = new Error(
'Imports and requires are not supported inside inline <script> tags yet.'
);
err.loc = node.loc && node.loc.start;
throw err;
}

if (!asset.options.bundleNodeModules) {
const isRelativeImport = /^[/~.]/.test(node.value);
if (!isRelativeImport) return;
Expand Down
116 changes: 110 additions & 6 deletions test/html.js
Expand Up @@ -283,16 +283,16 @@ describe('html', function() {

let html = await fs.readFile(__dirname + '/dist/index.html', 'utf8');

// mergeStyles
// minifyJson
assert(
html.includes(
'<style>h1{color:red}div{font-size:20px}</style><style media="print">div{color:blue}</style>'
)
html.includes('<script type="application/json">{"user":"me"}</script>')
);

// minifyJson
// mergeStyles
assert(
html.includes('<script type="application/json">{"user":"me"}</script>')
html.includes(
'<style>h1{color:red}div{font-size:20px}</style><style media="print">div{color:#00f}</style>'
)
);

// minifySvg is false
Expand Down Expand Up @@ -589,4 +589,108 @@ describe('html', function() {
]
});
});

it('should process inline JS', async function() {
let b = await bundle(__dirname + '/integration/html-inline-js/index.html', {
production: true
});

const bundleContent = (await fs.readFile(b.name)).toString();
assert(!bundleContent.includes('someArgument'));
});

it('should process inline styles', async function() {
let b = await bundle(
__dirname + '/integration/html-inline-styles/index.html',
{production: true}
);

await assertBundleTree(b, {
name: 'index.html',
assets: ['index.html'],
childBundles: [
{
type: 'jpg',
assets: ['bg.jpg'],
childBundles: []
},
{
type: 'jpg',
assets: ['img.jpg'],
childBundles: []
}
]
});
});

it('should process inline styles using lang', async function() {
let b = await bundle(
__dirname + '/integration/html-inline-sass/index.html',
{production: true}
);

await assertBundleTree(b, {
name: 'index.html',
assets: ['index.html']
});

let html = await fs.readFile(__dirname + '/dist/index.html', 'utf8');

assert(html.includes('<style>.index{color:#00f}</style>'));
});

it('should process inline non-js scripts', async function() {
let b = await bundle(
__dirname + '/integration/html-inline-coffeescript/index.html',
{production: true}
);

await assertBundleTree(b, {
name: 'index.html',
assets: ['index.html']
});

let html = await fs.readFile(__dirname + '/dist/index.html', 'utf8');

assert(html.includes('alert("Hello, World!")'));
});

it('should handle inline css with @imports', async function() {
let b = await bundle(
__dirname + '/integration/html-inline-css-import/index.html',
{production: true}
);

await assertBundleTree(b, {
name: 'index.html',
assets: ['index.html'],
childBundles: [
{
type: 'css',
assets: ['test.css']
}
]
});

let html = await fs.readFile(__dirname + '/dist/index.html', 'utf8');
assert(html.includes('@import'));
});

it('should error on imports and requires in inline <script> tags', async function() {
let err;
try {
await bundle(
__dirname + '/integration/html-inline-js-require/index.html',
{production: true}
);
} catch (e) {
err = e;
}

assert(err);
assert.equal(
err.message,
'Imports and requires are not supported inside inline <script> tags yet.'
);
});
});
11 changes: 11 additions & 0 deletions test/integration/html-inline-coffeescript/index.html
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Inline JavaScript Parcel</title>
</head>
<body>
<script type="application/coffee">
alert "Hello, World!"
</script>
</body>
</html>
8 changes: 8 additions & 0 deletions test/integration/html-inline-css-import/index.html
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<body>
<style>
@import './test.css';
</style>
</body>
</html>
3 changes: 3 additions & 0 deletions test/integration/html-inline-css-import/test.css
@@ -0,0 +1,3 @@
h1 {
color: red
}
8 changes: 8 additions & 0 deletions test/integration/html-inline-js-require/index.html
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<body>
<script>
require('./test');
</script>
</body>
</html>
1 change: 1 addition & 0 deletions test/integration/html-inline-js-require/test.js
@@ -0,0 +1 @@
console.log('test')

0 comments on commit 7423541

Please sign in to comment.