UNPKG

@dazlab-team/angular-playable-builder

Version:

Builds Facebook Playable Ad source file from the Angular app sources.

206 lines 7.58 kB
"use strict"; // This is a special file to make playable from the source code produced by ng build --prod. // It mostly contains various heuristics and hacks, rather than the robust solution. // Copyright (c) Andrew Anisimov, Dazlab (https://dazlab.global/). Object.defineProperty(exports, "__esModule", { value: true }); exports.buildCustomWebpackBrowser = void 0; // 1. First hack will be to inline all scripts, styles and images produced by the standard build. // We honestly use the existing `html-inline` tool for that (https://github.com/substack/html-inline), // but with all the unnecessary stuff stripped out. // // 2. Second hack will be to replace some regular expression literals in the produces scripts // which Playable Preview Tool escapes for some reason, making the script invalid. // e.g. `/</g` is replaced with `new RegExp('<', 'g')` and so on. const build_angular_1 = require("@angular-devkit/build-angular"); const architect_1 = require("@angular-devkit/architect"); const stream_1 = require("stream"); const fs = require("fs"); const path = require("path"); const request = require('sync-request'); const trumpet = require('trumpet'); // FIXME: get rid of this dependency (replace with TS one) class ReadableString extends stream_1.Readable { constructor(str) { super(); this.str = str; this.sent = false; } _read() { if (!this.sent) { this.push(Buffer.from(this.str)); this.sent = true; } else { this.push(null); } } } const buildCustomWebpackBrowser = (options, context) => build_angular_1.executeBrowserBuilder(options, context, { indexHtml(html) { return transformIndexHtml(html, options.outputPath); } }); exports.buildCustomWebpackBrowser = buildCustomWebpackBrowser; function transformIndexHtml(html, outputPath) { const stream = new ReadableString(html) .pipe(inlineHtml(outputPath)); return new Promise((resolve, reject) => { let result = ''; stream.on('data', (data) => { result += data.toString(); }); stream.on('end', () => { resolve(result); }); stream.on('error', (err) => { reject(err); }); }); } function streamReplace(needle, replacer) { let chunks = [], len = 0, pos = 0; return new stream_1.Transform({ transform(chunk, encoding, callback) { chunks.push(chunk); len += chunk.length; if (pos === 1) { const data = Buffer.concat(chunks, len) .toString() .replace(needle, replacer); // TODO: examine and profile garbage chunks = []; len = 0; this.push(data); } pos = 1 ^ pos; callback(null); }, flush(callback) { if (chunks.length) { this.push(Buffer.concat(chunks, len) .toString() .replace(needle, replacer)); } callback(null); } }); } function inlineHtml(basedir) { const tr = trumpet(); tr.selectAll('script[src]', function (node) { const file = fix(node.getAttribute('src')); node.removeAttribute('src'); node.removeAttribute('defer'); // this is the minor change in html-inline lib. fs.createReadStream(file) .pipe(streamReplace(/\/([<>]+?)\/([gi]{1,2})/g, 'new RegExp(\'$1\', \'$2\')')) // THIS IS THE MAIN CHANGE IN html-inline .pipe(node.createWriteStream()); }); tr.selectAll('img[src]', function (node) { inline64(node, 'src'); }); tr.selectAll('link[href]', function (node) { const rel = (node.getAttribute('rel') || '').toLowerCase(); if (rel === 'stylesheet') return; inline64(node, 'href'); }); tr.selectAll('link[href]', function (node) { const rel = node.getAttribute('rel').toLowerCase(); if (rel !== 'stylesheet') return; const file = fix(node.getAttribute('href')); const w = node.createWriteStream({ outer: true }); w.write('<style>'); const r = fs.createReadStream(file); r.pipe(w, { end: false }); r.on('end', function () { w.end('</style>'); }); }); tr.selectAll('style', function (node) { const r = node.createReadStream({ outer: true }); r.pipe(streamReplace(/url\((.*?)\)/g, (str, url) => { const data = url_base64(url); const type = mime(url); return `url("data:${type};base64,${data}")`; })) .pipe(node.createWriteStream({ outer: true })); }); return tr; function fix(p) { if (path.isAbsolute(p)) { return path.resolve(basedir, path.relative('/', p)); } else { return path.resolve(basedir, p); } } function enc(s) { return s.replace(/"/g, '&#34;') .replace(/>/g, '&gt;') .replace(/</g, '&lt;'); } function mime(filename) { const ext = path.extname(filename) .replace(/^\./, '') .toLowerCase(); return { svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/jpeg', woff: 'application/font-woff', woff2: 'application/font-woff' }[ext] || 'image/png'; } function url_base64(url) { const res = request('GET', url); return res.getBody().toString('base64'); } function inline64(node, name) { const href = node.getAttribute(name); if (/^data:/.test(href)) return; const w = node.createWriteStream({ outer: true }); const attrs = node.getAttributes(); w.write('<' + node.name); Object.keys(attrs).forEach(function (key) { if (key === name) return; w.write(' ' + key + '="' + enc(attrs[key]) + '"'); }); const type = node.getAttribute('type') || mime(href); w.write(' ' + name + '="data:' + type + ';base64,'); let last = null; let stream = fs.createReadStream(fix(href)); stream.pipe(new stream_1.Transform({ transform(chunk, encoding, callback) { let buf; if (last) { buf = Buffer.concat([last, chunk]); last = null; } else { buf = Buffer.from(chunk); } let b; if (buf.length % 3 === 0) { b = buf; } else { b = buf.slice(0, buf.length - buf.length % 3); last = buf.slice(buf.length - buf.length % 3); } w.write(b.toString('base64')); callback(); }, final() { if (last) w.write(last.toString('base64')); w.end('">'); } })); } } exports.default = architect_1.createBuilder(exports.buildCustomWebpackBrowser); //# sourceMappingURL=index.js.map