Skip to content

Commit

Permalink
feat(*): initial implementation based on https://github.com/bazelbuil…
Browse files Browse the repository at this point in the history
  • Loading branch information
jbedard committed Nov 23, 2019
1 parent 1788010 commit 5033768
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# html-insert-assets
Insert assets such as .js, .css into an HTML file.

Originally forked from https://github.com/bazelbuild/rules_nodejs/tree/0.41.0/packages/inject-html.
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,18 @@
"url": "https://github.com/jbedard/insert-assets.git"
},
"author": "Jason Bedard",
"license": "MIT"
"license": "MIT",
"main": "./src/main.js",
"bin": {
"html-insert-assets": "./src/main.js"
},
"scripts": {
"test": "jasmine test/*.js"
},
"dependencies": {
"parse5": "^5.1.1"
},
"devDependencies": {
"jasmine": "^3.5.0"
}
}
123 changes: 123 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Originally forked from https://github.com/bazelbuild/rules_nodejs/tree/0.41.0/packages/inject-html

const parse5 = require('parse5');
const treeAdapter = require('parse5/lib/tree-adapters/default');
const fs = require('fs');

function findElementByName(d, name) {
if (treeAdapter.isTextNode(d)) return undefined;
if (d.tagName && d.tagName.toLowerCase() === name) {
return d;
}
if (!treeAdapter.getChildNodes(d)) {
return undefined;
}
for (let i = 0; i < treeAdapter.getChildNodes(d).length; i++) {
const f = treeAdapter.getChildNodes(d)[i];
const result = findElementByName(f, name);
if (result) return result;
}
return undefined;
}

function main(params, read = fs.readFileSync, write = fs.writeFileSync, timestamp = Date.now) {
const outputFile = params.shift();
const inputFile = params.shift();
const rootDirs = [];
while (params.length && params[0] !== '--assets') {
let r = params.shift();
if (!r.endsWith('/')) {
r += '/';
}
rootDirs.push(r);
}
// Always trim the longest prefix
rootDirs.sort((a, b) => b.length - a.length);
params.shift(); // --assets

const document = parse5.parse(read(inputFile, {encoding: 'utf-8'}), {treeAdapter});

const body = findElementByName(document, 'body');
if (!body) {
throw ('No <body> tag found in HTML document');
}

const head = findElementByName(document, 'head');
if (!head) {
throw ('No <head> tag found in HTML document');
}

/**
* Trims the longest prefix from the path
*/
function relative(execPath) {
if (execPath.startsWith('external/')) {
execPath = execPath.substring('external/'.length);
}
for (const r of rootDirs) {
if (execPath.startsWith(r)) {
return execPath.substring(r.length);
}
}
return execPath;
}

const jsFiles = params.filter(s => /\.m?js$/i.test(s));
for (const s of jsFiles) {
// Differential loading: for filenames like
// foo.mjs
// bar.es2015.js
// we use a <script type="module" tag so these are only run in browsers that have ES2015 module
// loading
if (/\.(es2015\.|m)js$/i.test(s)) {
const moduleScript = treeAdapter.createElement('script', undefined, [
{name: 'type', value: 'module'},
{name: 'src', value: `/${relative(s)}?v=${timestamp()}`},
]);
treeAdapter.appendChild(body, moduleScript);
} else {
// Other filenames we assume are for non-ESModule browsers, so if the file has a matching
// ESModule script we add a 'nomodule' attribute
function hasMatchingModule(file, files) {
const noExt = file.substring(0, file.length - 3);
const testMjs = (noExt + '.mjs').toLowerCase();
const testEs2015 = (noExt + '.es2015.js').toLowerCase();
const matches = files.filter(t => {
const lc = t.toLowerCase();
return lc === testMjs || lc === testEs2015;
});
return matches.length > 0;
}

// Note: empty string value is equivalent to a bare attribute, according to
// https://github.com/inikulin/parse5/issues/1
const nomodule = hasMatchingModule(s, jsFiles) ? [{name: 'nomodule', value: ''}] : [];

const noModuleScript = treeAdapter.createElement('script', undefined, nomodule.concat([
{name: 'src', value: `/${relative(s)}?v=${timestamp()}`},
]));
treeAdapter.appendChild(body, noModuleScript);
}
}

for (const s of params.filter(s => /\.css$/.test(s))) {
const stylesheet = treeAdapter.createElement('link', undefined, [
{name: 'rel', value: 'stylesheet'},
{name: 'href', value: `/${relative(s)}?v=${timestamp()}`},
]);
treeAdapter.appendChild(head, stylesheet);
}

const content = parse5.serialize(document, {treeAdapter});
write(outputFile, content, {encoding: 'utf-8'});
return 0;
}

module.exports = {main};

if (require.main === module) {
// We always require the arguments are encoded into a flagfile
// so that we don't exhaust the command-line limit.
const params = fs.readFileSync(process.argv[2], {encoding: 'utf-8'}).split('\n').filter(l => !!l);
process.exitCode = main(params);
}
Empty file added test/data/file.css
Empty file.
1 change: 1 addition & 0 deletions test/data/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html></html>
1 change: 1 addition & 0 deletions test/data/index_golden.html_
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><head><link rel="stylesheet" href="/file.css?v=123"></head><body><script src="/script.js?v=123"></script></body></html>
4 changes: 4 additions & 0 deletions test/data/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const el = document.createElement('div');
const text = 'Hello, World';
el.innerText = text;
document.body.appendChild(el);
73 changes: 73 additions & 0 deletions test/spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const inserter = require('../src/main');

describe('HTML inserter', () => {
const inFile = 'data/some/index.html';
const outFile = 'out/some/index.html';

let output;
function read(file) {
if (file === inFile) return `<html><head></head><body></body></html>`;
throw new Error(`no content for ${file}`);
}
function write(_, content) {
output = content;
}

it('should do be a no-op', () => {
expect(inserter.main([outFile, inFile], read, write)).toBe(0);
expect(output).toBe('<html><head></head><body></body></html>');
});

it('should inject script tag', () => {
expect(inserter.main([outFile, inFile, '--assets', 'path/to/my.js'], read, write, () => 123)).toBe(0);
expect(output).toBe(
'<html><head></head><body><script src="/path/to/my.js?v=123"></script></body></html>');
});

it('should allow the "module js" extension', () => {
expect(inserter.main([outFile, inFile, '--assets', 'path/to/my.mjs'], read, write, () => 123))
.toBe(0);
expect(output).toBe(
'<html><head></head><body><script type="module" src="/path/to/my.mjs?v=123"></script></body></html>');
});

it('should allow the ".es2015.js" extension', () => {
expect(inserter.main(
[outFile, inFile, '--assets', 'path/to/my.es2015.js'], read, write, () => 123))
.toBe(0);
expect(output).toBe(
'<html><head></head><body><script type="module" src="/path/to/my.es2015.js?v=123"></script></body></html>');
});

it('should strip longest prefix', () => {
expect(inserter.main([outFile, inFile,
'path', 'path/to',
'--assets', 'path/to/my.js'], read, write, () => 123)).toBe(0);
expect(output).toBe(
'<html><head></head><body><script src="/my.js?v=123"></script></body></html>');
});

it('should strip external workspaces', () => {
expect(inserter.main([outFile, inFile,
'npm/node_modules/zone.js/dist',
'--assets', 'external/npm/node_modules/zone.js/dist/zone.min.js'], read, write, () => 123)).toBe(0);
expect(output).toBe(
'<html><head></head><body><script src="/zone.min.js?v=123"></script></body></html>');

});

it('should inject link tag', () => {
expect(inserter.main([outFile, inFile, '--assets', 'path/to/my.css'], read, write, () => 123)).toBe(0);
expect(output).toBe(
'<html><head><link rel="stylesheet" href="/path/to/my.css?v=123"></head><body></body></html>');
});

it('should create a pair of script tags for differential loading', () => {
expect(inserter.main(
[outFile, inFile, '--assets', 'path/to/my.js', 'path/to/my.es2015.js'], read, write,
() => 123))
.toBe(0);
expect(output).toBe(
'<html><head></head><body><script nomodule="" src="/path/to/my.js?v=123"></script><script type="module" src="/path/to/my.es2015.js?v=123"></script></body></html>');
});
});
89 changes: 89 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,92 @@
# yarn lockfile v1


balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=

brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"

[email protected]:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=

fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=

glob@^7.1.4:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"

inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"

inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==

jasmine-core@~3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4"
integrity sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==

jasmine@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.5.0.tgz#7101eabfd043a1fc82ac24e0ab6ec56081357f9e"
integrity sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==
dependencies:
glob "^7.1.4"
jasmine-core "~3.5.0"

minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"

once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"

parse5@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==

path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=

wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

0 comments on commit 5033768

Please sign in to comment.