ember-auto-import
Version:
Zero-config import from NPM packages
377 lines • 16.9 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Inserter = void 0;
const broccoli_plugin_1 = __importDefault(require("broccoli-plugin"));
const debug_1 = __importDefault(require("debug"));
const fs_1 = require("fs");
const fs_extra_1 = require("fs-extra");
const path_1 = require("path");
const parse5_1 = __importDefault(require("parse5"));
const debug = (0, debug_1.default)('ember-auto-import:inserter');
class Inserter extends broccoli_plugin_1.default {
constructor(allApp, bundler, config, options) {
super([allApp], {
annotation: 'ember-auto-import-inserter',
persistentOutput: true,
});
this.bundler = bundler;
this.config = config;
this.options = options;
this.outputCache = new Map();
}
build() {
return __awaiter(this, void 0, void 0, function* () {
let fastbootInfo = this.fastbootManifestInfo();
let chunks = this.categorizeChunks();
for (let filename of this.config.htmlEntrypoints()) {
let fullName = (0, path_1.join)(this.inputPaths[0], filename);
if ((0, fs_1.existsSync)(fullName)) {
this.processHTML(filename, fullName, fastbootInfo, chunks);
}
}
if (fastbootInfo && !fastbootInfo.readsHTML) {
// we need to add our chunks to the fastboot manifest, because this
// version of fastboot doesn't look for scripts in HTML.
let assets = this.bundler.buildResult.entrypoints.get('app');
if (assets) {
for (let asset of assets) {
if (asset.endsWith('.js')) {
fastbootInfo.vendorFiles.push(asset);
}
}
}
for (let asset of this.bundler.buildResult.lazyAssets) {
if (asset.endsWith('.js')) {
fastbootInfo.vendorFiles.push(asset);
}
}
this.cachedOutputFileSync('package.json', JSON.stringify(fastbootInfo.pkg, null, 2));
}
});
}
// not touching our output files helps prevent other parts of the build from
// reacting to spurious changes. For example, if we touch the HTML, we defeat
// CSS hot reloading by making ember-cli think the HTML file has changed.
cachedOutputFileSync(localFilename, content) {
if (this.outputCache.get(localFilename) !== content) {
this.outputCache.set(localFilename, content);
(0, fs_extra_1.outputFileSync)((0, path_1.join)(this.outputPath, localFilename), content, 'utf8');
}
}
processHTML(filename, fullName, fastbootInfo, targets) {
debug(`parsing %s`, filename);
let html = (0, fs_1.readFileSync)(fullName, 'utf8');
let ast = parse5_1.default.parse(html, { sourceCodeLocationInfo: true });
let stringInserter = new StringInserter(html);
if (this.options.insertScriptsAt) {
debug(`looking for custom script element: %s`, this.options.insertScriptsAt);
}
else {
debug(`looking for scripts with src: %s`, Object.keys(targets.scripts)
.map((bundleName) => this.config.maybeBundleEntrypoint(bundleName, 'js'))
.filter(Boolean));
}
if (this.options.insertStylesAt) {
debug(`looking for custom style element: %s`, this.options.insertStylesAt);
}
else {
debug(`looking for link with href: %s`, Object.keys(targets.styles)
.map((bundleName) => this.config.maybeBundleEntrypoint(bundleName, 'css'))
.filter(Boolean));
}
if (this.includesTests(ast)) {
targets = useTestTargets(targets);
}
let inserted = [];
traverse(ast, (element) => {
var _a, _b;
if (this.options.insertScriptsAt) {
if (element.tagName === this.options.insertScriptsAt) {
let entrypoint = element.attrs.find((a) => a.name === 'entrypoint');
if (!entrypoint) {
throw new Error(`<${element.tagName}/> element in ${filename} is missing required entrypoint attribute`);
}
this.replaceCustomScript(targets, fastbootInfo, stringInserter, element, entrypoint.value, inserted);
}
}
else if (element.tagName === 'script') {
let src = (_a = element.attrs.find((a) => a.name === 'src')) === null || _a === void 0 ? void 0 : _a.value;
if (src) {
debug(`found script with src=%s`, src);
this.insertScripts(targets, fastbootInfo, stringInserter, element, src, inserted);
}
}
if (this.options.insertStylesAt) {
if (element.tagName === this.options.insertStylesAt) {
let entrypoint = element.attrs.find((a) => a.name === 'entrypoint');
if (!entrypoint) {
throw new Error(`<${element.tagName}/> element in ${filename} is missing required entrypoint attribute`);
}
this.replaceCustomStyle(targets, stringInserter, element, entrypoint.value, inserted);
}
}
else if (element.tagName === 'link') {
if (element.attrs.some((a) => a.name === 'rel' && a.value === 'stylesheet')) {
let href = (_b = element.attrs.find((a) => a.name === 'href')) === null || _b === void 0 ? void 0 : _b.value;
if (href) {
debug(`found stylesheet with href=%s`, href);
this.insertStyles(targets, stringInserter, element, href, inserted);
}
}
}
});
if (targets.scripts.app &&
!inserted.find((i) => i.bundleName === 'app' && i.kind === 'script')) {
if (this.options.insertScriptsAt) {
throw new Error(`ember-auto-import cannot find <${this.options.insertScriptsAt} entrypoint="app"> in ${filename}.`);
}
else {
throw new Error(`ember-auto-import could not find a place to insert app scripts in ${filename}.`);
}
}
if (targets.styles.app &&
!inserted.find((i) => i.bundleName === 'app' && i.kind === 'styles')) {
if (this.options.insertStylesAt) {
throw new Error(`ember-auto-import cannot find <${this.options.insertStylesAt} entrypoint="app"> in ${filename}.`);
}
else {
throw new Error(`ember-auto-import could not find a place to insert app styles in ${filename}.`);
}
}
this.cachedOutputFileSync(filename, stringInserter.serialize());
}
insertScripts(targets, fastbootInfo, stringInserter, element, src, inserted) {
let bundleName = this.config.bundleNameForEntrypoint(src, 'js');
if (!bundleName) {
return;
}
let scriptChunks = targets.scripts[bundleName];
if (!scriptChunks) {
return;
}
inserted.push({ bundleName, kind: 'script' });
debug(`inserting %s`, scriptChunks);
let insertedSrc = scriptChunks
.map((chunk) => `\n<script src="${this.chunkURL(chunk)}"></script>`)
.join('');
if ((fastbootInfo === null || fastbootInfo === void 0 ? void 0 : fastbootInfo.readsHTML) && bundleName === 'app') {
// lazy chunks are eager in fastboot because webpack's lazy
// loading doesn't work in fastboot, because we share a single
// build with the browser and use a browser-specific
// lazy-loading implementation. It's probably better to make
// them eager on the server anyway, so they're handled as part
// of server startup.
insertedSrc += this.bundler.buildResult.lazyAssets
.map((chunk) => `\n<fastboot-script src="${this.chunkURL(chunk)}"></fastboot-script>`)
.join('');
}
stringInserter.insert(element.sourceCodeLocation.endOffset, insertedSrc);
}
replaceCustomScript(targets, fastbootInfo, stringInserter, element, bundleName, insertedEntrypoints) {
let loc = element.sourceCodeLocation;
stringInserter.remove(loc.startOffset, loc.endOffset - loc.startOffset);
let scriptChunks = targets.scripts[bundleName];
if (!scriptChunks) {
return;
}
insertedEntrypoints.push({ bundleName, kind: 'script' });
debug(`inserting %s`, scriptChunks);
let tags = scriptChunks.map((chunk) => this.scriptFromCustomElement(element, chunk));
if ((fastbootInfo === null || fastbootInfo === void 0 ? void 0 : fastbootInfo.readsHTML) && bundleName === 'app') {
// lazy chunks are eager in fastboot because webpack's lazy
// loading doesn't work in fastboot, because we share a single
// build with the browser and use a browser-specific
// lazy-loading implementation. It's probably better to make
// them eager on the server anyway, so they're handled as part
// of server startup.
tags = tags.concat(this.bundler.buildResult.lazyAssets.map((chunk) => this.scriptFromCustomElement(element, chunk, 'fastboot-script')));
}
stringInserter.insert(loc.endOffset, tags.join('\n'));
}
replaceCustomStyle(targets, stringInserter, element, bundleName, inserted) {
let loc = element.sourceCodeLocation;
stringInserter.remove(loc.startOffset, loc.endOffset - loc.startOffset);
let styleChunks = targets.styles[bundleName];
if (!styleChunks) {
return;
}
inserted.push({ kind: 'styles', bundleName });
debug(`inserting %s`, styleChunks);
let tags = styleChunks.map((chunk) => this.styleFromCustomElement(element, chunk));
stringInserter.insert(loc.endOffset, tags.join('\n'));
}
scriptFromCustomElement(element, chunk, tag = 'script') {
let output = `<${tag} src="${this.chunkURL(chunk)}"`;
for (let { name, value } of element.attrs) {
if (name !== 'entrypoint') {
output += ` ${name}`;
if (value) {
output += `="${value}"`;
}
}
}
output += `></${tag}>`;
return output;
}
styleFromCustomElement(element, chunk) {
let output = `<link rel="stylesheet" href="${this.chunkURL(chunk)}"`;
for (let { name, value } of element.attrs) {
if (name !== 'entrypoint') {
output += ` ${name}`;
if (value) {
output += `="${value}"`;
}
}
}
output += `/>`;
return output;
}
insertStyles(targets, stringInserter, element, href, inserted) {
let bundleName = this.config.bundleNameForEntrypoint(href, 'css');
if (!bundleName) {
return;
}
let styleChunks = targets.styles[bundleName];
if (!styleChunks) {
return;
}
inserted.push({ kind: 'styles', bundleName });
debug(`inserting %s`, styleChunks);
stringInserter.insert(element.sourceCodeLocation.endOffset, styleChunks
.map((chunk) => `\n<link rel="stylesheet" href="${this.chunkURL(chunk)}"/>`)
.join(''));
}
chunkURL(chunk) {
return chunk.replace(/^assets\//, this.options.publicAssetURL);
}
fastbootManifestInfo() {
var _a, _b;
let pkgPath = (0, path_1.join)(this.inputPaths[0], 'package.json');
if (!(0, fs_1.existsSync)(pkgPath)) {
return undefined;
}
let pkg = (0, fs_extra_1.readJSONSync)(pkgPath);
if (!pkg.fastboot) {
return undefined;
}
if (((_a = pkg.fastboot.schemaVersion) !== null && _a !== void 0 ? _a : 0) >= 5) {
// starting in schemaVersion 5, fastboot discovers scripts directly from
// the HTML, so we don't need to muck about with inserting things into a
// separate manifest
return { readsHTML: true };
}
else {
if (!((_b = pkg.fastboot.manifest) === null || _b === void 0 ? void 0 : _b.vendorFiles)) {
throw new Error(`bug: ember-auto-import can't find the fastboot manifest vendorFiles`);
}
return {
pkg,
readsHTML: false,
vendorFiles: pkg.fastboot.manifest.vendorFiles,
};
}
}
categorizeChunks() {
let targets = {
scripts: {},
styles: {},
};
for (let [bundleName, assets] of this.bundler.buildResult.entrypoints) {
let jsChunks = assets.filter((a) => a.endsWith('.js'));
if (jsChunks.length > 0) {
targets.scripts[bundleName] = jsChunks;
}
let cssChunks = assets.filter((a) => a.endsWith('.css'));
if (cssChunks.length > 0) {
targets.styles[bundleName] = cssChunks;
}
}
return targets;
}
includesTests(ast) {
let foundTests = false;
traverse(ast, (element) => {
var _a;
if (this.options.insertScriptsAt) {
if (element.tagName === this.options.insertScriptsAt) {
let entrypoint = element.attrs.find((a) => a.name === 'entrypoint');
if ((entrypoint === null || entrypoint === void 0 ? void 0 : entrypoint.value) === 'tests') {
foundTests = true;
}
}
}
else {
if (element.tagName === 'script') {
let src = (_a = element.attrs.find((a) => a.name === 'src')) === null || _a === void 0 ? void 0 : _a.value;
if (src &&
this.config.bundleNameForEntrypoint(src, 'js') === 'tests') {
foundTests = true;
}
}
}
});
return foundTests;
}
}
exports.Inserter = Inserter;
class StringInserter {
constructor(original) {
this.original = original;
this.mutations = [];
}
insert(location, str) {
this.mutations.push({ type: 'insert', location, str });
}
remove(location, length) {
this.mutations.push({ type: 'remove', location, length });
}
serialize() {
let output = [];
let mutations = this.mutations
.slice()
.sort((a, b) => a.location - b.location);
let cursor = 0;
while (mutations.length > 0) {
let nextMutation = mutations.shift();
output.push(this.original.slice(cursor, nextMutation.location));
if (nextMutation.type === 'insert') {
output.push(nextMutation.str);
cursor = nextMutation.location;
}
else {
cursor = nextMutation.location + nextMutation.length;
}
}
output.push(this.original.slice(cursor));
return output.join('');
}
}
function traverse(node, fn) {
if ('tagName' in node) {
fn(node);
}
for (let child of node.childNodes) {
if ('childNodes' in child) {
traverse(child, fn);
}
}
}
function useTestTargets(targets) {
return {
styles: targets.styles,
scripts: Object.assign(Object.assign({}, targets.scripts), { app: targets.scripts.tests, tests: [] }),
};
}
//# sourceMappingURL=inserter.js.map