@typed/content-hash
Version:
Content hash a directory of HTML/JS/CSS files and other static assets
205 lines • 8.27 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createHtmlPlugin = void 0;
const FxEnv_1 = require("@typed/fp/FxEnv");
const Option_1 = require("fp-ts/Option");
const ReadonlyArray_1 = require("fp-ts/ReadonlyArray");
const Tree_1 = require("fp-ts/Tree");
const path_1 = require("path");
const typed_colors_1 = require("typed-colors");
const logging_1 = require("../../application/services/logging");
const ensureRelative_1 = require("../ensureRelative");
const fsReadFile_1 = require("../fsReadFile");
const defaults_1 = require("./defaults");
const getFileExtension_1 = require("./getFileExtension");
const isExternalUrl_1 = require("./isExternalUrl");
const parseSrcSets_1 = require("./parseSrcSets");
const resolvePackage_1 = require("./resolvePackage");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse, parseDefaults } = require('himalaya');
const supportedFileExtension = ['.html'];
const foldDependencies = (0, Tree_1.foldMap)((0, ReadonlyArray_1.getMonoid)());
const searchMap = {
a: ['href', 'ping'],
applet: ['archive', 'code', 'codebase', 'object', 'src'],
area: ['href', 'ping'],
audio: ['src'],
base: ['href'],
blockquote: ['cite'],
body: ['background'],
button: ['formaction'],
del: ['cite'],
embed: ['src'],
form: ['action'],
frame: ['longdesc', 'src'],
head: ['profile'],
html: ['manifest'],
iframe: ['longdesc', 'src'],
img: ['longdesc', 'src', 'srcset'],
input: ['formaction', 'src'],
ins: ['cite'],
link: ['href'],
menuitem: ['icon'],
object: ['codebase', 'data'],
q: ['cite'],
script: ['src'],
source: ['src', 'srcset'],
table: ['background'],
tbody: ['background'],
td: ['background'],
tfoot: ['background'],
th: ['background'],
thead: ['background'],
tr: ['background'],
track: ['src'],
video: ['poster', 'src'],
};
function createHtmlPlugin({ buildDirectory, mainFields = defaults_1.MAIN_FIELDS }) {
const html = {
readFilePath: (filePath) => (0, FxEnv_1.Do)(function* (_) {
const ext = (0, getFileExtension_1.getFileExtension)(filePath);
if (!supportedFileExtension.includes(ext)) {
yield* _((0, logging_1.debug)(`${(0, typed_colors_1.red)(`[HTML]`)} Unsupported file extension ${filePath}`));
return Option_1.none;
}
yield* _((0, logging_1.debug)(`${(0, typed_colors_1.yellow)(`[HTML]`)} Reading ${filePath}...`));
const initial = yield* _((0, fsReadFile_1.fsReadFile)(filePath, { supportsSourceMaps: false, isBase64Encoded: false }));
yield* _((0, logging_1.debug)(`${(0, typed_colors_1.yellow)(`[HTML]`)} Finding Dependencies ${filePath}...`));
const document = findDependencies(initial, buildDirectory, mainFields);
return (0, Option_1.some)({ ...document, contentHash: Option_1.none, sourceMap: Option_1.none });
}),
};
return html;
}
exports.createHtmlPlugin = createHtmlPlugin;
function findDependencies(document, buildDirectory, mainFields) {
const directory = path_1.posix.dirname(document.filePath);
const ast = parse(document.contents, { ...parseDefaults, includePositions: true });
const dependencies = ast
.map(astToTree)
.flatMap(foldDependencies(isValidDependency(buildDirectory, directory, mainFields, document.contents)));
return { ...document, dependencies };
}
function astToTree(ast) {
return {
value: ast,
forest: (ast.children || []).map(astToTree),
};
}
function isValidDependency(buildDirectory, directory, mainFields, contents) {
return (ast) => {
var _a;
if (ast.type !== 'element') {
return [];
}
const tagName = ast.tagName.toLowerCase();
if (tagName === 'template' && !!((_a = ast.children) === null || _a === void 0 ? void 0 : _a[0].content)) {
const child = ast.children[0];
const start = child.position.start.index;
const content = child.content;
const childAst = parse(content, { ...parseDefaults, includePositions: true });
return childAst
.map(astToTree)
.flatMap(foldDependencies(isValidDependency(buildDirectory, directory, mainFields, content)))
.map((d) => ({ ...d, position: { start: d.position.start + start, end: d.position.end + start } }));
}
if (!(tagName in searchMap)) {
return [];
}
const attributesToSearch = searchMap[tagName];
return ast.attributes
.filter(({ key }) => attributesToSearch.includes(key))
.flatMap(getDependencies(buildDirectory, directory, mainFields, contents, ast));
};
}
function getDependencies(buildDirectory, directory, mainFields, contents, ast) {
const { position } = ast;
const astStart = position.start.index;
const astEnd = position.end.index;
const sourceString = contents.slice(astStart, astEnd);
const tagName = ast.tagName.toLowerCase();
return (attr) => {
const attrStart = astStart + findSourceIndex(sourceString, attr);
const attrEnd = attrStart + attr.value.length;
const isImgSrcSet = tagName === 'img' && attr.key === 'srcset';
const resolved = isImgSrcSet
? (0, parseSrcSets_1.parseSrcSets)(attr.value, attrStart).map((s) => resolveSpecifier(buildDirectory, directory, s.url, mainFields, s.position))
: [
resolveSpecifier(buildDirectory, directory, attr.value, mainFields, {
start: attrStart,
end: attrEnd,
}),
];
return resolved.filter(Option_1.isSome).map((o) => o.value);
};
}
function findSourceIndex(source, attr) {
const woQuotesIndex = source.indexOf(formatNoQuotes(attr));
if (woQuotesIndex > -1) {
return woQuotesIndex + attr.key.length + 1; // + the equals
}
const singleQuotesIndex = source.indexOf(formatSingleQuotes(attr));
if (singleQuotesIndex > -1) {
return singleQuotesIndex + attr.key.length + 2; // + the equals and first quote
}
const doubleQuotesIndex = source.indexOf(formatDoubleQuotes(attr));
if (doubleQuotesIndex > -1) {
return doubleQuotesIndex + attr.key.length + 2;
}
throw new Error(`Unable to find HTML attribute ${formatDoubleQuotes(attr)} in ${source}`);
}
function formatNoQuotes(attr) {
return `${attr.key}=${attr.value}`;
}
function formatSingleQuotes(attr) {
return `${attr.key}='${attr.value}'`;
}
function formatDoubleQuotes(attr) {
return `${attr.key}="${attr.value}"`;
}
function ensureRelativeSpecifier(specifier, buildDirectory, directory) {
if (specifier.startsWith('/')) {
return (0, ensureRelative_1.ensureRelative)(path_1.posix.relative(directory, path_1.posix.resolve(buildDirectory, specifier.slice(1))));
}
return specifier;
}
function resolveSpecifier(buildDirectory, directory, specifier, mainFields, position) {
const relativeSpecifier = ensureRelativeSpecifier(specifier, buildDirectory, directory);
const hasFileExtension = isFileExtension(path_1.posix.extname(relativeSpecifier));
if ((0, isExternalUrl_1.isExternalUrl)(relativeSpecifier)) {
return Option_1.none;
}
try {
const filePath = (0, resolvePackage_1.resolvePackage)({
moduleSpecifier: relativeSpecifier,
directory,
extensions: ['.js'],
mainFields,
});
const dep = {
specifier,
filePath,
fileExtension: (0, getFileExtension_1.getFileExtension)(filePath),
position,
};
return (0, Option_1.some)(dep);
}
catch (error) {
// If we're really sure it is supposed to be a file, throw the error
if (hasFileExtension) {
throw error;
}
return Option_1.none;
}
}
function isFileExtension(ext) {
if (!ext.trim()) {
return false;
}
const n = Number.parseFloat(ext);
if (!Number.isNaN(n)) {
return false;
}
return !ext.includes(' ');
}
//# sourceMappingURL=html.js.map