UNPKG

ember-auto-import

Version:
648 lines 29 kB
"use strict"; 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