ember-auto-import
Version:
Zero-config import from NPM packages
278 lines • 12.8 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 });
const debug_1 = __importDefault(require("debug"));
const util_1 = require("./util");
const lodash_1 = require("lodash");
const path_1 = require("path");
const semver_1 = require("semver");
const debug = (0, debug_1.default)('ember-auto-import:splitter');
class Splitter {
constructor(options) {
this.options = options;
this.lastDeps = null;
this.packageVersions = new Map();
}
deps() {
return __awaiter(this, void 0, void 0, function* () {
if (this.importsChanged()) {
this.lastDeps = yield this.computeDeps(this.options.analyzers);
debug('output %s', new LazyPrintDeps(this.lastDeps));
}
return this.lastDeps;
});
}
importsChanged() {
let imports = [...this.options.analyzers.keys()].map((analyzer) => analyzer.imports);
if (!this.lastImports || !(0, util_1.shallowEqual)(this.lastImports, imports)) {
this.lastImports = imports;
return true;
}
return false;
}
computeTargets(analyzers) {
return __awaiter(this, void 0, void 0, function* () {
let targets = new Map();
let templateTargets = new Map();
let imports = (0, lodash_1.flatten)([...analyzers.keys()].map((analyzer) => analyzer.imports));
yield Promise.all(imports.map((imp) => __awaiter(this, void 0, void 0, function* () {
if ('specifier' in imp) {
yield this.handleLiteralImport(imp, targets);
}
else {
yield this.handleTemplateImport(imp, templateTargets);
}
})));
return { targets, templateTargets };
});
}
handleLiteralImport(imp, targets) {
return __awaiter(this, void 0, void 0, function* () {
let target = imp.package.resolve(imp.specifier, imp.path);
if (!target) {
return;
}
if (target.type === 'url') {
// people can statically import from URLs if they want to, that's clearly
// nothing to do with us (though in practice the rest of ember-cli will
// generally be sad about this)
return;
}
if (target.type === 'local') {
// we're only trying to identify imports of external NPM
// packages, so relative imports are never relevant.
if (imp.isDynamic) {
throw new Error(`ember-auto-import does not support dynamic relative imports. "${imp.specifier}" is relative. To make this work, you need to upgrade to Embroider.`);
}
return;
}
let seenAlready = targets.get(imp.specifier);
if (seenAlready) {
yield this.assertSafeVersion(seenAlready, imp, target);
seenAlready.importedBy.push(imp);
}
else {
targets.set(imp.specifier, {
requestedSpecifier: imp.specifier,
resolvedSpecifier: target.resolvedSpecifier,
packageName: target.packageName,
packageRoot: target.packageRoot,
importedBy: [imp],
});
}
});
}
handleTemplateImport(imp, targets) {
return __awaiter(this, void 0, void 0, function* () {
let [leadingQuasi] = imp.cookedQuasis;
let target = imp.package.resolve(leadingQuasi, imp.path, true);
if (!target) {
throw new Error(`ember-auto-import is unable to handle '${leadingQuasi}'. ` +
`The attempted import of '${imp.cookedQuasis.join('')}' is located in ${imp.path}`);
}
if (target.type === 'local') {
return;
}
if (target.type === 'imprecise') {
throw new Error(`Dynamic imports must target unambiguous package names. '${leadingQuasi}' is ambiguous. ` +
`The attempted import of '${imp.cookedQuasis.join('')}' is located in ${imp.path}`);
}
if (target.type === 'url') {
return;
}
// this just makes the key look pleasantly like the original template
// string, there's nothing magical about "e" here, it just means "an
// expression goes here and we don't care which one".c
let specifierKey = imp.cookedQuasis.join('${e}');
let seenAlready = targets.get(specifierKey);
if (seenAlready) {
yield this.assertSafeVersion(seenAlready, imp, target);
seenAlready.importedBy.push(imp);
}
else {
targets.set(specifierKey, {
packageName: target.packageName,
packageRoot: target.packageRoot,
cookedQuasis: imp.cookedQuasis,
expressionNameHints: imp.expressionNameHints.map((hint, index) => hint || `arg${index}`),
importedBy: [imp],
});
}
});
}
versionOfPackage(packageRoot) {
return __awaiter(this, void 0, void 0, function* () {
if (this.packageVersions.has(packageRoot)) {
return this.packageVersions.get(packageRoot);
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
let pkg = require((0, path_1.join)(packageRoot, 'package.json'));
let version = pkg.version;
this.packageVersions.set(packageRoot, version);
return version;
});
}
assertSafeVersion(alreadyResolved, nextImport, nextTarget) {
return __awaiter(this, void 0, void 0, function* () {
if (alreadyResolved.packageRoot === nextTarget.packageRoot) {
// the next import is resolving to the same copy of the package we are
// already using. This is the normal and happy case.
return;
}
let requestedRange = nextImport.package.requestedRange(nextTarget.packageName);
if (!requestedRange) {
// this is probably an error condition, but it's not the error condition
// that this particular assertion is checking. Our job is just to make
// sure nobody's requested semver ranges are violated. If you don't have
// any range, we can't violate it. In practice if you lacked a declared
// dependency, Package#resolve would have failed earlier than this because
// it ensures we only resolve declared dependencies.
return;
}
let haveVersion = yield this.versionOfPackage(alreadyResolved.packageRoot);
if (!(0, semver_1.satisfies)(haveVersion, requestedRange, { includePrerelease: true })) {
throw new Error(`${nextImport.package.name} needs ${nextTarget.packageName} satisfying ${requestedRange}, but we have version ${haveVersion} because of ${alreadyResolved.importedBy
.map((i) => i.package.name)
.join(', ')}`);
}
});
}
computeDeps(analyzers) {
return __awaiter(this, void 0, void 0, function* () {
let targets = yield this.computeTargets(analyzers);
let deps = new Map();
this.options.bundles.names.forEach((bundleName) => {
deps.set(bundleName, {
staticImports: [],
staticTemplateImports: [],
dynamicImports: [],
dynamicTemplateImports: [],
});
});
for (let target of targets.targets.values()) {
let [dynamicUses, staticUses] = (0, lodash_1.partition)(target.importedBy, (imp) => imp.isDynamic);
if (staticUses.length > 0) {
let bundleName = this.chooseBundle(staticUses);
deps.get(bundleName).staticImports.push(target);
}
if (dynamicUses.length > 0) {
let bundleName = this.chooseBundle(dynamicUses);
deps.get(bundleName).dynamicImports.push(target);
}
}
for (let target of targets.templateTargets.values()) {
let [dynamicUses, staticUses] = (0, lodash_1.partition)(target.importedBy, (imp) => imp.isDynamic);
if (staticUses.length > 0) {
let bundleName = this.chooseBundle(staticUses);
deps.get(bundleName).staticTemplateImports.push(target);
}
if (dynamicUses.length > 0) {
let bundleName = this.chooseBundle(dynamicUses);
deps.get(bundleName).dynamicTemplateImports.push(target);
}
}
this.sortDependencies(deps);
return deps;
});
}
sortDependencies(deps) {
for (const bundle of deps.values()) {
this.sortBundle(bundle);
}
}
sortBundle(bundle) {
bundle.staticImports.sort((a, b) => a.requestedSpecifier.localeCompare(b.requestedSpecifier));
bundle.dynamicImports.sort((a, b) => a.requestedSpecifier.localeCompare(b.requestedSpecifier));
bundle.dynamicTemplateImports.sort((a, b) => a.cookedQuasis[0].localeCompare(b.cookedQuasis[0]));
}
// given that a module is imported by the given list of paths, which
// bundle should it go in?
chooseBundle(importedBy) {
let usedInBundles = {};
importedBy.forEach((usage) => {
usedInBundles[this.bundleFor(usage)] = true;
});
return this.options.bundles.names.find((bundle) => usedInBundles[bundle]);
}
bundleFor(usage) {
let bundleName = usage.treeType === undefined ||
typeof this.options.bundles.bundleForTreeType !== 'function'
? this.options.bundles.bundleForPath(usage.path)
: this.options.bundles.bundleForTreeType(usage.treeType);
if (this.options.bundles.names.indexOf(bundleName) === -1) {
throw new Error(`bundleForPath("${usage.path}") returned ${bundleName}" but the only configured bundle names are ${this.options.bundles.names.join(',')}`);
}
debug('bundleForPath("%s")=%s', usage.path, bundleName);
return bundleName;
}
}
exports.default = Splitter;
class LazyPrintDeps {
constructor(deps) {
this.deps = deps;
}
describeResolvedImport(imp) {
return {
requestedSpecifier: imp.requestedSpecifier,
resolvedSpecifier: imp.resolvedSpecifier,
packageRoot: imp.packageRoot,
importedBy: imp.importedBy.map(this.describeImport.bind(this)),
};
}
describeImport(imp) {
return {
package: imp.package.name,
path: imp.path,
};
}
describeTemplateImport(imp) {
return {
cookedQuasis: imp.cookedQuasis,
expressionNameHints: imp.expressionNameHints,
importedBy: imp.importedBy.map(this.describeImport.bind(this)),
};
}
toString() {
let output = {};
for (let [bundle, { staticImports, dynamicImports, dynamicTemplateImports },] of this.deps.entries()) {
output[bundle] = {
static: staticImports.map(this.describeResolvedImport.bind(this)),
dynamic: dynamicImports.map(this.describeResolvedImport.bind(this)),
dynamicTemplate: dynamicTemplateImports.map(this.describeTemplateImport.bind(this)),
};
}
return JSON.stringify(output, null, 2);
}
}
//# sourceMappingURL=splitter.js.map