sourceMap.sources = sourceMap.sources.map(function (sourceUrl, i) { // Work around a weird bug in node-sass' source map generation. // It uses options.filename as the url for imported sources. if (i > 0) { sourceUrl = data.imports[i - 1]; } if (/^file:\/\//.test(sourceUrl)) { return sourceUrl; } else { return urlTools.fsFilePathToFileUrl(sourceUrl); } });
return async function bundleWebpack(assetGraph) { let webpack; try { webpack = require('webpack'); } catch (e) { if (configPath) { throw new Error( `Webpack config path given (${configPath}), but webpack itself could not be found. Please install it in your project: ${ e.stack }` ); } } const triedConfigPaths = []; async function loadWebpackConfig() { // First try to look for the config in the assetGraph root: configPath = configPath ? pathModule.resolve(process.cwd(), configPath) : pathModule.resolve( urlTools.fileUrlToFsPath(assetGraph.root), 'webpack.config.js' ); triedConfigPaths.push(configPath); try { return require.main.require(configPath); } catch (err) { if (err.code !== 'MODULE_NOT_FOUND' || configPath) { throw err; } // webpack.config.js was not found in the assetgraph's root. // Look for it in the directory that contains the nearest package.json: const readPkgUpResult = await readPkgUp(); if (readPkgUpResult.path) { const alternativeConfigPath = pathModule.resolve( pathModule.dirname(readPkgUpResult.path), 'webpack.config.js' ); if (alternativeConfigPath !== configPath) { configPath = alternativeConfigPath; triedConfigPaths.push(configPath); return require.main.require(configPath); } } throw new Error('Could not load webpack config'); } } let config; try { config = await loadWebpackConfig(); } catch (err) { if (webpack) { assetGraph.info( new Error( `Webpack is installed, but could not load the webpack config (tried ${triedConfigPaths.join( ' ' )}): ${err.message}` ) ); } return; } config.context = config.context || urlTools.fileUrlToFsPath(assetGraph.root); config.output = config.output || {}; if (typeof config.output.path === 'string') { config.output.path = pathModule.resolve( config.context, config.output.path ); } config.output.devtoolModuleFilenameTemplate = config.output.devtoolModuleFilenameTemplate || 'file://[absolute-resource-path]'; let basePath = config.output.path; if (config.output.publicPath) { basePath = pathModule.resolve( config.context, config.output.publicPath.replace(/^\//, '') ); } const plugins = Array.isArray(config.plugins) ? config.plugins : []; for (const plugin of plugins) { if ( plugin.constructor.name === 'UglifyJsPlugin' && (plugin.options.mangle || plugin.options.compress) ) { plugin.options.mangle = plugin.options.compress = false; assetGraph.info( new Error( 'UglifyJsPlugin detected, turning off mangling and compression so outgoing relations can be detected. This is fine for assetgraph-builder, which will run UglifyJs later anyway' ) ); } } const hasHtmlWebpackPlugin = plugins.some( plugin => plugin.constructor.name === 'HtmlWebpackPlugin' ); if (!hasHtmlWebpackPlugin) { // HtmlWebpackPlugin is not in the picture, so we can assume that the // relevant entry points are referenced by assets that we have // in the graph. Only build the actually referenced entry points: let seenReferencedEntryPoints = false; const potentialBundlePaths = assetGraph .findRelations({ to: { url: { $regex: /\.js(?:\?|$)/ } } }) .map(relation => { return urlTools.fileUrlToFsPath( assetGraph.resolveUrl(relation.baseUrl, relation.to.url) ); }); if (!config.entry || potentialBundlePaths.length === 0) { assetGraph.info( new Error('No potential webpack bundles found, skipping') ); return; } /* eslint-disable no-inner-declarations */ function createAbsolutePath(name) { return pathModule.resolve( basePath, name ? config.output.filename.replace(/\[name]/g, name) : config.output.filename ); } if ( config.entry && typeof config.entry === 'object' && !Array.isArray(config.entry) ) { config.entry = Object.keys(config.entry).reduce( (entryPointsToBeBuilt, entryPointName) => { if ( potentialBundlePaths.includes(createAbsolutePath(entryPointName)) ) { entryPointsToBeBuilt[entryPointName] = config.entry[entryPointName]; } return entryPointsToBeBuilt; }, {} ); seenReferencedEntryPoints = Object.keys(config.entry).length > 0; } else { // array or string if (potentialBundlePaths.includes(createAbsolutePath())) { seenReferencedEntryPoints = true; } } if (!seenReferencedEntryPoints) { assetGraph.info( new Error('No matching webpack bundles found, skipping') ); return; } } // Webpack 4: Disable name mangling so that we can detect relations from the bundle // (same reason as why we disable UglifyJsPlugin) config.optimization = config.optimization || {}; config.optimization.minimize = false; const compiler = webpack(config); const outputFileSystem = new webpack.MemoryOutputFileSystem(); compiler.outputFileSystem = outputFileSystem; const stats = await Promise.fromNode(cb => compiler.run(cb)); if (stats.compilation && stats.compilation.errors) { for (const error of stats.compilation.errors) { assetGraph.warn(error); } } const statsObj = stats.toJson({ assets: true }); let existingAssetsWereReplaced; const assetInfos = []; const urlByAssetName = {}; for (const asset of statsObj.assets) { const url = urlTools.fsFilePathToFileUrl( pathModule.resolve(basePath, asset.name) ); const outputPath = urlTools.fsFilePathToFileUrl( pathModule.resolve(config.output.path, asset.name) ); assetInfos.push({ url, output: outputPath, name: asset.name }); urlByAssetName[asset.name] = url; const existingAsset = assetGraph.findAssets({ url })[0]; if (existingAsset) { assetGraph.info( new Error(`Replacing previously built artifact: ${url}`) ); existingAssetsWereReplaced = true; } } assetGraph.info(stats.toString({ colors: true })); if (existingAssetsWereReplaced) { assetGraph.info( new Error( `Please remove ${ config.output.path } before building with assetgraph to speed up the process` ) ); } await assetGraph.loadAssets( assetInfos.map(({ url, output }) => ({ url, isInitial: /\.html$/.test(url), rawSrc: outputFileSystem.readFileSync(urlTools.fileUrlToFsPath(output)) })) ); // Pick up CSS assets and attach them to each entry point for (const chunkName of Object.keys(statsObj.assetsByChunkName)) { let assets = statsObj.assetsByChunkName[chunkName]; if (!Array.isArray(assets)) { assets = [assets]; } const javaScriptAssets = assets.filter(asset => /\.js$/.test(asset)); const cssAssets = assets.filter(asset => /\.css$/.test(asset)); if (javaScriptAssets.length === 1 && cssAssets.length > 0) { const entryPointRelations = assetGraph.findRelations(relation => { if ( relation.type !== 'HtmlScript' || !relation.to || !relation.to.url ) { return; } const absoluteUrl = assetGraph.resolveUrl( relation.from.nonInlineAncestor.url, relation.to.url ); return absoluteUrl === urlByAssetName[javaScriptAssets[0]]; }); for (const entryPointRelation of entryPointRelations) { for (const cssAsset of cssAssets) { entryPointRelation.from.addRelation( { type: 'HtmlStyle', hrefType: 'rootRelative', to: assetGraph.findAssets({ url: urlByAssetName[cssAsset] })[0] }, 'last' ); } } } } // Fix up sources: [...] urls in source maps: for (const relation of assetGraph.findRelations({ type: 'SourceMapSource', from: { url: { $in: assetInfos.map(url => url.url) } } })) { if (relation.href.startsWith('file://webpack/')) { // Some fake webpack url, eg. "webpack/bootstrap 5c89cbd3893270d2912a" // that got file:// prepended due to devtoolFallbackModuleFilenameTemplate // Strip the file:// prefix and make it relative so that it sort of // looks like the webpack output: relation.to.url = relation.to.url.replace('file', 'webpack'); relation.hrefType = 'relative'; } else { relation.hrefType = 'rootRelative'; } } for (const asset of assetGraph.findAssets({ type: 'JavaScript', url: { $in: assetInfos.map(assetInfo => assetInfo.url) } })) { let replacementsMade = false; let requireEnsureAstTemplate; let jsonpScriptSrcAst; estraverse.traverse(asset.parseTree, { enter(node) { // Webpack 4 moves this to a separate function: // function jsonpScriptSrc(chunkId) { // return __webpack_require__.p + "" + chunkId + ".bundle.js" // } if ( node.type === 'FunctionDeclaration' && node.id.name === 'jsonpScriptSrc' && node.body.body.length === 1 && node.body.body[0].type === 'ReturnStatement' ) { jsonpScriptSrcAst = node.body.body[0].argument; } // Detect the __webpack_require__.e = function requireEnsure(chunkId, callback) { ... declaration at the top of a bundle if ( node.type === 'AssignmentExpression' && node.operator === '=' && node.left.type === 'MemberExpression' && node.left.object.type === 'Identifier' && node.left.object.name === '__webpack_require__' && node.left.property.type === 'Identifier' && node.left.property.name === 'e' && node.right.type === 'FunctionExpression' && node.right.id && node.right.id.type === 'Identifier' && node.right.id.name === 'requireEnsure' ) { node.right.params.push({ type: 'Identifier', name: '_assetgraph_url' }); // Replace script.src = ... in the function body: estraverse.traverse(node.right.body, { enter(node) { if ( node.type === 'ExpressionStatement' && node.expression.type === 'AssignmentExpression' && node.expression.left.type === 'MemberExpression' && node.expression.left.object.type === 'Identifier' && node.expression.left.object.name === 'script' && node.expression.left.property.type === 'Identifier' && node.expression.left.property.name === 'src' ) { const templateAst = node.expression.right; // Quick and dirty evaluator requireEnsureAstTemplate = chunkId => { let result = ''; (function visit(node) { if ( node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === '__webpack_require__' && node.property.type === 'Identifier' && node.property.name === 'p' ) { // FIXME: Fall back to config.output.path instead of hardcoding /dist/: result += config.output.publicPath || '/dist/'; } else if ( node.type === 'BinaryExpression' && node.operator === '+' ) { visit(node.left); visit(node.right); } else if ( node.type === 'LogicalExpression' && node.operator === '||' ) { const resultBeforeLeft = result; visit(node.left); if (result === resultBeforeLeft) { visit(node.right); } } else if ( node.type === 'MemberExpression' && node.computed && node.property.type === 'Identifier' && node.property.name === 'chunkId' && node.object.type === 'ObjectExpression' ) { for (const propertyNode of node.object.properties) { if ( propertyNode.key.type === 'Literal' && String(propertyNode.key.value) === String(chunkId) ) { visit(propertyNode.value); } } } else if (node.type === 'Literal') { result += String(node.value); } else if ( node.type === 'Identifier' && node.name === 'chunkId' ) { result += chunkId; } else if ( node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'jsonpScriptSrc' ) { visit(jsonpScriptSrcAst); } else { throw new Error( `unsupported ${ node.type }: ${require('escodegen').generate(node)}` ); } })(templateAst); return result; }; node.expression.right = { type: 'BinaryExpression', operator: '||', left: { type: 'Identifier', name: '_assetgraph_url' }, right: node.expression.right }; } } }); replacementsMade = true; } else if ( node.type === 'VariableDeclaration' && node.declarations.length === 1 && node.declarations[0].type === 'VariableDeclarator' && node.declarations[0].id.type === 'Identifier' && node.declarations[0].id.name === 'map' && node.declarations[0].init && node.declarations[0].init.type === 'ObjectExpression' && node.declarations[0].init.properties.length > 0 && node.declarations[0].init.properties.every( propertyNode => propertyNode.value.type === 'ArrayExpression' && propertyNode.value.elements.length < 3 ) ) { // var map = { // './splita': [ // 1, // 1 // ], // './splita.js': [ // 1, // 1 // ], // './splitb': [ // 2, // 0 // ], // './splitb.js': [ // 2, // 0 // ] // }; // Add a 3rd element to each value that uses .toString('url') for (const propertyNode of node.declarations[0].init.properties) { // Not sure this is necessary yet, but pad with undefined so we always // add to the 3rd position while (propertyNode.value.elements.length < 2) { propertyNode.value.elements.push({ type: 'Identifier', value: 'undefined' }); } propertyNode.value.elements.push({ type: 'CallExpression', callee: { type: 'MemberExpression', computed: false, object: { type: 'Literal', value: requireEnsureAstTemplate( String(propertyNode.value.elements[1].value) ) }, property: { type: 'Identifier', name: 'toString' } }, arguments: [ { type: 'Literal', value: 'url' } ] }); } replacementsMade = true; } else if ( node.type === 'FunctionDeclaration' && node.id.type === 'Identifier' && node.id.name === 'webpackAsyncContext' ) { // function webpackAsyncContext(req) { // var ids = map[req]; // if (!ids) // return Promise.reject(new Error('Cannot find module \'' + req + '\'.')); // return __webpack_require__.e(ids[1]).then(function () { // return __webpack_require__(ids[0]); // }); // } // Add ids[2] to the argument list in the __webpack_require__.e call so that the JavaScriptStaticUrl is passed on const lastStatement = node.body.body[node.body.body.length - 1]; if ( lastStatement && lastStatement.type === 'ReturnStatement' && lastStatement.argument.type === 'CallExpression' && lastStatement.argument.callee.type === 'MemberExpression' && lastStatement.argument.callee.object.type === 'CallExpression' && lastStatement.argument.callee.object.callee.type === 'MemberExpression' && lastStatement.argument.callee.object.callee.object.type === 'Identifier' && lastStatement.argument.callee.object.callee.object.name === '__webpack_require__' && lastStatement.argument.callee.object.callee.property.type === 'Identifier' && lastStatement.argument.callee.object.callee.property.name === 'e' && lastStatement.argument.callee.object.arguments.length === 1 && lastStatement.argument.callee.object.arguments[0].type === 'MemberExpression' && lastStatement.argument.callee.object.arguments[0].computed && lastStatement.argument.callee.object.arguments[0].object.type === 'Identifier' && lastStatement.argument.callee.object.arguments[0].object.name === 'ids' && lastStatement.argument.callee.object.arguments[0].property .type === 'Literal' ) { lastStatement.argument.callee.object.arguments.push({ type: 'MemberExpression', computed: true, object: { type: 'Identifier', name: 'ids' }, property: { type: 'Literal', value: 2 } }); replacementsMade = true; } } else if ( node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === '__webpack_require__' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'e' && node.arguments.length >= 1 && node.arguments[0].type === 'Literal' && typeof node.arguments[0].value === 'number' && requireEnsureAstTemplate ) { node.arguments.push({ type: 'CallExpression', callee: { type: 'MemberExpression', computed: false, object: { type: 'Literal', value: requireEnsureAstTemplate(node.arguments[0].value) }, property: { type: 'Identifier', name: 'toString' } }, arguments: [ { type: 'Literal', value: 'url' } ] }); replacementsMade = true; } else if ( node.type === 'FunctionExpression' && node.params.length === 3 && node.params[0].type === 'Identifier' && node.params[0].name === 'module' && node.params[1].type === 'Identifier' && node.params[1].name === 'exports' && node.params[2].type === 'Identifier' && node.params[2].name === '__webpack_require__' && node.body.type === 'BlockStatement' && node.body.body.length === 1 && node.body.body[0].type === 'ExpressionStatement' && node.body.body[0].expression.type === 'AssignmentExpression' && node.body.body[0].expression.left.type === 'MemberExpression' && node.body.body[0].expression.left.object.type === 'Identifier' && node.body.body[0].expression.left.object.name === 'module' && node.body.body[0].expression.left.property.type === 'Identifier' && node.body.body[0].expression.left.property.name === 'exports' && node.body.body[0].expression.right.type === 'BinaryExpression' && node.body.body[0].expression.right.operator === '+' && node.body.body[0].expression.right.left.type === 'MemberExpression' && node.body.body[0].expression.right.left.object.type === 'Identifier' && node.body.body[0].expression.right.left.object.name === node.params[2].name && node.body.body[0].expression.right.left.property.type === 'Identifier' && node.body.body[0].expression.right.left.property.name === 'p' && node.body.body[0].expression.right.right.type === 'Literal' && typeof node.body.body[0].expression.right.right.value === 'string' ) { node.body.body[0].expression.right = { type: 'CallExpression', range: node.body.body[0].expression.right.range, callee: { type: 'MemberExpression', computed: false, object: { type: 'Literal', // FIXME: Fall back to config.output.path instead of hardcoding /dist/: value: (config.output.publicPath || '/dist/') + node.body.body[0].expression.right.right.value }, property: { type: 'Identifier', name: 'toString' } }, arguments: [ { type: 'Literal', value: 'url' } ] }; replacementsMade = true; } } }); if (replacementsMade) { asset.markDirty(); for (const relation of [...asset.outgoingRelations]) { asset.removeRelation(relation); } asset._outgoingRelations = undefined; asset.isPopulated = false; asset.populate(); } } };
/** * Some asset classes support inspection and manipulation using a high * level interface. If you modify the parse tree, you have to call * `asset.markDirty()` so any cached serializations of the asset are * invalidated. * * These are the formats you'll get: * * `Html` and `Xml`: * jsdom document object (https://github.com/tmpvar/jsdom). * * `Css` * CSSOM CSSStyleSheet object (https://github.com/NV/CSSOM). * * `JavaScript` * estree AST object (via acorn). * * `Json` * Regular JavaScript object (the result of JSON.parse on the decoded source). * * `CacheManifest` * A JavaScript object with a key for each section present in the * manifest (`CACHE`, `NETWORK`, `REMOTE`). The value is an array with * an item for each entry in the section. Refer to the source for * details. * * @member {Oject} Asset#parseTree */ /** * Load the Asset * * Returns a promise that is resolved when the asset is loaded. * This is Asset's only async method, as soon as it is * loaded, everything can happen synchronously. * * Usually you'll want to use `transforms.loadAssets`, which will * handle this automatically. * * @async * @return {Promise<Asset>} The loaded Asset */ async load({ metadataOnly = false } = {}) { try { if (!this.isLoaded) { if (!this.url) { throw new Error('Asset.load: No url, cannot load'); } const url = this.url; const protocol = url.substr(0, url.indexOf(':')).toLowerCase(); if (protocol === 'file') { const pathname = urlTools.fileUrlToFsPath(url); if (metadataOnly) { const stats = await fs.statAsync(pathname); if (stats.isDirectory()) { this.fileRedirectTargetUrl = urlTools.fsFilePathToFileUrl( pathname.replace(/(\/)?$/, '/index.html') ); // Make believe it's loaded: this._rawSrc = Buffer.from([]); } } else { try { this._rawSrc = await fs.readFileAsync(pathname); this._updateRawSrcAndLastKnownByteLength(this._rawSrc); } catch (err) { if (err.code === 'EISDIR' || err.code === 'EINVAL') { this.fileRedirectTargetUrl = urlTools.fsFilePathToFileUrl( pathname.replace(/(\/)?$/, '/index.html') ); this.isRedirect = true; } else { throw err; } } } } else if (protocol === 'http' || protocol === 'https') { const { headers = {}, ...requestOptions } = this.assetGraph.requestOptions || {}; const firstIncomingRelation = this.incomingRelations[0]; let Referer; if ( firstIncomingRelation && firstIncomingRelation.from.protocol && firstIncomingRelation.from.protocol.startsWith('http') ) { Referer = firstIncomingRelation.from.url; } const response = await this.assetGraph.teepee.request({ ...requestOptions, headers: { ...headers, Referer }, method: metadataOnly ? 'HEAD' : 'GET', url, json: false }); this.statusCode = response.statusCode; if (!metadataOnly) { this._rawSrc = response.body; this._updateRawSrcAndLastKnownByteLength(this._rawSrc); } if (response.headers.location) { this.location = response.headers.location; this.isRedirect = true; } const contentTypeHeaderValue = response.headers['content-type']; if (contentTypeHeaderValue) { const matchContentType = contentTypeHeaderValue.match( /^\s*([\w\-+.]+\/[\w-+.]+)(?:\s|;|$)/i ); if (matchContentType) { this.contentType = matchContentType[1].toLowerCase(); const matchCharset = contentTypeHeaderValue.match( /;\s*charset\s*=\s*(['"]|)\s*([\w-]+)\s*\1(?:\s|;|$)/i ); if (matchCharset) { this._encoding = matchCharset[2].toLowerCase(); } } else { const err = new Error( `Invalid Content-Type response header received: ${contentTypeHeaderValue}` ); err.asset = this; this.assetGraph.warn(err); } } else if (response.statusCode >= 200 && response.statusCode < 300) { const err = new Error('No Content-Type response header received'); err.asset = this; this.assetGraph.warn(err); } if (response.headers.etag) { this.etag = response.headers.etag; } if (response.headers['cache-control']) { this.cacheControl = response.headers['cache-control']; } if (response.headers['content-security-policy']) { this.contentSecurityPolicy = response.headers['content-security-policy']; } if (response.headers['content-security-policy-report-only']) { this.contentSecurityPolicyReportOnly = response.headers['content-security-policy-report-only']; } for (const headerName of ['date', 'last-modified']) { if (response.headers[headerName]) { this[ headerName.replace(/-([a-z])/, ($0, ch) => ch.toUpperCase()) ] = new Date(response.headers[headerName]); } } } else if (!knownAndUnsupportedProtocols[protocol]) { const err = new Error( `No resolver found for protocol: ${protocol}\n\tIf you think this protocol should exist, please contribute it here:\n\thttps://github.com/Munter/schemes#contributing` ); if (this.assetGraph) { this.assetGraph.warn(err); } else { throw err; } } } // Try to upgrade to a subclass based on the currently available type information: this._inferredType = undefined; let type = this._inferType(); if ( (!type || type === 'Image') && (this._rawSrc || typeof this._text === 'string') && !this.hasOwnProperty('contentType') ) { const detectedContentType = await determineFileType( this._rawSrc || this._text ); if (detectedContentType) { if (detectedContentType !== 'text/plain') { // Setting text/plain explicitly here would fool _inferType later this.contentType = detectedContentType; } const typeFromDetectedContentType = AssetGraph.typeByContentType[this.contentType]; if (typeFromDetectedContentType) { if ( !type || (type === 'Image' && AssetGraph[typeFromDetectedContentType].prototype.isImage) || (type === 'Font' && AssetGraph[typeFromDetectedContentType].prototype.isFont) ) { type = typeFromDetectedContentType; } } } } this._tryUpgrade(type); this.emit('load', this); if (this.assetGraph) { this.populate(true); } return this; } catch (err) { err.message = err.message || err.code || err.name; const includingAssetUrls = this.incomingRelations.map( incomingRelation => { return incomingRelation.from.urlOrDescription; } ); if (includingAssetUrls.length > 0) { err.message += `\nIncluding assets:\n ${includingAssetUrls.join( '\n ' )}\n`; } err.asset = this; throw err; } }
.parEach(function (labelDefinition) { var resolverName = labelDefinition.type || (/\.jsb\d+$/.test(labelDefinition.url) ? 'senchaJsBuilder' : 'fixedDirectory'); try { assetGraph.resolverByProtocol[labelDefinition.name] = resolvers[resolverName](urlTools.fsFilePathToFileUrl(labelDefinition.url)); } catch (err) { err.message = 'transforms.registerLabelsAsCustomProtocols: Error initializing resolver: ' + err.message; return this(err); } this(); })
var walker = new uglifyJs.TreeWalker(function (node, descend) { var stack = walker.stack, parentNode = walker.parent(); [node.start, node.end].forEach(function (token) { if (token && token.comments_before) { token.comments_before.forEach(function (comment) { var matchSourceUrlOrSourceMappingUrl = comment.value.match(/[@#]\s*source(Mapping)?URL=([^\s\n]*)/); if (matchSourceUrlOrSourceMappingUrl && seenComments.indexOf(comment) === -1) { if (matchSourceUrlOrSourceMappingUrl[1] === 'Mapping') { outgoingRelations.push(new AssetGraph.JavaScriptSourceMappingUrl({ from: this, node: comment, to: { url: matchSourceUrlOrSourceMappingUrl[2], // Source maps are currently served as application/json, so prevent the target asset // from being loaded as a Json asset: type: 'SourceMap' } })); } else { outgoingRelations.push(new AssetGraph.JavaScriptSourceUrl({ from: this, node: comment, to: { url: matchSourceUrlOrSourceMappingUrl[2] } })); } seenComments.push(comment); } }, this); } }, this); // Detect global 'use strict' directives if (parentNode === this.parseTree && node instanceof uglifyJs.AST_Directive && node.value === 'use strict') { this.strict = true; } if (node instanceof uglifyJs.AST_Call) { var parentParentNode = stack[stack.length - 3]; if (node.expression instanceof uglifyJs.AST_Dot && node.expression.property === 'module' && node.expression.expression instanceof uglifyJs.AST_SymbolRef && node.expression.expression.name === 'angular') { var diveIntoAngularMethodCall = function (argumentNodes, templateCacheVariableName) { var angularWalker = new uglifyJs.TreeWalker(function (node) { var parentNode = angularWalker.parent(); if (node instanceof uglifyJs.AST_Object) { node.properties.forEach(function (keyValue) { if (keyValue.key === 'templateUrl' && keyValue.value instanceof uglifyJs.AST_String) { outgoingRelations.push(new AssetGraph.JavaScriptAngularJsTemplate({ from: this, to: { type: 'Html', url: keyValue.value.value }, node: keyValue, parentNode: node })); } else if (keyValue.key === 'template' && keyValue.value instanceof uglifyJs.AST_String) { outgoingRelations.push(new AssetGraph.JavaScriptAngularJsTemplate({ from: this, to: new AssetGraph.Html({ text: keyValue.value.value }), node: keyValue, parentNode: node })); } }, this); } else if (node instanceof uglifyJs.AST_SimpleStatement) { if (parentNode instanceof uglifyJs.AST_Function) { // Use the statements array of the function instead: parentNode = parentNode.body; } if (node.body instanceof uglifyJs.AST_Call && node.body.expression instanceof uglifyJs.AST_Dot && node.body.expression.property === 'put' && node.body.expression.expression instanceof uglifyJs.AST_SymbolRef && node.body.expression.expression.name === templateCacheVariableName && node.body.args.length === 2 && node.body.args[0] instanceof uglifyJs.AST_String && node.body.args[1] instanceof uglifyJs.AST_String) { outgoingRelations.push(new AssetGraph.JavaScriptAngularJsTemplateCacheAssignment({ from: this, to: new AssetGraph.Html({ isExternalizable: false, text: node.body.args[1].value }), node: node, parentNode: parentNode })); } } }.bind(this)); argumentNodes.forEach(function (argumentNode) { argumentNode.walk(angularWalker); }); }.bind(this); var stackPosition = stack.length - 1; while (stack[stackPosition - 1] instanceof uglifyJs.AST_Dot && stack[stackPosition - 2] instanceof uglifyJs.AST_Call) { var callNode = stack[stackPosition - 2], methodName = stack[stackPosition - 1].property, argumentNodes = callNode.args, templateCacheVariableName; if (methodName === 'run' && argumentNodes.length > 0 && argumentNodes[0] instanceof uglifyJs.AST_Array && argumentNodes[0].elements.length === 2 && argumentNodes[0].elements[0] instanceof uglifyJs.AST_String && argumentNodes[0].elements[0].value === '$templateCache' && argumentNodes[0].elements[1] instanceof uglifyJs.AST_Function) { templateCacheVariableName = argumentNodes[0].elements[1].argnames[0].name; } diveIntoAngularMethodCall(argumentNodes, templateCacheVariableName); stackPosition -= 2; } } if (node.expression instanceof uglifyJs.AST_Symbol && node.expression.name === 'INCLUDE') { if (node.args.length === 1 && node.args[0] instanceof uglifyJs.AST_String) { outgoingRelations.push(new AssetGraph.JavaScriptInclude({ from: this, to: { url: node.args[0].value }, node: node, detachableNode: parentNode instanceof uglifyJs.AST_Seq ? node : parentNode, parentNode: parentNode instanceof uglifyJs.AST_Seq ? parentNode : parentParentNode })); } else { syntaxErrors.push(new errors.SyntaxError({ message: 'Invalid INCLUDE syntax: Must take a single string argument:' + node.print_to_string(), asset: this })); } } else if (node.expression instanceof uglifyJs.AST_Symbol && node.expression.name === 'GETTEXT') { if (node.args.length === 1) { // TRHTML(GETTEXT(...)) is covered by TRHTML below: if (!(parentNode instanceof uglifyJs.AST_Call) || !(parentNode.expression instanceof uglifyJs.AST_Symbol) || parentNode.expression.name !== 'TRHTML') { node.args[0] = tryFoldConstantToString(node.args[0]); if (node.args[0] instanceof uglifyJs.AST_String) { outgoingRelations.push(new AssetGraph.JavaScriptGetText({ parentNode: parentNode, from: this, to: { url: node.args[0].value }, node: node })); } else { syntaxErrors.push(new errors.SyntaxError({ message: 'Invalid GETTEXT syntax: ' + node.print_to_string(), asset: this })); } } } else { syntaxErrors.push(new errors.SyntaxError({ message: 'Invalid GETTEXT syntax: ' + node.print_to_string(), asset: this })); } } else if (node.expression instanceof uglifyJs.AST_Symbol && node.expression.name === 'GETSTATICURL') { outgoingRelations.push(new AssetGraph.JavaScriptGetStaticUrl({ from: this, parentNode: parentNode, node: node, to: new AssetGraph.StaticUrlMap({ parseTree: node.clone().args }) })); } else if (node.expression instanceof uglifyJs.AST_Symbol && node.expression.name === 'TRHTML') { var outgoingRelation; if (node.args[0] instanceof uglifyJs.AST_Call && node.args[0].expression instanceof uglifyJs.AST_Symbol && node.args[0].expression.name === 'GETTEXT' && node.args[0].args.length === 1) { node.args[0].args[0] = tryFoldConstantToString(node.args[0].args[0]); if (node.args[0].args[0] instanceof uglifyJs.AST_String) { outgoingRelation = new AssetGraph.JavaScriptTrHtml({ from: this, parentNode: parentNode, node: node, to: { url: node.args[0].args[0].value } }); } } else { node.args[0] = tryFoldConstantToString(node.args[0]); if (node.args[0] instanceof uglifyJs.AST_String) { outgoingRelation = new AssetGraph.JavaScriptTrHtml({ from: this, parentNode: parentNode, node: node, to: new AssetGraph.Html({ node: node, text: node.args[0].value }) }); } } if (outgoingRelation) { outgoingRelations.push(outgoingRelation); } else { syntaxErrors.push(new errors.SyntaxError({ message: 'Invalid TRHTML syntax: ' + node.print_to_string(), asset: this })); } } else if (node.expression instanceof uglifyJs.AST_Symbol && (node.expression.name === 'require' || node.expression.name === 'requirejs') && ((node.args.length === 2 && node.args[1] instanceof uglifyJs.AST_Function) || node.args.length === 1) && node.args[0] instanceof uglifyJs.AST_Array) { // There's no 3 argument version of require, so check whether the require is succeeded by define('moduleName', ...); // like flattenRequireJs and r.js would output. If it is, don't model it as a relation. var parentIndex = stack.length - 1, isSucceededByDefineWithStringArg = false, seenDefine = false, walkFn = function (_node) { if (_node === node) { seenDefine = true; } else if (seenDefine && isNamedDefineNode(_node)) { isSucceededByDefineWithStringArg = true; } }; OUTER: while (parentIndex >= 0) { if (stack[parentIndex] instanceof uglifyJs.AST_Block) { var blockNode = stack[parentIndex]; for (var i = blockNode.body.indexOf(stack[parentIndex + 1]) ; i < blockNode.body.length ; i += 1) { if (blockNode.body[i] instanceof uglifyJs.AST_SimpleStatement && isNamedDefineNode(blockNode.body[i].body)) { isSucceededByDefineWithStringArg = true; break OUTER; } } break OUTER; } else if (stack[parentIndex] instanceof uglifyJs.AST_Seq) { stack[parentIndex].walk(new uglifyJs.TreeWalker(walkFn)); break OUTER; } else { parentIndex -= 1; } } if (!isSucceededByDefineWithStringArg) { var arrayNode = node.args[0]; arrayNode.elements.forEach(function (arrayItemAst, i) { arrayItemAst = arrayNode.elements[i] = tryFoldConstantToString(arrayItemAst); if (arrayItemAst instanceof uglifyJs.AST_String) { if (['require', 'exports', 'module'].indexOf(arrayItemAst.value) === -1) { var outgoingRelation = new AssetGraph.JavaScriptAmdRequire({ requireJsConfig: this.assetGraph && this.assetGraph.requireJsConfig, // Hmmm from: this, callNode: node, arrayNode: arrayNode, node: arrayItemAst }); outgoingRelation.to = { url: outgoingRelation.targetUrl, isRequired: true }; outgoingRelations.push(outgoingRelation); } } else { infos.push(new errors.SyntaxError('Skipping non-string JavaScriptAmdRequire item: ' + node.print_to_string())); } }, this); } } else if (this.isRequired && node.expression instanceof uglifyJs.AST_Symbol && node.expression.name === 'define') { if (node.args.length === 2 && node.args[0] instanceof uglifyJs.AST_Array) { var arrayNode = node.args[0]; arrayNode.elements.forEach(function (arrayItemAst, i) { arrayNode.elements[i] = arrayItemAst = tryFoldConstantToString(arrayItemAst); if (arrayItemAst instanceof uglifyJs.AST_String) { if (['require', 'exports', 'module'].indexOf(arrayItemAst.value) === -1) { var outgoingRelation = new AssetGraph.JavaScriptAmdDefine({ requireJsConfig: this.assetGraph && this.assetGraph.requireJsConfig, // Hmmm from: this, callNode: node, arrayNode: arrayNode, node: arrayItemAst }); outgoingRelation.to = { url: outgoingRelation.targetUrl, isRequired: true }; outgoingRelations.push(outgoingRelation); } } else { infos.push(new errors.SyntaxError('Skipping non-string JavaScriptAmdDefine item: ' + node.print_to_string())); } }, this); } // Keep track of the fact that we're in the body of a define(function () {...}) that might contain // JavaScriptRequireJsCommonJsCompatibilityRequire relations var lastArgument = node.args.length > 0 && node.args[node.args.length - 1]; if (lastArgument && lastArgument instanceof uglifyJs.AST_Function) { nestedDefineNodes.push(node); descend(); nestedDefineNodes.pop(); return true; // Tell the TreeWalker not to descend again } } else if (node.expression instanceof uglifyJs.AST_Symbol && node.expression.name === 'require' && node.args.length === 1 && node.args[0] instanceof uglifyJs.AST_String) { if (nestedDefineNodes.length > 0) { var parentDefineNode = nestedDefineNodes[nestedDefineNodes.length - 1]; if (!(parentDefineNode.args[0] instanceof uglifyJs.AST_String)) { var outgoingRelation = new AssetGraph.JavaScriptRequireJsCommonJsCompatibilityRequire({ parentDefineNode: parentDefineNode, requireJsConfig: this.assetGraph && this.assetGraph.requireJsConfig, // Hmmm from: this, node: node }); outgoingRelation.to = { url: outgoingRelation.targetUrl, isRequired: true }; outgoingRelations.push(outgoingRelation); } } else if (!this.isRequired) { var baseUrl = this.nonInlineAncestor.url; if (/^file:/.test(baseUrl)) { var resolvedCommonJsModuleName = resolveCommonJsModuleName(urlTools.fileUrlToFsPath(baseUrl), node.args[0].value); // Skip built-in and unresolvable modules (they just resolve to 'fs', 'util', etc., not a file name): if (!resolvedCommonJsModuleName) { warnings.push(new errors.SyntaxError({message: 'Couldn\'t resolve ' + node.print_to_string() + ', skipping', relationType: 'JavaScriptCommonJsRequire'})); } else if (/^\//.test(resolvedCommonJsModuleName)) { outgoingRelations.push(new AssetGraph.JavaScriptCommonJsRequire({ from: this, to: { url: urlTools.fsFilePathToFileUrl(resolvedCommonJsModuleName) }, node: node })); } } else { warnings.push(new errors.SyntaxError({message: 'Skipping JavaScriptCommonJsRequire (only supported from file: urls): ' + node.print_to_string(), relationType: 'JavaScriptCommonJsRequire'})); } } } } }.bind(this));