ember-auto-import
Version:
Zero-config import from NPM packages
648 lines • 29 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
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.mergeConfig = mergeConfig;
const path_1 = require("path");
const lodash_1 = require("lodash");
const fs_1 = require("fs");
const handlebars_1 = __importDefault(require("handlebars"));
const js_string_escape_1 = __importDefault(require("js-string-escape"));
const broccoli_plugin_1 = __importDefault(require("broccoli-plugin"));
const shared_internals_1 = require("@embroider/shared-internals");
const shared_internals_2 = require("@embroider/shared-internals");
const typescript_memoize_1 = require("typescript-memoize");
const debug_1 = __importDefault(require("debug"));
const fs_extra_1 = require("fs-extra");
const mini_css_extract_plugin_1 = __importDefault(require("mini-css-extract-plugin"));
const minimatch_1 = __importDefault(require("minimatch"));
const util_1 = require("./util");
const resolver_plugin_1 = require("./resolver-plugin");
const EXTENSIONS = ['.js', '.ts', '.json'];
const debug = (0, debug_1.default)('ember-auto-import:webpack');
handlebars_1.default.registerHelper('js-string-escape', js_string_escape_1.default);
handlebars_1.default.registerHelper('join', function (list, connector) {
return list.join(connector);
});
const entryTemplate = handlebars_1.default.compile(`
module.exports = (function(){
var d = _eai_d;
var r = _eai_r;
window.emberAutoImportDynamic = function(specifier) {
if (arguments.length === 1) {
return r('_eai_dyn_' + specifier);
} else {
return r('_eai_dynt_' + specifier)(Array.prototype.slice.call(arguments, 1))
}
};
window.emberAutoImportSync = function(specifier) {
{{! this is only used for synchronous importSync() using a template string }}
return r('_eai_sync_' + specifier)(Array.prototype.slice.call(arguments, 1))
};
{{!- es compatibility. Where we're using "require", webpack doesn't necessarily know to apply its own ES compatibility stuff -}}
function esc(m) {
return m && m.__esModule ? m : Object.assign({ default: m }, m);
}
{{#each staticImports as |module|}}
d('{{js-string-escape module.requestedSpecifier}}', [EAI_DISCOVERED_EXTERNALS_START, '{{js-string-escape module.requestedSpecifier}}', EAI_DISCOVERED_EXTERNALS_END], function() { return esc(require('{{js-string-escape module.resolvedSpecifier}}')); });
{{/each}}
{{#each dynamicImports as |module|}}
d('_eai_dyn_{{js-string-escape module.requestedSpecifier}}', [], function() { return import('{{js-string-escape module.resolvedSpecifier}}'); });
{{/each}}
{{#each staticTemplateImports as |module|}}
d('_eai_sync_{{js-string-escape module.key}}', [], function() {
return function({{module.args}}) {
return esc(require({{{module.template}}}));
}
});
{{/each}}
{{#each dynamicTemplateImports as |module|}}
d('_eai_dynt_{{js-string-escape module.key}}', [], function() {
return function({{module.args}}) {
return import({{{module.template}}});
}
});
{{/each}}
{{#if needsApp}}
require('./app.cjs');
{{/if}}
})();
`, { noEscape: true });
// this goes in a file by itself so we can tell webpack not to parse it. That
// allows us to grab the "require" and "define" from our enclosing scope without
// webpack messing with them.
//
// It's important that we're using our enclosing scope and not jumping directly
// to window.require (which would be easier), because the entire Ember app may be
// inside a closure with a "require" that isn't the same as "window.require".
const loader = `
window._eai_r = require;
window._eai_d = define;
`;
class WebpackBundler extends broccoli_plugin_1.default {
constructor(priorTrees, opts) {
super(priorTrees, {
persistentOutput: true,
needsCache: true,
annotation: 'ember-auto-import-webpack',
});
this.opts = opts;
this.writeCache = new Map();
this.externalizedByUs = new Set();
}
get buildResult() {
if (!this.lastBuildResult) {
throw new Error(`bug: no buildResult available yet`);
}
return this.lastBuildResult;
}
get webpack() {
return this.setup().webpack;
}
get stagingDir() {
return this.setup().stagingDir;
}
setup() {
var _a;
if (this.state) {
return this.state;
}
// resolve the real path, because we're going to do path comparisons later
// that could fail if this is not canonical.
//
// cast is ok because we passed needsCache to super
let stagingDir = (0, fs_1.realpathSync)(this.cachePath);
let entry = {};
this.opts.bundles.names.forEach((bundle) => {
entry[bundle] = [
(0, path_1.join)(stagingDir, 'l.cjs'),
(0, path_1.join)(stagingDir, `${bundle}.cjs`),
];
});
let { plugin: stylePlugin, loader: styleLoader } = this.setupStyleLoader();
let config = {
mode: this.opts.environment === 'production' ? 'production' : 'development',
entry,
performance: {
hints: false,
},
// this controls webpack's own runtime code generation. You still need
// preset-env to preprocess the libraries themselves (which is already
// part of this.opts.babelConfig)
target: `browserslist:${this.opts.rootPackage.browserslist()}`,
output: {
path: (0, path_1.join)(this.outputPath, 'assets'),
publicPath: this.opts.rootPackage.publicAssetURL(),
filename: `chunk.[id].[chunkhash].js`,
chunkFilename: `chunk.[id].[chunkhash].js`,
libraryTarget: 'var',
library: '__ember_auto_import__',
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
resolveLoader: {
alias: {
// these loaders are our dependencies, not the app's dependencies. I'm
// not overriding the default loader resolution rules in case the app also
// wants to control those.
'babel-loader-8': require.resolve('babel-loader'),
'eai-style-loader': require.resolve('style-loader'),
'eai-css-loader': require.resolve('css-loader'),
},
},
resolve: {
extensions: EXTENSIONS,
mainFields: ['browser', 'module', 'main'],
alias: Object.assign({
// this is because of the allowAppImports feature needs to be able to import things
// like app-name/lib/something from within webpack handled code but that needs to be
// able to resolve to app-root/app/lib/something.
[this.opts.rootPackage.name]: `${this.opts.rootPackage.root}/app`,
}),
},
plugins: removeUndefined([
stylePlugin,
new resolver_plugin_1.AutoImportResolverPlugin(this.opts.rootPackage.root, this.opts.v2AddonResolver),
]),
module: {
noParse: (file) => file === (0, path_1.join)(stagingDir, 'l.cjs'),
rules: [
this.babelRule(stagingDir, (filename) => !this.fileIsInApp(filename), this.opts.rootPackage.cleanBabelConfig()),
this.babelRule(stagingDir, (filename) => this.fileIsInApp(filename), this.opts.rootPackage.babelOptions),
{
test: /\.css$/i,
use: [
styleLoader,
{
loader: 'eai-css-loader',
options: (_a = [...this.opts.packages].find((pkg) => pkg.cssLoaderOptions)) === null || _a === void 0 ? void 0 : _a.cssLoaderOptions,
},
],
},
],
},
node: false,
externals: this.externalsHandler,
};
if ([...this.opts.packages].find((pkg) => pkg.forbidsEval)) {
config.devtool = 'source-map';
}
mergeConfig(config, ...[...this.opts.packages].map((pkg) => pkg.webpackConfig));
debug('webpackConfig %j', config);
this.state = { webpack: this.opts.webpack(config), stagingDir };
return this.state;
}
setupStyleLoader() {
var _a, _b;
if (this.opts.environment === 'production' ||
this.opts.rootPackage.isFastBootEnabled) {
return {
loader: mini_css_extract_plugin_1.default.loader,
plugin: new mini_css_extract_plugin_1.default(Object.assign({ filename: `chunk.[id].[chunkhash].css`, chunkFilename: `chunk.[id].[chunkhash].css` }, (_a = [...this.opts.packages].find((pkg) => pkg.miniCssExtractPluginOptions)) === null || _a === void 0 ? void 0 : _a.miniCssExtractPluginOptions)),
};
}
else
return {
loader: {
loader: 'eai-style-loader',
options: (_b = [...this.opts.packages].find((pkg) => pkg.styleLoaderOptions)) === null || _b === void 0 ? void 0 : _b.styleLoaderOptions,
},
plugin: undefined,
};
}
skipBabel() {
let output = [];
for (let pkg of this.opts.packages) {
let skip = pkg.skipBabel;
if (skip) {
output = output.concat(skip);
}
}
return output;
}
fileIsInApp(filename) {
let packageCache = shared_internals_2.PackageCache.shared('ember-auto-import', this.opts.rootPackage.root);
const pkg = packageCache.ownerOfFile(filename);
return (pkg === null || pkg === void 0 ? void 0 : pkg.root) === this.opts.rootPackage.root;
}
babelRule(stagingDir, filter, babelConfig) {
let shouldTranspile = (0, shared_internals_1.babelFilter)(this.skipBabel(), this.opts.rootPackage.root);
return {
test: (filename) => {
// We don't apply babel to our own stagingDir (it contains only our own
// entrypoints that we wrote, and it can use `import()`, which we want
// to leave directly for webpack).
//
// And we otherwise defer to the `skipBabel` setting as implemented by
// `@embroider/shared-internals`.
return ((0, path_1.dirname)(filename) !== stagingDir &&
shouldTranspile(filename) &&
filter(filename));
},
use: {
loader: 'babel-loader-8',
options: babelConfig,
},
};
}
get externalsHandler() {
let packageCache = shared_internals_2.PackageCache.shared('ember-auto-import', this.opts.rootPackage.root);
return (params, callback) => {
var _a, _b;
let { context, request, contextInfo } = params;
if (!context || !request) {
return callback();
}
if (request.startsWith('!')) {
return callback();
}
let pkg = packageCache.ownerOfFile(context);
if (!pkg) {
// we're not inside any identifiable NPM package
return callback();
}
let name = (0, shared_internals_1.packageName)(request);
if (!name) {
if (!(0, path_1.isAbsolute)(request) && pkg.root === this.opts.rootPackage.root) {
let appRelativeContext = (0, path_1.relative)((0, path_1.resolve)(this.opts.rootPackage.root, 'app'), context);
name = this.opts.rootPackage.name;
let candidateName = path_1.posix.join(name, appRelativeContext, request);
if (candidateName.startsWith(name)) {
request = candidateName;
}
else {
// the relative request does not target the traditional "app" dir
return callback();
}
}
else {
// we're only interested in handling inter-package resolutions
return callback();
}
}
// Handling full-name imports that point at the app itself e.g. app-name/lib/thingy
if (name === this.opts.rootPackage.name) {
if (this.importMatchesAppImports((0, util_1.stripQuery)(request.slice(name.length + 1)))) {
// webpack should handle this because it's another file in the app that matches allowAppImports
return callback();
}
else {
// use ember's module because this is part of the app that doesn't match allowAppImports
this.externalizedByUs.add(request);
return callback(undefined, 'commonjs ' + request);
}
}
// if we're not in a v2 addon and the file that is doing the import doesn't match one of the allowAppImports patterns
// then we don't implement the "fallback behaviour" below i.e. this won't be handled by ember-auto-import
if (!pkg.isV2Addon() &&
!this.matchesAppImports(pkg, contextInfo === null || contextInfo === void 0 ? void 0 : contextInfo.issuer)) {
return callback();
}
let renamedModule = this.opts.v2AddonResolver.handleRenaming(request);
if (renamedModule !== request) {
name = (0, shared_internals_1.packageName)(renamedModule);
if (!name) {
throw new Error(`bug in ember-auto-import: renamed module ${request} -> ${renamedModule} resulted in a relative path, which should never happen`);
}
request = renamedModule;
}
if (pkg.isV2Addon()) {
if ((_a = pkg.meta.externals) === null || _a === void 0 ? void 0 : _a.includes(name)) {
this.externalizedByUs.add(request);
return callback(undefined, 'commonjs ' + request);
}
if (!pkg.hasDependency(name)) {
// v2 addons are allowed to resolve these special virtual peers from
// the app
if (shared_internals_1.emberVirtualPeerDeps.has(name) &&
((_b = packageCache
.resolve(name, packageCache.get(packageCache.appRoot))) === null || _b === void 0 ? void 0 : _b.isV2Addon())) {
return callback();
}
// v2 addons are not allowed to "accidentally" resolve
// non-dependencies at build time
this.externalizedByUs.add(request);
return callback(undefined, 'commonjs ' + request);
}
}
try {
let found = packageCache.resolve(name, pkg);
if (!found.isEmberPackage() || found.isV2Addon()) {
// if we're importing a non-ember package or a v2 addon, we don't
// externalize. Those are all "normal" looking packages that should be
// resolvable statically.
return callback();
}
else {
// the package exists but it is a v1 ember addon, so it's not
// resolvable at build time, so we externalize it.
this.externalizedByUs.add(request);
return callback(undefined, 'commonjs ' + request);
}
}
catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err;
}
// real package doesn't exist, so externalize it
this.externalizedByUs.add(request);
return callback(undefined, 'commonjs ' + request);
}
};
}
*withResolvableExtensions(importSpecifier) {
if (importSpecifier.match(/\.\w+$/)) {
yield importSpecifier;
}
else {
for (let ext of EXTENSIONS) {
yield `${importSpecifier}${ext}`;
}
}
}
importMatchesAppImports(relativeImportSpecifier) {
for (let candidate of this.withResolvableExtensions(relativeImportSpecifier)) {
if (this.opts.rootPackage.allowAppImports.some((pattern) => (0, minimatch_1.default)(candidate, pattern))) {
return true;
}
}
return false;
}
matchesAppImports(pkg, requestingFile) {
if (!requestingFile) {
return false;
}
if (pkg.root !== this.opts.rootPackage.root) {
return false;
}
return this.opts.rootPackage.allowAppImports.some((pattern) => (0, minimatch_1.default)((0, path_1.relative)((0, path_1.join)(pkg.root, 'app'), requestingFile), pattern));
}
build() {
return __awaiter(this, void 0, void 0, function* () {
let bundleDeps = yield this.opts.splitter.deps();
for (let [bundle, deps] of bundleDeps.entries()) {
this.writeEntryFile(bundle, deps);
}
this.writeLoaderFile();
this.linkDeps(bundleDeps);
let stats = yield this.runWebpack();
this.lastBuildResult = this.summarizeStats(stats, bundleDeps);
this.addDiscoveredExternals(this.lastBuildResult);
});
}
cachedWriteFileSync(absoluteFilename, content) {
if (this.writeCache.get(absoluteFilename) !== content) {
this.writeCache.set(absoluteFilename, content);
(0, fs_1.writeFileSync)(absoluteFilename, content);
}
}
addDiscoveredExternals(build) {
for (let assetFiles of build.entrypoints.values()) {
for (let assetFile of assetFiles) {
let inputSrc = (0, fs_1.readFileSync)((0, path_1.resolve)(this.outputPath, assetFile), 'utf8');
let outputSrc = inputSrc.replace(/\[EAI_DISCOVERED_EXTERNALS_START,\s*['"](.*?)['"],\s*EAI_DISCOVERED_EXTERNALS_END\]/g, (_substr, matched) => {
let deps = build
.externalDepsFor(matched)
.filter((dep) => this.externalizedByUs.has(dep));
return '[' + deps.map((d) => `'${d}'`).join(',') + ']';
});
this.cachedWriteFileSync((0, path_1.resolve)(this.outputPath, assetFile), outputSrc);
}
}
}
externalDepsSearcher(stats) {
let externals = new Map();
function gatherExternals(module, output = new Set()) {
if (externals.has(module)) {
for (let ext of externals.get(module)) {
output.add(ext);
}
}
else {
let ownExternals = new Set();
externals.set(module, ownExternals);
for (let dep of module.dependencies) {
let nextModule = stats.compilation.moduleGraph.getModule(dep);
if (nextModule) {
if (nextModule.externalType) {
ownExternals.add(nextModule.request);
}
else {
gatherExternals(nextModule, ownExternals);
}
}
}
for (let o of ownExternals) {
output.add(o);
}
}
return output;
}
return (request) => {
for (let module of stats.compilation.modules) {
for (let dep of module.dependencies) {
// the request is understood to be jsStringEscaped already
if ((0, js_string_escape_1.default)(dep.request) === request) {
return [
...gatherExternals(stats.compilation.moduleGraph.getModule(dep)),
];
}
}
}
return [];
};
}
summarizeStats(_stats, bundleDeps) {
let { entrypoints, assets } = _stats.toJson();
// webpack's types are written rather loosely, implying that these two
// properties may not be present. They really always are, as far as I can
// tell, but we need to check here anyway to satisfy the type checker.
if (!entrypoints) {
throw new Error(`unexpected webpack output: no entrypoints`);
}
if (!assets) {
throw new Error(`unexpected webpack output: no assets`);
}
let output = {
entrypoints: new Map(),
lazyAssets: [],
externalDepsFor: this.externalDepsSearcher(_stats),
};
let nonLazyAssets = new Set();
for (let id of Object.keys(entrypoints)) {
let { assets: entrypointAssets } = entrypoints[id];
if (!entrypointAssets) {
throw new Error(`unexpected webpack output: no entrypoint.assets`);
}
// our built-in bundles can be "empty" while still existing because we put
// setup code in them, so they get a special check for non-emptiness.
// Whereas any other bundle that was manually configured by the user
// should always be emitted.
if (!this.opts.bundles.isBuiltInBundleName(id) ||
nonEmptyBundle(id, bundleDeps)) {
output.entrypoints.set(id, entrypointAssets.map((a) => 'assets/' + a.name));
}
entrypointAssets.forEach((asset) => nonLazyAssets.add(asset.name));
}
for (let asset of assets) {
if (!nonLazyAssets.has(asset.name)) {
output.lazyAssets.push('assets/' + asset.name);
}
}
return output;
}
writeEntryFile(name, deps) {
this.cachedWriteFileSync((0, path_1.join)(this.stagingDir, `${name}.cjs`), entryTemplate({
staticImports: deps.staticImports,
dynamicImports: deps.dynamicImports,
dynamicTemplateImports: deps.dynamicTemplateImports.map(mapTemplateImports),
staticTemplateImports: deps.staticTemplateImports.map(mapTemplateImports),
publicAssetURL: this.opts.rootPackage.publicAssetURL(),
needsApp: name === 'tests',
}));
}
writeLoaderFile() {
this.cachedWriteFileSync((0, path_1.join)(this.stagingDir, `l.cjs`), loader);
}
linkDeps(bundleDeps) {
for (let deps of bundleDeps.values()) {
for (let resolved of deps.staticImports) {
this.ensureLinked(resolved);
}
for (let resolved of deps.dynamicImports) {
this.ensureLinked(resolved);
}
for (let resolved of deps.staticTemplateImports) {
this.ensureLinked(resolved);
}
for (let resolved of deps.dynamicTemplateImports) {
this.ensureLinked(resolved);
}
}
}
ensureLinked({ packageName, packageRoot, }) {
(0, fs_extra_1.ensureDirSync)((0, path_1.dirname)((0, path_1.join)(this.stagingDir, 'node_modules', packageName)));
if (!(0, fs_extra_1.existsSync)((0, path_1.join)(this.stagingDir, 'node_modules', packageName))) {
(0, fs_extra_1.symlinkSync)(packageRoot, (0, path_1.join)(this.stagingDir, 'node_modules', packageName), 'junction');
}
}
runWebpack() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
this.webpack.run((err, stats) => {
const statsString = stats ? stats.toString() : '';
if (err) {
this.opts.consoleWrite(statsString);
reject(err);
return;
}
if (stats === null || stats === void 0 ? void 0 : stats.hasErrors()) {
this.opts.consoleWrite(statsString);
reject(new Error('webpack returned errors to ember-auto-import'));
return;
}
if ((stats === null || stats === void 0 ? void 0 : stats.hasWarnings()) || process.env.AUTO_IMPORT_VERBOSE) {
this.opts.consoleWrite(statsString);
}
// this cast is justified because we already checked hasErrors above
resolve(stats);
});
});
});
}
}
exports.default = WebpackBundler;
__decorate([
(0, typescript_memoize_1.Memoize)()
], WebpackBundler.prototype, "externalsHandler", null);
function mergeConfig(dest, ...srcs) {
return (0, lodash_1.mergeWith)(dest, ...srcs, combine);
}
function combine(objValue, srcValue, key) {
if (key === 'noParse') {
return eitherPattern(objValue, srcValue);
}
if (key === 'externals') {
return [srcValue, objValue].flat();
}
// arrays concat
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
}
// webpack configs have several places where they accept:
// - RegExp
// - [RegExp]
// - (resource: string) => boolean
// - string
// - [string]
// This function combines any of these with a logical OR.
function eitherPattern(...patterns) {
let flatPatterns = (0, lodash_1.flatten)(patterns);
return function (resource) {
for (let pattern of flatPatterns) {
if (pattern instanceof RegExp) {
if (pattern.test(resource)) {
return true;
}
}
else if (typeof pattern === 'string') {
if (pattern === resource) {
return true;
}
}
else if (typeof pattern === 'function') {
if (pattern(resource)) {
return true;
}
}
}
return false;
};
}
function mapTemplateImports(imp) {
return {
key: imp.importedBy[0].cookedQuasis.join('${e}'),
args: imp.expressionNameHints.join(','),
template: '`' +
(0, lodash_1.zip)(imp.cookedQuasis, imp.expressionNameHints)
.map(([q, e]) => q + (e ? '${' + e + '}' : ''))
.join('') +
'`',
};
}
function nonEmptyBundle(name, bundleDeps) {
let deps = bundleDeps.get(name);
if (!deps) {
return false;
}
return (deps.staticImports.length > 0 ||
deps.staticTemplateImports.length > 0 ||
deps.dynamicImports.length > 0 ||
deps.dynamicTemplateImports.length > 0);
}
// this little helper is needed because typescript can't see through normal
// usage of Array.prototype.filter.
function removeUndefined(list) {
return list.filter((item) => typeof item !== 'undefined');
}
//# sourceMappingURL=webpack.js.map