Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Process inline styles and scripts #1456

Merged
merged 15 commits into from Jul 21, 2018
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')