From 21ed6ee42e298b7eb640ed35912e9c0355c1270d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 4 Sep 2018 15:58:52 -0700 Subject: [PATCH] Add ?module support for HTML files Closes #113 --- modules/actions/serveFile.js | 14 ++- modules/actions/serveHTMLModule.js | 50 ++++++++++ modules/actions/serveJavaScriptModule.js | 28 +----- modules/middleware/findFile.js | 6 +- modules/utils/rewriteBareModuleIdentifiers.js | 23 +++++ package.json | 1 + yarn.lock | 96 ++++++++++++++++++- 7 files changed, 187 insertions(+), 31 deletions(-) create mode 100644 modules/actions/serveHTMLModule.js create mode 100644 modules/utils/rewriteBareModuleIdentifiers.js diff --git a/modules/actions/serveFile.js b/modules/actions/serveFile.js index 366ee91..1d1a556 100644 --- a/modules/actions/serveFile.js +++ b/modules/actions/serveFile.js @@ -1,4 +1,5 @@ const serveAutoIndexPage = require("./serveAutoIndexPage"); +const serveHTMLModule = require("./serveHTMLModule"); const serveJavaScriptModule = require("./serveJavaScriptModule"); const serveStaticFile = require("./serveStaticFile"); const serveMetadata = require("./serveMetadata"); @@ -16,7 +17,18 @@ function serveFile(req, res) { } if (req.query.module != null) { - return serveJavaScriptModule(req, res); + if (req.entry.contentType === "application/javascript") { + return serveJavaScriptModule(req, res); + } + + if (req.entry.contentType === "text/html") { + return serveHTMLModule(req, res); + } + + return res + .status(403) + .type("text") + .send("?module mode is available only for JavaScript and HTML files"); } serveStaticFile(req, res); diff --git a/modules/actions/serveHTMLModule.js b/modules/actions/serveHTMLModule.js new file mode 100644 index 0000000..1b2cdf2 --- /dev/null +++ b/modules/actions/serveHTMLModule.js @@ -0,0 +1,50 @@ +const etag = require("etag"); +const cheerio = require("cheerio"); + +const getContentTypeHeader = require("../utils/getContentTypeHeader"); +const rewriteBareModuleIdentifiers = require("../utils/rewriteBareModuleIdentifiers"); + +function serveHTMLModule(req, res) { + try { + const $ = cheerio.load(req.entry.content.toString("utf8")); + + $("script[type=module]").each((index, element) => { + $(element).html( + rewriteBareModuleIdentifiers($(element).html(), req.packageConfig) + ); + }); + + const code = $.html(); + + res + .set({ + "Content-Length": Buffer.byteLength(code), + "Content-Type": getContentTypeHeader(req.entry.contentType), + "Cache-Control": "public, max-age=31536000, immutable", // 1 year + ETag: etag(code), + "Cache-Tag": "file, html-file, html-module" + }) + .send(code); + } catch (error) { + console.error(error); + + const errorName = error.constructor.name; + const errorMessage = error.message.replace( + /^.*?\/unpkg-.+?\//, + `/${req.packageSpec}/` + ); + const codeFrame = error.codeFrame; + const debugInfo = `${errorName}: ${errorMessage}\n\n${codeFrame}`; + + res + .status(500) + .type("text") + .send( + `Cannot generate module for ${req.packageSpec}${ + req.filename + }\n\n${debugInfo}` + ); + } +} + +module.exports = serveHTMLModule; diff --git a/modules/actions/serveJavaScriptModule.js b/modules/actions/serveJavaScriptModule.js index ad7ec60..5772c95 100644 --- a/modules/actions/serveJavaScriptModule.js +++ b/modules/actions/serveJavaScriptModule.js @@ -1,35 +1,9 @@ const etag = require("etag"); -const babel = require("babel-core"); const getContentTypeHeader = require("../utils/getContentTypeHeader"); -const unpkgRewrite = require("../plugins/unpkgRewrite"); - -function rewriteBareModuleIdentifiers(code, packageConfig) { - const dependencies = Object.assign( - {}, - packageConfig.peerDependencies, - packageConfig.dependencies - ); - - const options = { - // Ignore .babelrc and package.json babel config - // because we haven't installed dependencies so - // we can't load plugins; see #84 - babelrc: false, - plugins: [unpkgRewrite(dependencies)] - }; - - return babel.transform(code, options).code; -} +const rewriteBareModuleIdentifiers = require("../utils/rewriteBareModuleIdentifiers"); function serveJavaScriptModule(req, res) { - if (req.entry.contentType !== "application/javascript") { - return res - .status(403) - .type("text") - .send("?module mode is available only for JavaScript files"); - } - try { const code = rewriteBareModuleIdentifiers( req.entry.content.toString("utf8"), diff --git a/modules/middleware/findFile.js b/modules/middleware/findFile.js index 92d3344..f55a494 100644 --- a/modules/middleware/findFile.js +++ b/modules/middleware/findFile.js @@ -84,8 +84,10 @@ function searchEntries(tarballStream, entryName, wantsHTML) { // and the client wants HTML. if ( entry.name === entryName || - // Allow accessing e.g. `/lib/index.html` using `/lib/` - (wantsHTML && entry.name === `${entryName}/index.html`) || + // Allow accessing e.g. `/index.html` using `/` + (wantsHTML && + entry.name === + (entryName === "" ? "index.html" : `${entryName}/index.html`)) || // Allow accessing e.g. `/index.js` or `/index.json` using // `/index` for compatibility with CommonJS (!wantsHTML && entry.name === `${entryName}.js`) || diff --git a/modules/utils/rewriteBareModuleIdentifiers.js b/modules/utils/rewriteBareModuleIdentifiers.js new file mode 100644 index 0000000..603d7bd --- /dev/null +++ b/modules/utils/rewriteBareModuleIdentifiers.js @@ -0,0 +1,23 @@ +const babel = require("babel-core"); + +const unpkgRewrite = require("../plugins/unpkgRewrite"); + +function rewriteBareModuleIdentifiers(code, packageConfig) { + const dependencies = Object.assign( + {}, + packageConfig.peerDependencies, + packageConfig.dependencies + ); + + const options = { + // Ignore .babelrc and package.json babel config + // because we haven't installed dependencies so + // we can't load plugins; see #84 + babelrc: false, + plugins: [unpkgRewrite(dependencies)] + }; + + return babel.transform(code, options).code; +} + +module.exports = rewriteBareModuleIdentifiers; diff --git a/package.json b/package.json index 7f679a7..5eec884 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "babel-preset-stage-2": "^6.24.1", "babel-register": "^6.26.0", "body-parser": "^1.18.2", + "cheerio": "^1.0.0-rc.2", "cors": "^2.8.1", "countries-list": "^1.3.2", "date-fns": "^1.28.1", diff --git a/yarn.lock b/yarn.lock index 22ca323..3f0b4b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -68,6 +68,10 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" +"@types/node@*": + version "10.9.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.4.tgz#0f4cb2dc7c1de6096055357f70179043c33e9897" + abab@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -1210,6 +1214,10 @@ bonjour@^3.5.0: multicast-dns "^6.0.1" multicast-dns-service-types "^1.1.0" +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + boom@2.x.x: version "2.10.1" resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" @@ -1563,6 +1571,17 @@ charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" +cheerio@^1.0.0-rc.2: + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" @@ -1996,6 +2015,15 @@ css-loader@0.26.1: postcss-modules-values "^1.1.0" source-list-map "^0.1.4" +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + css-selector-tokenizer@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" @@ -2004,6 +2032,10 @@ css-selector-tokenizer@^0.7.0: fastparse "^1.1.1" regexpu-core "^1.0.0" +css-what@2.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" + cssesc@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" @@ -2278,16 +2310,51 @@ doctrine@^2.0.2, doctrine@^2.1.0: dependencies: esutils "^2.0.2" +dom-serializer@0, dom-serializer@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" +domelementtype@1, domelementtype@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + domexception@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" dependencies: webidl-conversions "^4.0.2" +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + dependencies: + dom-serializer "0" + domelementtype "1" + dot-prop@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" @@ -2381,6 +2448,10 @@ enhanced-resolve@^3.4.0: object-assign "^4.0.1" tapable "^0.2.7" +entities@^1.1.1, entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + errno@^0.1.3: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" @@ -3449,6 +3520,17 @@ html-minifier@^3.5.8: relateurl "0.2.x" uglify-js "3.3.x" +htmlparser2@^3.9.1: + version "3.9.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" + dependencies: + domelementtype "^1.3.0" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^2.0.2" + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -4606,7 +4688,7 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@^4.13.1, lodash@^4.17.10: +lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -5179,6 +5261,12 @@ npmlog@^4.0.2: gauge "~2.7.3" set-blocking "~2.0.0" +nth-check@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4" + dependencies: + boolbase "~1.0.0" + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -5406,6 +5494,12 @@ parse5@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + dependencies: + "@types/node" "*" + parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"