diff --git a/README.md b/README.md index 4597d461e..a0043b938 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,12 @@ The `au new` command now simplify wraps `npx makes aurelia/v1`. Users can direct Run `npm test` to run the unit tests. +To run and individual test, you can use command with filters. For example: + +```powershell +npx jasmine spec/lib/build/bundled-source.spec.js --filter="transform saves cache" +``` + ## Release new aurelia-cli version Just run `npm version patch` (or minor or major) diff --git a/bin/aurelia-cli.js b/bin/aurelia-cli.js index 22b8fadc0..626a351b0 100755 --- a/bin/aurelia-cli.js +++ b/bin/aurelia-cli.js @@ -1,12 +1,16 @@ #!/usr/bin/env node +/** + * @import { CLI } from '../dist/cli' + */ + const resolve = require('resolve'); const semver = require('semver'); const nodeVersion = process.versions.node; -if (semver.lt(nodeVersion, '10.12.0')) { +if (semver.lt(nodeVersion, '14.17.0')) { console.error(`You are running Node.js v${nodeVersion}. -aurelia-cli requires Node.js v10.12.0 or above. +aurelia-cli requires Node.js v14.17.0 or above. Please upgrade to latest Node.js https://nodejs.org`); process.exit(1); } @@ -22,10 +26,11 @@ let originalBaseDir = process.cwd(); resolve('aurelia-cli', { basedir: originalBaseDir }, function(error, projectLocalCli) { + /** @type {CLI} */ let cli; if (commandName === 'new' || error) { - cli = new (require('../lib/index').CLI); + cli = new (require('../dist/index').CLI); cli.options.runningGlobally = true; } else { cli = new (require(projectLocalCli).CLI); diff --git a/build/clean-dir.mjs b/build/clean-dir.mjs new file mode 100644 index 000000000..7cbe1fe95 --- /dev/null +++ b/build/clean-dir.mjs @@ -0,0 +1,4 @@ +import { rmSync, mkdirSync } from 'node:fs'; + +rmSync('./dist', { recursive: true, force: true}); +mkdirSync('./dist'); diff --git a/build/copy-files.mjs b/build/copy-files.mjs new file mode 100644 index 000000000..869247aec --- /dev/null +++ b/build/copy-files.mjs @@ -0,0 +1,6 @@ +import * as cpx from 'cpx'; + +cpx.copy('src/**/*.json', 'dist') + .on('copy', (e) => console.log(`Copied: ${e.srcPath}`)); + cpx.copy('src/**/*.txt', 'dist') + .on('copy', (e) => console.log(`Copied: ${e.srcPath}`)); diff --git a/eslint.config.mjs b/eslint.config.mjs index 91eac93d3..23f8238f0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,35 +1,46 @@ -import globals from "globals"; -import eslint from "@eslint/js"; +// @ts-check -export default [ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import stylistic from '@stylistic/eslint-plugin'; + +export default tseslint.config( { - ignores: ["lib/build/amodro-trace", "**/dist"], + ignores: ['src/build/amodro-trace', '**/dist', '**/lib', './spec', './build', './bin'] }, eslint.configs.recommended, + tseslint.configs.recommended, { languageOptions: { - globals: { - ...globals.node, - ...globals.jasmine, - }, - - ecmaVersion: 2019, - sourceType: "commonjs", + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } }, - + plugins: { + '@stylistic': stylistic + } + }, + { rules: { - "no-prototype-builtins": 0, - "no-console": 0, - "getter-return": 0, - "no-inner-declarations": 0, - - "comma-dangle": ["error", { - arrays: "never", - objects: "never", - imports: "never", - exports: "never", - functions: "never", + 'no-prototype-builtins': 0, + 'no-console': 0, + 'getter-return': 0, + 'no-inner-declarations': 0, + 'comma-dangle': ['error', { + arrays: 'never', + objects: 'never', + imports: 'never', + exports: 'never', + functions: 'never' }], - }, + 'prefer-rest-params': 'warn', + 'prefer-spread': 'warn', + '@stylistic/quotes': ['warn', 'single'], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-require-imports': 'warn', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error' + } } -]; +); diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 000000000..9ba208f4e --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "allowJs": true, + "types": ["node"], + "module": "nodenext", + "moduleResolution": "nodenext", + }, + "include": ["bin", "build", "eslint.config.mjs"] +} diff --git a/lib/build/amodro-trace/lib/lang.js b/lib/build/amodro-trace/lib/lang.js deleted file mode 100644 index d3fd9cadd..000000000 --- a/lib/build/amodro-trace/lib/lang.js +++ /dev/null @@ -1,231 +0,0 @@ -var define = function(fn) { module.exports = fn(); }; - -/** - * @license Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved. - * Available via the MIT or new BSD license. - * see: http://github.com/jrburke/requirejs for details - */ - -/*jslint plusplus: true */ -/*global define, java */ - -define(function () { - 'use strict'; - - var lang, isJavaObj, - hasOwn = Object.prototype.hasOwnProperty; - - function hasProp(obj, prop) { - return hasOwn.call(obj, prop); - } - - isJavaObj = function () { - return false; - }; - - //Rhino, but not Nashorn (detected by importPackage not existing) - //Can have some strange foreign objects. - if (typeof java !== 'undefined' && java.lang && java.lang.Object && typeof importPackage !== 'undefined') { - isJavaObj = function (obj) { - return obj instanceof java.lang.Object; - }; - } - - lang = { - // makeJsArrayString added after porting to this project - //Converts an JS array of strings to a string representation. - //Not using JSON.stringify() for Rhino's sake. - makeJsArrayString: function (ary) { - return '["' + ary.map(function (item) { - //Escape any double quotes, backslashes - return lang.jsEscape(item); - }).join('","') + '"]'; - }, - - backSlashRegExp: /\\/g, - ostring: Object.prototype.toString, - - isArray: Array.isArray || function (it) { - return lang.ostring.call(it) === "[object Array]"; - }, - - isFunction: function(it) { - return lang.ostring.call(it) === "[object Function]"; - }, - - isRegExp: function(it) { - return it && it instanceof RegExp; - }, - - hasProp: hasProp, - - //returns true if the object does not have an own property prop, - //or if it does, it is a falsy value. - falseProp: function (obj, prop) { - return !hasProp(obj, prop) || !obj[prop]; - }, - - //gets own property value for given prop on object - getOwn: function (obj, prop) { - return hasProp(obj, prop) && obj[prop]; - }, - - _mixin: function(dest, source, override){ - var name; - for (name in source) { - if(source.hasOwnProperty(name) && - (override || !dest.hasOwnProperty(name))) { - dest[name] = source[name]; - } - } - - return dest; // Object - }, - - /** - * mixin({}, obj1, obj2) is allowed. If the last argument is a boolean, - * then the source objects properties are force copied over to dest. - */ - mixin: function(dest){ - var parameters = Array.prototype.slice.call(arguments), - override, i, l; - - if (!dest) { dest = {}; } - - if (parameters.length > 2 && typeof arguments[parameters.length-1] === 'boolean') { - override = parameters.pop(); - } - - for (i = 1, l = parameters.length; i < l; i++) { - lang._mixin(dest, parameters[i], override); - } - return dest; // Object - }, - - /** - * Does a deep mix of source into dest, where source values override - * dest values if a winner is needed. - * @param {Object} dest destination object that receives the mixed - * values. - * @param {Object} source source object contributing properties to mix - * in. - * @return {[Object]} returns dest object with the modification. - */ - deepMix: function(dest, source) { - lang.eachProp(source, function (value, prop) { - if (typeof value === 'object' && value && - !lang.isArray(value) && !lang.isFunction(value) && - !(value instanceof RegExp)) { - - if (!dest[prop]) { - dest[prop] = {}; - } - lang.deepMix(dest[prop], value); - } else { - dest[prop] = value; - } - }); - return dest; - }, - - /** - * Does a type of deep copy. Do not give it anything fancy, best - * for basic object copies of objects that also work well as - * JSON-serialized things, or has properties pointing to functions. - * For non-array/object values, just returns the same object. - * @param {Object} obj copy properties from this object - * @param {Object} [result] optional result object to use - * @return {Object} - */ - deeplikeCopy: function (obj) { - var type, result; - - if (lang.isArray(obj)) { - result = []; - obj.forEach(function(value) { - result.push(lang.deeplikeCopy(value)); - }); - return result; - } - - type = typeof obj; - if (obj === null || obj === undefined || type === 'boolean' || - type === 'string' || type === 'number' || lang.isFunction(obj) || - lang.isRegExp(obj)|| isJavaObj(obj)) { - return obj; - } - - //Anything else is an object, hopefully. - result = {}; - lang.eachProp(obj, function(value, key) { - result[key] = lang.deeplikeCopy(value); - }); - return result; - }, - - delegate: (function () { - // boodman/crockford delegation w/ cornford optimization - function TMP() {} - return function (obj, props) { - TMP.prototype = obj; - var tmp = new TMP(); - TMP.prototype = null; - if (props) { - lang.mixin(tmp, props); - } - return tmp; // Object - }; - }()), - - /** - * Helper function for iterating over an array. If the func returns - * a true value, it will break out of the loop. - */ - each: function each(ary, func) { - if (ary) { - var i; - for (i = 0; i < ary.length; i += 1) { - if (func(ary[i], i, ary)) { - break; - } - } - } - }, - - /** - * Cycles over properties in an object and calls a function for each - * property value. If the function returns a truthy value, then the - * iteration is stopped. - */ - eachProp: function eachProp(obj, func) { - var prop; - for (prop in obj) { - if (hasProp(obj, prop)) { - if (func(obj[prop], prop)) { - break; - } - } - } - }, - - //Similar to Function.prototype.bind, but the "this" object is specified - //first, since it is easier to read/figure out what "this" will be. - bind: function bind(obj, fn) { - return function () { - return fn.apply(obj, arguments); - }; - }, - - //Escapes a content string to be be a string that has characters escaped - //for inclusion as part of a JS string. - jsEscape: function (content) { - return content.replace(/(["'\\])/g, '\\$1') - .replace(/[\f]/g, "\\f") - .replace(/[\b]/g, "\\b") - .replace(/[\n]/g, "\\n") - .replace(/[\t]/g, "\\t") - .replace(/[\r]/g, "\\r"); - } - }; - return lang; -}); diff --git a/lib/build/amodro-trace/lib/parse.js b/lib/build/amodro-trace/lib/parse.js deleted file mode 100644 index 10861808c..000000000 --- a/lib/build/amodro-trace/lib/parse.js +++ /dev/null @@ -1,847 +0,0 @@ -// Taken from r.js, preserving its style for now to easily port changes in the -// near term. -var define = function(ary, fn) { - module.exports = fn.apply(undefined, - (ary.map(function(id) { return require(id); }))); -}; - -/*jslint plusplus: true */ -/*global define: false */ -define(['meriyah', './lang'], function (meriyah, lang) { - 'use strict'; - - function arrayToString(ary) { - var output = '['; - if (ary) { - ary.forEach(function (item, i) { - output += (i > 0 ? ',' : '') + '"' + lang.jsEscape(item) + '"'; - }); - } - output += ']'; - - return output; - } - - //This string is saved off because JSLint complains - //about obj.arguments use, as 'reserved word' - var argPropName = 'arguments', - //Default object to use for "scope" checking for UMD identifiers. - emptyScope = {}, - mixin = lang.mixin, - hasProp = lang.hasProp; - - //From an meriyah example for traversing its ast. - function traverse(object, visitor) { - var child; - - if (!object) { - return; - } - - if (visitor.call(null, object) === false) { - return false; - } - for (var i = 0, keys = Object.keys(object); i < keys.length; i++) { - child = object[keys[i]]; - if (typeof child === 'object' && child !== null) { - if (traverse(child, visitor) === false) { - return false; - } - } - } - } - - //Like traverse, but visitor returning false just - //stops that subtree analysis, not the rest of tree - //visiting. - function traverseBroad(object, visitor) { - var child; - - if (!object) { - return; - } - - if (visitor.call(null, object) === false) { - return false; - } - for (var i = 0, keys = Object.keys(object); i < keys.length; i++) { - child = object[key]; - if (typeof child === 'object' && child !== null) { - traverseBroad(child, visitor); - } - } - } - - /** - * Pulls out dependencies from an array literal with just string members. - * If string literals, will just return those string values in an array, - * skipping other items in the array. - * - * @param {Node} node an AST node. - * - * @returns {Array} an array of strings. - * If null is returned, then it means the input node was not a valid - * dependency. - */ - function getValidDeps(node) { - if (!node || node.type !== 'ArrayExpression' || !node.elements) { - return; - } - - var deps = []; - - node.elements.some(function (elem) { - if (elem.type === 'Literal') { - deps.push(elem.value); - } - }); - - return deps.length ? deps : undefined; - } - - // Detects regular or arrow function expressions as the desired expression - // type. - function isFnExpression(node) { - return (node && (node.type === 'FunctionExpression' || - node.type === 'ArrowFunctionExpression')); - } - - /** - * Main parse function. Returns a string of any valid require or - * define/require.def calls as part of one JavaScript source string. - * @param {String} moduleName the module name that represents this file. - * It is used to create a default define if there is not one already for the - * file. This allows properly tracing dependencies for builds. Otherwise, if - * the file just has a require() call, the file dependencies will not be - * properly reflected: the file will come before its dependencies. - * @param {String} moduleName - * @param {String} fileName - * @param {String} fileContents - * @param {Object} options optional options. insertNeedsDefine: true will - * add calls to require.needsDefine() if appropriate. - * @returns {String} JS source string or null, if no require or - * define/require.def calls are found. - */ - function parse(moduleName, fileName, fileContents, options) { - options = options || {}; - - //Set up source input - var i, moduleCall, depString, - moduleDeps = [], - result = '', - moduleList = [], - needsDefine = true, - astRoot = meriyah.parseScript(fileContents, {next: true, webcompat: true}); - - parse.recurse(astRoot, function (callName, config, name, deps, node, factoryIdentifier, fnExpScope) { - if (!deps) { - deps = []; - } - - if (callName === 'define' && (!name || name === moduleName)) { - needsDefine = false; - } - - if (!name) { - //If there is no module name, the dependencies are for - //this file/default module name. - moduleDeps = moduleDeps.concat(deps); - } else { - moduleList.push({ - name: name, - deps: deps - }); - } - - if (callName === 'define' && factoryIdentifier && hasProp(fnExpScope, factoryIdentifier)) { - return factoryIdentifier; - } - - //If define was found, no need to dive deeper, unless - //the config explicitly wants to dig deeper. - return !!options.findNestedDependencies; - }, options); - - if (options.insertNeedsDefine && needsDefine) { - result += 'require.needsDefine("' + moduleName + '");'; - } - - if (moduleDeps.length || moduleList.length) { - for (i = 0; i < moduleList.length; i++) { - moduleCall = moduleList[i]; - if (result) { - result += '\n'; - } - - //If this is the main module for this file, combine any - //"anonymous" dependencies (could come from a nested require - //call) with this module. - if (moduleCall.name === moduleName) { - moduleCall.deps = moduleCall.deps.concat(moduleDeps); - moduleDeps = []; - } - - depString = arrayToString(moduleCall.deps); - result += 'define("' + moduleCall.name + '",' + - depString + ');'; - } - if (moduleDeps.length) { - if (result) { - result += '\n'; - } - depString = arrayToString(moduleDeps); - result += 'define("' + moduleName + '",' + depString + ');'; - } - } - - return result || null; - } - - parse.traverse = traverse; - parse.traverseBroad = traverseBroad; - parse.isFnExpression = isFnExpression; - - /** - * Handles parsing a file recursively for require calls. - * @param {Array} parentNode the AST node to start with. - * @param {Function} onMatch function to call on a parse match. - * @param {Object} [options] This is normally the build config options if - * it is passed. - * @param {Object} [fnExpScope] holds list of function expresssion - * argument identifiers, set up internally, not passed in - */ - parse.recurse = function (object, onMatch, options, fnExpScope) { - //Like traverse, but skips if branches that would not be processed - //after has application that results in tests of true or false boolean - //literal values. - var keys, child, result, i, params, param, tempObject, - hasHas = options && options.has; - - fnExpScope = fnExpScope || emptyScope; - - if (!object) { - return; - } - - //If has replacement has resulted in if(true){} or if(false){}, take - //the appropriate branch and skip the other one. - if (hasHas && object.type === 'IfStatement' && object.test.type && - object.test.type === 'Literal') { - if (object.test.value) { - //Take the if branch - this.recurse(object.consequent, onMatch, options, fnExpScope); - } else { - //Take the else branch - this.recurse(object.alternate, onMatch, options, fnExpScope); - } - } else { - result = this.parseNode(object, onMatch, fnExpScope); - if (result === false) { - return; - } else if (typeof result === 'string') { - return result; - } - - //Build up a "scope" object that informs nested recurse calls if - //the define call references an identifier that is likely a UMD - //wrapped function expression argument. - //Catch (function(a) {... wrappers - if (object.type === 'ExpressionStatement' && object.expression && - object.expression.type === 'CallExpression' && object.expression.callee && - isFnExpression(object.expression.callee)) { - tempObject = object.expression.callee; - } - // Catch !function(a) {... wrappers - if (object.type === 'UnaryExpression' && object.argument && - object.argument.type === 'CallExpression' && object.argument.callee && - isFnExpression(object.argument.callee)) { - tempObject = object.argument.callee; - } - if (tempObject && tempObject.params && tempObject.params.length) { - params = tempObject.params; - fnExpScope = mixin({}, fnExpScope, true); - for (i = 0; i < params.length; i++) { - param = params[i]; - if (param.type === 'Identifier') { - fnExpScope[param.name] = true; - } - } - } - - for (i = 0, keys = Object.keys(object); i < keys.length; i++) { - child = object[keys[i]]; - if (typeof child === 'object' && child !== null) { - result = this.recurse(child, onMatch, options, fnExpScope); - if (typeof result === 'string' && hasProp(fnExpScope, result)) { - //The result was still in fnExpScope so break. Otherwise, - //was a return from a a tree that had a UMD definition, - //but now out of that scope so keep siblings. - break; - } - } - } - - //Check for an identifier for a factory function identifier being - //passed in as a function expression, indicating a UMD-type of - //wrapping. - if (typeof result === 'string') { - if (hasProp(fnExpScope, result)) { - //result still in scope, keep jumping out indicating the - //identifier still in use. - return result; - } - - return; - } - } - }; - - /** - * Finds require("") calls inside a CommonJS anonymous module wrapped - * in a define function, given an AST node for the definition function. - * @param {Node} node the AST node for the definition function. - * @returns {Array} and array of dependency names. Can be of zero length. - */ - parse.getAnonDepsFromNode = function (node) { - var deps = [], - funcArgLength; - - if (node) { - this.findRequireDepNames(node, deps); - - //If no deps, still add the standard CommonJS require, exports, - //module, in that order, to the deps, but only if specified as - //function args. In particular, if exports is used, it is favored - //over the return value of the function, so only add it if asked. - funcArgLength = node.params && node.params.length; - if (funcArgLength) { - deps = (funcArgLength > 1 ? ["require", "exports", "module"] : - ["require"]).concat(deps); - } - } - return deps; - }; - - parse.isDefineNodeWithArgs = function (node) { - return node && node.type === 'CallExpression' && - node.callee && node.callee.type === 'Identifier' && - node.callee.name === 'define' && node[argPropName]; - }; - - /** - * Finds the function in define(function (require, exports, module){}); - * @param {Array} node - * @returns {Boolean} - */ - parse.findAnonDefineFactory = function (node) { - var match; - - traverse(node, function (node) { - var arg0, arg1; - - if (parse.isDefineNodeWithArgs(node)) { - - //Just the factory function passed to define - arg0 = node[argPropName][0]; - if (isFnExpression(arg0)) { - match = arg0; - return false; - } - - //A string literal module ID followed by the factory function. - arg1 = node[argPropName][1]; - if (arg0.type === 'Literal' && isFnExpression(arg1)) { - match = arg1; - return false; - } - } - }); - - return match; - }; - - /** - * Finds any config that is passed to requirejs. That includes calls to - * require/requirejs.config(), as well as require({}, ...) and - * requirejs({}, ...) - * @param {String} fileContents - * - * @returns {Object} a config details object with the following properties: - * - config: {Object} the config object found. Can be undefined if no - * config found. - * - range: {Array} the start index and end index in the contents where - * the config was found. Can be undefined if no config found. - * Can throw an error if the config in the file cannot be evaluated in - * a build context to valid JavaScript. - */ - parse.findConfig = function (fileContents) { - /*jslint evil: true */ - var jsConfig, foundConfig, stringData, foundRange, quote, quoteMatch, - quoteRegExp = /(:\s|\[\s*)(['"])/, - astRoot = meriyah.parseScript(fileContents, { - loc: true, - next: true, - webcompat: true - }); - - traverse(astRoot, function (node) { - var arg, - requireType = parse.hasRequire(node); - - if (requireType && (requireType === 'require' || - requireType === 'requirejs' || - requireType === 'requireConfig' || - requireType === 'requirejsConfig')) { - - arg = node[argPropName] && node[argPropName][0]; - - if (arg && arg.type === 'ObjectExpression') { - stringData = parse.nodeToString(fileContents, arg); - jsConfig = stringData.value; - foundRange = stringData.range; - return false; - } - } else { - arg = parse.getRequireObjectLiteral(node); - if (arg) { - stringData = parse.nodeToString(fileContents, arg); - jsConfig = stringData.value; - foundRange = stringData.range; - return false; - } - } - }); - - if (jsConfig) { - // Eval the config - quoteMatch = quoteRegExp.exec(jsConfig); - quote = (quoteMatch && quoteMatch[2]) || '"'; - foundConfig = eval('(' + jsConfig + ')'); - } - - return { - config: foundConfig, - range: foundRange, - quote: quote - }; - }; - - /** Returns the node for the object literal assigned to require/requirejs, - * for holding a declarative config. - */ - parse.getRequireObjectLiteral = function (node) { - if (node.id && node.id.type === 'Identifier' && - (node.id.name === 'require' || node.id.name === 'requirejs') && - node.init && node.init.type === 'ObjectExpression') { - return node.init; - } - }; - - /** - * Finds all dependencies specified in dependency arrays and inside - * simplified commonjs wrappers. - * @param {String} fileName - * @param {String} fileContents - * - * @returns {Array} an array of dependency strings. The dependencies - * have not been normalized, they may be relative IDs. - */ - parse.findDependencies = function (fileName, fileContents, options) { - // modified to accept parsed result for efficiency. - var dependencies = [], astRoot; - if (fileContents && fileContents.type) { - astRoot = fileContents - } else { - astRoot = meriyah.parseScript(fileContents, {next: true, webcompat: true}); - } - - parse.recurse(astRoot, function (callName, config, name, deps) { - if (deps) { - dependencies = dependencies.concat(deps); - } - }, options); - - return dependencies; - }; - - /** - * Finds only CJS dependencies, ones that are the form - * require('stringLiteral') - */ - parse.findCjsDependencies = function (fileName, fileContents) { - var dependencies = []; - - traverse(meriyah.parseScript(fileContents, {next: true, webcompat: true}), function (node) { - var arg; - - if (node && node.type === 'CallExpression' && node.callee && - node.callee.type === 'Identifier' && - node.callee.name === 'require' && node[argPropName] && - node[argPropName].length === 1) { - arg = node[argPropName][0]; - if (arg.type === 'Literal') { - dependencies.push(arg.value); - } - } - }); - - return dependencies; - }; - - //function define() {} - parse.hasDefDefine = function (node) { - return node.type === 'FunctionDeclaration' && node.id && - node.id.type === 'Identifier' && node.id.name === 'define'; - }; - - //define.amd = ... - parse.hasDefineAmd = function (node) { - return node && node.type === 'AssignmentExpression' && - node.left && node.left.type === 'MemberExpression' && - node.left.object && node.left.object.name === 'define' && - node.left.property && node.left.property.name === 'amd'; - }; - - //define.amd reference, as in: if (define.amd) - parse.refsDefineAmd = function (node) { - return node && node.type === 'MemberExpression' && - node.object && node.object.name === 'define' && - node.object.type === 'Identifier' && - node.property && node.property.name === 'amd' && - node.property.type === 'Identifier'; - }; - - //require(), requirejs(), require.config() and requirejs.config() - parse.hasRequire = function (node) { - var callName, - c = node && node.callee; - - if (node && node.type === 'CallExpression' && c) { - if (c.type === 'Identifier' && - (c.name === 'require' || - c.name === 'requirejs')) { - //A require/requirejs({}, ...) call - callName = c.name; - } else if (c.type === 'MemberExpression' && - c.object && - c.object.type === 'Identifier' && - (c.object.name === 'require' || - c.object.name === 'requirejs') && - c.property && c.property.name === 'config') { - // require/requirejs.config({}) call - callName = c.object.name + 'Config'; - } - } - - return callName; - }; - - //define() - parse.hasDefine = function (node) { - return node && node.type === 'CallExpression' && node.callee && - node.callee.type === 'Identifier' && - node.callee.name === 'define'; - }; - - /** - * Determines if define(), require({}|[]) or requirejs was called in the - * file. Also finds out if define() is declared and if define.amd is called. - */ - parse.usesAmdOrRequireJs = function (fileName, fileContents) { - var uses; - - traverse(meriyah.parseScript(fileContents, {next: true, webcompat: true}), function (node) { - var type, callName, arg; - - if (parse.hasDefDefine(node)) { - //function define() {} - type = 'declaresDefine'; - } else if (parse.hasDefineAmd(node)) { - type = 'defineAmd'; - } else { - callName = parse.hasRequire(node); - if (callName) { - arg = node[argPropName] && node[argPropName][0]; - if (arg && (arg.type === 'ObjectExpression' || - arg.type === 'ArrayExpression')) { - type = callName; - } - } else if (parse.hasDefine(node)) { - type = 'define'; - } - } - - if (type) { - if (!uses) { - uses = {}; - } - uses[type] = true; - } - }); - - return uses; - }; - - /** - * Determines if require(''), exports.x =, module.exports =, - * __dirname, __filename are used. So, not strictly traditional CommonJS, - * also checks for Node variants. - */ - parse.usesCommonJs = function (fileName, fileContents) { - var uses = null, - assignsExports = false; - - - traverse(meriyah.parseScript(fileContents, {next: true, webcompat: true}), function (node) { - var type, - // modified to fix a bug on (true || exports.name = {}) - // https://github.com/requirejs/r.js/issues/980 - exp = node.expression || node.init || node; - - if (node.type === 'Identifier' && - (node.name === '__dirname' || node.name === '__filename')) { - type = node.name.substring(2); - } else if (node.type === 'VariableDeclarator' && node.id && - node.id.type === 'Identifier' && - node.id.name === 'exports') { - //Hmm, a variable assignment for exports, so does not use cjs - //exports. - type = 'varExports'; - } else if (exp && exp.type === 'AssignmentExpression' && exp.left && - exp.left.type === 'MemberExpression' && exp.left.object) { - if (exp.left.object.name === 'module' && exp.left.property && - exp.left.property.name === 'exports') { - type = 'moduleExports'; - } else if (exp.left.object.name === 'exports' && - exp.left.property) { - type = 'exports'; - } else if (exp.left.object.type === 'MemberExpression' && - exp.left.object.object.name === 'module' && - exp.left.object.property.name === 'exports' && - exp.left.object.property.type === 'Identifier') { - type = 'moduleExports'; - } - - } else if (node && node.type === 'CallExpression' && node.callee && - node.callee.type === 'Identifier' && - node.callee.name === 'require' && node[argPropName] && - node[argPropName].length === 1 && - node[argPropName][0].type === 'Literal') { - type = 'require'; - } - - if (type) { - if (type === 'varExports') { - assignsExports = true; - } else if (type !== 'exports' || !assignsExports) { - if (!uses) { - uses = {}; - } - uses[type] = true; - } - } - }); - - return uses; - }; - - - parse.findRequireDepNames = function (node, deps) { - traverse(node, function (node) { - var arg; - - if (node && node.type === 'CallExpression' && node.callee && - node.callee.type === 'Identifier' && - node.callee.name === 'require' && - node[argPropName] && node[argPropName].length === 1) { - - arg = node[argPropName][0]; - if (arg.type === 'Literal') { - deps.push(arg.value); - } - } - }); - }; - - /** - * Determines if a specific node is a valid require or define/require.def - * call. - * @param {Array} node - * @param {Function} onMatch a function to call when a match is found. - * It is passed the match name, and the config, name, deps possible args. - * The config, name and deps args are not normalized. - * @param {Object} fnExpScope an object whose keys are all function - * expression identifiers that should be in scope. Useful for UMD wrapper - * detection to avoid parsing more into the wrapped UMD code. - * - * @returns {String} a JS source string with the valid require/define call. - * Otherwise null. - */ - parse.parseNode = function (node, onMatch, fnExpScope) { - var name, deps, cjsDeps, arg, factory, exp, refsDefine, bodyNode, - args = node && node[argPropName], - callName = parse.hasRequire(node), - isUmd = false; - - if (callName === 'require' || callName === 'requirejs') { - //A plain require/requirejs call - arg = node[argPropName] && node[argPropName][0]; - if (arg && arg.type !== 'ArrayExpression') { - if (arg.type === 'ObjectExpression') { - //A config call, try the second arg. - arg = node[argPropName][1]; - } - } - - deps = getValidDeps(arg); - if (!deps) { - return; - } - - return onMatch("require", null, null, deps, node); - } else if (parse.hasDefine(node) && args && args.length) { - name = args[0]; - deps = args[1]; - factory = args[2]; - - if (name.type === 'ArrayExpression') { - //No name, adjust args - factory = deps; - deps = name; - name = null; - } else if (isFnExpression(name)) { - //Just the factory, no name or deps - factory = name; - name = deps = null; - } else if (name.type === 'Identifier' && args.length === 1 && - hasProp(fnExpScope, name.name)) { - //define(e) where e is a UMD identifier for the factory - //function. - isUmd = true; - factory = name; - name = null; - } else if (name.type !== 'Literal') { - //An object literal, just null out - name = deps = factory = null; - } - - if (name && name.type === 'Literal' && deps) { - if (isFnExpression(deps)) { - //deps is the factory - factory = deps; - deps = null; - } else if (deps.type === 'ObjectExpression') { - //deps is object literal, null out - deps = factory = null; - } else if (deps.type === 'Identifier') { - if (args.length === 2) { - //define('id', factory) - deps = factory = null; - } else if (args.length === 3 && isFnExpression(factory)) { - //define('id', depsIdentifier, factory) - //Since identifier, cannot know the deps, but do not - //error out, assume they are taken care of outside of - //static parsing. - deps = null; - } - } - } - - if (deps && deps.type === 'ArrayExpression') { - deps = getValidDeps(deps); - } else if (isFnExpression(factory)) { - //If no deps and a factory function, could be a commonjs sugar - //wrapper, scan the function for dependencies. - cjsDeps = parse.getAnonDepsFromNode(factory); - if (cjsDeps.length) { - deps = cjsDeps; - } - } else if (deps || (factory && !isUmd)) { - //Does not match the shape of an AMD call. - return; - } - - //Just save off the name as a string instead of an AST object. - if (name && name.type === 'Literal') { - name = name.value; - } - - return onMatch("define", null, name, deps, node, - (factory && factory.type === 'Identifier' ? factory.name : undefined), - fnExpScope); - } else if (node.type === 'CallExpression' && node.callee && - isFnExpression(node.callee) && - node.callee.body && node.callee.body.body && - node.callee.body.body.length === 1 && - node.callee.body.body[0].type === 'IfStatement') { - bodyNode = node.callee.body.body[0]; - //Look for a define(Identifier) case, but only if inside an - //if that has a define.amd test - if (bodyNode.consequent && bodyNode.consequent.body) { - exp = bodyNode.consequent.body; - if (Array.isArray(exp) && exp.length) exp = exp[0]; - - if (exp.type === 'ExpressionStatement' && exp.expression && - parse.hasDefine(exp.expression) && - exp.expression.arguments && - exp.expression.arguments.length === 1 && - exp.expression.arguments[0].type === 'Identifier') { - - //Calls define(Identifier) as first statement in body. - //Confirm the if test references define.amd - traverse(bodyNode.test, function (node) { - if (parse.refsDefineAmd(node)) { - refsDefine = true; - return false; - } - }); - - if (refsDefine) { - return onMatch("define", null, null, null, exp.expression, - exp.expression.arguments[0].name, fnExpScope); - } - } - } - } - }; - - /** - * Converts an AST node into a JS source string by extracting - * the node's location from the given contents string. Assumes - * meriyah.parseScript() with loc was done. - * @param {String} contents - * @param {Object} node - * @returns {String} a JS source string. - */ - parse.nodeToString = function (contents, node) { - var extracted, - loc = node.loc, - lines = contents.split('\n'), - firstLine = loc.start.line > 1 ? - lines.slice(0, loc.start.line - 1).join('\n') + '\n' : - '', - preamble = firstLine + - lines[loc.start.line - 1].substring(0, loc.start.column); - - if (loc.start.line === loc.end.line) { - extracted = lines[loc.start.line - 1].substring(loc.start.column, - loc.end.column); - } else { - extracted = lines[loc.start.line - 1].substring(loc.start.column) + - '\n' + - lines.slice(loc.start.line, loc.end.line - 1).join('\n') + - '\n' + - lines[loc.end.line - 1].substring(0, loc.end.column); - } - - return { - value: extracted, - range: [ - preamble.length, - preamble.length + extracted.length - ] - }; - }; - - return parse; -}); diff --git a/lib/build/amodro-trace/lib/transform.js b/lib/build/amodro-trace/lib/transform.js deleted file mode 100644 index d81204413..000000000 --- a/lib/build/amodro-trace/lib/transform.js +++ /dev/null @@ -1,466 +0,0 @@ -// Taken from r.js, preserving its style for now to easily port changes in the -// near term. -var define = function(ary, fn) { - module.exports = fn.apply(undefined, - (ary.map(function(id) { return require(id); }))); -}; - -/** - * @license Copyright (c) 2012-2015, The Dojo Foundation All Rights Reserved. - * Available via the MIT or new BSD license. - * see: http://github.com/jrburke/requirejs for details - */ - -/*global define */ - -define([ 'meriyah', './parse', './lang'], -function (meriyah, parse, lang) { - 'use strict'; - var transform, - jsExtRegExp = /\.js$/g, - baseIndentRegExp = /^([ \t]+)/, - indentRegExp = /\{[\r\n]+([ \t]+)/, - keyRegExp = /^[_A-Za-z]([A-Za-z\d_]*)$/, - bulkIndentRegExps = { - '\n': /\n/g, - '\r\n': /\r\n/g - }; - - function applyIndent(str, indent, lineReturn) { - var regExp = bulkIndentRegExps[lineReturn]; - return str.replace(regExp, '$&' + indent); - } - - transform = { - toTransport: function (namespace, moduleName, path, contents, onFound, options) { - options = options || {}; - - var astRoot, contentLines, modLine, - foundAnon, - scanCount = 0, - scanReset = false, - defineInfos = [], - applySourceUrl = function (contents) { - if (options.useSourceUrl) { - contents = 'eval("' + lang.jsEscape(contents) + - '\\n//# sourceURL=' + (path.indexOf('/') === 0 ? '' : '/') + - path + - '");\n'; - } - return contents; - }; - - try { - astRoot = meriyah.parseScript(contents, { - loc: true, - next: true, - webcompat: true - }); - } catch (e) { - var logger = options.logger; - if (logger && logger.warn) { - if (jsExtRegExp.test(path)) { - logger.warn('toTransport skipping ' + path + - ': ' + e.toString()); - } - } - return contents; - } - - //Find the define calls and their position in the files. - parse.traverse(astRoot, function (node) { - var args, firstArg, firstArgLoc, factoryNode, - needsId, depAction, foundId, init, - sourceUrlData, range, - namespaceExists = false; - - // If a bundle script with a define declaration, do not - // parse any further at this level. Likely a built layer - // by some other tool. - if (node.type === 'VariableDeclarator' && - node.id && node.id.name === 'define' && - node.id.type === 'Identifier') { - init = node.init; - if (init && init.callee && - init.callee.type === 'CallExpression' && - init.callee.callee && - init.callee.callee.type === 'Identifier' && - init.callee.callee.name === 'require' && - init.callee.arguments && init.callee.arguments.length === 1 && - init.callee.arguments[0].type === 'Literal' && - init.callee.arguments[0].value && - init.callee.arguments[0].value.indexOf('amdefine') !== -1) { - // the var define = require('amdefine')(module) case, - // keep going in that case. - } else { - return false; - } - } - - namespaceExists = namespace && - node.type === 'CallExpression' && - node.callee && node.callee.object && - node.callee.object.type === 'Identifier' && - node.callee.object.name === namespace && - node.callee.property.type === 'Identifier' && - node.callee.property.name === 'define'; - - if (namespaceExists || parse.isDefineNodeWithArgs(node)) { - //The arguments are where its at. - args = node.arguments; - if (!args || !args.length) { - return; - } - - firstArg = args[0]; - firstArgLoc = firstArg.loc; - - if (args.length === 1) { - if (firstArg.type === 'Identifier') { - //The define(factory) case, but - //only allow it if one Identifier arg, - //to limit impact of false positives. - needsId = true; - depAction = 'empty'; - } else if (parse.isFnExpression(firstArg)) { - //define(function(){}) - factoryNode = firstArg; - needsId = true; - depAction = 'scan'; - } else if (firstArg.type === 'ObjectExpression') { - //define({}); - needsId = true; - depAction = 'skip'; - } else if (firstArg.type === 'Literal' && - typeof firstArg.value === 'number') { - //define(12345); - needsId = true; - depAction = 'skip'; - } else if (firstArg.type === 'UnaryExpression' && - firstArg.operator === '-' && - firstArg.argument && - firstArg.argument.type === 'Literal' && - typeof firstArg.argument.value === 'number') { - //define('-12345'); - needsId = true; - depAction = 'skip'; - } else if (firstArg.type === 'MemberExpression' && - firstArg.object && - firstArg.property && - firstArg.property.type === 'Identifier') { - //define(this.key); - needsId = true; - depAction = 'empty'; - } - } else if (firstArg.type === 'ArrayExpression') { - //define([], ...); - needsId = true; - depAction = 'skip'; - } else if (firstArg.type === 'Literal' && - typeof firstArg.value === 'string') { - //define('string', ....) - //Already has an ID. - foundId = firstArg.value; - needsId = false; - if (args.length === 2 && - parse.isFnExpression(args[1])) { - //Needs dependency scanning. - factoryNode = args[1]; - depAction = 'scan'; - } else { - depAction = 'skip'; - } - } else { - //Unknown define entity, keep looking, even - //in the subtree for this node. - return; - } - - range = { - foundId: foundId, - needsId: needsId, - depAction: depAction, - namespaceExists: namespaceExists, - node: node, - defineLoc: node.loc, - firstArgLoc: firstArgLoc, - factoryNode: factoryNode, - sourceUrlData: sourceUrlData - }; - - //Only transform ones that do not have IDs. If it has an - //ID but no dependency array, assume it is something like - //a phonegap implementation, that has its own internal - //define that cannot handle dependency array constructs, - //and if it is a named module, then it means it has been - //set for transport form. - if (range.needsId) { - if (foundAnon) { - var logger = options.logger; - if (logger && logger.warn) { - logger.warn(path + ' has more than one anonymous ' + - 'define. May be a built file from another ' + - 'build system like, Ender. Skipping normalization.'); - } - defineInfos = []; - return false; - } else { - foundAnon = range; - defineInfos.push(range); - } - } else if (depAction === 'scan') { - scanCount += 1; - if (scanCount > 1) { - //Just go back to an array that just has the - //anon one, since this is an already optimized - //file like the phonegap one. - if (!scanReset) { - defineInfos = foundAnon ? [foundAnon] : []; - scanReset = true; - } - } else { - defineInfos.push(range); - } - } else { - // need to pass foundId to onFound callback - defineInfos.push(range); - } - } - }); - - - if (!defineInfos.length) { - return applySourceUrl(contents); - } - - //Reverse the matches, need to start from the bottom of - //the file to modify it, so that the ranges are still true - //further up. - defineInfos.reverse(); - - contentLines = contents.split('\n'); - - modLine = function (loc, contentInsertion) { - var startIndex = loc.start.column, - //start.line is 1-based, not 0 based. - lineIndex = loc.start.line - 1, - line = contentLines[lineIndex]; - contentLines[lineIndex] = line.substring(0, startIndex) + - contentInsertion + - line.substring(startIndex, - line.length); - }; - - defineInfos.forEach(function (info) { - var deps, - contentInsertion = '', - depString = ''; - - //Do the modifications "backwards", in other words, start with the - //one that is farthest down and work up, so that the ranges in the - //defineInfos still apply. So that means deps, id, then namespace. - if (info.needsId && moduleName) { - contentInsertion += "'" + moduleName + "',"; - } - - if (info.depAction === 'scan') { - deps = parse.getAnonDepsFromNode(info.factoryNode); - - if (deps.length) { - depString = '[' + deps.map(function (dep) { - return "'" + dep + "'"; - }) + ']'; - } else { - depString = '[]'; - } - depString += ','; - - if (info.factoryNode) { - //Already have a named module, need to insert the - //dependencies after the name. - modLine(info.factoryNode.loc, depString); - } else { - contentInsertion += depString; - } - } - - if (contentInsertion) { - modLine(info.firstArgLoc, contentInsertion); - } - - //Do namespace last so that ui does not mess upthe parenRange - //used above. - if (namespace && !info.namespaceExists) { - modLine(info.defineLoc, namespace + '.'); - } - - //Notify any listener for the found info - if (onFound) { - onFound(info); - } - }); - - contents = contentLines.join('\n'); - - return applySourceUrl(contents); - }, - - /** - * Modify the contents of a require.config/requirejs.config call. This - * call will LOSE any existing comments that are in the config string. - * - * @param {String} fileContents String that may contain a config call - * @param {Function} onConfig Function called when the first config - * call is found. It will be passed an Object which is the current - * config, and the onConfig function should return an Object to use - * as the config. - * @return {String} the fileContents with the config changes applied. - */ - modifyConfig: function (fileContents, onConfig) { - var details = parse.findConfig(fileContents), - config = details.config; - - if (config) { - config = onConfig(config); - if (config) { - return transform.serializeConfig(config, - fileContents, - details.range[0], - details.range[1], - { - quote: details.quote - }); - } - } - - return fileContents; - }, - - serializeConfig: function (config, fileContents, start, end, options) { - //Calculate base level of indent - var indent, match, configString, outDentRegExp, - baseIndent = '', - startString = fileContents.substring(0, start), - existingConfigString = fileContents.substring(start, end), - lineReturn = existingConfigString.indexOf('\r') === -1 ? '\n' : '\r\n', - lastReturnIndex = startString.lastIndexOf('\n'); - - //Get the basic amount of indent for the require config call. - if (lastReturnIndex === -1) { - lastReturnIndex = 0; - } - - match = baseIndentRegExp.exec(startString.substring(lastReturnIndex + 1, start)); - if (match && match[1]) { - baseIndent = match[1]; - } - - //Calculate internal indentation for config - match = indentRegExp.exec(existingConfigString); - if (match && match[1]) { - indent = match[1]; - } - - if (!indent || indent.length < baseIndent) { - indent = ' '; - } else { - indent = indent.substring(baseIndent.length); - } - - outDentRegExp = new RegExp('(' + lineReturn + ')' + indent, 'g'); - - configString = transform.objectToString(config, { - indent: indent, - lineReturn: lineReturn, - outDentRegExp: outDentRegExp, - quote: options && options.quote - }); - - //Add in the base indenting level. - configString = applyIndent(configString, baseIndent, lineReturn); - - return startString + configString + fileContents.substring(end); - }, - - /** - * Tries converting a JS object to a string. This will likely suck, and - * is tailored to the type of config expected in a loader config call. - * So, hasOwnProperty fields, strings, numbers, arrays and functions, - * no weird recursively referenced stuff. - * @param {Object} obj the object to convert - * @param {Object} options options object with the following values: - * {String} indent the indentation to use for each level - * {String} lineReturn the type of line return to use - * {outDentRegExp} outDentRegExp the regexp to use to outdent functions - * {String} quote the quote type to use, ' or ". Optional. Default is " - * @param {String} totalIndent the total indent to print for this level - * @return {String} a string representation of the object. - */ - objectToString: function (obj, options, totalIndent) { - var startBrace, endBrace, nextIndent, - first = true, - value = '', - lineReturn = options.lineReturn, - indent = options.indent, - outDentRegExp = options.outDentRegExp, - quote = options.quote || '"'; - - totalIndent = totalIndent || ''; - nextIndent = totalIndent + indent; - - if (obj === null) { - value = 'null'; - } else if (obj === undefined) { - value = 'undefined'; - } else if (typeof obj === 'number' || typeof obj === 'boolean') { - value = obj; - } else if (typeof obj === 'string') { - //Use double quotes in case the config may also work as JSON. - value = quote + lang.jsEscape(obj) + quote; - } else if (lang.isArray(obj)) { - lang.each(obj, function (item, i) { - value += (i !== 0 ? ',' + lineReturn : '' ) + - nextIndent + - transform.objectToString(item, - options, - nextIndent); - }); - - startBrace = '['; - endBrace = ']'; - } else if (lang.isFunction(obj) || lang.isRegExp(obj)) { - //The outdent regexp just helps pretty up the conversion - //just in node. Rhino strips comments and does a different - //indent scheme for Function toString, so not really helpful - //there. - value = obj.toString().replace(outDentRegExp, '$1'); - } else { - //An object - lang.eachProp(obj, function (v, prop) { - value += (first ? '': ',' + lineReturn) + - nextIndent + - (keyRegExp.test(prop) ? prop : quote + lang.jsEscape(prop) + quote )+ - ': ' + - transform.objectToString(v, - options, - nextIndent); - first = false; - }); - startBrace = '{'; - endBrace = '}'; - } - - if (startBrace) { - value = startBrace + - lineReturn + - value + - lineReturn + totalIndent + - endBrace; - } - - return value; - } - }; - - return transform; -}); diff --git a/lib/build/amodro-trace/read/es.js b/lib/build/amodro-trace/read/es.js deleted file mode 100644 index 8ff1491b9..000000000 --- a/lib/build/amodro-trace/read/es.js +++ /dev/null @@ -1,9 +0,0 @@ -const transform = require('@babel/core').transform; - -// use babel to translate native es module into AMD module -module.exports = function es(fileName, fileContents) { - return transform(fileContents, { - babelrc: false, - plugins: [['@babel/plugin-transform-modules-amd', {loose: true}]] - }).code; -}; diff --git a/lib/build/bundler.js b/lib/build/bundler.js deleted file mode 100644 index b9e3a7327..000000000 --- a/lib/build/bundler.js +++ /dev/null @@ -1,378 +0,0 @@ -const Bundle = require('./bundle').Bundle; -const BundledSource = require('./bundled-source').BundledSource; -const CLIOptions = require('../cli-options').CLIOptions; -const LoaderPlugin = require('./loader-plugin').LoaderPlugin; -const Configuration = require('../configuration').Configuration; -const path = require('path'); -const fs = require('../file-system'); -const Utils = require('./utils'); -const logger = require('aurelia-logging').getLogger('Bundler'); -const stubModule = require('./stub-module'); - -exports.Bundler = class { - constructor(project, packageAnalyzer, packageInstaller) { - this.project = project; - this.packageAnalyzer = packageAnalyzer; - this.packageInstaller = packageInstaller; - this.bundles = []; - this.itemLookup = {}; - this.items = []; - this.environment = CLIOptions.getEnvironment(); - // --auto-install is checked here instead of in app's tasks/run.js - // this enables all existing apps to use this feature. - this.autoInstall = CLIOptions.hasFlag('auto-install'); - this.triedAutoInstalls = new Set(); - - let defaultBuildOptions = { - minify: 'stage & prod', - sourcemaps: 'dev & stage', - rev: false - }; - - this.buildOptions = new Configuration(project.build.options, defaultBuildOptions); - this.loaderOptions = project.build.loader; - - this.loaderConfig = { - baseUrl: project.paths.root, - paths: ensurePathsRelativelyFromRoot(project.paths || {}), - packages: [], - stubModules: [], - shim: {} - }; - - Object.assign(this.loaderConfig, this.project.build.loader.config); - - this.loaderOptions.plugins = (this.loaderOptions.plugins || []).map(x => { - let plugin = new LoaderPlugin(this.loaderOptions.type, x); - - if (plugin.stub && this.loaderConfig.stubModules.indexOf(plugin.name) === -1) { - this.loaderConfig.stubModules.push(plugin.name); - } - - return plugin; - }); - } - - static create(project, packageAnalyzer, packageInstaller) { - let bundler = new exports.Bundler(project, packageAnalyzer, packageInstaller); - - return Promise.all( - project.build.bundles.map(x => Bundle.create(bundler, x).then(bundle => { - bundler.addBundle(bundle); - })) - ).then(() => { - //Order the bundles so that the bundle containing the config is processed last. - if (bundler.bundles.length) { - let configTargetBundleIndex = bundler.bundles.findIndex(x => x.config.name === bundler.loaderOptions.configTarget); - bundler.bundles.splice(bundler.bundles.length, 0, bundler.bundles.splice(configTargetBundleIndex, 1)[0]); - bundler.configTargetBundle = bundler.bundles[bundler.bundles.length - 1]; - } - }).then(() => bundler); - } - - itemIncludedInBuild(item) { - if (typeof item === 'string' || !item.env) { - return true; - } - - let value = item.env; - let parts = value.split('&').map(x => x.trim().toLowerCase()); - - return parts.indexOf(this.environment) !== -1; - } - - addFile(file, inclusion) { - let key = normalizeKey(file.path); - let found = this.itemLookup[key]; - - if (!found) { - found = new BundledSource(this, file); - this.itemLookup[key] = found; - this.items.push(found); - } - - if (inclusion) { - inclusion.addItem(found); - } else { - subsume(this.bundles, found); - } - - return found; - } - - updateFile(file, inclusion) { - let found = this.itemLookup[normalizeKey(file.path)]; - - if (found) { - found.update(file); - } else { - this.addFile(file, inclusion); - } - } - - addBundle(bundle) { - this.bundles.push(bundle); - } - - configureDependency(dependency) { - return analyzeDependency(this.packageAnalyzer, dependency) - .catch(e => { - let nodeId = dependency.name || dependency; - - if (this.autoInstall && !this.triedAutoInstalls.has(nodeId)) { - this.triedAutoInstalls.add(nodeId); - return this.packageInstaller.install([nodeId]) - // try again after install - .then(() => this.configureDependency(nodeId)); - } - - logger.error(`Unable to analyze ${nodeId}`); - logger.info(e); - throw e; - }); - } - - build(opts) { - let onRequiringModule, onNotBundled; - if (opts && typeof opts.onRequiringModule === 'function') { - onRequiringModule = opts.onRequiringModule; - } - if (opts && typeof opts.onNotBundled === 'function') { - onNotBundled = opts.onNotBundled; - } - - const doTranform = (initSet) => { - let deps = new Set(initSet); - - this.items.forEach(item => { - // Transformed items will be ignored - // by flag item.requiresTransform. - let _deps = item.transform(); - if (_deps) _deps.forEach(d => deps.add(d)); - }); - - if (deps.size) { - // removed all fulfilled deps - this.bundles.forEach(bundle => { - // Only need to check raw module ids, not nodeIdCompat aliases. - // Because deps here are clean module ids. - bundle.getRawBundledModuleIds().forEach(id => { - deps.delete(id); - - if (id.endsWith('/index')) { - // if id is 'resources/index', shortId is 'resources'. - let shortId = id.slice(0, -6); - if (deps.delete(shortId)) { - // ok, someone try to use short name - bundle.addAlias(shortId, id); - } - } - }); - }); - } - - if (deps.size) { - let _leftOver = new Set(); - - return Utils.runSequentially( - Array.from(deps).sort(), - d => { - return new Promise(resolve => { - resolve(onRequiringModule && onRequiringModule(d)); - }).then( - result => { - // ignore this module id - if (result === false) return; - - // require other module ids instead - if (Array.isArray(result) && result.length) { - result.forEach(dd => _leftOver.add(dd)); - return; - } - - // got full content of this module - if (typeof result === 'string') { - let fakeFilePath = path.resolve(this.project.paths.root, d); - - let ext = path.extname(d).toLowerCase(); - if (!ext || Utils.knownExtensions.indexOf(ext) === -1) { - fakeFilePath += '.js'; - } - // we use '/' as separator even on Windows - // because module id is using '/' as separator - this.addFile({ - path: fakeFilePath, - contents: result - }); - return; - } - - // process normally if result is not recognizable - return this.addMissingDep(d); - }, - // proceed normally after error - err => { - logger.error(err); - return this.addMissingDep(d); - } - ); - } - ).then(() => doTranform(_leftOver)); - } - - return Promise.resolve(); - }; - - logger.info('Tracing files ...'); - - return Promise.resolve() - .then(() => doTranform()) - .then(() => { - if (!onNotBundled) return; - const notBundled = this.items.filter(t => !t.includedIn); - if (notBundled.length) onNotBundled(notBundled); - }) - .catch(e => { - logger.error('Failed to do transforms'); - logger.info(e); - throw e; - }); - } - - write() { - return Promise.all(this.bundles.map(bundle => bundle.write(this.project.build.targets[0]))) - .then(async() => { - for (let i = this.bundles.length; i--; ) { - await this.bundles[i].writeBundlePathsToIndex(this.project.build.targets[0]); - } - }); - } - - getDependencyInclusions() { - return this.bundles.reduce((a, b) => a.concat(b.getDependencyInclusions()), []); - } - - addMissingDep(id) { - const localFilePath = path.resolve(this.project.paths.root, id); - - // load additional local file missed by gulp tasks, - // this could be json/yaml file that user wanted in - // aurelia.json 'text!' plugin - if (Utils.couldMissGulpPreprocess(id) && fs.existsSync(localFilePath)) { - this.addFile({ - path: localFilePath, - contents: fs.readFileSync(localFilePath) - }); - return Promise.resolve(); - } - - return this.addNpmResource(id); - } - - addNpmResource(id) { - // match scoped npm module and normal npm module - const match = id.match(/^((?:@[^/]+\/[^/]+)|(?:[^@][^/]*))(\/.+)?$/); - - if (!match) { - logger.error(`Not valid npm module Id: ${id}`); - return Promise.resolve(); - } - - const nodeId = match[1]; - const resourceId = match[2] && match[2].slice(1); - - const depInclusion = this.getDependencyInclusions().find(di => di.description.name === nodeId); - - if (depInclusion) { - if (resourceId) { - return depInclusion.traceResource(resourceId); - } - - return depInclusion.traceMain(); - } - - let stub = stubModule(nodeId, this.project.paths.root); - if (typeof stub === 'string') { - this.addFile({ - path: path.resolve(this.project.paths.root, nodeId + '.js'), - contents: stub - }); - return Promise.resolve(); - } - - return this.configureDependency(stub || nodeId) - .then(description => { - if (resourceId) { - description.loaderConfig.lazyMain = true; - } - - if (stub) { - logger.info(`Auto stubbing module: ${nodeId}`); - } else { - logger.info(`Auto tracing ${description.banner}`); - } - - return this.configTargetBundle.addDependency(description); - }) - .then(inclusion => { - // now dependencyInclusion is created - // try again to use magical traceResource - if (resourceId) { - return inclusion.traceResource(resourceId); - } - }) - .catch(e => { - logger.error('Failed to add Nodejs module ' + id); - logger.info(e); - // don't stop - }); - } -}; - -function analyzeDependency(packageAnalyzer, dependency) { - if (typeof dependency === 'string') { - return packageAnalyzer.analyze(dependency); - } - - return packageAnalyzer.reverseEngineer(dependency); -} - -function subsume(bundles, item) { - for (let i = 0, ii = bundles.length; i < ii; ++i) { - if (bundles[i].trySubsume(item)) { - return; - } - } - logger.warn(item.path + ' is not captured by any bundle file. You might need to adjust the bundles source matcher in aurelia.json.'); -} - -function normalizeKey(p) { - return path.normalize(p); -} - -function ensurePathsRelativelyFromRoot(p) { - let keys = Object.keys(p); - let original = JSON.stringify(p, null, 2); - let warn = false; - - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - if (key !== 'root' && p[key].indexOf(p.root + '/') === 0) { - warn = true; - p[key] = p[key].slice(p.root.length + 1); - } - // trim off last '/' - if (p[key].endsWith('/')) { - p[key] = p[key].slice(0, -1); - } - } - - if (warn) { - logger.warn('Warning: paths in the "paths" object in aurelia.json must be relative from the root path. Change '); - logger.warn(original); - logger.warn('to: '); - logger.warn(JSON.stringify(p, null, 2)); - } - - return p; -} diff --git a/lib/build/index.js b/lib/build/index.js deleted file mode 100644 index c99e12c94..000000000 --- a/lib/build/index.js +++ /dev/null @@ -1,95 +0,0 @@ -const {Transform} = require('stream'); -const Bundler = require('./bundler').Bundler; -const PackageAnalyzer = require('./package-analyzer').PackageAnalyzer; -const PackageInstaller = require('./package-installer').PackageInstaller; -const cacheDir = require('./utils').cacheDir; -const fs = require('fs'); - -let bundler; -let project; -let isUpdating = false; - -exports.src = function(p) { - if (bundler) { - isUpdating = true; - return Promise.resolve(bundler); - } - - project = p; - return Bundler.create( - project, - new PackageAnalyzer(project), - new PackageInstaller(project) - ).then(b => bundler = b); -}; - -exports.createLoaderCode = function(p) { - const createLoaderCode = require('./loader').createLoaderCode; - project = p || project; - return buildLoaderConfig(project) - .then(() => { - let platform = project.build.targets[0]; - return createLoaderCode(platform, bundler); - }); -}; - -exports.createLoaderConfig = function(p) { - const createLoaderConfig = require('./loader').createLoaderConfig; - project = p || project; - - return buildLoaderConfig(project) - .then(() => { - let platform = project.build.targets[0]; - return createLoaderConfig(platform, bundler); - }); -}; - -exports.bundle = function() { - return new Transform({ - objectMode: true, - transform: function(file, encoding, callback) { - callback(null, capture(file)); - } - }); -}; - -exports.dest = function(opts) { - return bundler.build(opts) - .then(() => bundler.write()); -}; - -exports.clearCache = function() { - // delete cache folder outside of cwd - return fs.promises.rm(cacheDir, { recursive: true, force: true }); -}; - -function buildLoaderConfig(p) { - project = p || project; - let configPromise = Promise.resolve(); - - if (!bundler) { - //If a bundler doesn't exist then chances are we have not run through getting all the files, and therefore the "bundles" will not be complete - configPromise = configPromise.then(() => { - return Bundler.create( - project, - new PackageAnalyzer(project), - new PackageInstaller(project) - ).then(b => bundler = b); - }); - } - - return configPromise.then(() => { - return bundler.build(); - }); -} - -function capture(file) { - // ignore type declaration file generated by TypeScript compiler - if (file.path.endsWith('d.ts')) return; - - if (isUpdating) { - bundler.updateFile(file); - } else { - bundler.addFile(file); - } -} diff --git a/lib/build/loader-plugin.js b/lib/build/loader-plugin.js deleted file mode 100644 index 0fc124515..000000000 --- a/lib/build/loader-plugin.js +++ /dev/null @@ -1,31 +0,0 @@ -const {moduleIdWithPlugin} = require('./utils'); - -exports.LoaderPlugin = class { - constructor(type, config) { - this.type = type; - this.config = config; - this.name = config.name; - this.stub = config.stub; - this.test = config.test ? new RegExp(config.test) : regExpFromExtensions(config.extensions); - } - - matches(filePath) { - return this.test.test(filePath); - } - - transform(moduleId, filePath, contents) { - contents = `define('${this.createModuleId(moduleId)}',[],function(){return ${JSON.stringify(contents)};});`; - return contents; - } - - createModuleId(moduleId) { - // for backward compatibility, use 'text' as plugin name, - // to not break existing app with additional json plugin in aurelia.json - return moduleIdWithPlugin(moduleId, 'text', this.type); - } -}; - -function regExpFromExtensions(extensions) { - return new RegExp('^.*(' + extensions.map(x => '\\' + x).join('|') + ')$'); -} - diff --git a/lib/build/package-analyzer.js b/lib/build/package-analyzer.js deleted file mode 100644 index e435b07ae..000000000 --- a/lib/build/package-analyzer.js +++ /dev/null @@ -1,192 +0,0 @@ -const fs = require('../file-system'); -const path = require('path'); -const DependencyDescription = require('./dependency-description').DependencyDescription; -const logger = require('aurelia-logging').getLogger('PackageAnalyzer'); -const Utils = require('./utils'); - -exports.PackageAnalyzer = class { - constructor(project) { - this.project = project; - } - - analyze(packageName) { - let description = new DependencyDescription(packageName, 'npm'); - - return loadPackageMetadata(this.project, description) - .then(() => { - if (!description.metadataLocation) { - throw new Error(`Unable to find package metadata (package.json) of ${description.name}`); - } - }) - .then(() => determineLoaderConfig(this.project, description)) - .then(() => description); - } - - reverseEngineer(loaderConfig) { - loaderConfig = JSON.parse(JSON.stringify(loaderConfig)); - let description = new DependencyDescription(loaderConfig.name); - description.loaderConfig = loaderConfig; - - if (!loaderConfig.packageRoot && (!loaderConfig.path || loaderConfig.path.indexOf('node_modules') !== -1)) { - description.source = 'npm'; - } else { - description.source = 'custom'; - if (!loaderConfig.packageRoot) { - fillUpPackageRoot(this.project, description); - } - } - - return loadPackageMetadata(this.project, description) - .then(() => { - if (!loaderConfig.path) { - // fillup main and path - determineLoaderConfig(this.project, description); - } else { - if (!loaderConfig.main) { - if (description.source === 'custom' && loaderConfig.path === loaderConfig.packageRoot) { - // fillup main and path - determineLoaderConfig(this.project, description); - } else { - const fullPath = path.resolve(this.project.paths.root, loaderConfig.path); - if (fullPath === description.location) { - // fillup main and path - determineLoaderConfig(this.project, description); - return; - } - - // break single path into main and dir - let pathParts = path.parse(fullPath); - - // when path is node_modules/package/foo/bar - // set path to node_modules/package - // set main to foo/bar - loaderConfig.path = path.relative(this.project.paths.root, description.location).replace(/\\/g, '/'); - - if (pathParts.dir.length > description.location.length + 1) { - const main = path.join(pathParts.dir.slice(description.location.length + 1), Utils.removeJsExtension(pathParts.base)); - loaderConfig.main = main.replace(/\\/g, '/'); - } else if (pathParts.dir.length === description.location.length) { - loaderConfig.main = Utils.removeJsExtension(pathParts.base).replace(/\\/g, '/'); - } else { - throw new Error(`Path: "${loaderConfig.path}" is not in: ${description.location}`); - } - } - } else { - loaderConfig.main = Utils.removeJsExtension(loaderConfig.main).replace(/\\/g, '/'); - } - } - }) - .then(() => description); - } -}; - -function fillUpPackageRoot(project, description) { - let _path = description.loaderConfig.path; - - let ext = path.extname(_path).toLowerCase(); - if (!ext || Utils.knownExtensions.indexOf(ext) === -1) { - // main file could be non-js file like css/font-awesome.css - _path += '.js'; - } - - if (fs.isFile(path.resolve(project.paths.root, _path))) { - description.loaderConfig.packageRoot = path.dirname(description.loaderConfig.path).replace(/\\/g, '/'); - } - - if (!description.loaderConfig.packageRoot) { - description.loaderConfig.packageRoot = description.loaderConfig.path; - } -} - -function loadPackageMetadata(project, description) { - return setLocation(project, description) - .then(() => { - if (description.metadataLocation) { - return fs.readFile(description.metadataLocation).then(data => { - description.metadata = JSON.parse(data.toString()); - }); - } - }) - .catch(e => { - logger.error(`Unable to load package metadata (package.json) of ${description.name}:`); - logger.info(e); - }); -} - -// loaderConfig.path is simplified when use didn't provide explicit config. -// In auto traced nodejs package, loaderConfig.path always matches description.location. -// We then use auto-generated moduleId aliases in dependency-inclusion to make AMD -// module system happy. -function determineLoaderConfig(project, description) { - let location = path.resolve(description.location); - let mainPath = Utils.nodejsLoad(location); - - if (!description.loaderConfig) { - description.loaderConfig = {name: description.name}; - } - - description.loaderConfig.path = path.relative(project.paths.root, description.location).replace(/\\/g, '/'); - - if (mainPath) { - description.loaderConfig.main = Utils.removeJsExtension(mainPath.slice(location.length + 1).replace(/\\/g, '/')); - } else { - logger.warn(`The "${description.name}" package has no valid main file, fall back to index.js.`); - description.loaderConfig.main = 'index'; - } -} - -function setLocation(project, description) { - switch (description.source) { - case 'npm': - return getPackageFolder(project, description) - .then(packageFolder => { - description.location = packageFolder; - - return tryFindMetadata(project, description); - }); - case 'custom': - description.location = path.resolve(project.paths.root, description.loaderConfig.packageRoot); - - return tryFindMetadata(project, description); - default: - return Promise.reject(`The package source "${description.source}" is not supported.`); - } -} - -function tryFindMetadata(project, description) { - return fs.stat(path.join(description.location, 'package.json')) - .then(() => description.metadataLocation = path.join(description.location, 'package.json')) - .catch(() => {}); -} - -function getPackageFolder(project, description) { - if (!description.loaderConfig || !description.loaderConfig.path) { - return new Promise(resolve => { - resolve(Utils.resolvePackagePath(description.name)); - }); - } - - return lookupPackageFolderRelativeStrategy(project.paths.root, description.loaderConfig.path); -} - -// Looks for the node_modules folder from the root path of aurelia -// with the defined loaderConfig. -function lookupPackageFolderRelativeStrategy(root, relativePath) { - let pathParts = relativePath.replace(/\\/g, '/').split('/'); - let packageFolder = ''; - let stopOnNext = false; - - for (let i = 0; i < pathParts.length; ++i) { - let part = pathParts[i]; - - packageFolder = path.join(packageFolder, part); - - if (stopOnNext && !part.startsWith('@')) { - break; - } else if (part === 'node_modules') { - stopOnNext = true; - } - } - - return Promise.resolve(path.resolve(root, packageFolder)); -} diff --git a/lib/cli.js b/lib/cli.js deleted file mode 100644 index b23680f4a..000000000 --- a/lib/cli.js +++ /dev/null @@ -1,132 +0,0 @@ -const path = require('path'); -const Container = require('aurelia-dependency-injection').Container; -const fs = require('./file-system'); -const ui = require('./ui'); -const Project = require('./project').Project; -const CLIOptions = require('./cli-options').CLIOptions; -const LogManager = require('aurelia-logging'); -const Logger = require('./logger').Logger; - -exports.CLI = class { - constructor(options) { - this.options = options || new CLIOptions(); - this.container = new Container(); - this.ui = new ui.ConsoleUI(this.options); - this.configureContainer(); - this.logger = LogManager.getLogger('CLI'); - } - - // Note: cannot use this.logger.error inside run() - // because logger is not configured yet! - // this.logger.error prints nothing in run(), - // directly use this.ui.log. - run(cmd, args) { - const version = `${this.options.runningGlobally ? 'Global' : 'Local'} aurelia-cli v${require('../package.json').version}`; - - if (cmd === '--version' || cmd === '-v') { - return this.ui.log(version); - } - - return (cmd === 'new' ? Promise.resolve() : this._establishProject()) - .then(project => { - this.ui.log(version); - - if (project && this.options.runningLocally) { - this.project = project; - this.container.registerInstance(Project, project); - } else if (project && this.options.runningGlobally) { - this.ui.log('The current directory is likely an Aurelia-CLI project, but no local installation of Aurelia-CLI could be found. ' + - '(Do you need to restore node modules using npm install?)'); - return Promise.resolve(); - } else if (!project && this.options.runningLocally) { - this.ui.log('It appears that the Aurelia CLI is running locally from ' + __dirname + '. However, no project directory could be found. ' + - 'The Aurelia CLI has to be installed globally (npm install -g aurelia-cli) and locally (npm install aurelia-cli) in an Aurelia CLI project directory'); - return Promise.resolve(); - } - - return this.createCommand(cmd, args) - .then((command) => command.execute(args)); - }); - } - - configureLogger() { - LogManager.addAppender(this.container.get(Logger)); - let level = CLIOptions.hasFlag('debug') ? LogManager.logLevel.debug : LogManager.logLevel.info; - LogManager.setLevel(level); - } - - configureContainer() { - this.container.registerInstance(CLIOptions, this.options); - this.container.registerInstance(ui.UI, this.ui); - } - - createCommand(commandText, commandArgs) { - return new Promise(resolve => { - if (!commandText) { - resolve(this.createHelpCommand()); - return; - } - - let parts = commandText.split(':'); - let commandModule = parts[0]; - let commandName = parts[1] || 'default'; - - try { - let alias = require('./commands/alias.json')[commandModule]; - let found = this.container.get(require(`./commands/${alias || commandModule}/command`)); - Object.assign(this.options, { args: commandArgs }); - // need to configure logger after getting args - this.configureLogger(); - resolve(found); - } catch { - if (this.project) { - this.project.resolveTask(commandModule).then(taskPath => { - if (taskPath) { - Object.assign(this.options, { - taskPath: taskPath, - args: commandArgs, - commandName: commandName - }); - // need to configure logger after getting args - this.configureLogger(); - - resolve(this.container.get(require('./commands/gulp'))); - } else { - this.ui.log(`Invalid Command: ${commandText}`); - resolve(this.createHelpCommand()); - } - }); - } else { - this.ui.log(`Invalid Command: ${commandText}`); - resolve(this.createHelpCommand()); - } - } - }); - } - - createHelpCommand() { - return this.container.get(require('./commands/help/command')); - } - - _establishProject() { - return determineWorkingDirectory(process.cwd()) - .then(dir => dir ? Project.establish(dir) : this.ui.log('No Aurelia project found.')); - } -}; - -function determineWorkingDirectory(dir) { - let parent = path.join(dir, '..'); - - if (parent === dir) { - return Promise.resolve(); // resolve to nothing - } - - return fs.stat(path.join(dir, 'aurelia_project')) - .then(() => dir) - .catch(() => determineWorkingDirectory(parent)); -} - -process.on('unhandledRejection', (reason) => { - console.log('Uncaught promise rejection:'); - console.log(reason); -}); diff --git a/lib/commands/config/command.js b/lib/commands/config/command.js deleted file mode 100644 index 6f6e19ab2..000000000 --- a/lib/commands/config/command.js +++ /dev/null @@ -1,44 +0,0 @@ -const UI = require('../../ui').UI; -const CLIOptions = require('../../cli-options').CLIOptions; -const Container = require('aurelia-dependency-injection').Container; -const os = require('os'); - -const Configuration = require('./configuration'); -const ConfigurationUtilities = require('./util'); - -module.exports = class { - static inject() { return [Container, UI, CLIOptions]; } - - constructor(container, ui, options) { - this.container = container; - this.ui = ui; - this.options = options; - } - - execute(args) { - this.config = new Configuration(this.options); - this.util = new ConfigurationUtilities(this.options, args); - let key = this.util.getArg(0) || ''; - let value = this.util.getValue(this.util.getArg(1)); - let save = !CLIOptions.hasFlag('no-save'); - let backup = !CLIOptions.hasFlag('no-backup'); - let action = this.util.getAction(value); - - this.displayInfo(`Performing configuration action '${action}' on '${key}'`, (value ? `with '${value}'` : '')); - this.displayInfo(this.config.execute(action, key, value)); - - if (action !== 'get') { - if (save) { - this.config.save(backup).then((name) => { - this.displayInfo('Configuration saved. ' + (backup ? `Backup file '${name}' created.` : 'No backup file was created.')); - }); - } else { - this.displayInfo(`Action was '${action}', but no save was performed!`); - } - } - } - - displayInfo(message) { - return this.ui.log(message + os.EOL); - } -}; diff --git a/lib/commands/config/configuration.js b/lib/commands/config/configuration.js deleted file mode 100644 index 36c27e827..000000000 --- a/lib/commands/config/configuration.js +++ /dev/null @@ -1,150 +0,0 @@ -const os = require('os'); -const copySync = require('../../file-system').copySync; -const readFileSync = require('../../file-system').readFileSync; -const writeFile = require('../../file-system').writeFile; - -class Configuration { - constructor(options) { - this.options = options; - this.aureliaJsonPath = options.originalBaseDir + '/aurelia_project/aurelia.json'; - this.project = JSON.parse(readFileSync(this.aureliaJsonPath)); - } - - configEntry(key, createKey) { - let entry = this.project; - let keys = key.split('.'); - - if (!keys[0]) { - return entry; - } - - while (entry && keys.length) { - key = this.parsedKey(keys.shift()); - if (entry[key.value] === undefined || entry[key.value] === null) { - if (!createKey) { - return entry[key.value]; - } - let checkKey = this.parsedKey(keys.length ? keys[0] : createKey); - if (checkKey.index) { - entry[key.value] = []; - } else if (checkKey.key) { - entry[key.value] = {}; - } - } - entry = entry[key.value]; - - // TODO: Add support for finding objects based on input values? - // TODO: Add support for finding string in array? - } - - return entry; - } - - parsedKey(key) { - if (/\[(\d+)\]/.test(key)) { - return { index: true, key: false, value: +(RegExp.$1) }; - } - - return { index: false, key: true, value: key }; - } - - normalizeKey(key) { - const re = /([^.])\[/; - while (re.exec(key)) { - key = key.replace(re, RegExp.$1 + '.['); - } - - let keys = key.split('.'); - for (let i = 0; i < keys.length; i++) { - if (/\[(\d+)\]/.test(keys[i])) { - // console.log(`keys[${i}] is index: ${keys[i]}`); - } else if (/\[(.+)\]/.test(keys[i])) { - // console.log(`keys[${i}] is indexed name: ${keys[i]}`); - keys[i] = RegExp.$1; - } else { - // console.log(`keys[${i}] is name: ${keys[i]}`); - } - } - - return keys.join('.'); - } - - execute(action, key, value) { - let originalKey = key; - - key = this.normalizeKey(key); - - if (action === 'get') { - return `Configuration key '${key}' is:` + os.EOL + JSON.stringify(this.configEntry(key), null, 2); - } - - let keys = key.split('.'); - key = this.parsedKey(keys.pop()); - let parent = keys.join('.'); - - if (action === 'set') { - let entry = this.configEntry(parent, key.value); - if (entry) { - entry[key.value] = value; - } else { - console.log('Failed to set property', this.normalizeKey(originalKey), '!'); - } - } else if (action === 'clear') { - let entry = this.configEntry(parent); - if (entry && (key.value in entry)) { - delete entry[key.value]; - } else { - console.log('No property', this.normalizeKey(originalKey), 'to clear!'); - } - } else if (action === 'add') { - let entry = this.configEntry(parent, key.value); - if (Array.isArray(entry[key.value]) && !Array.isArray(value)) { - value = [value]; - } if (Array.isArray(value) && !Array.isArray(entry[key.value])) { - entry[key.value] = (entry ? [entry[key.value]] : []); - } if (Array.isArray(value)) { - entry[key.value].push.apply(entry[key.value], value); - } else if (Object(value) === value) { - if (Object(entry[key.value]) !== entry[key.value]) { - entry[key.value] = {}; - } - Object.assign(entry[key.value], value); - } else { - entry[key.value] = value; - } - } else if (action === 'remove') { - let entry = this.configEntry(parent); - - if (Array.isArray(entry) && key.index) { - entry.splice(key.value, 1); - } else if (Object(entry) === entry && key.key) { - delete entry[key.value]; - } else if (!entry) { - console.log('No property', this.normalizeKey(originalKey), 'to remove from!'); - } else { - console.log("Can't remove value from", entry[key.value], '!'); - } - } - key = this.normalizeKey(originalKey); - return `Configuration key '${key}' is now:` + os.EOL + JSON.stringify(this.configEntry(key), null, 2); - } - - save(backup) { - if (backup === undefined) backup = true; - - const unique = new Date().toISOString().replace(/[T\D]/g, ''); - let arr = this.aureliaJsonPath.split(/[\\/]/); - const name = arr.pop(); - const path = arr.join('/'); - const bak = `${name}.${unique}.bak`; - - if (backup) { - copySync(this.aureliaJsonPath, [path, bak].join('/')); - } - - return writeFile(this.aureliaJsonPath, JSON.stringify(this.project, null, 2), 'utf8') - .then(() => { return bak; }); - } -} - -module.exports = Configuration; diff --git a/lib/commands/generate/command.js b/lib/commands/generate/command.js deleted file mode 100644 index 36361954d..000000000 --- a/lib/commands/generate/command.js +++ /dev/null @@ -1,53 +0,0 @@ -const UI = require('../../ui').UI; -const CLIOptions = require('../../cli-options').CLIOptions; -const Container = require('aurelia-dependency-injection').Container; -const Project = require('../../project').Project; -const string = require('../../string'); -const os = require('os'); - -module.exports = class { - static inject() { return [Container, UI, CLIOptions, Project]; } - - constructor(container, ui, options, project) { - this.container = container; - this.ui = ui; - this.options = options; - this.project = project; - } - - execute(args) { - if (args.length < 1) { - return this.displayGeneratorInfo('No Generator Specified. Available Generators:'); - } - - this.project.installTranspiler(); - - return this.project.resolveGenerator(args[0]).then(generatorPath => { - Object.assign(this.options, { - generatorPath: generatorPath, - args: args.slice(1) - }); - - if (generatorPath) { - let generator = this.project.getExport(require(generatorPath)); - - if (generator.inject) { - generator = this.container.get(generator); - generator = generator.execute.bind(generator); - } - - return generator(); - } - - return this.displayGeneratorInfo(`Invalid Generator: ${args[0]}. Available Generators:`); - }); - } - - displayGeneratorInfo(message) { - return this.ui.displayLogo() - .then(() => this.ui.log(message + os.EOL)) - .then(() => this.project.getGeneratorMetadata()) - .then(metadata => string.buildFromMetadata(metadata, this.ui.getWidth())) - .then(str => this.ui.log(str)); - } -}; diff --git a/lib/commands/gulp.js b/lib/commands/gulp.js deleted file mode 100644 index 92b202e38..000000000 --- a/lib/commands/gulp.js +++ /dev/null @@ -1,73 +0,0 @@ -const UI = require('../ui').UI; -const CLIOptions = require('../cli-options').CLIOptions; -const Container = require('aurelia-dependency-injection').Container; -const Project = require('../project').Project; - -module.exports = class { - static inject() { return [Container, UI, CLIOptions, Project]; } - - constructor(container, ui, options, project) { - this.container = container; - this.ui = ui; - this.options = options; - this.project = project; - } - - execute() { - return new Promise((resolve, reject) => { - const gulp = require('gulp'); - this.connectLogging(gulp); - - this.project.installTranspiler(); - - makeInjectable(gulp, 'series', this.container); - makeInjectable(gulp, 'parallel', this.container); - - process.nextTick(() => { - let task = this.project.getExport(require(this.options.taskPath), this.options.commandName); - - gulp.series(task)(error => { - if (error) reject(error); - else resolve(); - }); - }); - }); - } - - connectLogging(gulp) { - gulp.on('start', e => { - if (e.name[0] === '<') return; - this.ui.log(`Starting '${e.name}'...`); - }); - - gulp.on('stop', e => { - if (e.name[0] === '<') return; - this.ui.log(`Finished '${e.name}'`); - }); - - gulp.on('error', e => this.ui.log(e)); - } -}; - -function makeInjectable(gulp, name, container) { - let original = gulp[name]; - - gulp[name] = function() { - let args = new Array(arguments.length); - - for (let i = 0, ii = arguments.length; i < ii; ++i) { - let task = arguments[i]; - - if (task.inject) { - let taskName = task.name; - task = container.get(task); - task = task.execute.bind(task); - task.displayName = taskName; - } - - args[i] = task; - } - - return original.apply(gulp, args); - }; -} diff --git a/lib/commands/help/command.js b/lib/commands/help/command.js deleted file mode 100644 index caf37af3d..000000000 --- a/lib/commands/help/command.js +++ /dev/null @@ -1,45 +0,0 @@ -const UI = require('../../ui').UI; -const CLIOptions = require('../../cli-options').CLIOptions; -const Optional = require('aurelia-dependency-injection').Optional; -const Project = require('../../project').Project; -const string = require('../../string'); - -module.exports = class { - static inject() { return [CLIOptions, UI, Optional.of(Project)]; } - - constructor(options, ui, project) { - this.options = options; - this.ui = ui; - this.project = project; - } - - execute() { - return this.ui.displayLogo() - .then(() => { - if (this.options.runningGlobally) { - return this.getGlobalCommandText(); - } - - return this.getLocalCommandText(); - }).then(text => this.ui.log(text)); - } - - getGlobalCommandText() { - return string.buildFromMetadata([ - require('../new/command.json'), - require('./command.json') - ], this.ui.getWidth()); - } - - getLocalCommandText() { - const commands = [ - require('../generate/command.json'), - require('../config/command.json'), - require('./command.json') - ]; - - return this.project.getTaskMetadata().then(metadata => { - return string.buildFromMetadata(metadata.concat(commands), this.ui.getWidth()); - }); - } -}; diff --git a/lib/file-system.js b/lib/file-system.js deleted file mode 100644 index 5187903b6..000000000 --- a/lib/file-system.js +++ /dev/null @@ -1,133 +0,0 @@ -const fs = require('fs'); -const nodePath = require('path'); - -exports.fs = fs; - -/** - * @deprecated - * fs.exists() is deprecated. - * See https://nodejs.org/api/fs.html#fs_fs_exists_path_callback. - * Functions using it can also not be properly tested. - */ -exports.exists = function(path) { - return new Promise(resolve => fs.exists(path, resolve)); -}; - -exports.stat = function(path) { - return new Promise((resolve, reject) => { - fs.stat(path, (error, stats) => { - if (error) reject(error); - else resolve(stats); - }); - }); -}; - -exports.existsSync = function(path) { - return fs.existsSync(path); -}; - -exports.mkdir = function(path) { - return new Promise((resolve, reject) => { - fs.mkdir(path, error => { - if (error) reject(error); - else resolve(); - }); - }); -}; - -exports.mkdirp = function(path) { - return new Promise((resolve, reject) => { - fs.mkdir(path, {recursive: true}, error => { - if (error) reject(error); - else resolve(); - }); - }); -}; - -exports.readdir = function(path) { - return new Promise((resolve, reject) => { - fs.readdir(path, (error, files) => { - if (error) reject(error); - else resolve(files); - }); - }); -}; - -exports.appendFile = function(path, text, cb) { - fs.appendFile(path, text, cb); -}; - -exports.readdirSync = function(path) { - return fs.readdirSync(path); -}; - -exports.readFile = function(path, encoding) { - if (encoding !== null) { - encoding = encoding || 'utf8'; - } - - return new Promise((resolve, reject) => { - fs.readFile(path, encoding, (error, data) => { - if (error) reject(error); - else resolve(data); - }); - }); -}; - -exports.readFileSync = fs.readFileSync; - -exports.readFileSync = function(path, encoding) { - if (encoding !== null) { - encoding = encoding || 'utf8'; - } - - return fs.readFileSync(path, encoding); -}; - -exports.copySync = function(sourceFile, targetFile) { - fs.writeFileSync(targetFile, fs.readFileSync(sourceFile)); -}; - -exports.resolve = function(path) { - return nodePath.resolve(path); -}; - -exports.join = function() { - return nodePath.join.apply(this, Array.prototype.slice.call(arguments)); -}; - -exports.statSync = function(path) { - return fs.statSync(path); -}; - -exports.isFile = function(path) { - try { - return fs.statSync(path).isFile(); - } catch { - // ignore - return false; - } -}; - -exports.isDirectory = function(path) { - try { - return fs.statSync(path).isDirectory(); - } catch { - // ignore - return false; - } -}; - -exports.writeFile = function(path, content, encoding) { - return new Promise((resolve, reject) => { - fs.mkdir(nodePath.dirname(path), {recursive: true}, err => { - if (err) reject(err); - else { - fs.writeFile(path, content, encoding || 'utf8', error => { - if (error) reject(error); - else resolve(); - }); - } - }); - }); -}; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index ef1afe2e1..000000000 --- a/lib/index.js +++ /dev/null @@ -1,12 +0,0 @@ -require('aurelia-polyfills'); - -exports.CLI = require('./cli').CLI; -exports.CLIOptions = require('./cli-options').CLIOptions; -exports.UI = require('./ui').UI; -exports.Project = require('./project').Project; -exports.ProjectItem = require('./project-item').ProjectItem; -exports.build = require('./build'); -exports.Configuration = require('./configuration').Configuration; -exports.reportWebpackReadiness = require('./build/webpack-reporter'); -exports.NPM = require('./package-managers/npm').NPM; -exports.Yarn = require('./package-managers/yarn').Yarn; diff --git a/lib/logger.js b/lib/logger.js deleted file mode 100644 index bf1abb716..000000000 --- a/lib/logger.js +++ /dev/null @@ -1,37 +0,0 @@ -const UI = require('./ui').UI; -const c = require('ansi-colors'); - -exports.Logger = class { - static inject() { return [UI]; } - - constructor(ui) { - this.ui = ui; - } - - debug(logger, message) { - this.log(logger, c.bold('DEBUG'), message, arguments); - } - - info(logger, message) { - this.log(logger, c.bold('INFO'), message, arguments); - } - - warn(logger, message) { - this.log(logger, c.bgYellow('WARN'), message, arguments); - } - - error(logger, message) { - this.log(logger, c.bgRed('ERROR'), message, arguments); - } - - log(logger, level, message, rest) { - let msg = `${level} [${logger.id}] ${message}`; - let args = Array.prototype.slice.call(rest, 2); - - if (args.length > 0) { - msg += ` ${args.map(x => JSON.stringify(x)).join(' ')}`; - } - - this.ui.log(msg); - } -}; diff --git a/lib/package-managers/base-package-manager.js b/lib/package-managers/base-package-manager.js deleted file mode 100644 index ad46d8c03..000000000 --- a/lib/package-managers/base-package-manager.js +++ /dev/null @@ -1,44 +0,0 @@ -const {spawn} = require('child_process'); -const npmWhich = require('npm-which'); -const isWindows = process.platform === "win32"; - -exports.BasePackageManager = class { - constructor(executableName) { - this.executableName = executableName; - } - - install(packages = [], workingDirectory = process.cwd(), command = 'install') { - return this.run(command, packages, workingDirectory); - } - - run(command, args = [], workingDirectory = process.cwd()) { - let executable = this.getExecutablePath(workingDirectory); - if (isWindows) { - executable = JSON.stringify(executable); // Add quotes around path - } - - return new Promise((resolve, reject) => { - this.proc = spawn( - executable, - [command, ...args], - { stdio: "inherit", cwd: workingDirectory, shell: isWindows } - ) - .on('close', resolve) - .on('error', reject); - }); - } - - getExecutablePath(directory) { - try { - return npmWhich(directory).sync(this.executableName); - } catch { - return null; - } - } - - isAvailable(directory) { - return !!this.getExecutablePath(directory); - } -}; - -exports.default = exports.BasePackageManager; diff --git a/lib/package-managers/npm.js b/lib/package-managers/npm.js deleted file mode 100644 index 95469d197..000000000 --- a/lib/package-managers/npm.js +++ /dev/null @@ -1,9 +0,0 @@ -const BasePackageManager = require('./base-package-manager').default; - -exports.NPM = class extends BasePackageManager { - constructor() { - super('npm'); - } -}; - -exports.default = exports.NPM; diff --git a/lib/package-managers/yarn.js b/lib/package-managers/yarn.js deleted file mode 100644 index 6194db2a1..000000000 --- a/lib/package-managers/yarn.js +++ /dev/null @@ -1,13 +0,0 @@ -const BasePackageManager = require('./base-package-manager').default; - -exports.Yarn = class extends BasePackageManager { - constructor() { - super('yarn'); - } - - install(packages = [], workingDirectory = process.cwd()) { - return super.install(packages, workingDirectory, !packages.length ? 'install' : 'add'); - } -}; - -exports.default = exports.Yarn; diff --git a/lib/project-item.js b/lib/project-item.js deleted file mode 100644 index e119e3bd8..000000000 --- a/lib/project-item.js +++ /dev/null @@ -1,86 +0,0 @@ -const path = require('path'); -const fs = require('./file-system'); -const Utils = require('./build/utils'); - -// Legacy code, kept only for supporting `au generate` -exports.ProjectItem = class { - constructor(name, isDirectory) { - this.name = name; - this.isDirectory = !!isDirectory; - } - - get children() { - if (!this._children) { - this._children = []; - } - - return this._children; - } - - add() { - if (!this.isDirectory) { - throw new Error('You cannot add items to a non-directory.'); - } - - for (let i = 0; i < arguments.length; ++i) { - let child = arguments[i]; - - if (this.children.indexOf(child) !== -1) { - continue; - } - - child.parent = this; - this.children.push(child); - } - - return this; - } - - calculateRelativePath(fromLocation) { - if (this === fromLocation) { - return ''; - } - - let parentRelativePath = (this.parent && this.parent !== fromLocation) - ? this.parent.calculateRelativePath(fromLocation) - : ''; - - return path.posix.join(parentRelativePath, this.name); - } - - create(relativeTo) { - let fullPath = relativeTo ? this.calculateRelativePath(relativeTo) : this.name; - - // Skip empty folder - if (this.isDirectory && this.children.length) { - return fs.stat(fullPath) - .then(result => result) - .catch(() => fs.mkdir(fullPath)) - .then(() => Utils.runSequentially(this.children, child => child.create(fullPath))); - } - - if (this.text) { - return fs.writeFile(fullPath, this.text); - } - - return Promise.resolve(); - } - - - setText(text) { - this.text = text; - return this; - } - - getText() { - return this.text; - } - - static text(name, text) { - return new exports.ProjectItem(name, false).setText(text); - } - - static directory(p) { - return new exports.ProjectItem(p, true); - } -}; diff --git a/lib/project.js b/lib/project.js deleted file mode 100644 index a906f8a27..000000000 --- a/lib/project.js +++ /dev/null @@ -1,142 +0,0 @@ -const path = require('path'); -const fs = require('./file-system'); -const _ = require('lodash'); -const ProjectItem = require('./project-item').ProjectItem; - -exports.Project = class { - static establish(dir) { - process.chdir(dir); - - return fs.readFile(path.join(dir, 'aurelia_project', 'aurelia.json')).then(model => { - return fs.readFile(path.join(dir, 'package.json')).then(pack => { - return new exports.Project(dir, JSON.parse(model.toString()), JSON.parse(pack.toString())); - }); - }); - } - - constructor(directory, model, pack) { - this.directory = directory; - this.model = model; - this.package = pack; - this.taskDirectory = path.join(directory, 'aurelia_project/tasks'); - this.generatorDirectory = path.join(directory, 'aurelia_project/generators'); - this.aureliaJSONPath = path.join(directory, 'aurelia_project', 'aurelia.json'); - - this.locations = Object.keys(model.paths).map(key => { - this[key] = ProjectItem.directory(model.paths[key]); - - if (key !== 'root') { - this[key] = ProjectItem.directory(model.paths[key]); - this[key].parent = this.root; - } - - return this[key]; - }); - this.locations.push(this.generators = ProjectItem.directory('aurelia_project/generators')); - this.locations.push(this.tasks = ProjectItem.directory('aurelia_project/tasks')); - } - - // Legacy code. This code and those ProjectItem.directory above, were kept only - // for supporting `au generate` - commitChanges() { - return Promise.all(this.locations.map(x => x.create(this.directory))); - } - - makeFileName(name) { - return _.kebabCase(name); - } - - makeClassName(name) { - const camel = _.camelCase(name); - return camel.slice(0, 1).toUpperCase() + camel.slice(1); - } - - makeFunctionName(name) { - return _.camelCase(name); - } - - installTranspiler() { - switch (this.model.transpiler.id) { - case 'babel': - installBabel.call(this); - break; - case 'typescript': - installTypeScript(); - break; - default: - throw new Error(`${this.model.transpiler.id} is not a supported transpiler.`); - } - } - - getExport(m, name) { - return name ? m[name] : m.default; - } - - getGeneratorMetadata() { - return getMetadata(this.generatorDirectory); - } - - getTaskMetadata() { - return getMetadata(this.taskDirectory); - } - - resolveGenerator(name) { - let potential = path.join(this.generatorDirectory, `${name}${this.model.transpiler.fileExtension}`); - return fs.stat(potential).then(() => potential).catch(() => null); - } - - resolveTask(name) { - let potential = path.join(this.taskDirectory, `${name}${this.model.transpiler.fileExtension}`); - return fs.stat(potential).then(() => potential).catch(() => null); - } -}; - -function getMetadata(dir) { - return fs.readdir(dir).then(files => { - return Promise.all( - files - .sort() - .map(file => path.join(dir, file)) - .filter(file => path.extname(file) === '.json') - .map(file => fs.readFile(file).then(data => JSON.parse(data.toString()))) - ); - }); -} - -function installBabel() { - require('@babel/register')({ - babelrc: false, - configFile: false, - plugins: [ - ['@babel/plugin-proposal-decorators', { legacy: true }], - ['@babel/plugin-transform-class-properties', { loose: true }], - ['@babel/plugin-transform-modules-commonjs', {loose: true}] - ], - only: [/aurelia_project/] - }); -} - -function installTypeScript() { - let ts = require('typescript'); - - let json = require.extensions['.json']; - delete require.extensions['.json']; - - require.extensions['.ts'] = function(module, filename) { - let source = fs.readFileSync(filename); - let result = ts.transpile(source, { - module: ts.ModuleKind.CommonJS, - declaration: false, - noImplicitAny: false, - noResolve: true, - removeComments: true, - noLib: false, - emitDecoratorMetadata: true, - experimentalDecorators: true - }); - - return module._compile(result, filename); - }; - - require.extensions['.json'] = json; -} diff --git a/package.json b/package.json index 79b9173aa..41317eb01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aurelia-cli", - "version": "3.0.4", + "version": "3.1.0-beta.1", "description": "The command line tooling for Aurelia.", "keywords": [ "aurelia", @@ -16,21 +16,29 @@ "aurelia": "bin/aurelia-cli.js", "au": "bin/aurelia-cli.js" }, + "type": "commonjs", "scripts": { - "lint": "eslint lib spec", - "pretest": "npm run lint", + "prebuild": "node build/clean-dir.mjs && npm run lint", + "build": "tsc", + "postbuild": "node build/copy-files.mjs", + "lint": "eslint src", + "pretest": "npm run lint && npm run build", "test": "jasmine", "coverage": "c8 jasmine", "test:watch": "nodemon -x 'npm test'", + "prepare": "npm run build", "preversion": "npm test", "version": "standard-changelog && git add CHANGELOG.md", "postversion": "git push && git push --tags && npm publish" }, "license": "MIT", "author": "Rob Eisenberg (http://robeisenberg.com/)", - "main": "lib/index.js", + "main": "dist/index.js", + "typings": "dist/index.d.ts", "files": [ "bin", + "build", + "dist", "lib" ], "repository": { @@ -81,21 +89,33 @@ "string_decoder": "^1.3.0", "terser": "^5.36.0", "timers-browserify": "^2.0.12", + "tslib": "^2.8.1", "tty-browserify": "0.0.1", - "typescript": "^5.6.3", + "typescript": "^5.8.3", "url": "^0.11.4", "util": "^0.12.5", "vm-browserify": "^1.1.2" }, "devDependencies": { + "@eslint/js": "^9.24.0", + "@stylistic/eslint-plugin": "^4.2.0", + "@types/convert-source-map": "^2.0.3", + "@types/gulp": "^4.0.17", + "@types/jasmine": "^5.1.7", + "@types/lodash": "^4.17.16", + "@types/map-stream": "^0.0.3", "@types/node": "^22.8.1", + "@types/resolve": "^1.20.6", + "@types/vinyl-fs": "^3.0.5", "c8": "^10.1.2", - "eslint": "^9.13.0", + "cpx": "^1.5.0", + "eslint": "^9.24.0", "globals": "^15.11.0", "jasmine": "^5.4.0", "jasmine-spec-reporter": "^7.0.0", "nodemon": "^3.1.7", "standard-changelog": "^6.0.0", + "typescript-eslint": "^8.29.0", "yargs": "^17.7.2" } } diff --git a/spec/helpers/setup.js b/spec/helpers/setup.js new file mode 100644 index 000000000..4f40e333e --- /dev/null +++ b/spec/helpers/setup.js @@ -0,0 +1,8 @@ +const isDebugMode = typeof v8debug === 'object' || + /--debug|--inspect/.test(process.execArgv.join(' ')) || + process.env.VSCODE_INSPECTOR_OPTIONS; + +if (isDebugMode) { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 1000; // 1 minute timeout in debug mode + console.log('Debug mode detected - extended timeouts enabled'); +} diff --git a/spec/jsconfig.json b/spec/jsconfig.json new file mode 100644 index 000000000..77fe27c7c --- /dev/null +++ b/spec/jsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": null, + "rootDirs": ["../src", "./"], + "allowJs": true, + "module": "CommonJS", + "types": ["node", "jasmine"] + }, + "include": [ + "./**/*.js" + ] +} diff --git a/spec/lib/build/ast-matcher.spec.js b/spec/lib/build/ast-matcher.spec.js index cc9671000..4e5cceb8a 100644 --- a/spec/lib/build/ast-matcher.spec.js +++ b/spec/lib/build/ast-matcher.spec.js @@ -1,6 +1,6 @@ const meriyah = require('meriyah'); -const astm = require('../../../lib/build/ast-matcher'); +const astm = require('../../../dist/build/ast-matcher'); const extract = astm.extract; const compilePattern = astm.compilePattern; const astMatcher = astm.astMatcher; diff --git a/spec/lib/build/bundle.spec.js b/spec/lib/build/bundle.spec.js index df972f420..caf2839bb 100644 --- a/spec/lib/build/bundle.spec.js +++ b/spec/lib/build/bundle.spec.js @@ -1,9 +1,10 @@ const BundlerMock = require('../../mocks/bundler'); -const Bundle = require('../../../lib/build/bundle').Bundle; +const Bundle = require('../../../dist/build/bundle').Bundle; +const _calculateRelativeSourceMapsRoot = require('../../../dist/build/bundle')._calculateRelativeSourceMapsRoot; const CLIOptionsMock = require('../../mocks/cli-options'); -const DependencyDescription = require('../../../lib/build/dependency-description').DependencyDescription; -const SourceInclusion = require('../../../lib/build/source-inclusion').SourceInclusion; -const DependencyInclusion = require('../../../lib/build/dependency-inclusion').DependencyInclusion; +const DependencyDescription = require('../../../dist/build/dependency-description').DependencyDescription; +const SourceInclusion = require('../../../dist/build/source-inclusion').SourceInclusion; +const DependencyInclusion = require('../../../dist/build/dependency-inclusion').DependencyInclusion; const path = require('path'); const minimatch = require('minimatch'); @@ -459,3 +460,45 @@ describe('the Bundle module', () => { }); }); }); + +describe('function _calculateRelativeSourceMapsRoot', () => { + const testCases = [ + // Basic UNIX cases + { projectDir: '/usr/home/my-app', outputDir: './dist', expected: '..' }, + { projectDir: '/usr/home/my-app/', outputDir: './dist/', expected: '..' }, + { projectDir: '/usr/home/my-app', outputDir: 'dist', expected: '..' }, + { projectDir: '/usr/home/my-app/', outputDir: 'dist/', expected: '..' }, + // Basic Windows cases + { projectDir: 'C:/My Documents/MyApp', outputDir: './dist', expected: '..' }, + { projectDir: 'C:/My Documents/MyApp/', outputDir: './dist/', expected: '..' }, + { projectDir: 'C:/My Documents/MyApp', outputDir: 'dist', expected: '..' }, + { projectDir: 'C:/My Documents/MyApp/', outputDir: 'dist/', expected: '..' }, + // Basic Windows cases with backslashes + { projectDir: 'C:\\My Documents\\MyApp', outputDir: '.\\dist', expected: '..' }, + { projectDir: 'C:\\My Documents\\MyApp\\', outputDir: '.\\dist\\', expected: '..' }, + { projectDir: 'C:\\My Documents\\MyApp', outputDir: 'dist', expected: '..' }, + { projectDir: 'C:\\My Documents\\MyApp\\', outputDir: 'dist\\', expected: '..' }, + // Windows mixed slashes + { projectDir: 'C:\\My Documents\\MyApp', outputDir: './dist', expected: '..' }, + { projectDir: 'C:\\My Documents\\MyApp\\', outputDir: './dist/', expected: '..' }, + { projectDir: 'C:/My Documents/MyApp/', outputDir: 'dist\\', expected: '..' }, + // Output directory outside of project root + { projectDir: '/usr/home/my-app', outputDir: '../wwwroot/scripts', expected: '../../my-app' }, + { projectDir: 'C:\\My Documents\\MyApp', outputDir: '../wwwroot/scripts', expected: '../../MyApp' }, + // Relative project root paths, basic cases + { projectDir: './my-app', outputDir: './dist', expected: '..' }, + { projectDir: 'my-app', outputDir: 'dist', expected: '..' }, + { projectDir: '.\\MyApp', outputDir: '.\\dist', expected: '..' }, + { projectDir: 'MyApp\\', outputDir: 'dist\\', expected: '..' }, + // Relative project root paths, output directory outside of project root + { projectDir: './my-app', outputDir: '../wwwroot/scripts', expected: '../../my-app' }, + { projectDir: '.\\MyApp\\', outputDir: '..\\wwwroot\\scripts\\', expected: '../../MyApp' } + ]; + + testCases.forEach(({ projectDir, outputDir, expected }) => { + it(`returns "${expected}" for projectDir "${projectDir}" and outputDir "${outputDir}"`, () => { + const result = _calculateRelativeSourceMapsRoot(projectDir, outputDir); + expect(result).toBe(expected); + }); + }); +}); diff --git a/spec/lib/build/bundled-source.spec.js b/spec/lib/build/bundled-source.spec.js index efc98835b..d81989c69 100644 --- a/spec/lib/build/bundled-source.spec.js +++ b/spec/lib/build/bundled-source.spec.js @@ -1,7 +1,7 @@ const path = require('path'); const BundlerMock = require('../../mocks/bundler'); -const BundledSource = require('../../../lib/build/bundled-source').BundledSource; -const Utils = require('../../../lib/build/utils'); +const BundledSource = require('../../../dist/build/bundled-source').BundledSource; +const Utils = require('../../../dist/build/utils'); const cwd = process.cwd(); @@ -882,4 +882,99 @@ module.exports = require('./bar.js'); expect(Utils.setCache).not.toHaveBeenCalled(); }); }); + + it('transform saves transformed source map to cache', () => { + let file = { + path: path.resolve(cwd, 'node_modules/foo/bar/lo.js'), + contents: "export {default as t} from './t.js';", + sourceMap: {"version":3,"file":"lo.js","sourceRoot":"","sources":["lo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,IAAI,CAAC,EAAC,MAAM,QAAQ,CAAA"} + }; + + Utils.getCache = jasmine.createSpy('getCache').and.returnValue(undefined); + Utils.setCache = jasmine.createSpy('setCache'); + + let bs = new BundledSource(bundler, file); + bs._getProjectRoot = () => 'src'; + bs.includedBy = { + includedBy: { + description: { + name: 'foo', + mainId: 'foo/index', + loaderConfig: { + name: 'foo', + path: '../node_modules/foo', + main: 'index' + }, + browserReplacement: () => undefined + } + } + }; + bs._getLoaderPlugins = () => []; + bs._getLoaderConfig = () => ({paths: {}}); + bs._getUseCache = () => true; + + let deps = bs.transform(); + let contents = "define('foo/bar/lo',[\"exports\", './t'], function (_exports, _t) { \"use strict\"; _exports.__esModule = true; _exports.t = void 0; _t = _interopRequireDefault(_t); _exports.t = _t.default; function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }});"; + let transformedSourceMap = {version:3,file:undefined,names:['_t', '_interopRequireDefault', '_exports', 't', 'default', 'e', '__esModule'],sourceRoot:undefined,sources:['lo.ts'],sourcesContent:[undefined],mappings:";;;;;EAAAA,EAAA,GAAAC,sBAAA,CAAAD,EAAA;EAA2BE,QAAA,CAAAC,CAAA,GAAAH,EAAA,CAAAI,OAAA;EAAA,SAAAH,uBAAAI,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAD,OAAA,EAAAC,CAAA;AAAA",ignoreList:[]}; + + expect(deps).toEqual(['foo/bar/t']); + expect(bs.requiresTransform).toBe(false); + expect(bs.contents.replace(/\r|\n/g, '')) + .toBe(contents); + expect(bs.sourceMap).toEqual(transformedSourceMap) + + expect(Utils.getCache).toHaveBeenCalled(); + expect(Utils.setCache).toHaveBeenCalled(); + expect(Utils.setCache.calls.argsFor(0)[1].deps).toEqual(['./t']); + expect(Utils.setCache.calls.argsFor(0)[1].contents.replace(/\r|\n/g, '')).toBe(contents); + expect(Utils.setCache.calls.argsFor(0)[1].transformedSourceMap).toEqual(transformedSourceMap); + }); + + it('transform uses cache for transformed source map', () => { + let file = { + path: path.resolve(cwd, 'node_modules/foo/bar/lo.js'), + contents: "export {default as t} from './t.js';", + sourceMap: {"version":3,"file":"lo.js","sourceRoot":"","sources":["lo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,IAAI,CAAC,EAAC,MAAM,QAAQ,CAAA"} + }; + + let contents = "define('foo/bar/lo',[\"exports\", './t'], function (_exports, _t) { \"use strict\"; _exports.__esModule = true; _exports.t = void 0; _t = _interopRequireDefault(_t); _exports.t = _t.default; function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }});"; + let transformedSourceMap = {version:3,file:undefined,names:['_t', '_interopRequireDefault', '_exports', 't', 'default', 'e', '__esModule'],sourceRoot:undefined,sources:['lo.ts'],sourcesContent:[undefined],mappings:";;;;;EAAAA,EAAA,GAAAC,sBAAA,CAAAD,EAAA;EAA2BE,QAAA,CAAAC,CAAA,GAAAH,EAAA,CAAAI,OAAA;EAAA,SAAAH,uBAAAI,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAD,OAAA,EAAAC,CAAA;AAAA",ignoreList:[]}; + + Utils.getCache = jasmine.createSpy('getCache').and.returnValue({ + deps: ['./t'], + contents, + transformedSourceMap + }); + Utils.setCache = jasmine.createSpy('setCache'); + + let bs = new BundledSource(bundler, file); + bs._getProjectRoot = () => 'src'; + bs.includedBy = { + includedBy: { + description: { + name: 'foo', + mainId: 'foo/index', + loaderConfig: { + name: 'foo', + path: '../node_modules/foo', + main: 'index' + }, + browserReplacement: () => undefined + } + } + }; + bs._getLoaderPlugins = () => []; + bs._getLoaderConfig = () => ({paths: {}}); + bs._getUseCache = () => true; + + let deps = bs.transform(); + expect(deps).toEqual(['foo/bar/t']); + expect(bs.requiresTransform).toBe(false); + expect(bs.contents.replace(/\r|\n/g, '')) + .toBe(contents); + expect(bs.sourceMap).toEqual(transformedSourceMap); + + expect(Utils.getCache).toHaveBeenCalled(); + expect(Utils.setCache).not.toHaveBeenCalled(); + }); }); diff --git a/spec/lib/build/bundler.spec.js b/spec/lib/build/bundler.spec.js index b3836a29b..fee1c362a 100644 --- a/spec/lib/build/bundler.spec.js +++ b/spec/lib/build/bundler.spec.js @@ -1,5 +1,5 @@ const path = require('path'); -const Bundler = require('../../../lib/build/bundler').Bundler; +const Bundler = require('../../../dist/build/bundler').Bundler; const PackageAnalyzer = require('../../mocks/package-analyzer'); const CLIOptionsMock = require('../../mocks/cli-options'); const mockfs = require('../../mocks/mock-fs'); diff --git a/spec/lib/build/dependency-description.spec.js b/spec/lib/build/dependency-description.spec.js index c71d6e620..7e9602bb4 100644 --- a/spec/lib/build/dependency-description.spec.js +++ b/spec/lib/build/dependency-description.spec.js @@ -1,5 +1,5 @@ const path = require('path'); -const DependencyDescription = require('../../../lib/build/dependency-description').DependencyDescription; +const DependencyDescription = require('../../../dist/build/dependency-description').DependencyDescription; describe('The DependencyDescription', () => { let sut; @@ -42,12 +42,12 @@ describe('The DependencyDescription', () => { it('gets browser replacement but leave . for main replacement', () => { sut.metadata = { browser: { - "readable-stream": "./lib/readable-stream-browser.js", + "readable-stream": "./dist/readable-stream-browser.js", ".": "dist/jszip.min.js" } }; expect(sut.browserReplacement()).toEqual({ - "readable-stream": "./lib/readable-stream-browser" + "readable-stream": "./dist/readable-stream-browser" }); }); }); diff --git a/spec/lib/build/dependency-inclusion.spec.js b/spec/lib/build/dependency-inclusion.spec.js index 7452fbc26..cf44cd645 100644 --- a/spec/lib/build/dependency-inclusion.spec.js +++ b/spec/lib/build/dependency-inclusion.spec.js @@ -1,7 +1,7 @@ const BundlerMock = require('../../mocks/bundler'); -const SourceInclusion = require('../../../lib/build/source-inclusion').SourceInclusion; -const DependencyInclusion = require('../../../lib/build/dependency-inclusion').DependencyInclusion; -const DependencyDescription = require('../../../lib/build/dependency-description').DependencyDescription; +const SourceInclusion = require('../../../dist/build/source-inclusion').SourceInclusion; +const DependencyInclusion = require('../../../dist/build/dependency-inclusion').DependencyInclusion; +const DependencyDescription = require('../../../dist/build/dependency-description').DependencyDescription; const mockfs = require('../../mocks/mock-fs'); const Minimatch = require('minimatch').Minimatch; const path = require('path'); @@ -212,8 +212,8 @@ describe('the DependencyInclusion module', () => { name: 'my-package', main: 'index', resources: [ - 'lib/foo.js', - 'lib/foo.css' + 'dist/foo.js', + 'dist/foo.css' ] }; @@ -222,8 +222,8 @@ describe('the DependencyInclusion module', () => { .then(() => { expect(bundle.includes.length).toBe(3); expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'index.js')); - expect(bundle.includes[1].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'lib', 'foo.js')); - expect(bundle.includes[2].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'lib', 'foo.css')); + expect(bundle.includes[1].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'dist', 'foo.js')); + expect(bundle.includes[2].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'dist', 'foo.css')); expect(bundle.addAlias).toHaveBeenCalledWith('my-package', 'my-package/index'); done(); }) @@ -251,15 +251,15 @@ describe('the DependencyInclusion module', () => { }; mockfs({ - 'node_modules/my-package/lib/foo.js': 'some-content' + 'node_modules/my-package/dist/foo.js': 'some-content' }); let sut = new DependencyInclusion(bundle, description); sut._getProjectRoot = () => 'src'; - sut.traceResource('lib/foo') + sut.traceResource('dist/foo') .then(() => { expect(bundle.includes.length).toBe(1); - expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'lib', 'foo.js')); + expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'dist', 'foo.js')); expect(bundle.addAlias).not.toHaveBeenCalled(); done(); }) @@ -364,16 +364,16 @@ describe('the DependencyInclusion module', () => { }; mockfs({ - 'node_modules/my-package/lib/foo.json': 'some-content' + 'node_modules/my-package/dist/foo.json': 'some-content' }); let sut = new DependencyInclusion(bundle, description); sut._getProjectRoot = () => 'src'; - sut.traceResource('lib/foo') + sut.traceResource('dist/foo') .then(() => { expect(bundle.includes.length).toBe(1); - expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'lib', 'foo.json')); - expect(bundle.addAlias).toHaveBeenCalledWith('my-package/lib/foo', 'my-package/lib/foo.json'); + expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'dist', 'foo.json')); + expect(bundle.addAlias).toHaveBeenCalledWith('my-package/dist/foo', 'my-package/dist/foo.json'); done(); }) .catch(e => done.fail(e)); @@ -400,16 +400,16 @@ describe('the DependencyInclusion module', () => { }; mockfs({ - 'node_modules/my-package/lib/foo/index.js': 'some-content' + 'node_modules/my-package/dist/foo/index.js': 'some-content' }); let sut = new DependencyInclusion(bundle, description); sut._getProjectRoot = () => 'src'; - sut.traceResource('lib/foo') + sut.traceResource('dist/foo') .then(() => { expect(bundle.includes.length).toBe(1); - expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'lib', 'foo', 'index.js')); - expect(bundle.addAlias).toHaveBeenCalledWith('my-package/lib/foo', 'my-package/lib/foo/index'); + expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'dist', 'foo', 'index.js')); + expect(bundle.addAlias).toHaveBeenCalledWith('my-package/dist/foo', 'my-package/dist/foo/index'); done(); }) .catch(e => done.fail(e)); @@ -436,16 +436,16 @@ describe('the DependencyInclusion module', () => { }; mockfs({ - 'node_modules/my-package/lib/foo/index.json': 'some-content' + 'node_modules/my-package/dist/foo/index.json': 'some-content' }); let sut = new DependencyInclusion(bundle, description); sut._getProjectRoot = () => 'src'; - sut.traceResource('lib/foo') + sut.traceResource('dist/foo') .then(() => { expect(bundle.includes.length).toBe(1); - expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'lib', 'foo', 'index.json')); - expect(bundle.addAlias).toHaveBeenCalledWith('my-package/lib/foo', 'my-package/lib/foo/index.json'); + expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'dist', 'foo', 'index.json')); + expect(bundle.addAlias).toHaveBeenCalledWith('my-package/dist/foo', 'my-package/dist/foo/index.json'); done(); }) .catch(e => done.fail(e)); @@ -472,17 +472,17 @@ describe('the DependencyInclusion module', () => { }; mockfs({ - 'node_modules/my-package/lib/foo/package.json': '{"main":"fmain","module":"fmodule"}', - 'node_modules/my-package/lib/foo/fmodule.js': 'some-content' + 'node_modules/my-package/dist/foo/package.json': '{"main":"fmain","module":"fmodule"}', + 'node_modules/my-package/dist/foo/fmodule.js': 'some-content' }); let sut = new DependencyInclusion(bundle, description); sut._getProjectRoot = () => 'src'; - sut.traceResource('lib/foo') + sut.traceResource('dist/foo') .then(() => { expect(bundle.includes.length).toBe(1); - expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'lib', 'foo', 'fmodule.js')); - expect(bundle.addAlias).toHaveBeenCalledWith('my-package/lib/foo', 'my-package/lib/foo/fmodule'); + expect(bundle.includes[0].pattern).toBe(path.join('..', 'node_modules', 'my-package', 'dist', 'foo', 'fmodule.js')); + expect(bundle.addAlias).toHaveBeenCalledWith('my-package/dist/foo', 'my-package/dist/foo/fmodule'); done(); }) .catch(e => done.fail(e)); @@ -504,10 +504,10 @@ describe('the DependencyInclusion module', () => { description.loaderConfig = { path: '../node_modules/my-package', name: 'my-package', - main: 'lib/cjs/foo', + main: 'dist/cjs/foo', resources: [ - 'lib/cjs/foo1.js', - 'lib/cjs/foo1.css' + 'dist/cjs/foo1.js', + 'dist/cjs/foo1.css' ] }; @@ -515,9 +515,9 @@ describe('the DependencyInclusion module', () => { sut.traceResources() .then(() => { expect(sut.conventionalAliases()).toEqual({ - 'my-package/foo': 'my-package/lib/cjs/foo', - 'my-package/foo1': 'my-package/lib/cjs/foo1', - 'my-package/foo1.css': 'my-package/lib/cjs/foo1.css' + 'my-package/foo': 'my-package/dist/cjs/foo', + 'my-package/foo1': 'my-package/dist/cjs/foo1', + 'my-package/foo1.css': 'my-package/dist/cjs/foo1.css' }); done(); }) diff --git a/spec/lib/build/find-deps.spec.js b/spec/lib/build/find-deps.spec.js index 0548edefc..d0f3c0c12 100644 --- a/spec/lib/build/find-deps.spec.js +++ b/spec/lib/build/find-deps.spec.js @@ -1,4 +1,4 @@ -const fd = require('../../../lib/build/find-deps'); +const fd = require('../../../dist/build/find-deps'); const mockfs = require('../../mocks/mock-fs'); const findJsDeps = fd.findJsDeps; const findHtmlDeps = fd.findHtmlDeps; diff --git a/spec/lib/build/inject-css.spec.js b/spec/lib/build/inject-css.spec.js index 1f93ae4a4..3d14701fb 100644 --- a/spec/lib/build/inject-css.spec.js +++ b/spec/lib/build/inject-css.spec.js @@ -1,4 +1,4 @@ -const fixupCSSUrls = require('../../../lib/build/inject-css').fixupCSSUrls; +const fixupCSSUrls = require('../../../dist/build/inject-css').fixupCSSUrls; // tests partly copied from // https://github.com/webpack-contrib/style-loader/blob/master/test/fixUrls.test.js diff --git a/spec/lib/build/module-id-processor.spec.js b/spec/lib/build/module-id-processor.spec.js index 0d91bdc37..1fa741cd0 100644 --- a/spec/lib/build/module-id-processor.spec.js +++ b/spec/lib/build/module-id-processor.spec.js @@ -1,4 +1,4 @@ -const { toDotDot, fromDotDot, getAliases } = require('../../../lib/build/module-id-processor'); +const { toDotDot, fromDotDot, getAliases } = require('../../../dist/build/module-id-processor'); describe('module-id-processor', () => { const moduleId = '../src/elements/hello-world.ts'; diff --git a/spec/lib/build/package-analyzer.spec.js b/spec/lib/build/package-analyzer.spec.js index c5e6feaff..946bd7ba3 100644 --- a/spec/lib/build/package-analyzer.spec.js +++ b/spec/lib/build/package-analyzer.spec.js @@ -1,6 +1,6 @@ const path = require('path'); const mockfs = require('../../mocks/mock-fs'); -const PackageAnalyzer = require('../../../lib/build/package-analyzer').PackageAnalyzer; +const PackageAnalyzer = require('../../../dist/build/package-analyzer').PackageAnalyzer; describe('The PackageAnalyzer', () => { let project; @@ -424,15 +424,15 @@ describe('The PackageAnalyzer', () => { it('analyze() reads package.json as package metadata with implicit /index.js in main path', done => { // setup mock package.json const fsConfig = {}; - fsConfig[path.join('node_modules/my-package', 'package.json')] = '{ "name": "my-package", "main": "./lib" }'; - fsConfig[path.join('node_modules/my-package/lib', 'index.js')] = 'some-content'; + fsConfig[path.join('node_modules/my-package', 'package.json')] = '{ "name": "my-package", "main": "./dist" }'; + fsConfig[path.join('node_modules/my-package/dist', 'index.js')] = 'some-content'; fsConfig[project.paths.root] = {}; mockfs(fsConfig); sut.analyze('my-package') .then(description => { expect(description.metadata.name).toBe('my-package'); - expect(description.loaderConfig.main).toBe('lib/index'); + expect(description.loaderConfig.main).toBe('dist/index'); done(); }) .catch(e => done.fail(e)); @@ -441,15 +441,15 @@ describe('The PackageAnalyzer', () => { it('analyze() reads package.json as package metadata with explicit /index.js in main path', done => { // setup mock package.json const fsConfig = {}; - fsConfig[path.join('node_modules/my-package', 'package.json')] = '{ "name": "my-package", "main": "lib/index.js" }'; - fsConfig[path.join('node_modules/my-package/lib', 'index.js')] = 'some-content'; + fsConfig[path.join('node_modules/my-package', 'package.json')] = '{ "name": "my-package", "main": "dist/index.js" }'; + fsConfig[path.join('node_modules/my-package/dist', 'index.js')] = 'some-content'; fsConfig[project.paths.root] = {}; mockfs(fsConfig); sut.analyze('my-package') .then(description => { expect(description.metadata.name).toBe('my-package'); - expect(description.loaderConfig.main).toBe('lib/index'); + expect(description.loaderConfig.main).toBe('dist/index'); done(); }) .catch(e => done.fail(e)); diff --git a/spec/lib/build/package-installer.spec.js b/spec/lib/build/package-installer.spec.js index 7ad4be5a4..e9af908b7 100644 --- a/spec/lib/build/package-installer.spec.js +++ b/spec/lib/build/package-installer.spec.js @@ -1,5 +1,5 @@ const mockfs = require('../../mocks/mock-fs'); -const PackageInstaller = require('../../../lib/build/package-installer').PackageInstaller; +const PackageInstaller = require('../../../dist/build/package-installer').PackageInstaller; describe('The PackageInstaller', () => { let project; diff --git a/spec/lib/build/source-inclusion.spec.js b/spec/lib/build/source-inclusion.spec.js index 888923b2c..8b599dfd9 100644 --- a/spec/lib/build/source-inclusion.spec.js +++ b/spec/lib/build/source-inclusion.spec.js @@ -1,5 +1,5 @@ const BundlerMock = require('../../mocks/bundler'); -const SourceInclusion = require('../../../lib/build/source-inclusion').SourceInclusion; +const SourceInclusion = require('../../../dist/build/source-inclusion').SourceInclusion; const mockfs = require('../../mocks/mock-fs'); const Minimatch = require('minimatch').Minimatch; const path = require('path'); diff --git a/spec/lib/build/stub-module.spec.js b/spec/lib/build/stub-module.spec.js index 6dba460f0..3c3679916 100644 --- a/spec/lib/build/stub-module.spec.js +++ b/spec/lib/build/stub-module.spec.js @@ -1,57 +1,57 @@ -const stubModule = require('../../../lib/build/stub-module'); +const stubModule = require('../../../dist/build/stub-module').stubModule; describe('StubCoreNodejsModule', () => { - it('stubs some core module with subfix -browserify', () => { - expect(stubModule('os', 'src')).toEqual({ + it('stubs some core module with subfix -browserify', async () => { + expect(await stubModule('os', 'src')).toEqual({ name: 'os', path: '../node_modules/os-browserify' }); }); - it('stubs domain', () => { - expect(stubModule('domain', 'src')).toEqual({ + it('stubs domain', async () => { + expect(await stubModule('domain', 'src')).toEqual({ name: 'domain', path: '../node_modules/domain-browser' }); }); - it('stubs http', () => { - expect(stubModule('http', 'src')).toEqual({ + it('stubs http', async () => { + expect(await stubModule('http', 'src')).toEqual({ name: 'http', path: '../node_modules/stream-http' }); }); - it('stubs querystring', () => { - expect(stubModule('querystring', 'src')).toEqual({ + it('stubs querystring', async () => { + expect(await stubModule('querystring', 'src')).toEqual({ name: 'querystring', path: '../node_modules/querystring-browser-stub' }); }); - it('stubs fs', () => { - expect(stubModule('fs', 'src')).toEqual({ + it('stubs fs', async () => { + expect(await stubModule('fs', 'src')).toEqual({ name: 'fs', path: '../node_modules/fs-browser-stub' }); }); - it('ignores sys', () => { - expect(stubModule('sys', 'src')).toBeUndefined(); + it('ignores sys', async () => { + expect(await stubModule('sys', 'src')).toBeUndefined(); }); - it('stubModule stubs zlib', () => { - expect(stubModule('zlib', 'src')).toEqual({ + it('stubModule stubs zlib', async () => { + expect(await stubModule('zlib', 'src')).toEqual({ name: 'zlib', path: '../node_modules/browserify-zlib' }); }); - it('stubs empty module for some core module', () => { - expect(stubModule('dns', 'src')).toBe('define(function(){return {};});'); + it('stubs empty module for some core module', async () => { + expect(await stubModule('dns', 'src')).toBe('define(function(){return {};});'); }); - it('stubs empty module for __ignore__', () => { - expect(stubModule('__ignore__', 'src')).toBe('define(function(){return {};});'); + it('stubs empty module for __ignore__', async () => { + expect(await stubModule('__ignore__', 'src')).toBe('define(function(){return {};});'); }); }); diff --git a/spec/lib/build/utils.spec.js b/spec/lib/build/utils.spec.js index ce63fa83a..a765a3254 100644 --- a/spec/lib/build/utils.spec.js +++ b/spec/lib/build/utils.spec.js @@ -1,6 +1,6 @@ const path = require('path'); const mockfs = require('../../mocks/mock-fs'); -const Utils = require('../../../lib/build/utils'); +const Utils = require('../../../dist/build/utils'); describe('the Utils.runSequentially function', () => { it('calls the callback function for all items', (d) => { diff --git a/spec/lib/cli-options.spec.ts b/spec/lib/cli-options.spec.ts index a28968e42..dd4a65700 100644 --- a/spec/lib/cli-options.spec.ts +++ b/spec/lib/cli-options.spec.ts @@ -11,7 +11,7 @@ describe('The cli-options', () => { }; mockfs(fsConfig); - cliOptions = new(require('../../lib/cli-options').CLIOptions)(); + cliOptions = new(require('../../dist/cli-options').CLIOptions)(); }); afterEach(() => { diff --git a/spec/lib/cli.spec.js b/spec/lib/cli.spec.js index fe6e4b47b..5dbe712a4 100644 --- a/spec/lib/cli.spec.js +++ b/spec/lib/cli.spec.js @@ -1,20 +1,27 @@ +/** + * @import {Project} from "../../src/project" + * @import { CLI } from "../../src/cli" + */ const mockfs = require('../mocks/mock-fs'); describe('The cli', () => { let fs; let path; + /** @type {CLI} */ let cli; + /** @type {Project} */ let Project; + /** @type {Project} */ let project; let dir; let aureliaProject; beforeEach(() => { - fs = require('../../lib/file-system'); + fs = require('../../dist/file-system'); path = require('path'); - cli = new (require('../../lib/cli').CLI)(); - Project = require('../../lib/project').Project; + cli = new (require('../../dist/cli').CLI)(); + Project = require('../../dist/project').Project; project = {}; dir = 'workspaces'; @@ -82,17 +89,17 @@ describe('The cli', () => { }); describe('The createHelpCommand() function', () => { - it('gets the help command', () => { + it('gets the help command', async () => { mockfs({ - 'lib/commands/help/command.js': 'module.exports = {}', - 'lib/string.js': 'module.exports = {}' + 'dist/commands/help/command.js': 'module.exports = {}', + 'dist/string.js': 'module.exports = {}' }); spyOn(cli.container, 'get'); - cli.createHelpCommand(); + await cli.createHelpCommand(); expect(cli.container.get) - .toHaveBeenCalledWith(require('../../lib/commands/help/command')); + .toHaveBeenCalledWith(require('../../dist/commands/help/command').default); }); }); diff --git a/spec/lib/commands/config/configuration.spec.js b/spec/lib/commands/config/configuration.spec.js index dde4535e1..b4417d960 100644 --- a/spec/lib/commands/config/configuration.spec.js +++ b/spec/lib/commands/config/configuration.spec.js @@ -1,8 +1,8 @@ const mockfs = require('../../../mocks/mock-fs'); describe('The config command - configuration', () => { - const CLIOptions = require('../../../../lib/cli-options').CLIOptions; - const Configuration = require('../../../../lib/commands/config/configuration'); + const CLIOptions = require('../../../../dist/cli-options').CLIOptions; + const Configuration = require('../../../../dist/commands/config/configuration').Configuration; let configuration; let project; let projectControl; diff --git a/spec/lib/commands/config/util.spec.js b/spec/lib/commands/config/util.spec.js index cc2c0e0c3..0842062c4 100644 --- a/spec/lib/commands/config/util.spec.js +++ b/spec/lib/commands/config/util.spec.js @@ -1,8 +1,8 @@ const mockfs = require('../../../mocks/mock-fs'); describe('The config command - util', () => { - const CLIOptions = require('../../../../lib/cli-options').CLIOptions; - const ConfigurationUtilities = require('../../../../lib/commands/config/util'); + const CLIOptions = require('../../../../dist/cli-options').CLIOptions; + const ConfigurationUtilities = require('../../../../dist/commands/config/util').ConfigurationUtilities; beforeEach(() => { mockfs({ diff --git a/spec/lib/configuration.spec.js b/spec/lib/configuration.spec.js index 8109b5b8e..381ecd68f 100644 --- a/spec/lib/configuration.spec.js +++ b/spec/lib/configuration.spec.js @@ -1,4 +1,4 @@ -const Configuration = require('../../lib/configuration').Configuration; +const Configuration = require('../../dist/configuration').Configuration; const CLIOptionsMock = require('../mocks/cli-options'); describe('the Configuration module', () => { diff --git a/spec/lib/file-system.spec.js b/spec/lib/file-system.spec.js index 388192419..f91805c6c 100644 --- a/spec/lib/file-system.spec.js +++ b/spec/lib/file-system.spec.js @@ -16,7 +16,7 @@ describe('The file-system module', () => { beforeEach(() => { path = require('path'); - fs = require('../../lib/file-system'); + fs = require('../../dist/file-system'); readDir = 'read'; readFile = { diff --git a/spec/lib/project-item.spec.js b/spec/lib/project-item.spec.js index 1b0ce580c..0d5017604 100644 --- a/spec/lib/project-item.spec.js +++ b/spec/lib/project-item.spec.js @@ -1,7 +1,7 @@ const path = require('path'); -const fs = require('../../lib/file-system'); +const fs = require('../../dist/file-system'); const mockfs = require('../mocks/mock-fs'); -const {ProjectItem} = require('../../lib/project-item'); +const {ProjectItem} = require('../../dist/project-item'); describe('The ProjectItem module', () => { it('ProjectItem.text() captures text', () => { diff --git a/spec/lib/project.spec.js b/spec/lib/project.spec.js index 8435b2661..cd097cc05 100644 --- a/spec/lib/project.spec.js +++ b/spec/lib/project.spec.js @@ -1,19 +1,23 @@ +/** + * @import {Project} from "../../src/project" + */ const mockfs = require('../mocks/mock-fs'); describe('The project module', () => { let path; let fs; - + /** @type {Project} */ let Project; + /** @type {Project} */ let project; beforeEach(() => { path = require('path'); - fs = require('../../lib/file-system'); + fs = require('../../dist/file-system'); - Project = require('../../lib/project').Project; + Project = require('../../dist/project').Project; mockfs(); diff --git a/spec/lib/ui.spec.js b/spec/lib/ui.spec.js index 7ba03be59..15cf76c40 100644 --- a/spec/lib/ui.spec.js +++ b/spec/lib/ui.spec.js @@ -1,4 +1,4 @@ -const {ConsoleUI} = require('../../lib/ui'); +const {ConsoleUI} = require('../../dist/ui'); describe('The UI module', () => { let flags = []; diff --git a/spec/mocks/bundler.js b/spec/mocks/bundler.js index 193106b8d..42abe81e8 100644 --- a/spec/mocks/bundler.js +++ b/spec/mocks/bundler.js @@ -1,7 +1,7 @@ -const Configuration = require('../../lib/configuration').Configuration; -const CLIOptions = require('../../lib/cli-options').CLIOptions; +const Configuration = require('../../dist/configuration').Configuration; +const CLIOptions = require('../../dist/cli-options').CLIOptions; const ProjectMock = require('./project-mock'); -const LoaderPlugin = require('../../lib/build/loader-plugin').LoaderPlugin; +const LoaderPlugin = require('../../dist/build/loader-plugin').LoaderPlugin; module.exports = class Bundler { constructor() { diff --git a/spec/mocks/cli-options.js b/spec/mocks/cli-options.js index 9236f20b5..709b23a3d 100644 --- a/spec/mocks/cli-options.js +++ b/spec/mocks/cli-options.js @@ -1,4 +1,4 @@ -let OriginalCLIOptions = require('../../lib/cli-options').CLIOptions; +let OriginalCLIOptions = require('../../dist/cli-options').CLIOptions; module.exports = class CLIOptionsMock { constructor() { diff --git a/src/aurelia-json.d.ts b/src/aurelia-json.d.ts new file mode 100644 index 000000000..87089afa2 --- /dev/null +++ b/src/aurelia-json.d.ts @@ -0,0 +1,109 @@ +declare namespace AureliaJson { + interface IProject { + paths: IPaths; + /** Accessed from `aurelia_project/process-css.ts` and `aurelia_project/run.ts` */ + platform: IPlatform; + transpiler: ITranspiler; + /** Accessed from `aurelia_project/process-markup.ts` */ + markupProcessor: { + source: string[]; + } + /** Accessed from `aurelia_project/process-css.ts` */ + cssProcessor: { + source: string[]; + } + /** Accessed from `aurelia_project/process-json.ts` */ + jsonProcessor: { + source: string[]; + } + build: IBuild, + packageManager?: 'npm' | 'yarn'; + } + + interface IBuild { + targets: ITarget[]; + options: IBuildOptions; + bundles: IBundle[]; + loader: ILoader; + } + + interface IPaths { + root: string; + resources: string; + elements: string; + attributes: string; + valueConverters: string; + bindingBehaviors: string; + } + + interface IPlatform { + /** Accessed from `aurelia_project/run.ts` */ + port: number; + host: string; + open: false; + index: string; + /** Accessed from `aurelia_project/process-css.ts` and `aurelia_project/run.ts` */ + baseDir: string; + output: string; + } + + interface ITranspiler { + id: string; + fileExtension: string; + } + + interface ITarget { + id?: string; + displayName?: string; + port: number; + index: string; + baseDir: string; + baseUrl?: string; + output: string; + useAbsolutePath?: boolean; + } + + interface IBuildOptions { + minify: string; + sourcemaps: string; + rev: string | boolean; + cache?: string; + } + + interface IBundle { + options: IBuildOptions; + name: string; + source: string[] | { + exclude: string[], + include: string[] + }; + prepend: (string | { env?: string; path: string; })[]; + dependencies: (string | { name: string, env: string, main?: string, path?: string })[] + append: (string | { env?: string; path: string; })[]; + } + interface ILoader { + type: LoaderType; + configTarget: string; + includeBundleMetadataInConfig: string; + plugins: ILoaderPlugin[]; + config: ILoaderConfig; + } + + interface ILoaderPlugin { + name: string; + extensions: string[]; + stub: boolean; + test: string; + } + + interface ILoaderConfig { + baseUrl: string; + paths: IPaths; + packages: string[]; + stubModules: string[]; + shim: object; + wrapShim?: boolean + } +} + +type LoaderType = 'require' | 'system'; diff --git a/src/build/amodro-trace/lib/lang.ts b/src/build/amodro-trace/lib/lang.ts new file mode 100644 index 000000000..4e2363cc8 --- /dev/null +++ b/src/build/amodro-trace/lib/lang.ts @@ -0,0 +1,227 @@ +/** + * @license Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/jrburke/requirejs for details + */ + +/*jslint plusplus: true */ +/*global define, java */ + +'use strict'; + +let isJavaObj: (obj: object) => boolean, + hasOwn = Object.prototype.hasOwnProperty; + +function hasProp(obj: object, prop: PropertyKey) { + return hasOwn.call(obj, prop); +} + +isJavaObj = function () { + return false; +}; + +//Rhino, but not Nashorn (detected by importPackage not existing) +//Can have some strange foreign objects. +const java = global['java']; +if (typeof java !== 'undefined' && java.lang && java.lang.Object && typeof global['importPackage'] !== 'undefined') { + isJavaObj = function (obj) { + return obj instanceof java.lang.Object; + }; +} + +export class lang { + // makeJsArrayString added after porting to this project + //Converts an JS array of strings to a string representation. + //Not using JSON.stringify() for Rhino's sake. + static makeJsArrayString(ary: string[]) { + return '["' + ary.map(function (item) { + //Escape any double quotes, backslashes + return lang.jsEscape(item); + }).join('","') + '"]'; + } + + static backSlashRegExp = /\\/g; + static ostring = Object.prototype.toString; + + static isArray = Array.isArray || function (it) { + return lang.ostring.call(it) === "[object Array]"; + } + + static isFunction(it) { + return lang.ostring.call(it) === "[object Function]"; + } + + static isRegExp(it) { + return it && it instanceof RegExp; + } + + static hasProp = hasProp; + + //returns true if the object does not have an own property prop, + //or if it does, it is a falsy value. + static falseProp (obj: object, prop: string) { + return !hasProp(obj, prop) || !obj[prop]; + } + + //gets own property value for given prop on object + static getOwn(obj: object, prop: string) { + return hasProp(obj, prop) && obj[prop]; + } + + private static _mixin(dest, source, override){ + var name; + for (name in source) { + if(source.hasOwnProperty(name) && + (override || !dest.hasOwnProperty(name))) { + dest[name] = source[name]; + } + } + + return dest; // Object + } + + /** + * mixin({}, obj1, obj2) is allowed. If the last argument is a boolean, + * then the source objects properties are force copied over to dest. + */ + static mixin(dest, ...objects){ + var parameters = Array.prototype.slice.call(arguments), + override, i, l; + + if (!dest) { dest = {}; } + + if (parameters.length > 2 && typeof arguments[parameters.length-1] === 'boolean') { + override = parameters.pop(); + } + + for (i = 1, l = parameters.length; i < l; i++) { + lang._mixin(dest, parameters[i], override); + } + return dest; // Object + } + + /** + * Does a deep mix of source into dest, where source values override + * dest values if a winner is needed. + * @param {Object} dest destination object that receives the mixed + * values. + * @param {Object} source source object contributing properties to mix + * in. + * @return {[Object]} returns dest object with the modification. + */ + static deepMix(dest, source) { + lang.eachProp(source, function (value, prop) { + if (typeof value === 'object' && value && + !lang.isArray(value) && !lang.isFunction(value) && + !(value instanceof RegExp)) { + + if (!dest[prop]) { + dest[prop] = {}; + } + lang.deepMix(dest[prop], value); + } else { + dest[prop] = value; + } + }); + return dest; + } + + /** + * Does a type of deep copy. Do not give it anything fancy, best + * for basic object copies of objects that also work well as + * JSON-serialized things, or has properties pointing to functions. + * For non-array/object values, just returns the same object. + * @param {Object} obj copy properties from this object + * @param {Object} [result] optional result object to use + * @return {Object} + */ + static deeplikeCopy (obj) { + var type, result; + + if (lang.isArray(obj)) { + result = []; + obj.forEach(function(value) { + result.push(lang.deeplikeCopy(value)); + }); + return result; + } + + type = typeof obj; + if (obj === null || obj === undefined || type === 'boolean' || + type === 'string' || type === 'number' || lang.isFunction(obj) || + lang.isRegExp(obj)|| isJavaObj(obj)) { + return obj; + } + + //Anything else is an object, hopefully. + result = {}; + lang.eachProp(obj, function(value, key) { + result[key] = lang.deeplikeCopy(value); + }); + return result; + } + + static delegate = (function () { + // boodman/crockford delegation w/ cornford optimization + function TMP() {} + return function (obj: object, props: string[]) { + TMP.prototype = obj; + var tmp = new TMP(); + TMP.prototype = null; + if (props) { + lang.mixin(tmp, props); + } + return tmp; // Object + }; + }()); + + /** + * Helper function for iterating over an array. If the func returns + * a true value, it will break out of the loop. + */ + static each(ary, func) { + if (ary) { + var i; + for (i = 0; i < ary.length; i += 1) { + if (func(ary[i], i, ary)) { + break; + } + } + } + } + + /** + * Cycles over properties in an object and calls a function for each + * property value. If the function returns a truthy value, then the + * iteration is stopped. + */ + static eachProp(obj, func) { + var prop; + for (prop in obj) { + if (hasProp(obj, prop)) { + if (func(obj[prop], prop)) { + break; + } + } + } + } + + //Similar to Function.prototype.bind, but the "this" object is specified + //first, since it is easier to read/figure out what "this" will be. + static bind(obj, fn) { + return function () { + return fn.apply(obj, arguments); + }; + } + + //Escapes a content string to be be a string that has characters escaped + //for inclusion as part of a JS string. + static jsEscape(content: string) { + return content.replace(/(["'\\])/g, '\\$1') + .replace(/[\f]/g, "\\f") + .replace(/[\b]/g, "\\b") + .replace(/[\n]/g, "\\n") + .replace(/[\t]/g, "\\t") + .replace(/[\r]/g, "\\r"); + } +}; diff --git a/src/build/amodro-trace/lib/parse.ts b/src/build/amodro-trace/lib/parse.ts new file mode 100644 index 000000000..a5a03a5ae --- /dev/null +++ b/src/build/amodro-trace/lib/parse.ts @@ -0,0 +1,849 @@ +import * as meriyah from 'meriyah'; +import { lang } from './lang'; +// // Taken from r.js, preserving its style for now to easily port changes in the +// // near term. +// var define = function(ary, fn) { +// module.exports = fn.apply(undefined, +// (ary.map(function(id) { return require(id); }))); +// }; + +// /*jslint plusplus: true */ +// /*global define: false */ +// define(['meriyah', './lang'], function (meriyah, lang) { +// 'use strict'; + + function arrayToString(ary) { + var output = '['; + if (ary) { + ary.forEach(function (item, i) { + output += (i > 0 ? ',' : '') + '"' + lang.jsEscape(item) + '"'; + }); + } + output += ']'; + + return output; + } + + //This string is saved off because JSLint complains + //about obj.arguments use, as 'reserved word' + var argPropName = 'arguments', + //Default object to use for "scope" checking for UMD identifiers. + emptyScope = {}, + mixin = lang.mixin, + hasProp = lang.hasProp; + + //From an meriyah example for traversing its ast. + function traverse(object, visitor) { + var child; + + if (!object) { + return; + } + + if (visitor.call(null, object) === false) { + return false; + } + for (var i = 0, keys = Object.keys(object); i < keys.length; i++) { + child = object[keys[i]]; + if (typeof child === 'object' && child !== null) { + if (traverse(child, visitor) === false) { + return false; + } + } + } + } + + //Like traverse, but visitor returning false just + //stops that subtree analysis, not the rest of tree + //visiting. + function traverseBroad(object, visitor) { + var child; + + if (!object) { + return; + } + + if (visitor.call(null, object) === false) { + return false; + } + for (var i = 0, keys = Object.keys(object); i < keys.length; i++) { + child = object[keys[i]]; + if (typeof child === 'object' && child !== null) { + traverseBroad(child, visitor); + } + } + } + + /** + * Pulls out dependencies from an array literal with just string members. + * If string literals, will just return those string values in an array, + * skipping other items in the array. + * + * @param {Node} node an AST node. + * + * @returns {Array} an array of strings. + * If null is returned, then it means the input node was not a valid + * dependency. + */ + function getValidDeps(node) { + if (!node || node.type !== 'ArrayExpression' || !node.elements) { + return; + } + + var deps = []; + + node.elements.some(function (elem) { + if (elem.type === 'Literal') { + deps.push(elem.value); + } + }); + + return deps.length ? deps : undefined; + } + + // Detects regular or arrow function expressions as the desired expression + // type. + function isFnExpression(node) { + return (node && (node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression')); + } + + /** + * Main parse function. Returns a string of any valid require or + * define/require.def calls as part of one JavaScript source string. + * @param {String} moduleName the module name that represents this file. + * It is used to create a default define if there is not one already for the + * file. This allows properly tracing dependencies for builds. Otherwise, if + * the file just has a require() call, the file dependencies will not be + * properly reflected: the file will come before its dependencies. + * @param {String} moduleName + * @param {String} fileName + * @param {String} fileContents + * @param {Object} options optional options. insertNeedsDefine: true will + * add calls to require.needsDefine() if appropriate. + * @returns {String} JS source string or null, if no require or + * define/require.def calls are found. + */ + export function parse(moduleName, fileName, fileContents, options) { + options = options || {}; + + //Set up source input + var i, moduleCall, depString, + moduleDeps = [], + result = '', + moduleList = [], + needsDefine = true, + astRoot = meriyah.parseScript(fileContents, {next: true, webcompat: true}); + + parse.recurse(astRoot, function (callName, config, name, deps, node, factoryIdentifier, fnExpScope) { + if (!deps) { + deps = []; + } + + if (callName === 'define' && (!name || name === moduleName)) { + needsDefine = false; + } + + if (!name) { + //If there is no module name, the dependencies are for + //this file/default module name. + moduleDeps = moduleDeps.concat(deps); + } else { + moduleList.push({ + name: name, + deps: deps + }); + } + + if (callName === 'define' && factoryIdentifier && hasProp(fnExpScope, factoryIdentifier)) { + return factoryIdentifier; + } + + //If define was found, no need to dive deeper, unless + //the config explicitly wants to dig deeper. + return !!options.findNestedDependencies; + }, options, undefined); + + if (options.insertNeedsDefine && needsDefine) { + result += 'require.needsDefine("' + moduleName + '");'; + } + + if (moduleDeps.length || moduleList.length) { + for (i = 0; i < moduleList.length; i++) { + moduleCall = moduleList[i]; + if (result) { + result += '\n'; + } + + //If this is the main module for this file, combine any + //"anonymous" dependencies (could come from a nested require + //call) with this module. + if (moduleCall.name === moduleName) { + moduleCall.deps = moduleCall.deps.concat(moduleDeps); + moduleDeps = []; + } + + depString = arrayToString(moduleCall.deps); + result += 'define("' + moduleCall.name + '",' + + depString + ');'; + } + if (moduleDeps.length) { + if (result) { + result += '\n'; + } + depString = arrayToString(moduleDeps); + result += 'define("' + moduleName + '",' + depString + ');'; + } + } + + return result || null; + } + + parse.traverse = traverse; + parse.traverseBroad = traverseBroad; + parse.isFnExpression = isFnExpression; + + /** + * Handles parsing a file recursively for require calls. + * @param {Array} parentNode the AST node to start with. + * @param {Function} onMatch function to call on a parse match. + * @param {Object} [options] This is normally the build config options if + * it is passed. + * @param {Object} [fnExpScope] holds list of function expresssion + * argument identifiers, set up internally, not passed in + */ + parse.recurse = function (object, onMatch, options, fnExpScope) { + //Like traverse, but skips if branches that would not be processed + //after has application that results in tests of true or false boolean + //literal values. + var keys, child, result, i, params, param, tempObject, + hasHas = options && options.has; + + fnExpScope = fnExpScope || emptyScope; + + if (!object) { + return; + } + + //If has replacement has resulted in if(true){} or if(false){}, take + //the appropriate branch and skip the other one. + if (hasHas && object.type === 'IfStatement' && object.test.type && + object.test.type === 'Literal') { + if (object.test.value) { + //Take the if branch + this.recurse(object.consequent, onMatch, options, fnExpScope); + } else { + //Take the else branch + this.recurse(object.alternate, onMatch, options, fnExpScope); + } + } else { + result = this.parseNode(object, onMatch, fnExpScope); + if (result === false) { + return; + } else if (typeof result === 'string') { + return result; + } + + //Build up a "scope" object that informs nested recurse calls if + //the define call references an identifier that is likely a UMD + //wrapped function expression argument. + //Catch (function(a) {... wrappers + if (object.type === 'ExpressionStatement' && object.expression && + object.expression.type === 'CallExpression' && object.expression.callee && + isFnExpression(object.expression.callee)) { + tempObject = object.expression.callee; + } + // Catch !function(a) {... wrappers + if (object.type === 'UnaryExpression' && object.argument && + object.argument.type === 'CallExpression' && object.argument.callee && + isFnExpression(object.argument.callee)) { + tempObject = object.argument.callee; + } + if (tempObject && tempObject.params && tempObject.params.length) { + params = tempObject.params; + fnExpScope = mixin({}, fnExpScope, true); + for (i = 0; i < params.length; i++) { + param = params[i]; + if (param.type === 'Identifier') { + fnExpScope[param.name] = true; + } + } + } + + for (i = 0, keys = Object.keys(object); i < keys.length; i++) { + child = object[keys[i]]; + if (typeof child === 'object' && child !== null) { + result = this.recurse(child, onMatch, options, fnExpScope); + if (typeof result === 'string' && hasProp(fnExpScope, result)) { + //The result was still in fnExpScope so break. Otherwise, + //was a return from a a tree that had a UMD definition, + //but now out of that scope so keep siblings. + break; + } + } + } + + //Check for an identifier for a factory function identifier being + //passed in as a function expression, indicating a UMD-type of + //wrapping. + if (typeof result === 'string') { + if (hasProp(fnExpScope, result)) { + //result still in scope, keep jumping out indicating the + //identifier still in use. + return result; + } + + return; + } + } + }; + + /** + * Finds require("") calls inside a CommonJS anonymous module wrapped + * in a define function, given an AST node for the definition function. + * @param {Node} node the AST node for the definition function. + * @returns {Array} and array of dependency names. Can be of zero length. + */ + parse.getAnonDepsFromNode = function (node) { + var deps = [], + funcArgLength; + + if (node) { + this.findRequireDepNames(node, deps); + + //If no deps, still add the standard CommonJS require, exports, + //module, in that order, to the deps, but only if specified as + //function args. In particular, if exports is used, it is favored + //over the return value of the function, so only add it if asked. + funcArgLength = node.params && node.params.length; + if (funcArgLength) { + deps = (funcArgLength > 1 ? ["require", "exports", "module"] : + ["require"]).concat(deps); + } + } + return deps; + }; + + parse.isDefineNodeWithArgs = function (node) { + return node && node.type === 'CallExpression' && + node.callee && node.callee.type === 'Identifier' && + node.callee.name === 'define' && node[argPropName]; + }; + + /** + * Finds the function in define(function (require, exports, module){}); + * @param {Array} node + * @returns {Boolean} + */ + parse.findAnonDefineFactory = function (node) { + var match; + + traverse(node, function (node) { + var arg0, arg1; + + if (parse.isDefineNodeWithArgs(node)) { + + //Just the factory function passed to define + arg0 = node[argPropName][0]; + if (isFnExpression(arg0)) { + match = arg0; + return false; + } + + //A string literal module ID followed by the factory function. + arg1 = node[argPropName][1]; + if (arg0.type === 'Literal' && isFnExpression(arg1)) { + match = arg1; + return false; + } + } + }); + + return match; + }; + + /** + * Finds any config that is passed to requirejs. That includes calls to + * require/requirejs.config(), as well as require({}, ...) and + * requirejs({}, ...) + * @param {String} fileContents + * + * @returns {Object} a config details object with the following properties: + * - config: {Object} the config object found. Can be undefined if no + * config found. + * - range: {Array} the start index and end index in the contents where + * the config was found. Can be undefined if no config found. + * Can throw an error if the config in the file cannot be evaluated in + * a build context to valid JavaScript. + */ + parse.findConfig = function (fileContents) { + /*jslint evil: true */ + var jsConfig, foundConfig, stringData, foundRange, quote, quoteMatch, + quoteRegExp = /(:\s|\[\s*)(['"])/, + astRoot = meriyah.parseScript(fileContents, { + loc: true, + next: true, + webcompat: true + }); + + traverse(astRoot, function (node) { + var arg, + requireType = parse.hasRequire(node); + + if (requireType && (requireType === 'require' || + requireType === 'requirejs' || + requireType === 'requireConfig' || + requireType === 'requirejsConfig')) { + + arg = node[argPropName] && node[argPropName][0]; + + if (arg && arg.type === 'ObjectExpression') { + stringData = parse.nodeToString(fileContents, arg); + jsConfig = stringData.value; + foundRange = stringData.range; + return false; + } + } else { + arg = parse.getRequireObjectLiteral(node); + if (arg) { + stringData = parse.nodeToString(fileContents, arg); + jsConfig = stringData.value; + foundRange = stringData.range; + return false; + } + } + }); + + if (jsConfig) { + // Eval the config + quoteMatch = quoteRegExp.exec(jsConfig); + quote = (quoteMatch && quoteMatch[2]) || '"'; + foundConfig = eval('(' + jsConfig + ')'); + } + + return { + config: foundConfig, + range: foundRange, + quote: quote + }; + }; + + /** Returns the node for the object literal assigned to require/requirejs, + * for holding a declarative config. + */ + parse.getRequireObjectLiteral = function (node) { + if (node.id && node.id.type === 'Identifier' && + (node.id.name === 'require' || node.id.name === 'requirejs') && + node.init && node.init.type === 'ObjectExpression') { + return node.init; + } + }; + + /** + * Finds all dependencies specified in dependency arrays and inside + * simplified commonjs wrappers. + * @param {String} fileName + * @param {String} fileContents + * + * @returns {Array} an array of dependency strings. The dependencies + * have not been normalized, they may be relative IDs. + */ + parse.findDependencies = function (fileName, fileContents, options?) { + // modified to accept parsed result for efficiency. + var dependencies = [], astRoot; + if (fileContents && fileContents.type) { + astRoot = fileContents + } else { + astRoot = meriyah.parseScript(fileContents, {next: true, webcompat: true}); + } + + parse.recurse(astRoot, function (callName, config, name, deps) { + if (deps) { + dependencies = dependencies.concat(deps); + } + }, options, undefined); + + return dependencies; + }; + + /** + * Finds only CJS dependencies, ones that are the form + * require('stringLiteral') + */ + parse.findCjsDependencies = function (fileName, fileContents) { + var dependencies = []; + + traverse(meriyah.parseScript(fileContents, {next: true, webcompat: true}), function (node) { + var arg; + + if (node && node.type === 'CallExpression' && node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'require' && node[argPropName] && + node[argPropName].length === 1) { + arg = node[argPropName][0]; + if (arg.type === 'Literal') { + dependencies.push(arg.value); + } + } + }); + + return dependencies; + }; + + //function define() {} + parse.hasDefDefine = function (node) { + return node.type === 'FunctionDeclaration' && node.id && + node.id.type === 'Identifier' && node.id.name === 'define'; + }; + + //define.amd = ... + parse.hasDefineAmd = function (node) { + return node && node.type === 'AssignmentExpression' && + node.left && node.left.type === 'MemberExpression' && + node.left.object && node.left.object.name === 'define' && + node.left.property && node.left.property.name === 'amd'; + }; + + //define.amd reference, as in: if (define.amd) + parse.refsDefineAmd = function (node) { + return node && node.type === 'MemberExpression' && + node.object && node.object.name === 'define' && + node.object.type === 'Identifier' && + node.property && node.property.name === 'amd' && + node.property.type === 'Identifier'; + }; + + //require(), requirejs(), require.config() and requirejs.config() + parse.hasRequire = function (node) { + var callName, + c = node && node.callee; + + if (node && node.type === 'CallExpression' && c) { + if (c.type === 'Identifier' && + (c.name === 'require' || + c.name === 'requirejs')) { + //A require/requirejs({}, ...) call + callName = c.name; + } else if (c.type === 'MemberExpression' && + c.object && + c.object.type === 'Identifier' && + (c.object.name === 'require' || + c.object.name === 'requirejs') && + c.property && c.property.name === 'config') { + // require/requirejs.config({}) call + callName = c.object.name + 'Config'; + } + } + + return callName; + }; + + //define() + parse.hasDefine = function (node) { + return node && node.type === 'CallExpression' && node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'define'; + }; + + /** + * Determines if define(), require({}|[]) or requirejs was called in the + * file. Also finds out if define() is declared and if define.amd is called. + */ + parse.usesAmdOrRequireJs = function (fileName, fileContents) { + var uses; + + traverse(meriyah.parseScript(fileContents, {next: true, webcompat: true}), function (node) { + var type, callName, arg; + + if (parse.hasDefDefine(node)) { + //function define() {} + type = 'declaresDefine'; + } else if (parse.hasDefineAmd(node)) { + type = 'defineAmd'; + } else { + callName = parse.hasRequire(node); + if (callName) { + arg = node[argPropName] && node[argPropName][0]; + if (arg && (arg.type === 'ObjectExpression' || + arg.type === 'ArrayExpression')) { + type = callName; + } + } else if (parse.hasDefine(node)) { + type = 'define'; + } + } + + if (type) { + if (!uses) { + uses = {}; + } + uses[type] = true; + } + }); + + return uses; + }; + + /** + * Determines if require(''), exports.x =, module.exports =, + * __dirname, __filename are used. So, not strictly traditional CommonJS, + * also checks for Node variants. + */ + parse.usesCommonJs = function (fileName, fileContents) { + var uses = null, + assignsExports = false; + + + traverse(meriyah.parseScript(fileContents, {next: true, webcompat: true}), function (node) { + var type, + // modified to fix a bug on (true || exports.name = {}) + // https://github.com/requirejs/r.js/issues/980 + exp = node.expression || node.init || node; + + if (node.type === 'Identifier' && + (node.name === '__dirname' || node.name === '__filename')) { + type = node.name.substring(2); + } else if (node.type === 'VariableDeclarator' && node.id && + node.id.type === 'Identifier' && + node.id.name === 'exports') { + //Hmm, a variable assignment for exports, so does not use cjs + //exports. + type = 'varExports'; + } else if (exp && exp.type === 'AssignmentExpression' && exp.left && + exp.left.type === 'MemberExpression' && exp.left.object) { + if (exp.left.object.name === 'module' && exp.left.property && + exp.left.property.name === 'exports') { + type = 'moduleExports'; + } else if (exp.left.object.name === 'exports' && + exp.left.property) { + type = 'exports'; + } else if (exp.left.object.type === 'MemberExpression' && + exp.left.object.object.name === 'module' && + exp.left.object.property.name === 'exports' && + exp.left.object.property.type === 'Identifier') { + type = 'moduleExports'; + } + + } else if (node && node.type === 'CallExpression' && node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'require' && node[argPropName] && + node[argPropName].length === 1 && + node[argPropName][0].type === 'Literal') { + type = 'require'; + } + + if (type) { + if (type === 'varExports') { + assignsExports = true; + } else if (type !== 'exports' || !assignsExports) { + if (!uses) { + uses = {}; + } + uses[type] = true; + } + } + }); + + return uses; + }; + + + parse.findRequireDepNames = function (node, deps) { + traverse(node, function (node) { + var arg; + + if (node && node.type === 'CallExpression' && node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'require' && + node[argPropName] && node[argPropName].length === 1) { + + arg = node[argPropName][0]; + if (arg.type === 'Literal') { + deps.push(arg.value); + } + } + }); + }; + + /** + * Determines if a specific node is a valid require or define/require.def + * call. + * @param {Array} node + * @param {Function} onMatch a function to call when a match is found. + * It is passed the match name, and the config, name, deps possible args. + * The config, name and deps args are not normalized. + * @param {Object} fnExpScope an object whose keys are all function + * expression identifiers that should be in scope. Useful for UMD wrapper + * detection to avoid parsing more into the wrapped UMD code. + * + * @returns {String} a JS source string with the valid require/define call. + * Otherwise null. + */ + parse.parseNode = function (node, onMatch, fnExpScope) { + var name, deps, cjsDeps, arg, factory, exp, refsDefine, bodyNode, + args = node && node[argPropName], + callName = parse.hasRequire(node), + isUmd = false; + + if (callName === 'require' || callName === 'requirejs') { + //A plain require/requirejs call + arg = node[argPropName] && node[argPropName][0]; + if (arg && arg.type !== 'ArrayExpression') { + if (arg.type === 'ObjectExpression') { + //A config call, try the second arg. + arg = node[argPropName][1]; + } + } + + deps = getValidDeps(arg); + if (!deps) { + return; + } + + return onMatch("require", null, null, deps, node); + } else if (parse.hasDefine(node) && args && args.length) { + name = args[0]; + deps = args[1]; + factory = args[2]; + + if (name.type === 'ArrayExpression') { + //No name, adjust args + factory = deps; + deps = name; + name = null; + } else if (isFnExpression(name)) { + //Just the factory, no name or deps + factory = name; + name = deps = null; + } else if (name.type === 'Identifier' && args.length === 1 && + hasProp(fnExpScope, name.name)) { + //define(e) where e is a UMD identifier for the factory + //function. + isUmd = true; + factory = name; + name = null; + } else if (name.type !== 'Literal') { + //An object literal, just null out + name = deps = factory = null; + } + + if (name && name.type === 'Literal' && deps) { + if (isFnExpression(deps)) { + //deps is the factory + factory = deps; + deps = null; + } else if (deps.type === 'ObjectExpression') { + //deps is object literal, null out + deps = factory = null; + } else if (deps.type === 'Identifier') { + if (args.length === 2) { + //define('id', factory) + deps = factory = null; + } else if (args.length === 3 && isFnExpression(factory)) { + //define('id', depsIdentifier, factory) + //Since identifier, cannot know the deps, but do not + //error out, assume they are taken care of outside of + //static parsing. + deps = null; + } + } + } + + if (deps && deps.type === 'ArrayExpression') { + deps = getValidDeps(deps); + } else if (isFnExpression(factory)) { + //If no deps and a factory function, could be a commonjs sugar + //wrapper, scan the function for dependencies. + cjsDeps = parse.getAnonDepsFromNode(factory); + if (cjsDeps.length) { + deps = cjsDeps; + } + } else if (deps || (factory && !isUmd)) { + //Does not match the shape of an AMD call. + return; + } + + //Just save off the name as a string instead of an AST object. + if (name && name.type === 'Literal') { + name = name.value; + } + + return onMatch("define", null, name, deps, node, + (factory && factory.type === 'Identifier' ? factory.name : undefined), + fnExpScope); + } else if (node.type === 'CallExpression' && node.callee && + isFnExpression(node.callee) && + node.callee.body && node.callee.body.body && + node.callee.body.body.length === 1 && + node.callee.body.body[0].type === 'IfStatement') { + bodyNode = node.callee.body.body[0]; + //Look for a define(Identifier) case, but only if inside an + //if that has a define.amd test + if (bodyNode.consequent && bodyNode.consequent.body) { + exp = bodyNode.consequent.body; + if (Array.isArray(exp) && exp.length) exp = exp[0]; + + if (exp.type === 'ExpressionStatement' && exp.expression && + parse.hasDefine(exp.expression) && + exp.expression.arguments && + exp.expression.arguments.length === 1 && + exp.expression.arguments[0].type === 'Identifier') { + + //Calls define(Identifier) as first statement in body. + //Confirm the if test references define.amd + traverse(bodyNode.test, function (node) { + if (parse.refsDefineAmd(node)) { + refsDefine = true; + return false; + } + }); + + if (refsDefine) { + return onMatch("define", null, null, null, exp.expression, + exp.expression.arguments[0].name, fnExpScope); + } + } + } + } + }; + + /** + * Converts an AST node into a JS source string by extracting + * the node's location from the given contents string. Assumes + * meriyah.parseScript() with loc was done. + * @param {String} contents + * @param {Object} node + * @returns {String} a JS source string. + */ + parse.nodeToString = function (contents, node) { + var extracted, + loc = node.loc, + lines = contents.split('\n'), + firstLine = loc.start.line > 1 ? + lines.slice(0, loc.start.line - 1).join('\n') + '\n' : + '', + preamble = firstLine + + lines[loc.start.line - 1].substring(0, loc.start.column); + + if (loc.start.line === loc.end.line) { + extracted = lines[loc.start.line - 1].substring(loc.start.column, + loc.end.column); + } else { + extracted = lines[loc.start.line - 1].substring(loc.start.column) + + '\n' + + lines.slice(loc.start.line, loc.end.line - 1).join('\n') + + '\n' + + lines[loc.end.line - 1].substring(0, loc.end.column); + } + + return { + value: extracted, + range: [ + preamble.length, + preamble.length + extracted.length + ] + }; + }; + +// return parse; +// }); diff --git a/src/build/amodro-trace/lib/transform.ts b/src/build/amodro-trace/lib/transform.ts new file mode 100644 index 000000000..645d3da94 --- /dev/null +++ b/src/build/amodro-trace/lib/transform.ts @@ -0,0 +1,464 @@ +// Taken from r.js, preserving its style for now to easily port changes in the +// near term. +// var define = function(ary, fn) { +// export default fn.apply(undefined, +// (ary.map(function(id) { return require(id); }))); +// }; + +import * as meriyah from 'meriyah'; +import { lang } from './lang'; +import { parse } from './parse'; + +/** + * @license Copyright (c) 2012-2015, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/jrburke/requirejs for details + */ + +/*global define */ + +'use strict'; +var jsExtRegExp = /\.js$/g, + baseIndentRegExp = /^([ \t]+)/, + indentRegExp = /\{[\r\n]+([ \t]+)/, + keyRegExp = /^[_A-Za-z]([A-Za-z\d_]*)$/, + bulkIndentRegExps = { + '\n': /\n/g, + '\r\n': /\r\n/g + }; + +function applyIndent(str, indent, lineReturn) { + var regExp = bulkIndentRegExps[lineReturn]; + return str.replace(regExp, '$&' + indent); +} + +export class transform { + static toTransport(namespace, moduleName, path, contents, onFound, options) { + options = options || {}; + + var astRoot, contentLines, modLine, + foundAnon, + scanCount = 0, + scanReset = false, + defineInfos = [], + applySourceUrl = function (contents) { + if (options.useSourceUrl) { + contents = 'eval("' + lang.jsEscape(contents) + + '\\n//# sourceURL=' + (path.indexOf('/') === 0 ? '' : '/') + + path + + '");\n'; + } + return contents; + }; + + try { + astRoot = meriyah.parseScript(contents, { + loc: true, + next: true, + webcompat: true + }); + } catch (e) { + var logger = options.logger; + if (logger && logger.warn) { + if (jsExtRegExp.test(path)) { + logger.warn('toTransport skipping ' + path + + ': ' + e.toString()); + } + } + return contents; + } + + //Find the define calls and their position in the files. + (parse as any).traverse(astRoot, function (node) { + var args, firstArg, firstArgLoc, factoryNode, + needsId, depAction, foundId, init, + sourceUrlData, range, + namespaceExists = false; + + // If a bundle script with a define declaration, do not + // parse any further at this level. Likely a built layer + // by some other tool. + if (node.type === 'VariableDeclarator' && + node.id && node.id.name === 'define' && + node.id.type === 'Identifier') { + init = node.init; + if (init && init.callee && + init.callee.type === 'CallExpression' && + init.callee.callee && + init.callee.callee.type === 'Identifier' && + init.callee.callee.name === 'require' && + init.callee.arguments && init.callee.arguments.length === 1 && + init.callee.arguments[0].type === 'Literal' && + init.callee.arguments[0].value && + init.callee.arguments[0].value.indexOf('amdefine') !== -1) { + // the var define = require('amdefine')(module) case, + // keep going in that case. + } else { + return false; + } + } + + namespaceExists = namespace && + node.type === 'CallExpression' && + node.callee && node.callee.object && + node.callee.object.type === 'Identifier' && + node.callee.object.name === namespace && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'define'; + + if (namespaceExists ||(parse as any).isDefineNodeWithArgs(node)) { + //The arguments are where its at. + args = node.arguments; + if (!args || !args.length) { + return; + } + + firstArg = args[0]; + firstArgLoc = firstArg.loc; + + if (args.length === 1) { + if (firstArg.type === 'Identifier') { + //The define(factory) case, but + //only allow it if one Identifier arg, + //to limit impact of false positives. + needsId = true; + depAction = 'empty'; + } else if ((parse as any).isFnExpression(firstArg)) { + //define(function(){}) + factoryNode = firstArg; + needsId = true; + depAction = 'scan'; + } else if (firstArg.type === 'ObjectExpression') { + //define({}); + needsId = true; + depAction = 'skip'; + } else if (firstArg.type === 'Literal' && + typeof firstArg.value === 'number') { + //define(12345); + needsId = true; + depAction = 'skip'; + } else if (firstArg.type === 'UnaryExpression' && + firstArg.operator === '-' && + firstArg.argument && + firstArg.argument.type === 'Literal' && + typeof firstArg.argument.value === 'number') { + //define('-12345'); + needsId = true; + depAction = 'skip'; + } else if (firstArg.type === 'MemberExpression' && + firstArg.object && + firstArg.property && + firstArg.property.type === 'Identifier') { + //define(this.key); + needsId = true; + depAction = 'empty'; + } + } else if (firstArg.type === 'ArrayExpression') { + //define([], ...); + needsId = true; + depAction = 'skip'; + } else if (firstArg.type === 'Literal' && + typeof firstArg.value === 'string') { + //define('string', ....) + //Already has an ID. + foundId = firstArg.value; + needsId = false; + if (args.length === 2 && + (parse as any).isFnExpression(args[1])) { + //Needs dependency scanning. + factoryNode = args[1]; + depAction = 'scan'; + } else { + depAction = 'skip'; + } + } else { + //Unknown define entity, keep looking, even + //in the subtree for this node. + return; + } + + range = { + foundId: foundId, + needsId: needsId, + depAction: depAction, + namespaceExists: namespaceExists, + node: node, + defineLoc: node.loc, + firstArgLoc: firstArgLoc, + factoryNode: factoryNode, + sourceUrlData: sourceUrlData + }; + + //Only transform ones that do not have IDs. If it has an + //ID but no dependency array, assume it is something like + //a phonegap implementation, that has its own internal + //define that cannot handle dependency array constructs, + //and if it is a named module, then it means it has been + //set for transport form. + if (range.needsId) { + if (foundAnon) { + var logger = options.logger; + if (logger && logger.warn) { + logger.warn(path + ' has more than one anonymous ' + + 'define. May be a built file from another ' + + 'build system like, Ender. Skipping normalization.'); + } + defineInfos = []; + return false; + } else { + foundAnon = range; + defineInfos.push(range); + } + } else if (depAction === 'scan') { + scanCount += 1; + if (scanCount > 1) { + //Just go back to an array that just has the + //anon one, since this is an already optimized + //file like the phonegap one. + if (!scanReset) { + defineInfos = foundAnon ? [foundAnon] : []; + scanReset = true; + } + } else { + defineInfos.push(range); + } + } else { + // need to pass foundId to onFound callback + defineInfos.push(range); + } + } + }); + + + if (!defineInfos.length) { + return applySourceUrl(contents); + } + + //Reverse the matches, need to start from the bottom of + //the file to modify it, so that the ranges are still true + //further up. + defineInfos.reverse(); + + contentLines = contents.split('\n'); + + modLine = function (loc, contentInsertion) { + var startIndex = loc.start.column, + //start.line is 1-based, not 0 based. + lineIndex = loc.start.line - 1, + line = contentLines[lineIndex]; + contentLines[lineIndex] = line.substring(0, startIndex) + + contentInsertion + + line.substring(startIndex, + line.length); + }; + + defineInfos.forEach(function (info) { + var deps, + contentInsertion = '', + depString = ''; + + //Do the modifications "backwards", in other words, start with the + //one that is farthest down and work up, so that the ranges in the + //defineInfos still apply. So that means deps, id, then namespace. + if (info.needsId && moduleName) { + contentInsertion += "'" + moduleName + "',"; + } + + if (info.depAction === 'scan') { + deps =(parse as any).getAnonDepsFromNode(info.factoryNode); + + if (deps.length) { + depString = '[' + deps.map(function (dep) { + return "'" + dep + "'"; + }) + ']'; + } else { + depString = '[]'; + } + depString += ','; + + if (info.factoryNode) { + //Already have a named module, need to insert the + //dependencies after the name. + modLine(info.factoryNode.loc, depString); + } else { + contentInsertion += depString; + } + } + + if (contentInsertion) { + modLine(info.firstArgLoc, contentInsertion); + } + + //Do namespace last so that ui does not mess upthe parenRange + //used above. + if (namespace && !info.namespaceExists) { + modLine(info.defineLoc, namespace + '.'); + } + + //Notify any listener for the found info + if (onFound) { + onFound(info); + } + }); + + contents = contentLines.join('\n'); + + return applySourceUrl(contents); + } + + /** + * Modify the contents of a require.config/requirejs.config call. This + * call will LOSE any existing comments that are in the config string. + * + * @param {String} fileContents String that may contain a config call + * @param {Function} onConfig Function called when the first config + * call is found. It will be passed an Object which is the current + * config, and the onConfig function should return an Object to use + * as the config. + * @return {String} the fileContents with the config changes applied. + */ + static modifyConfig(fileContents, onConfig) { + var details =(parse as any).findConfig(fileContents), + config = details.config; + + if (config) { + config = onConfig(config); + if (config) { + return transform.serializeConfig(config, + fileContents, + details.range[0], + details.range[1], + { + quote: details.quote + }); + } + } + + return fileContents; + } + + static serializeConfig(config, fileContents, start, end, options) { + //Calculate base level of indent + var indent, match, configString, outDentRegExp, + baseIndent = '', + startString = fileContents.substring(0, start), + existingConfigString = fileContents.substring(start, end), + lineReturn = existingConfigString.indexOf('\r') === -1 ? '\n' : '\r\n', + lastReturnIndex = startString.lastIndexOf('\n'); + + //Get the basic amount of indent for the require config call. + if (lastReturnIndex === -1) { + lastReturnIndex = 0; + } + + match = baseIndentRegExp.exec(startString.substring(lastReturnIndex + 1, start)); + if (match && match[1]) { + baseIndent = match[1]; + } + + //Calculate internal indentation for config + match = indentRegExp.exec(existingConfigString); + if (match && match[1]) { + indent = match[1]; + } + + if (!indent || indent.length < baseIndent) { + indent = ' '; + } else { + indent = indent.substring(baseIndent.length); + } + + outDentRegExp = new RegExp('(' + lineReturn + ')' + indent, 'g'); + + configString = transform.objectToString(config, { + indent: indent, + lineReturn: lineReturn, + outDentRegExp: outDentRegExp, + quote: options && options.quote + }); + + //Add in the base indenting level. + configString = applyIndent(configString, baseIndent, lineReturn); + + return startString + configString + fileContents.substring(end); + } + + /** + * Tries converting a JS object to a string. This will likely suck, and + * is tailored to the type of config expected in a loader config call. + * So, hasOwnProperty fields, strings, numbers, arrays and functions, + * no weird recursively referenced stuff. + * @param {Object} obj the object to convert + * @param {Object} options options object with the following values: + * {String} indent the indentation to use for each level + * {String} lineReturn the type of line return to use + * {outDentRegExp} outDentRegExp the regexp to use to outdent functions + * {String} quote the quote type to use, ' or ". Optional. Default is " + * @param {String} totalIndent the total indent to print for this level + * @return {String} a string representation of the object. + */ + static objectToString(obj, options, totalIndent?) { + var startBrace, endBrace, nextIndent, + first = true, + value: string | number | boolean = '', + lineReturn = options.lineReturn, + indent = options.indent, + outDentRegExp = options.outDentRegExp, + quote = options.quote || '"'; + + totalIndent = totalIndent || ''; + nextIndent = totalIndent + indent; + + if (obj === null) { + value = 'null'; + } else if (obj === undefined) { + value = 'undefined'; + } else if (typeof obj === 'number' || typeof obj === 'boolean') { + value = obj; + } else if (typeof obj === 'string') { + //Use double quotes in case the config may also work as JSON. + value = quote + lang.jsEscape(obj) + quote; + } else if (lang.isArray(obj)) { + lang.each(obj, function (item, i) { + value += (i !== 0 ? ',' + lineReturn : '' ) + + nextIndent + + transform.objectToString(item, + options, + nextIndent); + }); + + startBrace = '['; + endBrace = ']'; + } else if (lang.isFunction(obj) || lang.isRegExp(obj)) { + //The outdent regexp just helps pretty up the conversion + //just in node. Rhino strips comments and does a different + //indent scheme for Function toString, so not really helpful + //there. + value = obj.toString().replace(outDentRegExp, '$1'); + } else { + //An object + lang.eachProp(obj, function (v, prop) { + value += (first ? '': ',' + lineReturn) + + nextIndent + + (keyRegExp.test(prop) ? prop : quote + lang.jsEscape(prop) + quote )+ + ': ' + + transform.objectToString(v, + options, + nextIndent); + first = false; + }); + startBrace = '{'; + endBrace = '}'; + } + + if (startBrace) { + value = startBrace + + lineReturn + + value + + lineReturn + totalIndent + + endBrace; + } + + return value; + } +}; diff --git a/lib/build/amodro-trace/read/cjs.js b/src/build/amodro-trace/read/cjs.ts similarity index 76% rename from lib/build/amodro-trace/read/cjs.js rename to src/build/amodro-trace/read/cjs.ts index 58769c9e8..637083923 100644 --- a/lib/build/amodro-trace/read/cjs.js +++ b/src/build/amodro-trace/read/cjs.ts @@ -2,7 +2,7 @@ * @license Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. */ -var parse = require('../lib/parse'); +import { parse } from '../lib/parse'; // modified to add forceWrap for dealing with // nodjs dist/commonjs/*js or dist/cjs/*.js @@ -10,13 +10,13 @@ var parse = require('../lib/parse'); // node_modules/@aspnet/signalr/dist/cjs/IHubProtocol.js // node_modules/@aspnet/signalr/dist/cjs/ILogger.js // https://github.com/requirejs/r.js/issues/980 -module.exports = function cjs(fileName, fileContents, forceWrap) { +export function cjs(fileName: string, fileContents: string, forceWrap: boolean) { // Strip out comments. var preamble = '', - commonJsProps = parse.usesCommonJs(fileName, fileContents); + commonJsProps = (parse as any).usesCommonJs(fileName, fileContents); // First see if the module is not already RequireJS-formatted. - if (!forceWrap && (parse.usesAmdOrRequireJs(fileName, fileContents) || !commonJsProps)) { + if (!forceWrap && ((parse as any).usesAmdOrRequireJs(fileName, fileContents) || !commonJsProps)) { return fileContents; } diff --git a/src/build/amodro-trace/read/es.ts b/src/build/amodro-trace/read/es.ts new file mode 100644 index 000000000..bd6b46750 --- /dev/null +++ b/src/build/amodro-trace/read/es.ts @@ -0,0 +1,11 @@ +import { transformSync } from '@babel/core'; +import * as amdPlugin from '@babel/plugin-transform-modules-amd'; + +// use babel to translate native es module into AMD module +export function es(fileName: string, fileContents: string, inputSourceMap: object | boolean): { code: string; map?: object } { + return transformSync(fileContents, { + babelrc: false, + plugins: [[amdPlugin, {loose: true}]], + inputSourceMap: inputSourceMap, + }); +}; diff --git a/lib/build/amodro-trace/write/all.js b/src/build/amodro-trace/write/all.ts similarity index 78% rename from lib/build/amodro-trace/write/all.js rename to src/build/amodro-trace/write/all.ts index dd757db6e..03ae05f53 100644 --- a/lib/build/amodro-trace/write/all.js +++ b/src/build/amodro-trace/write/all.ts @@ -1,10 +1,10 @@ // The order of these transforms is informed by how they were done in the // requirejs optimizer. -var transforms = [ - require('./stubs'), - require('./defines'), - require('./replace') -]; +import { stubs } from './stubs'; +import { defines} from './defines'; +import { replace } from './replace'; + +var transforms = [ stubs, defines, replace ]; /** * Chains all the default set of transforms to return one function to be used @@ -15,11 +15,11 @@ var transforms = [ * @return {Function} A function that can be used for multiple content transform * calls. */ -module.exports = function all(options) { +export function all(options) { options = options || {}; var transformFns = transforms.map(function(transform) { - return transform(options); + return (transform as any)(options); }); return function(context, moduleName, filePath, contents) { diff --git a/lib/build/amodro-trace/write/defines.js b/src/build/amodro-trace/write/defines.ts similarity index 89% rename from lib/build/amodro-trace/write/defines.js rename to src/build/amodro-trace/write/defines.ts index 1f6f298b4..451ae533a 100644 --- a/lib/build/amodro-trace/write/defines.js +++ b/src/build/amodro-trace/write/defines.ts @@ -1,7 +1,8 @@ -var lang = require('../lib/lang'), - parse = require('../lib/parse'), - transform = require('../lib/transform'), - falseProp = lang.falseProp, +import { lang } from '../lib/lang'; +import { parse } from '../lib/parse'; +import { transform } from '../lib/transform'; + +var falseProp = lang.falseProp, getOwn = lang.getOwn, makeJsArrayString = lang.makeJsArrayString; @@ -20,11 +21,11 @@ var lang = require('../lib/lang'), * @return {Function} A function that can be used for multiple content transform * calls. */ -function defines(options) { +export function defines(options) { options = options || {}; - return function(context, moduleName, filePath, _contents) { - var namedModule, + return function(context: IBundleSourceContext, moduleName: string, filePath: string, _contents: string) { + let namedModule: string, config = context.config, packageName = context.pkgsMainMap[moduleName]; @@ -36,7 +37,7 @@ function defines(options) { ); } - function onFound(info) { + function onFound(info: { foundId?: string }) { if (info.foundId) { namedModule = info.foundId; } @@ -86,7 +87,7 @@ function defines(options) { contents += '\n' + 'define("' + moduleName + '", ' + (shim.deps && shim.deps.length ? makeJsArrayString(shim.deps) + ', ' : '') + - exportsFn(shim.exports) + + exportsFn(shim.exports, undefined) + ');\n'; } } else { @@ -105,7 +106,7 @@ function defines(options) { }; } -function exportsFn(_exports, wrapShim) { +function exportsFn(_exports: string, wrapShim: boolean) { if (_exports) { if (wrapShim) return 'return root.' + _exports + ' = ' + _exports +';'; else return '(function (global) {\n' + @@ -136,11 +137,9 @@ function exportsFn(_exports, wrapShim) { * @return {String} transformed content. May not be different from * the input contents string. */ -function toTransport(context, moduleName, - filePath, contents, onFound, options) { +function toTransport(context: IBundleSourceContext, moduleName: string, + filePath: string, contents: string, onFound: Function, options: unknown) { options = options || {}; return transform.toTransport('', moduleName, filePath, contents, onFound, options); }; - -module.exports = defines; diff --git a/lib/build/amodro-trace/write/replace.js b/src/build/amodro-trace/write/replace.ts similarity index 94% rename from lib/build/amodro-trace/write/replace.js rename to src/build/amodro-trace/write/replace.ts index 73afd3dde..7555928d8 100644 --- a/lib/build/amodro-trace/write/replace.js +++ b/src/build/amodro-trace/write/replace.ts @@ -4,15 +4,15 @@ // and also dep string cleanup // remove tailing '/', '.js' -const meriyah = require('meriyah'); -const astMatcher = require('../../ast-matcher').astMatcher; +import * as meriyah from 'meriyah'; +import { astMatcher } from '../../ast-matcher'; // it is definitely a named AMD module at this stage var amdDep = astMatcher('define(__str, [__anl_deps], __any)'); var cjsDep = astMatcher('require(__any_dep)'); var isUMD = astMatcher('typeof define === "function" && define.amd'); var isUMD2 = astMatcher('typeof define == "function" && define.amd'); -module.exports = function stubs(options) { +export function replace(options) { options = options || {}; return function(context, moduleName, filePath, contents) { diff --git a/lib/build/amodro-trace/write/stubs.js b/src/build/amodro-trace/write/stubs.ts similarity index 95% rename from lib/build/amodro-trace/write/stubs.js rename to src/build/amodro-trace/write/stubs.ts index 1a70fd226..49e712cd0 100644 --- a/lib/build/amodro-trace/write/stubs.js +++ b/src/build/amodro-trace/write/stubs.ts @@ -5,7 +5,7 @@ * @return {Function} A function that can be used for multiple content transform * calls. */ -module.exports = function stubs(options) { +export function stubs(options) { options = options || {}; return function(context, moduleName, filePath, contents) { diff --git a/lib/build/ast-matcher.js b/src/build/ast-matcher.ts similarity index 76% rename from lib/build/ast-matcher.js rename to src/build/ast-matcher.ts index a12765646..fcd2320b8 100644 --- a/lib/build/ast-matcher.js +++ b/src/build/ast-matcher.ts @@ -1,4 +1,4 @@ -const meriyah = require('meriyah'); +import * as meriyah from 'meriyah'; const STOP = false; const SKIP_BRANCH = 1; @@ -8,16 +8,16 @@ const IGNORED_KEYS = ['start', 'end', 'loc', 'location', 'locations', 'line', 'c // From an meriyah example for traversing its ast. // modified to support branch skip. -function traverse(object, visitor) { - let child; +function traverse(object: meriyah.ESTree.Node, visitor: (node: meriyah.ESTree.CallExpression) => void) { + let child: meriyah.ESTree.Node; if (!object) return; - let r = visitor.call(null, object); + const r = visitor.call(null, object); if (r === STOP) return STOP; // stop whole traverse immediately if (r === SKIP_BRANCH) return; // skip going into AST branch for (let i = 0, keys = Object.keys(object); i < keys.length; i++) { - let key = keys[i]; + const key = keys[i]; if (IGNORED_KEYS.indexOf(key) !== -1) continue; child = object[key]; @@ -34,8 +34,8 @@ const ANL = 2; const STR = 3; const ARR = 4; -function matchTerm(pattern) { - let possible; +function matchTerm(pattern: meriyah.ESTree.Node) { + let possible: string; if (pattern.type === 'Identifier') { possible = pattern.name.toString(); } else if (pattern.type === 'ExpressionStatement' && @@ -45,7 +45,7 @@ function matchTerm(pattern) { if (!possible || !possible.startsWith('__')) return; - let type; + let type: number | undefined; if (possible === '__any' || possible.startsWith('__any_')) { type = ANY; } else if (possible === '__anl' || possible.startsWith('__anl_')) { @@ -65,19 +65,19 @@ function matchTerm(pattern) { * @param part The target partial syntax tree * @return Returns named matches, or false. */ -exports.extract = function(pattern, part) { +export function extract(pattern: meriyah.ESTree.Node, part: meriyah.ESTree.Node): unknown | false { if (!pattern) throw new Error('missing pattern'); // no match if (!part) return STOP; - let term = matchTerm(pattern); + const term = matchTerm(pattern); if (term) { // if single __any if (term.type === ANY) { if (term.name) { // if __any_foo // get result {foo: astNode} - let r = {}; + const r = {}; r[term.name] = part; return r; } @@ -89,7 +89,7 @@ exports.extract = function(pattern, part) { if (part.type === 'Literal') { if (term.name) { // get result {foo: value} - let r = {}; + const r = {}; r[term.name] = part.value; return r; } @@ -107,16 +107,16 @@ exports.extract = function(pattern, part) { if (!Array.isArray(part)) return STOP; if (pattern.length === 1) { - let arrTerm = matchTerm(pattern[0]); + const arrTerm = matchTerm(pattern[0]); if (arrTerm) { // if single __arr_foo if (arrTerm.type === ARR) { // find all or partial Literals in an array - let arr = part.filter(it => it.type === 'Literal').map(it => it.value); + const arr = part.filter(it => it.type === 'Literal').map(it => it.value); if (arr.length) { if (arrTerm.name) { // get result {foo: array} - let r = {}; + const r = {}; r[arrTerm.name] = arr; return r; } @@ -128,7 +128,7 @@ exports.extract = function(pattern, part) { } else if (arrTerm.type === ANL) { if (arrTerm.name) { // get result {foo: nodes array} - let r = {}; + const r = {}; r[arrTerm.name] = part; return r; } @@ -145,14 +145,14 @@ exports.extract = function(pattern, part) { } } - let allResult = {}; + const allResult = {}; for (let i = 0, keys = Object.keys(pattern); i < keys.length; i++) { - let key = keys[i]; + const key = keys[i]; if (IGNORED_KEYS.indexOf(key) !== -1) continue; - let nextPattern = pattern[key]; - let nextPart = part[key]; + const nextPattern = pattern[key]; + const nextPart = part[key]; if (!nextPattern || typeof nextPattern !== 'object') { // primitive value. string or null @@ -162,7 +162,7 @@ exports.extract = function(pattern, part) { return STOP; } - const result = exports.extract(nextPattern, nextPart); + const result = extract(nextPattern, nextPart); // no match if (result === STOP) return STOP; if (result) Object.assign(allResult, result); @@ -176,15 +176,15 @@ exports.extract = function(pattern, part) { * @param pattern The pattern used on matching, can be a string or meriyah node * @return Returns an meriyah node to be used as pattern in extract(pattern, part) */ -exports.compilePattern = function(pattern) { +export function compilePattern(pattern: string | meriyah.ESTree.Node) { // pass meriyah syntax tree obj - if (pattern && pattern.type) return pattern; + if (typeof pattern !== 'string' && pattern.type) return pattern; if (typeof pattern !== 'string') { throw new Error('input pattern is neither a string nor an meriyah node.'); } - let exp = meriyah.parseScript(pattern, {next: true, webcompat: true}); + let exp: meriyah.ESTree.Program | meriyah.ESTree.Statement | meriyah.ESTree.Expression = meriyah.parseScript(pattern, {next: true, webcompat: true}); if (exp.type !== 'Program' || !exp.body) { throw new Error(`Not a valid expression: "${pattern}".`); @@ -204,10 +204,15 @@ exports.compilePattern = function(pattern) { return exp; }; -function ensureParsed(codeOrNode) { +function ensureParsed(codeOrNode: string | meriyah.ESTree.Node) { // bypass parsed node - if (codeOrNode && codeOrNode.type) return codeOrNode; - return meriyah.parseScript(codeOrNode, {next: true, webcompat: true}); + if (typeof codeOrNode !== 'string' && 'type' in codeOrNode) { + return codeOrNode; + } + if (typeof codeOrNode === 'string') { + return meriyah.parseScript(codeOrNode, {next: true, webcompat: true}); + } + throw new Error('unknown `codeOrNode` type.') } /** @@ -240,15 +245,16 @@ function ensureParsed(codeOrNode) { * {match: {foo: "d", opts: ["e"]}, node: } * ] */ -exports.astMatcher = function(pattern) { - let pat = exports.compilePattern(pattern); +export function astMatcher(pattern: string | meriyah.ESTree.Node) { + const pat = compilePattern(pattern); - return function(jsStr) { - let node = ensureParsed(jsStr); - let matches = []; + return function(jsStr: string | meriyah.ESTree.Node) { + const node = ensureParsed(jsStr); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const matches: { match: any, node: meriyah.ESTree.CallExpression}[] = []; traverse(node, n => { - let m = exports.extract(pat, n); + const m = extract(pat, n); if (m) { matches.push({ match: m, @@ -276,41 +282,41 @@ exports.astMatcher = function(pattern) { * * => ['a', 'b', './c', './d'] */ -exports.jsDepFinder = function() { - if (arguments.length === 0) { +export function jsDepFinder(...args: string[]) { + if (args.length === 0) { throw new Error('No patterns provided.'); } let seed = 0; - let patterns = Array.prototype.map.call(arguments, p => + const patterns = Array.prototype.map.call(args, p => // replace __dep and __deps into // __str_1, __str_2, __arr_3 // wantArr is the result of (s?) - exports.compilePattern(p.replace(/__dep(s?)/g, (m, wantArr) => + compilePattern(p.replace(/__dep(s?)/g, (m, wantArr) => (wantArr ? '__arr_' : '__str_') + (++seed) )) ); - let len = patterns.length; + const len = patterns.length; - return function(jsStr) { - let node = ensureParsed(jsStr); + return function(jsStr: string | meriyah.ESTree.Node) { + const node = ensureParsed(jsStr); - let deps = []; + const deps = []; // directly use extract() instead of astMatcher() // for efficiency traverse(node, n => { for (let i = 0; i < len; i += 1) { - let result = exports.extract(patterns[i], n); + const result = extract(patterns[i], n); if (result) { // result is like {"1": "dep1", "2": ["dep2", "dep3"]} // we only want values Object.keys(result).forEach(k => { - let d = result[k]; + const d = result[k]; if (typeof d === 'string') deps.push(d); - else deps.push.apply(deps, d); + else deps.push(...d); }); // found a match, don't try other pattern diff --git a/lib/build/bundle.js b/src/build/bundle.ts similarity index 63% rename from lib/build/bundle.js rename to src/build/bundle.ts index b8ef64c54..cca13cf70 100644 --- a/lib/build/bundle.js +++ b/src/build/bundle.ts @@ -1,17 +1,38 @@ -const path = require('path'); -const os = require('os'); -const Terser = require('terser'); -const Convert = require('convert-source-map'); -const Minimatch = require('minimatch').Minimatch; -const fs = require('../file-system'); -const SourceInclusion = require('./source-inclusion').SourceInclusion; -const DependencyInclusion = require('./dependency-inclusion').DependencyInclusion; -const Configuration = require('../configuration').Configuration; -const Utils = require('./utils'); -const logger = require('aurelia-logging').getLogger('Bundle'); - -exports.Bundle = class { - constructor(bundler, config) { +import { Configuration } from '../configuration'; +import { Bundler } from './bundler'; + +import * as path from 'node:path'; +import * as os from 'node:os'; +import * as Terser from 'terser'; +import * as Convert from 'convert-source-map'; +import { Minimatch } from 'minimatch' +import * as fs from '../file-system'; +import { SourceInclusion } from './source-inclusion'; +import { DependencyInclusion } from './dependency-inclusion'; +import * as Utils from './utils'; +import { DependencyDescription } from './dependency-description'; +import { getLogger } from 'aurelia-logging'; +import { BundledSource } from './bundled-source'; +const logger = getLogger('Bundle'); + + +export class Bundle { + public readonly bundler: Bundler; + public readonly includes: (SourceInclusion | DependencyInclusion)[]; + public readonly excludes: Minimatch[]; + + public requiresBuild: boolean; + public readonly config: AureliaJson.IBundle; + private readonly dependencies: DependencyDescription[]; // TODO: remove? + private readonly prepend: string[]; + private readonly append: string[]; + public readonly moduleId: string; + public hash: string; + private readonly aliases: {[key: string]: string}; + private readonly buildOptions: Configuration; + private readonly fileCache: {[key: string]: { contents: string, path: string }}; + + private constructor(bundler: Bundler, config: AureliaJson.IBundle) { this.bundler = bundler; this.config = config; this.dependencies = []; @@ -19,62 +40,63 @@ exports.Bundle = class { this.append = (config.append || []).filter(x => bundler.itemIncludedInBuild(x)).map(x => typeof x === 'string' ? x : x.path); this.moduleId = config.name.replace(path.extname(config.name), ''); this.hash = ''; - this.includes = (config.source || []); - this.excludes = []; + this.aliases = {}; - if (this.includes instanceof Array) { - this.includes = this.includes.map(x => new SourceInclusion(this, x)); + + const includes = (config.source || []); + if (includes instanceof Array) { + this.excludes = []; + this.includes = includes.map(x => new SourceInclusion(this, x)); } else { - this.excludes = (this.includes.exclude || []).map(x => this.createMatcher(x)); - this.includes = this.includes.include.map(x => new SourceInclusion(this, x)); + this.excludes = (includes.exclude || []).map(x => this.createMatcher(x)); + this.includes = includes.include.map(x => new SourceInclusion(this, x)); } this.buildOptions = new Configuration(config.options, bundler.buildOptions.getAllOptions()); this.requiresBuild = true; this.fileCache = {}; } - static create(bundler, config) { - let bundle = new exports.Bundle(bundler, config); - let dependencies = config.dependencies || []; + static async create(bundler: Bundler, config: AureliaJson.IBundle): Promise { + const bundle = new Bundle(bundler, config); + const dependencies = config.dependencies || []; // ignore dep config with main:false // main:false support has been removed in auto-tracing - let dependenciesToBuild = dependencies - .filter(x => bundler.itemIncludedInBuild(x) && x.main !== false); + const dependenciesToBuild = dependencies + .filter(x => bundler.itemIncludedInBuild(x) && (x as { main?: string | false}).main !== false); - return Utils.runSequentially( + const descriptions = await Utils.runSequentially( dependenciesToBuild, - dep => bundler.configureDependency(dep)) - .then(descriptions => { - return Utils.runSequentially( - descriptions, - description => { - logger.info(`Manually adding ${description.banner}`); - return bundle.addDependency(description); - } - ); - }) - .then(() => bundle); + dep => bundler.configureDependency(dep)); + await Utils.runSequentially( + descriptions, + description => { + logger.info(`Manually adding ${description.banner}`); + return bundle.addDependency(description); + } + ); + return bundle; } - createMatcher(pattern) { + createMatcher(pattern: string) { return new Minimatch(pattern, { dot: true }); } - addAlias(fromId, toId) { + addAlias(fromId: string, toId: string) { this.aliases[fromId] = toId; } - addDependency(description) { + async addDependency(description: DependencyDescription) { this.dependencies.push(description); - let inclusion = new DependencyInclusion(this, description); + const inclusion = new DependencyInclusion(this, description); this.includes.push(inclusion); - return inclusion.traceResources().then(() => inclusion); + await inclusion.traceResources(); + return inclusion; } - trySubsume(item) { - let includes = this.includes; + trySubsume(item: BundledSource) { + const includes = this.includes; for (let i = 0, ii = includes.length; i < ii; ++i) { if (includes[i].trySubsume(item)) { @@ -91,10 +113,10 @@ exports.Bundle = class { } getAliases() { - let aliases = Object.assign({}, this.aliases); + const aliases = Object.assign({}, this.aliases); this.includes.forEach(inclusion => { - if (inclusion.conventionalAliases) { + if (inclusion instanceof DependencyInclusion && inclusion.conventionalAliases) { Object.assign(aliases, inclusion.conventionalAliases()); } }); @@ -103,16 +125,16 @@ exports.Bundle = class { } getRawBundledModuleIds() { - let allModuleIds = new Set(this.includes.reduce((a, b) => a.concat(b.getAllModuleIds()), [])); + const allModuleIds = new Set(this.includes.reduce((a, b) => a.concat(b.getAllModuleIds()), [])); Object.keys(this.getAliases()).forEach(d => allModuleIds.add(d)); return allModuleIds; } getBundledModuleIds() { - let allModuleIds = this.getRawBundledModuleIds(); - let allIds = []; + const allModuleIds = this.getRawBundledModuleIds(); + const allIds: string[] = []; Array.from(allModuleIds).sort().forEach(id => { - let matchingPlugin = this.bundler.loaderOptions.plugins.find(p => p.matches(id)); + const matchingPlugin = this.bundler.loaderOptions.plugins.find(p => p.matches(id)); if (matchingPlugin) { // make sure text! prefix is added, requirejs needs full form. // http://requirejs.org/docs/api.html#config-bundles @@ -136,8 +158,8 @@ exports.Bundle = class { // https://github.com/aurelia/cli/issues/955#issuecomment-439253048 // Topological sort for shim packages. - let bundleFiles = uniqueBy( - this.includes.reduce((a, b) => a.concat(b.getAllFiles()), []), + const bundleFiles = uniqueBy( + this.includes.reduce((a, b) => a.concat(b.getAllFiles()), []), file => file.path ).sort((a, b) => { // alphabetical sorting based on moduleId @@ -189,15 +211,15 @@ exports.Bundle = class { return [...special, ...sorted]; } - write(platform) { + write(platform: AureliaJson.ITarget) { if (!this.requiresBuild) { return Promise.resolve(); } let work = Promise.resolve(); - let loaderOptions = this.bundler.loaderOptions; - let buildOptions = this.buildOptions; - let files = []; + const loaderOptions = this.bundler.loaderOptions; + const buildOptions = this.buildOptions; + let files: IFile[] = []; if (this.prepend.length) { work = work.then(() => addFilesInOrder(this, this.prepend, files)); @@ -210,7 +232,7 @@ exports.Bundle = class { }); } - let bundleFiles = this.getBundledFiles(); + const bundleFiles = this.getBundledFiles(); // If file like jquery does AMD define by itself: define('jquery', ...), // which bypass writeTransform lib/build/amodro-trace/write/defines, @@ -222,10 +244,10 @@ exports.Bundle = class { // with systemjs, second definition overwrites first one. if (loaderOptions.type !== 'system') { - work = work.then(() => files = files.concat(bundleFiles)); + work = work.then(() => { files = files.concat(bundleFiles); }); } - let aliases = this.getAliases(); + const aliases = this.getAliases(); if (Object.keys(aliases).length) { // a virtual prepend file contains nodejs module aliases // for instance: @@ -236,7 +258,7 @@ exports.Bundle = class { let fromModuleId = fromId; let toModuleId = aliases[fromId]; - let matchingPlugin = this.bundler.loaderOptions.plugins.find(p => p.matches(fromId)); + const matchingPlugin = this.bundler.loaderOptions.plugins.find(p => p.matches(fromId)); if (matchingPlugin) { fromModuleId = matchingPlugin.createModuleId(fromModuleId); toModuleId = matchingPlugin.createModuleId(toModuleId); @@ -251,7 +273,7 @@ exports.Bundle = class { } if (loaderOptions.type === 'system') { - work = work.then(() => files = files.concat(bundleFiles)); + work = work.then(() => { files = files.concat(bundleFiles); }); } if (this.append.length) { @@ -259,27 +281,25 @@ exports.Bundle = class { } return work.then(async () => { - const Concat = require('concat-with-sourcemaps'); - let concat = new Concat(true, this.config.name, ';' + os.EOL); - const generateHashedPath = require('./utils').generateHashedPath; - const generateHash = require('./utils').generateHash; + const { default: Concat } = await import('concat-with-sourcemaps'); + const concat = new Concat(true, this.config.name, ';' + os.EOL); let needsSourceMap = false; for (let i = 0; i < files.length; ++i) { - let currentFile = files[i]; - let sourceMapEnabled = buildOptions.isApplicable('sourcemaps'); + const currentFile = files[i]; + const sourceMapEnabled = buildOptions.isApplicable('sourcemaps'); let sourceMap = sourceMapEnabled && currentFile.sourceMap ? JSON.parse(JSON.stringify(currentFile.sourceMap)) : undefined; - let parsedPath = currentFile.path && path.parse(currentFile.path); + const parsedPath = currentFile.path && path.parse(currentFile.path); - function acquireSourceMapForDependency(file) { + function acquireSourceMapForDependency(file: IFile) { if (!file || !file.path) { - return; + return null; } try { - let base64SourceMap = Convert.fromSource(file.contents.toString()); + const base64SourceMap = Convert.fromSource(file.contents.toString()); if (base64SourceMap) { return null; @@ -289,7 +309,7 @@ exports.Bundle = class { return null; } - let converter; + let converter: Convert.SourceMapConverter | null; try { converter = Convert.fromMapFileSource(file.contents.toString(), (filename) => @@ -342,41 +362,38 @@ exports.Bundle = class { } let bundleMap; - let contents = concat.content; + let contents: string | Buffer = concat.content; let bundleFileName = this.config.name; if (loaderOptions.configTarget === this.config.name) { //Add to the config bundle the loader config. Can't change index.html yet because we haven't generated hashes for all the files - concat.add(undefined, this.writeLoaderCode(platform)); + concat.add(undefined, await this.writeLoaderCode(platform)); contents = concat.content; if (buildOptions.isApplicable('rev')) { //Generate a unique hash based off of the bundle contents //Must generate hash after we write the loader config so that any other bundle changes (hash changes) can cause a new hash for the vendor file - this.hash = generateHash(concat.content).slice(0, 10); - bundleFileName = generateHashedPath(this.config.name, this.hash); + this.hash = Utils.generateHash(concat.content).slice(0, 10); + bundleFileName = Utils.generateHashedPath(this.config.name, this.hash); } } else if (buildOptions.isApplicable('rev')) { //Generate a unique hash based off of the bundle contents //Must generate hash after we write the loader config so that any other bundle changes (hash changes) can cause a new hash for the vendor file - this.hash = generateHash(concat.content).slice(0, 10); - bundleFileName = generateHashedPath(this.config.name, this.hash); + this.hash = Utils.generateHash(concat.content).slice(0, 10); + bundleFileName = Utils.generateHashedPath(this.config.name, this.hash); } - let mapFileName = bundleFileName + '.map'; - let mapSourceRoot = path.posix.relative( - path.posix.join(process.cwd(), platform.output), - process.cwd() - ); + const mapFileName = bundleFileName + '.map'; + const mapSourceRoot = calculateRelativeSourceMapsRoot(process.cwd(), platform.output); logger.info(`Writing ${bundleFileName}...`); if (buildOptions.isApplicable('bundleReport')) { - let sbuffer = []; - let jsonRep = {}; + const sbuffer = []; + const jsonRep = {}; sbuffer.push('>> ' + bundleFileName + ' total size : ' + (contents.length / 1024).toFixed(2) + 'kb, containing ' + files.length + ' files'); - let sortedFiles = files.sort((a, b) => { + const sortedFiles = files.sort((a, b) => { if (a.contents.length > b.contents.length) { return -1; } @@ -386,7 +403,7 @@ exports.Bundle = class { return 0; }); for (let i = 0; i < sortedFiles.length; ++i) { - let currentFile = sortedFiles[i]; + const currentFile = sortedFiles[i]; sbuffer.push('> ' + (currentFile.contents.length / 1024).toFixed(2) + 'kb (' + ((currentFile.contents.length / contents.length) * 100).toFixed(2) + '%) - ' + (currentFile.path || currentFile.contents.slice(0, 200))); if (currentFile.path) { jsonRep[currentFile.path] = jsonRep[currentFile.path] || {}; @@ -396,8 +413,8 @@ exports.Bundle = class { } logger.info(`Writing bundle reports for ${bundleFileName}...`); - fs.writeFile(`bundle-report-${bundleFileName}.txt`, sbuffer.join('\n')); - fs.writeFile(`bundle-report-${bundleFileName}.json`, JSON.stringify(jsonRep, null, 2)); + await fs.writeFile(`bundle-report-${bundleFileName}.txt`, sbuffer.join('\n')); + await fs.writeFile(`bundle-report-${bundleFileName}.json`, JSON.stringify(jsonRep, null, 2)); } if (buildOptions.isApplicable('minify')) { @@ -405,9 +422,9 @@ exports.Bundle = class { // https://github.com/fabiosantoscode/terser#terser-fast-minify-mode // It's a good balance on size and speed to turn off compress. // Turn off compress also bypasses https://github.com/terser-js/terser/issues/120 - let minificationOptions = {compress: false}; + const minificationOptions: Terser.MinifyOptions = {compress: false}; - let minifyOptions = buildOptions.getValue('minify'); + const minifyOptions = buildOptions.getValue('minify'); if (typeof minifyOptions === 'object') { Object.assign(minificationOptions, minifyOptions); } @@ -421,19 +438,28 @@ exports.Bundle = class { }; } - let minificationResult = await Terser.minify(String(contents), minificationOptions); + const minificationResult = await Terser.minify(String(contents), minificationOptions); contents = minificationResult.code; - bundleMap = needsSourceMap ? Convert.fromJSON(minificationResult.map).toObject() : undefined; + if (needsSourceMap){ + if (typeof minificationResult.map !== 'string'){ + console.error('`minificationResult.map` should be string!', minificationOptions); + throw new Error('`minificationResult.map` should be string!'); + } + bundleMap = Convert.fromJSON(minificationResult.map).toObject(); + } } else if (needsSourceMap) { bundleMap = Convert.fromJSON(concat.sourceMap) .setProperty('sourceRoot', mapSourceRoot) .toObject(); + if (typeof contents !== 'string') { + contents = contents.toString(); + } contents += os.EOL + '//# sourceMappingURL=' + path.basename(mapFileName); } - return fs.writeFile(path.posix.join(platform.output, bundleFileName), contents).then(() => { + return fs.writeFile(path.posix.join(platform.output, bundleFileName), contents).then(async () => { this.requiresBuild = false; if (bundleMap) { @@ -443,10 +469,11 @@ exports.Bundle = class { delete bundleMap.sourceRoot; bundleMap.sources = bundleMap.sources.map(s => path.posix.join(sourceRoot, s)); } - return fs.writeFile(path.posix.join(platform.output, mapFileName), JSON.stringify(bundleMap)) - .catch(() => { - logger.error(`Unable to write the sourcemap to ${path.posix.join(platform.output, mapFileName)}`); - }); + try { + return await fs.writeFile(path.posix.join(platform.output, mapFileName), JSON.stringify(bundleMap)); + } catch { + logger.error(`Unable to write the sourcemap to ${path.posix.join(platform.output, mapFileName)}`); + } } }).catch(e => { logger.error(`Unable to write the bundle to ${path.posix.join(platform.output, bundleFileName)}`); @@ -460,30 +487,31 @@ exports.Bundle = class { }); } - getFileFromCacheOrLoad(x) { + async getFileFromCacheOrLoad(x: string) { let found = this.fileCache[x]; if (found) { - return Promise.resolve(found); + return found; } - return fs.readFile(x).then(data => { - found = { contents: data.toString(), path: x }; + try { + const data = await fs.readFile(x); + found = { contents: data, path: x }; this.fileCache[x] = found; return found; - }).catch(e => { + } catch (e) { logger.error(`Error while trying to read ${x}`); throw e; - }); + } } - writeLoaderCode(platform) { - const createLoaderCode = require('./loader').createLoaderCode; - let config = createLoaderCode(platform, this.bundler); + async writeLoaderCode(platform: AureliaJson.ITarget) { + const createLoaderCode = (await import('./loader')).createLoaderCode; + const config = createLoaderCode(platform, this.bundler); return 'function _aureliaConfigureModuleLoader(){' + config + '}'; } - async writeBundlePathsToIndex(platform) { + async writeBundlePathsToIndex(platform: AureliaJson.ITarget) { try { if (!platform.index) { return; @@ -506,28 +534,45 @@ exports.Bundle = class { } }; -function addFilesInOrder(bundle, paths, files) { +function addFilesInOrder(bundle: Bundle, paths: string[], files: IFile[]): Promise { let index = -1; - function addFile() { + async function addFile(): Promise { index++; if (index < paths.length) { - return bundle.getFileFromCacheOrLoad(paths[index]) - .then(file => files.push(file)) - .then(addFile); + const file = await bundle.getFileFromCacheOrLoad(paths[index]); + files.push(file); + return addFile(); } - return Promise.resolve(); + return; } return addFile(); } -function uniqueBy(collection, key) { - const seen = {}; +function uniqueBy(collection: T[], key: (item: T) => PropertyKey) { + const seen: Partial<{ [K in keyof T]: boolean}> = {}; return collection.filter((item) => { const k = key(item); return seen.hasOwnProperty(k) ? false : (seen[k] = true); }); } + +/** + * Returns a POSIX-style relative path from `outputDir` back to `projectRoot`. + * Works on Windows, macOS and Linux. + * + * @param {string} projectRootDir - The root directory of the project. + * @param {string} outputDir - The output directory where the files are generated. + * @returns {string} A POSIX-style relative path from `outputDir` to `projectRoot`. + */ +function calculateRelativeSourceMapsRoot(projectRootDir: string, outputDir: string): string { + const absoluteProjectRootDir = path.resolve(projectRootDir.split('\\').join('/')); + const absoluteOutputDir = path.resolve(absoluteProjectRootDir, outputDir.split('\\').join('/')); + + return path.relative(absoluteOutputDir, absoluteProjectRootDir).split('\\').join('/'); +} + +export const _calculateRelativeSourceMapsRoot = calculateRelativeSourceMapsRoot; diff --git a/lib/build/bundled-source.js b/src/build/bundled-source.ts similarity index 71% rename from lib/build/bundled-source.js rename to src/build/bundled-source.ts index 15e562a69..43455121c 100644 --- a/lib/build/bundled-source.js +++ b/src/build/bundled-source.ts @@ -1,14 +1,27 @@ -const path = require('path'); -const findDeps = require('./find-deps').findDeps; -const cjsTransform = require('./amodro-trace/read/cjs'); -const esTransform = require('./amodro-trace/read/es'); -const allWriteTransforms = require('./amodro-trace/write/all'); -const Utils = require('./utils'); -const logger = require('aurelia-logging').getLogger('BundledSource'); -const { getAliases, toDotDot } = require('./module-id-processor'); - -exports.BundledSource = class { - constructor(bundler, file) { +import * as path from 'node:path'; +import { findDeps } from './find-deps'; +import { cjs as cjsTransform } from './amodro-trace/read/cjs'; +import { es as esTransform } from './amodro-trace/read/es'; +import { all as allWriteTransforms } from './amodro-trace/write/all'; +import * as Utils from './utils'; +import { getAliases, toDotDot } from './module-id-processor'; +import { getLogger } from 'aurelia-logging'; +import { type Bundler } from './bundler'; +import { type Bundle } from './bundle'; +import { type SourceInclusion } from './source-inclusion'; +const logger = getLogger('BundledSource'); + +export class BundledSource { + private readonly bundler: Bundler; + private file: IFile; + public includedIn: Bundle | null; + public includedBy: SourceInclusion | null; + private _contents: string | null; + private _transformedSourceMap = undefined; + private requiresTransform: boolean; + private _moduleId: string | undefined; + + constructor(bundler: Bundler, file: IFile) { this.bundler = bundler; this.file = file; this.includedIn = null; @@ -18,14 +31,14 @@ exports.BundledSource = class { } get sourceMap() { - return this.file.sourceMap; + return this._transformedSourceMap || this.file.sourceMap; } get path() { return this.file.path; } - set contents(value) { + set contents(value: string) { this._contents = value; } @@ -42,39 +55,39 @@ exports.BundledSource = class { } } - _getProjectRoot() { + private _getProjectRoot() { return this.bundler.project.paths.root; } - _getLoaderPlugins() { + private _getLoaderPlugins() { return this.bundler.loaderOptions.plugins; } - _getLoaderType() { + private _getLoaderType() { return this.bundler.loaderOptions.type; } - _getLoaderConfig() { + private _getLoaderConfig() { return this.bundler.loaderConfig; } - _getUseCache() { + private _getUseCache() { return this.bundler.buildOptions.isApplicable('cache'); } get moduleId() { if (this._moduleId) return this._moduleId; - let dependencyInclusion = this.dependencyInclusion; - let projectRoot = this._getProjectRoot(); - let moduleId; + const dependencyInclusion = this.dependencyInclusion; + const projectRoot = this._getProjectRoot(); + let moduleId: string; if (dependencyInclusion) { - let loaderConfig = dependencyInclusion.description.loaderConfig; - let root = path.resolve(projectRoot, loaderConfig.path); + const loaderConfig = dependencyInclusion.description.loaderConfig; + const root = path.resolve(projectRoot, loaderConfig.path); moduleId = path.join(loaderConfig.name, this.path.replace(root, '')); } else { - let modulePath = path.relative(projectRoot, this.path); + const modulePath = path.relative(projectRoot, this.path); moduleId = path.normalize(modulePath); } @@ -87,7 +100,7 @@ exports.BundledSource = class { return this._moduleId; } - update(file) { + update(file: IFile) { this.file = file; this._contents = null; this.requiresTransform = true; @@ -103,15 +116,15 @@ exports.BundledSource = class { return; } - let dependencyInclusion = this.dependencyInclusion; - let browserReplacement = dependencyInclusion && + const dependencyInclusion = this.dependencyInclusion; + const browserReplacement = dependencyInclusion && dependencyInclusion.description.browserReplacement(); - let loaderPlugins = this._getLoaderPlugins(); - let loaderType = this._getLoaderType(); - let loaderConfig = this._getLoaderConfig(); - let moduleId = this.moduleId; - let modulePath = this.path; + const loaderPlugins = this._getLoaderPlugins(); + const loaderType = this._getLoaderType(); + const loaderConfig = this._getLoaderConfig(); + const moduleId = this.moduleId; + const modulePath = this.path; getAliases(moduleId, loaderConfig.paths).forEach(alias => { this.bundler.configTargetBundle.addAlias(alias.fromId, alias.toId); @@ -119,9 +132,9 @@ exports.BundledSource = class { logger.debug(`Tracing ${moduleId}`); - let deps; + let deps: string[]; - let matchingPlugin = loaderPlugins.find(p => p.matches(modulePath)); + const matchingPlugin = loaderPlugins.find(p => p.matches(modulePath)); if (path.extname(modulePath).toLowerCase() === '.json') { // support text! prefix @@ -137,17 +150,17 @@ exports.BundledSource = class { } else { deps = []; - let context = {pkgsMainMap: {}, config: {shim: {}}}; - let desc = dependencyInclusion && dependencyInclusion.description; + const context: IBundleSourceContext = {pkgsMainMap: {}, config: {shim: {}}}; + const desc = dependencyInclusion && dependencyInclusion.description; if (desc && desc.mainId === moduleId) { // main file of node package context.pkgsMainMap[moduleId] = desc.name; } let wrapShim = false; - let replacement = {}; + const replacement = {}; if (dependencyInclusion) { - let description = dependencyInclusion.description; + const description = dependencyInclusion.description; if (description.loaderConfig.deps || description.loaderConfig.exports) { context.config.shim[description.name] = { @@ -158,7 +171,7 @@ exports.BundledSource = class { if (description.loaderConfig.deps) { // force deps for shimed package - deps.push.apply(deps, description.loaderConfig.deps); + deps.push(...description.loaderConfig.deps); } if (description.loaderConfig.wrapShim) { @@ -167,8 +180,8 @@ exports.BundledSource = class { if (browserReplacement) { for (let i = 0, keys = Object.keys(browserReplacement); i < keys.length; i++) { - let key = keys[i]; - let target = browserReplacement[key]; + const key = keys[i]; + const target = browserReplacement[key]; const baseId = description.name + '/index'; const sourceModule = key.startsWith('.') ? @@ -215,12 +228,13 @@ exports.BundledSource = class { if (cache) { this.contents = cache.contents; + this._transformedSourceMap = cache.transformedSourceMap; deps = cache.deps; } else { let contents; // forceCjsWrap bypasses a r.js parse bug. // See lib/amodro-trace/read/cjs.js for more info. - let forceCjsWrap = !!modulePath.match(/(\/|\\)(cjs|commonjs)(\/|\\)/i) || + const forceCjsWrap = !!modulePath.match(/(\/|\\)(cjs|commonjs)(\/|\\)/i) || // core-js uses "var define = ..." everywhere, we need to force cjs // before we can switch to dumberjs bundler (desc && desc.name === 'core-js'); @@ -230,7 +244,16 @@ exports.BundledSource = class { } catch { // file is not in amd/cjs format, try native es module try { - contents = esTransform(modulePath, this.contents); + let inputSourceMap: boolean | object = false; + if (this.file.sourceMap) { + inputSourceMap = typeof this.file.sourceMap === 'string' ? JSON.parse(this.file.sourceMap) : this.file.sourceMap; + } + const result = esTransform(modulePath, this.contents, inputSourceMap); + + contents = result.code; + if (result.map) { + this._transformedSourceMap = result.map; + } } catch (e) { logger.error('Could not convert to AMD module, skipping ' + modulePath); logger.error('Error was: ' + e); @@ -243,7 +266,7 @@ exports.BundledSource = class { const tracedDeps = findDeps(modulePath, contents, loaderType); if (tracedDeps && tracedDeps.length) { - deps.push.apply(deps, tracedDeps); + deps.push(...tracedDeps); } if (deps) { let needsCssInjection = false; @@ -269,7 +292,8 @@ exports.BundledSource = class { if (useCache && hash) { Utils.setCache(hash, { deps: deps, - contents: this.contents + contents: this.contents, + transformedSourceMap: !this._transformedSourceMap ? undefined : this._transformedSourceMap // avoid serializing `null` }); } } @@ -278,7 +302,7 @@ exports.BundledSource = class { this.requiresTransform = false; if (!deps || deps.length === 0) return; - let needed = new Set(); + const needed = new Set(); Array.from(new Set(deps)) // unique .map(d => { @@ -320,10 +344,10 @@ exports.BundledSource = class { } }; -function absoluteModuleId(baseId, moduleId) { +function absoluteModuleId(baseId: string, moduleId: string) { if (moduleId[0] !== '.') return moduleId; - let parts = baseId.split('/'); + const parts = baseId.split('/'); parts.pop(); moduleId.split('/').forEach(p => { @@ -338,20 +362,20 @@ function absoluteModuleId(baseId, moduleId) { return parts.join('/'); } -function relativeModuleId(baseId, moduleId) { +function relativeModuleId(baseId: string, moduleId: string) { if (moduleId[0] === '.') return moduleId; - let baseParts = baseId.split('/'); + const baseParts = baseId.split('/'); baseParts.pop(); - let parts = moduleId.split('/'); + const parts = moduleId.split('/'); while (parts.length && baseParts.length && baseParts[0] === parts[0]) { baseParts.shift(); parts.shift(); } - let left = baseParts.length; + const left = baseParts.length; if (left === 0) { parts.unshift('.'); } else { diff --git a/src/build/bundler.ts b/src/build/bundler.ts new file mode 100644 index 000000000..1327fbe77 --- /dev/null +++ b/src/build/bundler.ts @@ -0,0 +1,423 @@ +import { Bundle } from './bundle'; +import { BundledSource } from './bundled-source'; +import { CLIOptions } from '../cli-options'; +import { LoaderPlugin } from './loader-plugin'; +import { Configuration } from '../configuration'; +import * as path from 'node:path'; +import * as fs from '../file-system'; +import * as Utils from './utils'; +import { getLogger } from 'aurelia-logging'; +import { stubModule } from './stub-module'; + +import { type PackageAnalyzer } from './package-analyzer'; +import { type PackageInstaller } from './package-installer'; +import { type SourceInclusion } from './source-inclusion'; +import { type DependencyDescription } from './dependency-description'; +import { type DependencyInclusion } from './dependency-inclusion'; +import { type LoaderOptions } from './loader'; + +/** + * onRequiringModule callback is called before auto-tracing on a moduleId. It would not be called for any modules provided by app's src files or explicit dependencies config in aurelia.json. + + * Three types possible result (all can be returned in promise): + * 1. Boolean false: ignore this moduleId; + * 2. Array of strings like ['a', 'b']: require module id "a" and "b" instead; + * 3. A string: the full JavaScript content of this module + * 4. All other returns are ignored and go onto performing auto-tracing. + * + * Usage example in applications `build.ts` file: + * + * function writeBundles() { + * return buildCLI.dest({ + * // use onRequiringModule to ignore tracing "template/**\/*" + * onRequiringModule: moduleId => { + * if (moduleId.startsWith("template/")) { + * return false; + * } + * } + * }); + * + */ +export type BuildOptions = { onRequiringModule?: onRequiringModuleCallback; onNotBundled?: onNotBundledCallback }; +type onRequiringModuleResult = boolean | string | string[] | Promise +type onRequiringModuleCallback = (moduleId: string) => onRequiringModuleResult; +type onNotBundledCallback = (items: BundledSource[]) => void; + +const logger = getLogger('Bundler'); + +export class Bundler{ + public project: AureliaJson.IProject; + private packageAnalyzer: PackageAnalyzer; + private packageInstaller: PackageInstaller; + public readonly bundles: Bundle[]; + private readonly itemLookup: {[key: string]: BundledSource}; + private readonly items: BundledSource[]; + private readonly environment: string; + private readonly autoInstall: boolean; + private triedAutoInstalls: Set; + public buildOptions: Configuration; + public loaderOptions: LoaderOptions; + public loaderConfig: AureliaJson.ILoaderConfig; + public configTargetBundle: Bundle; + + constructor(project: AureliaJson.IProject, packageAnalyzer: PackageAnalyzer, packageInstaller: PackageInstaller) { + this.project = project; + this.packageAnalyzer = packageAnalyzer; + this.packageInstaller = packageInstaller; + this.bundles = []; + this.itemLookup = {}; + this.items = []; + this.environment = CLIOptions.getEnvironment(); + // --auto-install is checked here instead of in app's tasks/run.js + // this enables all existing apps to use this feature. + this.autoInstall = CLIOptions.hasFlag('auto-install'); + this.triedAutoInstalls = new Set(); + + const defaultBuildOptions = { + minify: 'stage & prod', + sourcemaps: 'dev & stage', + rev: false + }; + + this.buildOptions = new Configuration(project.build.options, defaultBuildOptions); + + this.loaderConfig = { + baseUrl: project.paths.root, + paths: ensurePathsRelativelyFromRoot(project.paths), + packages: [], + stubModules: [], + shim: {} + }; + + Object.assign(this.loaderConfig, this.project.build.loader.config); + + const plugins = (project.build.loader.plugins || []).map(x => { + const plugin = new LoaderPlugin(project.build.loader.type, x); + + if (plugin.stub && this.loaderConfig.stubModules.indexOf(plugin.name) === -1) { + this.loaderConfig.stubModules.push(plugin.name); + } + + return plugin; + }); + this.loaderOptions = {...project.build.loader, plugins}; + } + + static async create(project: AureliaJson.IProject, packageAnalyzer: PackageAnalyzer, packageInstaller: PackageInstaller) { + const bundler = new Bundler(project, packageAnalyzer, packageInstaller); + + await Promise.all( + project.build.bundles.map(x => Bundle.create(bundler, x).then(bundle => { + bundler.addBundle(bundle); + })) + ); + //Order the bundles so that the bundle containing the config is processed last. + if (bundler.bundles.length) { + const configTargetBundleIndex = bundler.bundles.findIndex(x_1 => x_1.config.name === bundler.loaderOptions.configTarget); + bundler.bundles.splice(bundler.bundles.length, 0, bundler.bundles.splice(configTargetBundleIndex, 1)[0]); + bundler.configTargetBundle = bundler.bundles[bundler.bundles.length - 1]; + } + return bundler; + } + + itemIncludedInBuild(item: string | { env?: string}) { + if (typeof item === 'string' || !item.env) { + return true; + } + + const value = item.env; + const parts = value.split('&').map(x => x.trim().toLowerCase()); + + return parts.indexOf(this.environment) !== -1; + } + + addFile(file: IFile, inclusion?: SourceInclusion) { + const key = normalizeKey(file.path); + let found = this.itemLookup[key]; + + if (!found) { + found = new BundledSource(this, file); + this.itemLookup[key] = found; + this.items.push(found); + } + + if (inclusion) { + inclusion.addItem(found); + } else { + subsume(this.bundles, found); + } + + return found; + } + + updateFile(file: IFile, inclusion?: SourceInclusion) { + const found = this.itemLookup[normalizeKey(file.path)]; + + if (found) { + found.update(file); + } else { + this.addFile(file, inclusion); + } + } + + addBundle(bundle: Bundle) { + this.bundles.push(bundle); + } + + async configureDependency(dependency: ILoaderConfig | string): Promise { + try { + return await analyzeDependency(this.packageAnalyzer, dependency); + } catch (e) { + const nodeId = typeof dependency === 'string' ? dependency : dependency.name; + + if (this.autoInstall && !this.triedAutoInstalls.has(nodeId)) { + this.triedAutoInstalls.add(nodeId); + await this.packageInstaller.install([nodeId]) + // try again after install + return await this.configureDependency(nodeId); + } + + logger.error(`Unable to analyze ${nodeId}`); + logger.info(e); + throw e; + } + } + + async build(opts?: BuildOptions) { + let onRequiringModule: onRequiringModuleCallback | undefined, + onNotBundled: onNotBundledCallback | undefined; + + if (opts?.onRequiringModule && typeof opts.onRequiringModule === 'function') { + onRequiringModule = opts.onRequiringModule; + } + if (opts?.onNotBundled && typeof opts.onNotBundled === 'function') { + onNotBundled = opts.onNotBundled; + } + + const doTransform = async (initSet?: Iterable): Promise => { + const deps = new Set(initSet); + + this.items.forEach(item => { + // Transformed items will be ignored + // by flag item.requiresTransform. + const _deps = item.transform(); + if (_deps) _deps.forEach(d => deps.add(d)); + }); + + if (deps.size) { + // removed all fulfilled deps + this.bundles.forEach(bundle => { + // Only need to check raw module ids, not nodeIdCompat aliases. + // Because deps here are clean module ids. + bundle.getRawBundledModuleIds().forEach(id => { + deps.delete(id); + + if (id.endsWith('/index')) { + // if id is 'resources/index', shortId is 'resources'. + const shortId = id.slice(0, -6); + if (deps.delete(shortId)) { + // ok, someone try to use short name + bundle.addAlias(shortId, id); + } + } + }); + }); + } + + if (deps.size) { + const _leftOver = new Set(); + + await Utils.runSequentially( + Array.from(deps).sort(), + async (d) => { + try { + const result = await (onRequiringModule?.(d) ?? undefined); + + // ignore this module id + if (result === false) return; + + // require other module ids instead + if (Array.isArray(result) && result.length) { + result.forEach(dd => _leftOver.add(dd)); + return; + } + + // got full content of this module + if (typeof result === 'string') { + let fakeFilePath = path.resolve(this.project.paths.root, d); + const ext = path.extname(d).toLowerCase(); + if (!ext || Utils.knownExtensions.indexOf(ext) === -1) { + fakeFilePath += '.js'; + } + // we use '/' as separator even on Windows + // because module id is using '/' as separator + this.addFile({ + path: fakeFilePath, + contents: result + }); + return; + } + + // process normally if result is not recognizable + await this.addMissingDep(d); + } catch (err) { + logger.error(err); + await this.addMissingDep(d); + } + } + ); + + await doTransform(_leftOver); + } + }; + + logger.info('Tracing files ...'); + + try { + await doTransform(); + + if (onNotBundled) { + const notBundled = this.items.filter(t => !t.includedIn); + if (notBundled.length) onNotBundled(notBundled); + } + } catch (e) { + logger.error('Failed to do transforms'); + logger.info(e); + throw e; + } + } + + async write() { + await Promise.all(this.bundles.map(bundle => bundle.write(this.project.build.targets[0]))); + for (let i = this.bundles.length; i--;) { + await this.bundles[i].writeBundlePathsToIndex(this.project.build.targets[0]); + } + } + + getDependencyInclusions() { + return this.bundles.reduce((a, b) => a.concat(b.getDependencyInclusions()), []); + } + + async addMissingDep(id: string) { + const localFilePath = path.resolve(this.project.paths.root, id); + + // load additional local file missed by gulp tasks, + // this could be json/yaml file that user wanted in + // aurelia.json 'text!' plugin + if (Utils.couldMissGulpPreprocess(id) && fs.existsSync(localFilePath)) { + this.addFile({ + path: localFilePath, + contents: fs.readFileSync(localFilePath) + }); + return; + } + + await this.addNpmResource(id); + } + + async addNpmResource(id: string) { + // match scoped npm module and normal npm module + const match = id.match(/^((?:@[^/]+\/[^/]+)|(?:[^@][^/]*))(\/.+)?$/); + + if (!match) { + logger.error(`Not valid npm module Id: ${id}`); + return; + } + + const nodeId = match[1]; + const resourceId = match[2] && match[2].slice(1); + + const depInclusion = this.getDependencyInclusions().find(di => di.description.name === nodeId); + + if (depInclusion) { + if (resourceId) { + return depInclusion.traceResource(resourceId); + } + + return depInclusion.traceMain(); + } + + const stub = await stubModule(nodeId, this.project.paths.root); + if (typeof stub === 'string') { + this.addFile({ + path: path.resolve(this.project.paths.root, nodeId + '.js'), + contents: stub + }); + return; + } + + try { + const description = await this.configureDependency(stub || nodeId); + + if (resourceId) { + description.loaderConfig.lazyMain = true; + } + + if (stub) { + logger.info(`Auto stubbing module: ${nodeId}`); + } else { + logger.info(`Auto tracing ${description.banner}`); + } + + const inclusion = await this.configTargetBundle.addDependency(description); + + // Now dependencyInclusion is created + // Try again to use magical traceResource + if (resourceId) { + return await inclusion.traceResource(resourceId); + } + } catch (e) { + logger.error('Failed to add Nodejs module ' + id); + logger.info(e); + // don't stop + } + } +}; + +function analyzeDependency(packageAnalyzer: PackageAnalyzer, dependency: ILoaderConfig | string) { + if (typeof dependency === 'string') { + return packageAnalyzer.analyze(dependency); + } + + return packageAnalyzer.reverseEngineer(dependency); +} + +function subsume(bundles: Bundle[], item: BundledSource) { + for (let i = 0, ii = bundles.length; i < ii; ++i) { + if (bundles[i].trySubsume(item)) { + return; + } + } + logger.warn(item.path + ' is not captured by any bundle file. You might need to adjust the bundles source matcher in aurelia.json.'); +} + +function normalizeKey(p: string) { + return path.normalize(p); +} + +function ensurePathsRelativelyFromRoot(p: AureliaJson.IPaths) { + const keys = Object.keys(p); + const original = JSON.stringify(p, null, 2); + let warn = false; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key !== 'root' && p[key].indexOf(p.root + '/') === 0) { + warn = true; + p[key] = p[key].slice(p.root.length + 1); + } + // trim off last '/' + if (p[key].endsWith('/')) { + p[key] = p[key].slice(0, -1); + } + } + + if (warn) { + logger.warn('Warning: paths in the "paths" object in aurelia.json must be relative from the root path. Change '); + logger.warn(original); + logger.warn('to: '); + logger.warn(JSON.stringify(p, null, 2)); + } + + return p; +} diff --git a/lib/build/dependency-description.js b/src/build/dependency-description.ts similarity index 68% rename from lib/build/dependency-description.js rename to src/build/dependency-description.ts index 5c3aaf5d1..0eae916af 100644 --- a/lib/build/dependency-description.js +++ b/src/build/dependency-description.ts @@ -1,9 +1,17 @@ -const path = require('path'); -const fs = require('../file-system'); -const Utils = require('./utils'); +import * as path from 'node:path'; +import * as fs from '../file-system'; +import * as Utils from './utils'; -exports.DependencyDescription = class { - constructor(name, source) { +export class DependencyDescription { + public name: string; + public source: string | undefined; + + public location: string | undefined; + public loaderConfig: ILoaderConfig | undefined; + public metadata: { version: string, browser: string } | undefined; + public metadataLocation: string | undefined; + + constructor(name: string, source?: string) { this.name = name; this.source = source; } @@ -18,11 +26,11 @@ exports.DependencyDescription = class { return `package: ${version}${' '.repeat(version.length < 10 ? (10 - version.length) : 0)} ${name}`; } - calculateMainPath(root) { - let config = this.loaderConfig; + calculateMainPath(root: string) { + const config = this.loaderConfig; let part = path.join(config.path, config.main); - let ext = path.extname(part).toLowerCase(); + const ext = path.extname(part).toLowerCase(); if (!ext || Utils.knownExtensions.indexOf(ext) === -1) { part = part + '.js'; } @@ -30,8 +38,8 @@ exports.DependencyDescription = class { return path.join(process.cwd(), root, part); } - readMainFileSync(root) { - let p = this.calculateMainPath(root); + readMainFileSync(root: string) { + const p = this.calculateMainPath(root); try { return fs.readFileSync(p).toString(); @@ -47,13 +55,13 @@ exports.DependencyDescription = class { // string browser field is handled in package-analyzer if (!browser || typeof browser === 'string') return; - let replacement = {}; + const replacement = {}; for (let i = 0, keys = Object.keys(browser); i < keys.length; i++) { - let key = keys[i]; + const key = keys[i]; // leave {".": "dist/index.js"} for main replacement if (key === '.') continue; - let target = browser[key]; + const target = browser[key]; let sourceModule = filePathToModuleId(key); @@ -76,7 +84,7 @@ exports.DependencyDescription = class { } }; -function filePathToModuleId(filePath) { +function filePathToModuleId(filePath: string) { let moduleId = path.normalize(filePath).replace(/\\/g, '/'); if (moduleId.toLowerCase().endsWith('.js')) { diff --git a/lib/build/dependency-inclusion.js b/src/build/dependency-inclusion.ts similarity index 64% rename from lib/build/dependency-inclusion.js rename to src/build/dependency-inclusion.ts index 83e21bf24..64a191855 100644 --- a/lib/build/dependency-inclusion.js +++ b/src/build/dependency-inclusion.ts @@ -1,13 +1,20 @@ -const path = require('path'); -const SourceInclusion = require('./source-inclusion').SourceInclusion; -const { minimatch } = require('minimatch'); -const Utils = require('./utils'); -const logger = require('aurelia-logging').getLogger('DependencyInclusion'); +import * as path from 'node:path'; +import { SourceInclusion } from './source-inclusion'; +import { minimatch } from 'minimatch'; +import * as Utils from './utils'; +import { Bundle } from './bundle'; +import { getLogger } from 'aurelia-logging'; +import { type DependencyDescription } from './dependency-description'; +const logger = getLogger('DependencyInclusion'); const knownNonJsExtensions = ['.json', '.css', '.svg', '.html']; -exports.DependencyInclusion = class { - constructor(bundle, description) { +export class DependencyInclusion { + private bundle: Bundle; + public description: DependencyDescription; + private mainTraced: boolean; + + constructor(bundle: Bundle, description: DependencyDescription) { this.bundle = bundle; this.description = description; this.mainTraced = false; @@ -17,9 +24,9 @@ exports.DependencyInclusion = class { if (this.mainTraced) return Promise.resolve(); this.mainTraced = true; - let mainId = this.description.mainId; - let ext = path.extname(mainId).toLowerCase(); - let mainIsJs = !ext || knownNonJsExtensions.indexOf(ext) === -1; + const mainId = this.description.mainId; + const ext = path.extname(mainId).toLowerCase(); + const mainIsJs = !ext || knownNonJsExtensions.indexOf(ext) === -1; if (mainIsJs || ext === path.extname(this.description.name).toLowerCase()) { // only create alias when main is js file @@ -45,8 +52,8 @@ exports.DependencyInclusion = class { work = work.then(() => this.traceMain()); } - let loaderConfig = this.description.loaderConfig; - let resources = loaderConfig.resources; + const loaderConfig = this.description.loaderConfig; + const resources = loaderConfig.resources; if (resources) { resources.forEach(x => { @@ -57,8 +64,8 @@ exports.DependencyInclusion = class { return work; } - traceResource(resource) { - let resolved = resolvedResource(resource, this.description, this._getProjectRoot()); + traceResource(resource: string) { + const resolved = resolvedResource(resource, this.description, this._getProjectRoot()); if (!resolved) { logger.error(`Error: could not find "${resource}" in package ${this.description.name}`); @@ -73,7 +80,8 @@ exports.DependencyInclusion = class { ); } - let covered = this.bundle.includes.find(inclusion => + const covered = this.bundle.includes.find(inclusion => + inclusion instanceof SourceInclusion && inclusion.includedBy === this && minimatch(resolved, inclusion.pattern) ); @@ -85,12 +93,12 @@ exports.DependencyInclusion = class { return this._tracePattern(resolved); } - _tracePattern(resource) { - let loaderConfig = this.description.loaderConfig; - let bundle = this.bundle; - let pattern = path.join(loaderConfig.path, resource); - let inclusion = new SourceInclusion(bundle, pattern, this); - let promise = inclusion.addAllMatchingResources(); + _tracePattern(resource: string) { + const loaderConfig = this.description.loaderConfig; + const bundle = this.bundle; + const pattern = path.join(loaderConfig.path, resource); + const inclusion = new SourceInclusion(bundle, pattern, this); + const promise = inclusion.addAllMatchingResources(); bundle.includes.push(inclusion); bundle.requiresBuild = true; return promise; @@ -100,27 +108,27 @@ exports.DependencyInclusion = class { // create conventional aliases like: // define('package/foo/bar', ['package/dist/type/foo/bar'], function(m) {return m;}); conventionalAliases() { - let ids = []; + const ids: string[] = []; this.bundle.includes.forEach(inclusion => { - if (inclusion.includedBy === this) { - ids.push.apply(ids, inclusion.getAllModuleIds()); + if (inclusion instanceof SourceInclusion && inclusion.includedBy === this) { + ids.push(...inclusion.getAllModuleIds()); } }); if (ids.length < 2) return {}; - let nameLength = this.description.name.length; + const nameLength = this.description.name.length; - let commonLen = commonLength(ids); + const commonLen = commonLength(ids); if (!commonLen || commonLen <= nameLength + 1 ) return {}; - let aliases = {}; + const aliases: {[key: string]: string} = {}; ids.forEach(id => { // for aurelia-templating-resources/dist/commonjs/if // compact name is aurelia-templating-resources/if - let compactResource = id.slice(commonLen); + const compactResource = id.slice(commonLen); if (compactResource) { - let compactId = this.description.name + '/' + compactResource; + const compactId = this.description.name + '/' + compactResource; aliases[compactId] = id; } }); @@ -147,9 +155,9 @@ exports.DependencyInclusion = class { } }; -function resolvedResource(resource, description, projectRoot) { +function resolvedResource(resource: string, description: DependencyDescription, projectRoot: string) { const base = path.resolve(projectRoot, description.loaderConfig.path); - let mainShift = description.loaderConfig.main.split('/'); + const mainShift = description.loaderConfig.main.split('/'); // when mainShift is [dist,commonjs] // try dist/commonjs/resource first @@ -173,21 +181,21 @@ function resolvedResource(resource, description, projectRoot) { return resolved; } -function validResource(resource, base) { +function validResource(resource: string, base: string) { const resourcePath = path.resolve(base, resource); const loaded = Utils.nodejsLoad(resourcePath); if (loaded) return path.relative(base, loaded).replace(/\\/g, '/'); } -function commonLength(ids) { - let parts = ids[0].split('/'); - let rest = ids.slice(1); +function commonLength(ids: string[]) { + const parts = ids[0].split('/'); + const rest = ids.slice(1); parts.pop(); // ignore last part let common = ''; for (let i = 0, len = parts.length; i < len; i++) { - let all = common + parts[i] + '/'; + const all = common + parts[i] + '/'; if (rest.every(id => id.startsWith(all))) { common = all; } else { diff --git a/lib/build/find-deps.js b/src/build/find-deps.ts similarity index 78% rename from lib/build/find-deps.js rename to src/build/find-deps.ts index eb84f8f94..e7d778306 100644 --- a/lib/build/find-deps.js +++ b/src/build/find-deps.ts @@ -1,12 +1,10 @@ -const meriyah = require('meriyah'); -const parse = require('./amodro-trace/lib/parse'); -const am = require('./ast-matcher'); -const jsDepFinder = am.jsDepFinder; -const astMatcher = am.astMatcher; -const htmlparser = require('htmlparser2'); -const path = require('path'); -const fs = require('../file-system'); -const Utils = require('./utils'); +import * as meriyah from 'meriyah'; +import { parse } from './amodro-trace/lib/parse'; +import { jsDepFinder, astMatcher} from './ast-matcher'; +import * as htmlparser from 'htmlparser2'; +import * as path from 'node:path'; +import * as fs from '../file-system'; +import * as Utils from './utils'; const amdNamedDefine = jsDepFinder( 'define(__dep, __any)', @@ -86,12 +84,12 @@ const auConfigModuleNames = { // https://github.com/aurelia/framework/pull/851 const auDevLogWithOptionalLevel = astMatcher('__any.developmentLogging(__any)'); -const auConfigureDepFinder = function(contents) { +const auConfigureDepFinder = function(contents: meriyah.ESTree.Program) { // the way to find configure function is not waterproof let configFunc; _checkConfigureFunc.find(check => { - let m = check(contents); + const m = check(contents); // only want single configure func if (m && m.length === 1) { configFunc = m[0]; @@ -101,17 +99,17 @@ const auConfigureDepFinder = function(contents) { if (!configFunc) return []; - let auVar = configFunc.match.auVar.name; + const auVar = configFunc.match.auVar.name; - let configureFuncBody = { + const configureFuncBody: meriyah.ESTree.BlockStatement = { type: 'BlockStatement', // The matched body is an array, wrap them under single node, // so that I don't need to call forEach to deal with them. - body: configFunc.match.body + body: configFunc.match.body as meriyah.ESTree.Statement[] }; let isLikelyAureliaConfigFile; - let isAureliaMainFile = !!(astMatcher(`${auVar}.start()`)(contents)); + const isAureliaMainFile = !!(astMatcher(`${auVar}.start()`)(contents)); if (!isAureliaMainFile) { // an aurelia plugin entry file is likely to call one of @@ -121,15 +119,15 @@ const auConfigureDepFinder = function(contents) { astMatcher(`${auVar}.plugin(__anl)`)(contents)); } - let deps = new Set(); - let add = _add.bind(deps); + const deps = new Set(); + const add = _add.bind(deps); if (isAureliaMainFile) { - let match = _methodCall(configureFuncBody); + const match = _methodCall(configureFuncBody); if (match) { // track aurelia dependency based on user configuration. match.forEach(m => { - let methodName = m.match.method.name; + const methodName = m.match.method.name; if (auConfigModuleNames.hasOwnProperty(methodName)) { auConfigModuleNames[methodName].forEach(add); } @@ -151,10 +149,10 @@ const auConfigureDepFinder = function(contents) { // if (environment.testing) { // aurelia.use.plugin('aurelia-testing'); // } - let allIfs = _findIf(configureFuncBody); + const allIfs = _findIf(configureFuncBody); if (allIfs) { allIfs.forEach(m => { - let volatileDeps = _auConfigureDeps(m.node); + const volatileDeps = _auConfigureDeps(m.node); volatileDeps.forEach(d => deps.delete(d)); }); } @@ -171,20 +169,20 @@ const inlineViewExtract = jsDepFinder( '__any.inlineView(__dep, __any)' ); -const auInlineViewDepsFinder = function(contents, loaderType) { - let match = inlineViewExtract(contents); +const auInlineViewDepsFinder = function(contents: meriyah.ESTree.Program, loaderType: LoaderType) { + const match = inlineViewExtract(contents); if (match.length === 0) return []; // If user accidentally calls inlineView more than once, // aurelia renders first inlineView without any complain. // But this assumes there is only one custom element // class implementation in current js file. - return exports.findHtmlDeps('', match[0], loaderType); + return findHtmlDeps('', match[0], loaderType); }; // helper to add deps to a set // accepts string, or array, or set. -function _add(deps) { +function _add(deps: string | string[]) { if (!deps) return; if (typeof deps === 'string') deps = [deps]; @@ -209,29 +207,29 @@ function _add(deps) { }); } -function isPackageName(id) { +function isPackageName(id: string) { if (id.startsWith('.')) return false; const parts = id.split('/'); // package name, or scope package name return parts.length === 1 || (parts.length === 2 && parts[0].startsWith('@')); } -function auDep(dep, loaderType) { +function auDep(dep: string, loaderType: LoaderType) { if (!dep) return dep; - let ext = path.extname(dep).toLowerCase(); + const ext = path.extname(dep).toLowerCase(); if (ext === '.html' || ext === '.css') { return Utils.moduleIdWithPlugin(dep, 'text', loaderType); } return dep; } -exports.findJsDeps = function(filename, contents, loaderType = 'require') { - let deps = new Set(); - let add = _add.bind(deps); +export function findJsDeps(filename: string, contents: string, loaderType: LoaderType = 'require') { + const deps = new Set(); + const add = _add.bind(deps); // for all following static analysis, // only parse once for efficiency - let parsed = meriyah.parseScript(contents, {next: true, webcompat: true}); + const parsed = meriyah.parseScript(contents, {next: true, webcompat: true}); add(parse.findDependencies(filename, parsed)); // clear commonjs wrapper deps @@ -250,8 +248,8 @@ exports.findJsDeps = function(filename, contents, loaderType = 'require') { add(auInlineViewDepsFinder(parsed, loaderType)); // aurelia view convention, try foo.html for every foo.js - let fileParts = path.parse(filename); - let htmlPair = fileParts.name + '.html'; + const fileParts = path.parse(filename); + const htmlPair = fileParts.name + '.html'; if (fs.existsSync(fileParts.dir + path.sep + htmlPair)) { add(auDep('./' + htmlPair, loaderType)); } @@ -259,12 +257,12 @@ exports.findJsDeps = function(filename, contents, loaderType = 'require') { return Array.from(deps); }; -exports.findHtmlDeps = function(filename, contents, loaderType = 'require') { - let deps = new Set(); - let add = _add.bind(deps); +export function findHtmlDeps(filename: string, contents: string, loaderType: LoaderType = 'require') { + const deps = new Set(); + const add = _add.bind(deps); - let parser = new htmlparser.Parser({ - onopentag: function(name, attrs) { + const parser = new htmlparser.Parser({ + onopentag: function(name: string, attrs: Record) { // if ((name === 'require' || name === 'import') && attrs.from) { add(auDep(attrs.from, loaderType)); @@ -285,13 +283,13 @@ exports.findHtmlDeps = function(filename, contents, loaderType = 'require') { return Array.from(deps); }; -exports.findDeps = function(filename, contents, loaderType = 'require') { - let ext = path.extname(filename).toLowerCase(); +export function findDeps(filename: string, contents: string, loaderType: LoaderType = 'require') { + const ext = path.extname(filename).toLowerCase(); if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { - return exports.findJsDeps(filename, contents, loaderType); + return findJsDeps(filename, contents, loaderType); } else if (ext === '.html' || ext === '.htm') { - return exports.findHtmlDeps(filename, contents, loaderType); + return findHtmlDeps(filename, contents, loaderType); } return []; diff --git a/src/build/index.ts b/src/build/index.ts new file mode 100644 index 000000000..5741af7ea --- /dev/null +++ b/src/build/index.ts @@ -0,0 +1,91 @@ +import { Transform, type TransformCallback } from 'node:stream'; +import { Bundler, type BuildOptions } from './bundler'; +import { PackageAnalyzer } from './package-analyzer'; +import { PackageInstaller } from './package-installer'; +import { cacheDir } from './utils'; +import * as fs from 'node:fs'; + +let bundler: Bundler | undefined; +let project: AureliaJson.IProject | undefined; +let isUpdating = false; + +export async function src(p: AureliaJson.IProject) { + if (bundler) { + isUpdating = true; + return bundler; + } + + project = p; + const b = await Bundler.create( + project, + new PackageAnalyzer(project), + new PackageInstaller(project) + ); + return bundler = b; +}; + +export async function createLoaderCode(p?: AureliaJson.IProject) { + const createLoaderCode = (await import('./loader')).createLoaderCode; + project = p || project; + await buildLoaderConfig(project); + const platform = project.build.targets[0]; + return createLoaderCode(platform, bundler); +}; + +export async function createLoaderConfig(p?: AureliaJson.IProject) { + const createLoaderConfig = (await import('./loader')).createLoaderConfig; + project = p || project; + + await buildLoaderConfig(project); + const platform = project.build.targets[0]; + return createLoaderConfig(platform, bundler); +}; + +export function bundle() { + return new Transform({ + objectMode: true, + transform: function(file: IFile, encoding: BufferEncoding, callback: TransformCallback) { + callback(null, capture(file)); + } + }); +}; + +export function dest(opts?: BuildOptions) { + return bundler.build(opts) + .then(() => bundler.write()); +}; + +export function clearCache() { + // delete cache folder outside of cwd + return fs.promises.rm(cacheDir, { recursive: true, force: true }); +}; + +async function buildLoaderConfig(p: AureliaJson.IProject) { + project = p || project; + let configPromise = Promise.resolve(); + + if (!bundler) { + //If a bundler doesn't exist then chances are we have not run through getting all the files, and therefore the "bundles" will not be complete + configPromise = configPromise.then(() => { + return Bundler.create( + project, + new PackageAnalyzer(project), + new PackageInstaller(project) + ).then(b => { bundler = b }); + }); + } + + await configPromise; + return bundler.build(); +} + +function capture(file: IFile) { + // ignore type declaration file generated by TypeScript compiler + if (file.path.endsWith('d.ts')) return; + + if (isUpdating) { + bundler.updateFile(file); + } else { + bundler.addFile(file); + } +} diff --git a/lib/build/inject-css.js b/src/build/inject-css.ts similarity index 77% rename from lib/build/inject-css.js rename to src/build/inject-css.ts index 7368f20cc..410267249 100644 --- a/lib/build/inject-css.js +++ b/src/build/inject-css.ts @@ -1,5 +1,5 @@ /* global document */ -let cssUrlMatcher = /url\s*\(\s*(?!['"]data)([^) ]+)\s*\)/gi; +const cssUrlMatcher = /url\s*\(\s*(?!['"]data)([^) ]+)\s*\)/gi; // copied from aurelia-templating-resources css-resource // This behaves differently from webpack's style-loader. @@ -9,12 +9,12 @@ let cssUrlMatcher = /url\s*\(\s*(?!['"]data)([^) ]+)\s*\)/gi; // We inject css into a style tag on html head, it means the 'foo/hello.png' // is related to current url (not css url on link tag), or tag in html // head (which is recommended setup of router if not using hash). -function fixupCSSUrls(address, css) { +export function fixupCSSUrls(address: string, css: string) { if (typeof css !== 'string') { throw new Error(`Failed loading required CSS file: ${address}`); } return css.replace(cssUrlMatcher, (match, p1) => { - let quote = p1.charAt(0); + const quote = p1.charAt(0); if (quote === '\'' || quote === '"') { p1 = p1.substr(1, p1.length - 2); } @@ -26,10 +26,10 @@ function fixupCSSUrls(address, css) { }); } -function absoluteModuleId(baseId, moduleId) { +function absoluteModuleId(baseId: string, moduleId: string) { if (moduleId[0] !== '.') return moduleId; - let parts = baseId.split('/'); + const parts = baseId.split('/'); parts.pop(); moduleId.split('/').forEach(p => { @@ -45,14 +45,14 @@ function absoluteModuleId(baseId, moduleId) { } // copied from aurelia-pal-browser DOM.injectStyles -function injectCSS(css, id) { +export function injectCSS(css: string, id: string) { if (typeof document === 'undefined' || !css) return; css = fixupCSSUrls(id, css); if (id) { - let oldStyle = document.getElementById(id); + const oldStyle = document.getElementById(id); if (oldStyle) { - let isStyleTag = oldStyle.tagName.toLowerCase() === 'style'; + const isStyleTag = oldStyle.tagName.toLowerCase() === 'style'; if (isStyleTag) { oldStyle.innerHTML = css; @@ -63,7 +63,7 @@ function injectCSS(css, id) { } } - let node = document.createElement('style'); + const node = document.createElement('style'); node.innerHTML = css; node.type = 'text/css'; @@ -73,7 +73,3 @@ function injectCSS(css, id) { document.head.appendChild(node); } - -injectCSS.fixupCSSUrls = fixupCSSUrls; - -module.exports = injectCSS; diff --git a/src/build/loader-plugin.ts b/src/build/loader-plugin.ts new file mode 100644 index 000000000..a2e6d6a33 --- /dev/null +++ b/src/build/loader-plugin.ts @@ -0,0 +1,46 @@ +import { moduleIdWithPlugin } from './utils'; + +export class LoaderPlugin { + public readonly type: LoaderType; + private readonly config: AureliaJson.ILoaderPlugin; + private readonly _test: RegExp; + + public get name() { + return this.config.name; + } + public get stub() { + return this.config.stub; + } + public get extensions() { + return this.config.extensions; + } + public get test(){ + return this.config.test; + } + + constructor(type: LoaderType, config: AureliaJson.ILoaderPlugin) { + this.type = type; + this.config = config; + this._test = config.test ? new RegExp(config.test) : regExpFromExtensions(config.extensions); + } + + public matches(filePath: string) { + return this._test.test(filePath); + } + + public transform(moduleId: string, filePath: string, contents: string) { + contents = `define('${this.createModuleId(moduleId)}',[],function(){return ${JSON.stringify(contents)};});`; + return contents; + } + + public createModuleId(moduleId: string) { + // for backward compatibility, use 'text' as plugin name, + // to not break existing app with additional json plugin in aurelia.json + return moduleIdWithPlugin(moduleId, 'text', this.type); + } +}; + +function regExpFromExtensions(extensions: string[]) { + return new RegExp('^.*(' + extensions.map(x => '\\' + x).join('|') + ')$'); +} + diff --git a/lib/build/loader.js b/src/build/loader.ts similarity index 57% rename from lib/build/loader.js rename to src/build/loader.ts index aafc858ee..3c522ba37 100644 --- a/lib/build/loader.js +++ b/src/build/loader.ts @@ -1,17 +1,23 @@ -const path = require('path'); - -const Configuration = require('../configuration').Configuration; - -exports.createLoaderCode = function createLoaderCode(platform, bundler) { - let loaderCode; - let loaderOptions = bundler.loaderOptions; +import * as path from 'node:path'; +import { Configuration } from '../configuration'; +import { type Bundler } from './bundler'; +import { type Bundle } from './bundle'; +import { type LoaderPlugin } from './loader-plugin'; + +export type LoaderOptions = Omit & { plugins: LoaderPlugin[] }; +type LoaderBundlesConfig = { bundles?: Record; map: Omit, 'bundles'>}; +type LoaderConfig = AureliaJson.ILoaderConfig & Partial; + +export function createLoaderCode(platform: AureliaJson.ITarget, bundler: Bundler) { + let loaderCode: string; + const loaderOptions = bundler.loaderOptions; switch (loaderOptions.type) { case 'require': - loaderCode = 'requirejs.config(' + JSON.stringify(exports.createRequireJSConfig(platform, bundler), null, 2) + ')'; + loaderCode = 'requirejs.config(' + JSON.stringify(createRequireJSConfig(platform, bundler), null, 2) + ')'; break; case 'system': - loaderCode = 'window.define=SystemJS.amdDefine; window.require=window.requirejs=SystemJS.amdRequire; SystemJS.config(' + JSON.stringify(exports.createSystemJSConfig(platform, bundler), null, 2) + ');'; + loaderCode = 'window.define=SystemJS.amdDefine; window.require=window.requirejs=SystemJS.amdRequire; SystemJS.config(' + JSON.stringify(createSystemJSConfig(platform, bundler), null, 2) + ');'; break; default: //TODO: Enhancement: Look at a designated folder for any custom configurations @@ -21,16 +27,16 @@ exports.createLoaderCode = function createLoaderCode(platform, bundler) { return loaderCode; }; -exports.createLoaderConfig = function createLoaderConfig(platform, bundler) { - let loaderConfig; - let loaderOptions = bundler.loaderOptions; +export function createLoaderConfig(platform: AureliaJson.ITarget, bundler: Bundler) { + let loaderConfig: LoaderConfig | undefined; + const loaderOptions = bundler.loaderOptions; switch (loaderOptions.type) { case 'require': - loaderConfig = exports.createRequireJSConfig(platform, bundler); + loaderConfig = createRequireJSConfig(platform, bundler); break; case 'system': - loaderConfig = exports.createSystemJSConfig(platform); + loaderConfig = createSystemJSConfig(platform); break; default: //TODO: Enhancement: Look at a designated folder for any custom configurations @@ -40,14 +46,14 @@ exports.createLoaderConfig = function createLoaderConfig(platform, bundler) { return loaderConfig; }; -exports.createRequireJSConfig = function createRequireJSConfig(platform, bundler) { - let loaderOptions = bundler.loaderOptions; - let loaderConfig = bundler.loaderConfig; - let bundles = bundler.bundles; - let configName = loaderOptions.configTarget; - let bundleMetadata = {}; - let includeBundles = shouldIncludeBundleMetadata(bundles, loaderOptions); - let config = Object.assign({}, loaderConfig); +function createRequireJSConfig(platform: AureliaJson.ITarget, bundler: Bundler) { + const loaderOptions = bundler.loaderOptions; + const loaderConfig = bundler.loaderConfig; + const bundles = bundler.bundles; + const configName = loaderOptions.configTarget; + const bundleMetadata: Record = {}; + const includeBundles = shouldIncludeBundleMetadata(bundles, loaderOptions); + const config: LoaderConfig = Object.assign({}, loaderConfig); let location = platform.baseUrl || platform.output; if (platform.useAbsolutePath) { @@ -57,9 +63,9 @@ exports.createRequireJSConfig = function createRequireJSConfig(platform, bundler } for (let i = 0; i < bundles.length; ++i) { - let currentBundle = bundles[i]; - let currentName = currentBundle.config.name; - let buildOptions = new Configuration(currentBundle.config.options, bundler.buildOptions.getAllOptions()); + const currentBundle = bundles[i]; + const currentName = currentBundle.config.name; + const buildOptions = new Configuration(currentBundle.config.options, bundler.buildOptions.getAllOptions()); if (currentName === configName) { //skip over the vendor bundle continue; } @@ -78,7 +84,7 @@ exports.createRequireJSConfig = function createRequireJSConfig(platform, bundler return config; }; -exports.createSystemJSConfig = function createSystemJSConfig(platform, bundler) { +function createSystemJSConfig(platform: AureliaJson.ITarget, bundler?: Bundler) { const loaderOptions = bundler.loaderOptions; const bundles = bundler.bundles; const configBundleName = loaderOptions.configTarget; @@ -88,13 +94,13 @@ exports.createSystemJSConfig = function createSystemJSConfig(platform, bundler) const bundlesConfig = bundles.map(bundle => systemJSConfigForBundle(bundle, bundler, location, includeBundles)) .filter(bundle => bundle.name !== configBundleName) - .reduce((c, bundle) => bundle.addBundleConfig(c), { map: { 'text': 'text' } }); + .reduce((c, bundle) => bundle.addBundleConfig(c), { map: { 'text': 'text' } } as LoaderBundlesConfig); return Object.assign(systemConfig, bundlesConfig); }; -function shouldIncludeBundleMetadata(bundles, loaderOptions) { - let setting = loaderOptions.includeBundleMetadataInConfig; +function shouldIncludeBundleMetadata(bundles: Bundle[], loaderOptions: LoaderOptions) { + const setting = loaderOptions.includeBundleMetadataInConfig; if (typeof setting === 'string') { switch (setting.toLowerCase()) { @@ -110,7 +116,7 @@ function shouldIncludeBundleMetadata(bundles, loaderOptions) { return setting === true; } -function systemJSConfigForBundle(bundle, bundler, location, includeBundles) { +function systemJSConfigForBundle(bundle: Bundle, bundler: Bundler, location: string, includeBundles: boolean) { const buildOptions = new Configuration(bundle.config.options, bundler.buildOptions.getAllOptions()); const mapTarget = location + '/' + bundle.moduleId + (buildOptions.isApplicable('rev') && bundle.hash ? '-' + bundle.hash : '') + path.extname(bundle.config.name); const moduleId = bundle.moduleId; @@ -118,7 +124,7 @@ function systemJSConfigForBundle(bundle, bundler, location, includeBundles) { return { name: bundle.config.name, - addBundleConfig: function(config) { + addBundleConfig: function(config: LoaderBundlesConfig) { config.map[moduleId] = mapTarget; if (includeBundles) { config.bundles = (config.bundles || {}); diff --git a/lib/build/module-id-processor.js b/src/build/module-id-processor.ts similarity index 58% rename from lib/build/module-id-processor.js rename to src/build/module-id-processor.ts index 1f1082df1..be5446e0d 100644 --- a/lib/build/module-id-processor.js +++ b/src/build/module-id-processor.ts @@ -1,15 +1,15 @@ // if moduleId is above surface (default src/), the '../../' confuses hell out of // requirejs as it tried to understand it as a relative module id. // replace '..' with '__dot_dot__' to enforce absolute module id. -const toDotDot = (moduleId) => moduleId.split('/').map(p => p === '..' ? '__dot_dot__' : p).join('/'); -const fromDotDot = (moduleId) => moduleId.split('/').map(p => p === '__dot_dot__' ? '..' : p).join('/'); +export const toDotDot = (moduleId: string) => moduleId.split('/').map(p => p === '..' ? '__dot_dot__' : p).join('/'); +export const fromDotDot = (moduleId: string) => moduleId.split('/').map(p => p === '__dot_dot__' ? '..' : p).join('/'); -const getAliases = (moduleId, paths) => { - const aliases = []; +export const getAliases = (moduleId: string, paths: AureliaJson.IPaths) => { + const aliases: {fromId: string, toId: string}[] = []; const _moduleId = fromDotDot(moduleId); for (let i = 0, keys = Object.keys(paths); i < keys.length; i++) { - let key = keys[i]; - let target = paths[key]; + const key = keys[i]; + const target = paths[key]; if (key === 'root') continue; if (key === target) continue; @@ -23,5 +23,3 @@ const getAliases = (moduleId, paths) => { return aliases; }; - -module.exports = { toDotDot, fromDotDot, getAliases }; diff --git a/src/build/package-analyzer.ts b/src/build/package-analyzer.ts new file mode 100644 index 000000000..81b955c5d --- /dev/null +++ b/src/build/package-analyzer.ts @@ -0,0 +1,185 @@ +import * as fs from '../file-system'; +import * as path from 'node:path'; +import { DependencyDescription } from './dependency-description'; +import * as Utils from './utils'; +import { getLogger } from 'aurelia-logging'; +const logger = getLogger('PackageAnalyzer'); + +export class PackageAnalyzer { + private project: AureliaJson.IProject; + + constructor(project: AureliaJson.IProject) { + this.project = project; + } + + async analyze(packageName: string): Promise { + const description = new DependencyDescription(packageName, 'npm'); + + await loadPackageMetadata(this.project, description); + if (!description.metadataLocation) { + throw new Error(`Unable to find package metadata (package.json) of ${description.name}`); + } + determineLoaderConfig(this.project, description); + return description; + } + + async reverseEngineer(loaderConfig: ILoaderConfig): Promise { + loaderConfig = JSON.parse(JSON.stringify(loaderConfig)); + const description = new DependencyDescription(loaderConfig.name); + description.loaderConfig = loaderConfig; + + if (!loaderConfig.packageRoot && (!loaderConfig.path || loaderConfig.path.indexOf('node_modules') !== -1)) { + description.source = 'npm'; + } else { + description.source = 'custom'; + if (!loaderConfig.packageRoot) { + fillUpPackageRoot(this.project, description); + } + } + + await loadPackageMetadata(this.project, description); + if (!loaderConfig.path) { + // fillup main and path + determineLoaderConfig(this.project, description); + } else { + if (!loaderConfig.main) { + if (description.source === 'custom' && loaderConfig.path === loaderConfig.packageRoot) { + // fillup main and path + determineLoaderConfig(this.project, description); + } else { + const fullPath = path.resolve(this.project.paths.root, loaderConfig.path); + if (fullPath === description.location) { + // fillup main and path + determineLoaderConfig(this.project, description); + return description; + } + + // break single path into main and dir + const pathParts = path.parse(fullPath); + + // when path is node_modules/package/foo/bar + // set path to node_modules/package + // set main to foo/bar + loaderConfig.path = path.relative(this.project.paths.root, description.location).replace(/\\/g, '/'); + + if (pathParts.dir.length > description.location.length + 1) { + const main = path.join(pathParts.dir.slice(description.location.length + 1), Utils.removeJsExtension(pathParts.base)); + loaderConfig.main = main.replace(/\\/g, '/'); + } else if (pathParts.dir.length === description.location.length) { + loaderConfig.main = Utils.removeJsExtension(pathParts.base).replace(/\\/g, '/'); + } else { + throw new Error(`Path: "${loaderConfig.path}" is not in: ${description.location}`); + } + } + } else { + loaderConfig.main = Utils.removeJsExtension(loaderConfig.main).replace(/\\/g, '/'); + } + } + return description; + } +}; + +function fillUpPackageRoot(project: AureliaJson.IProject, description: DependencyDescription) { + let _path = description.loaderConfig.path; + + const ext = path.extname(_path).toLowerCase(); + if (!ext || Utils.knownExtensions.indexOf(ext) === -1) { + // main file could be non-js file like css/font-awesome.css + _path += '.js'; + } + + if (fs.isFile(path.resolve(project.paths.root, _path))) { + description.loaderConfig.packageRoot = path.dirname(description.loaderConfig.path).replace(/\\/g, '/'); + } + + if (!description.loaderConfig.packageRoot) { + description.loaderConfig.packageRoot = description.loaderConfig.path; + } +} + +async function loadPackageMetadata(project: AureliaJson.IProject, description: DependencyDescription): Promise { + await setLocation(project, description); + try { + if (description.metadataLocation) { + const data = await fs.readFile(description.metadataLocation); + description.metadata = JSON.parse(data.toString()); + } + } catch (e) { + logger.error(`Unable to load package metadata (package.json) of ${description.name}:`); + logger.info(e); + } +} + +// loaderConfig.path is simplified when use didn't provide explicit config. +// In auto traced nodejs package, loaderConfig.path always matches description.location. +// We then use auto-generated moduleId aliases in dependency-inclusion to make AMD +// module system happy. +function determineLoaderConfig(project: AureliaJson.IProject, description: DependencyDescription) { + const location = path.resolve(description.location); + const mainPath = Utils.nodejsLoad(location); + + if (!description.loaderConfig) { + description.loaderConfig = {name: description.name}; + } + + description.loaderConfig.path = path.relative(project.paths.root, description.location).replace(/\\/g, '/'); + + if (mainPath) { + description.loaderConfig.main = Utils.removeJsExtension(mainPath.slice(location.length + 1).replace(/\\/g, '/')); + } else { + logger.warn(`The "${description.name}" package has no valid main file, fall back to index.js.`); + description.loaderConfig.main = 'index'; + } +} + +async function setLocation(project: AureliaJson.IProject, description: DependencyDescription) { + switch (description.source) { + case 'npm': + { const packageFolder = await getPackageFolder(project, description); + description.location = packageFolder; + return await tryFindMetadata(project, description); } + case 'custom': + description.location = path.resolve(project.paths.root, description.loaderConfig.packageRoot); + + return tryFindMetadata(project, description); + default: + return Promise.reject(`The package source "${description.source}" is not supported.`); + } +} + +async function tryFindMetadata(project: AureliaJson.IProject, description: DependencyDescription) { + try { + await fs.stat(path.join(description.location, 'package.json')); + return description.metadataLocation = path.join(description.location, 'package.json'); + } catch { /* empty */ } +} + +async function getPackageFolder(project: AureliaJson.IProject, description: DependencyDescription) { + if (!description.loaderConfig || !description.loaderConfig.path) { + return await Utils.resolvePackagePath(description.name) + } + + return lookupPackageFolderRelativeStrategy(project.paths.root, description.loaderConfig.path); +} + +// Looks for the node_modules folder from the root path of aurelia +// with the defined loaderConfig. +function lookupPackageFolderRelativeStrategy(root: string, relativePath: string) { + const pathParts = relativePath.replace(/\\/g, '/').split('/'); + let packageFolder = ''; + let stopOnNext = false; + + for (let i = 0; i < pathParts.length; ++i) { + const part = pathParts[i]; + + packageFolder = path.join(packageFolder, part); + + if (stopOnNext && !part.startsWith('@')) { + break; + } else if (part === 'node_modules') { + stopOnNext = true; + } + } + + return Promise.resolve(path.resolve(root, packageFolder)); +} diff --git a/lib/build/package-installer.js b/src/build/package-installer.ts similarity index 62% rename from lib/build/package-installer.js rename to src/build/package-installer.ts index d2c53028a..7b4764dc8 100644 --- a/lib/build/package-installer.js +++ b/src/build/package-installer.ts @@ -1,9 +1,15 @@ -const path = require('path'); -const fs = require('../file-system'); -const logger = require('aurelia-logging').getLogger('Package-installer'); +import * as path from 'node:path'; +import * as fs from '../file-system'; +import { BasePackageManager } from '../package-managers/base-package-manager'; +import { getLogger } from 'aurelia-logging'; +const logger = getLogger('Package-installer'); -exports.PackageInstaller = class { - constructor(project) { + +export class PackageInstaller { + private project: AureliaJson.IProject; + private _packageManager: string | undefined; + + constructor(project: AureliaJson.IProject) { this.project = project; } @@ -27,22 +33,22 @@ exports.PackageInstaller = class { return packageManager; } - install(packages) { + async install(packages: string[]) { let packageManager = this.determinePackageManager(); - let Ctor; + let Ctor: new () => BasePackageManager; logger.info(`Using '${packageManager}' to install the package(s). You can change this by setting the 'packageManager' property in the aurelia.json file to 'npm' or 'yarn'.`); try { - Ctor = require(`../package-managers/${packageManager}`).default; + Ctor = (await import(`../package-managers/${packageManager}`)).default; } catch (e) { logger.error(`Could not load the ${packageManager} package installer. Falling back to NPM`, e); packageManager = 'npm'; - Ctor = require(`../package-managers/${packageManager}`).default; + Ctor = (await import(`../package-managers/${packageManager}`)).default; } - let installer = new Ctor(); + const installer = new Ctor(); logger.info(`[${packageManager}] installing ${packages}. It would take a while.`); diff --git a/lib/build/source-inclusion.js b/src/build/source-inclusion.ts similarity index 52% rename from lib/build/source-inclusion.js rename to src/build/source-inclusion.ts index 5de9125ee..97709fed0 100644 --- a/lib/build/source-inclusion.js +++ b/src/build/source-inclusion.ts @@ -1,8 +1,22 @@ -const path = require('path'); -const mapStream = require('map-stream'); +import * as path from 'node:path'; +import mapStream from 'map-stream'; +import { type Bundle } from './bundle'; +import { type Minimatch } from 'minimatch'; +import { BundledSource } from './bundled-source'; +import { DependencyInclusion } from './dependency-inclusion'; +import * as vfs from 'vinyl-fs'; -exports.SourceInclusion = class { - constructor(bundle, pattern, includedBy) { +export class SourceInclusion { + public bundle: Bundle; + private orignalPattern: string; + public includedBy: DependencyInclusion; + public readonly pattern: string; + private matcher: Minimatch; + private excludes: Minimatch[]; + private items: BundledSource[]; + private vfs: typeof vfs; + + constructor(bundle: Bundle, pattern: string, includedBy?: DependencyInclusion) { this.bundle = bundle; this.orignalPattern = pattern; // source-inclusion could be included by a dependency-inclusion @@ -19,23 +33,23 @@ exports.SourceInclusion = class { this.excludes = this.bundle.excludes; this.items = []; - this.vfs = require('vinyl-fs'); + this.vfs = vfs; } - addItem(item) { + addItem(item: BundledSource) { item.includedBy = this; item.includedIn = this.bundle; this.items.push(item); } - _isExcluded(item) { - let found = this.excludes.findIndex(exclusion => { + _isExcluded(item: BundledSource) { + const found = this.excludes.findIndex(exclusion => { return exclusion.match(item.path); }); return found > -1; } - trySubsume(item) { + trySubsume(item: BundledSource) { if (this.matcher.match(item.path) && !this._isExcluded(item)) { this.addItem(item); return true; @@ -45,16 +59,16 @@ exports.SourceInclusion = class { } addAllMatchingResources() { - return new Promise((resolve, reject) => { - let bundler = this.bundle.bundler; - let pattern = path.resolve(this._getProjectRoot(), this.pattern); + return new Promise((resolve, reject) => { + const bundler = this.bundle.bundler; + const pattern = path.resolve(this._getProjectRoot(), this.pattern); - let subsume = (file, cb) => { + const subsume = (file: IFile, cb: mapStream.Callback) => { bundler.addFile(file, this); cb(null, file); }; - this.vfs.src(pattern).pipe(mapStream(subsume)) + this.vfs.src(pattern).pipe(mapStream(subsume) as unknown as NodeJS.WritableStream) .on('error', e => { console.log(`Error while adding all matching resources of pattern "${this.pattern}": ${e.message}`); reject(e); @@ -63,7 +77,7 @@ exports.SourceInclusion = class { }); } - _getProjectRoot() { + private _getProjectRoot() { return this.bundle.bundler.project.paths.root; } diff --git a/lib/build/stub-module.js b/src/build/stub-module.ts similarity index 63% rename from lib/build/stub-module.js rename to src/build/stub-module.ts index 698019aff..500cd3cfa 100644 --- a/lib/build/stub-module.js +++ b/src/build/stub-module.ts @@ -1,6 +1,7 @@ -const path = require('path'); -const Utils = require('./utils'); -const logger = require('aurelia-logging').getLogger('StubNodejs'); +import * as path from 'node:path'; +import * as Utils from './utils'; +import { getLogger } from 'aurelia-logging'; +const logger = getLogger('StubNodejs'); // stub core Node.js modules based on https://github.com/webpack/node-libs-browser/blob/master/index.js // no need stub for following modules, they got same name on npm package @@ -29,32 +30,33 @@ const UNAVAIABLE_CORE_MODULES = [ const EMPTY_MODULE = 'define(function(){return {};});'; -function resolvePath(packageName, root) { - return path.relative(root, Utils.resolvePackagePath(packageName)).replace(/\\/g, '/'); +async function resolvePath(packageName: string, root: string) { + const rel = await Utils.resolvePackagePath(packageName); + return path.relative(root, rel).replace(/\\/g, '/'); } // note all paths here assumes local node_modules folder -module.exports = function(moduleId, root) { +export async function stubModule(moduleId: string, root: string) { // with subfix -browserify if (['crypto', 'https', 'os', 'path', 'stream', 'timers', 'tty', 'vm'].indexOf(moduleId) !== -1) { - return {name: moduleId, path: resolvePath(`${moduleId}-browserify`, root)}; + return {name: moduleId, path: await resolvePath(`${moduleId}-browserify`, root)}; } if (moduleId === 'domain') { logger.warn('core Node.js module "domain" is deprecated'); - return {name: 'domain', path: resolvePath('domain-browser', root)}; + return {name: 'domain', path: await resolvePath('domain-browser', root)}; } if (moduleId === 'http') { - return {name: 'http', path: resolvePath('stream-http', root)}; + return {name: 'http', path: await resolvePath('stream-http', root)}; } if (moduleId === 'querystring') { - return {name: 'querystring', path: resolvePath('querystring-browser-stub', root)}; + return {name: 'querystring', path: await resolvePath('querystring-browser-stub', root)}; } if (moduleId === 'fs') { - return {name: 'fs', path: resolvePath('fs-browser-stub', root)}; + return {name: 'fs', path: await resolvePath('fs-browser-stub', root)}; } if (moduleId === 'sys') { @@ -62,7 +64,7 @@ module.exports = function(moduleId, root) { } if (moduleId === 'zlib') { - return {name: 'zlib', path: resolvePath('browserify-zlib', root)}; + return {name: 'zlib', path: await resolvePath('browserify-zlib', root)}; } if (UNAVAIABLE_CORE_MODULES.indexOf(moduleId) !== -1) { @@ -80,7 +82,7 @@ module.exports = function(moduleId, root) { if (moduleId === '__inject_css__') { return { name: '__inject_css__', - path: resolvePath('aurelia-cli', root), + path: await resolvePath('aurelia-cli', root), main: 'lib/build/inject-css' }; } diff --git a/lib/build/utils.js b/src/build/utils.ts similarity index 65% rename from lib/build/utils.js rename to src/build/utils.ts index 85ff6baf1..b515029dc 100644 --- a/lib/build/utils.js +++ b/src/build/utils.ts @@ -1,16 +1,17 @@ -const path = require('path'); -const crypto = require('crypto'); -const fs = require('../file-system'); -const tmpDir = require('os').tmpdir(); +import * as path from 'node:path'; +import * as os from 'node:os'; +import * as crypto from 'crypto'; +import * as fs from '../file-system'; -exports.knownExtensions = ['.js', '.cjs', '.mjs', '.json', '.css', '.svg', '.html']; +const tmpDir = os.tmpdir(); +export const knownExtensions = ['.js', '.cjs', '.mjs', '.json', '.css', '.svg', '.html']; -exports.couldMissGulpPreprocess = function(id) { +export function couldMissGulpPreprocess(id: string) { const ext = path.extname(id).toLowerCase(); return ext && ext !== '.js' && ext !== '.html' && ext !== '.css'; }; -function getPackagePaths() { +async function getPackagePaths() { // require.resolve(packageName) cannot resolve package has no main. // for instance: font-awesome v4.7.0 // manually try resolve paths @@ -19,13 +20,13 @@ function getPackagePaths() { ...require.resolve.paths('not-core/'), // additional search from app's folder, this is necessary to support // lerna hoisting where cli is out of app's local node_modules folder. - ...require('resolve/lib/node-modules-paths')(process.cwd(), {}) + ...(await import('resolve/lib/node-modules-paths')).default(process.cwd(), {}) ]; } // resolve npm package path -exports.resolvePackagePath = function(packageName) { - const packagePaths = getPackagePaths(); +export async function resolvePackagePath(packageName: string) { + const packagePaths = await getPackagePaths(); for (let i = 0, len = packagePaths.length; i < len; i++) { const dirname = path.join(packagePaths[i], packageName); if (fs.isDirectory(dirname)) return dirname; @@ -34,7 +35,7 @@ exports.resolvePackagePath = function(packageName) { throw new Error(`cannot resolve npm package folder for "${packageName}"`); }; -exports.moduleIdWithPlugin = function(moduleId, pluginName, type) { +export function moduleIdWithPlugin(moduleId: string, pluginName: string, type: 'require' | 'system') { switch (type) { case 'require': return pluginName + '!' + moduleId; @@ -46,15 +47,15 @@ exports.moduleIdWithPlugin = function(moduleId, pluginName, type) { }; const CACHE_DIR = path.resolve(tmpDir, 'aurelia-cli-cache'); -exports.cacheDir = CACHE_DIR; +export const cacheDir = CACHE_DIR; -function cachedFilePath(hash) { +function cachedFilePath(hash: string) { const folder = hash.slice(0, 2); const fileName = hash.slice(2); return path.resolve(CACHE_DIR, folder, fileName); } -exports.getCache = function(hash) { +export function getCache(hash: string) { const filePath = cachedFilePath(hash); try { return JSON.parse(fs.readFileSync(filePath)); @@ -63,80 +64,83 @@ exports.getCache = function(hash) { } }; -exports.setCache = function(hash, object) { +export function setCache(hash: string, object: unknown) { const filePath = cachedFilePath(hash); // async write - fs.writeFile(filePath, JSON.stringify(object)); + fs.writeFileSync(filePath, JSON.stringify(object)); }; -exports.runSequentially = function(tasks, cb) { +export async function runSequentially(tasks: T[], cb: (task: T, index: number) => Promise): Promise { let index = -1; - let result = []; + const result: U[] = []; - function exec() { - index ++; + async function exec() { + index++; if (index < tasks.length) { - return cb(tasks[index], index).then(r => result.push(r)).then(exec); + const r = await cb(tasks[index], index); + result.push(r); + await exec(); } - - return Promise.resolve(); } - return exec().then(() => result); + await exec(); + return result; }; -exports.generateHashedPath = function(pth, hash) { +export function generateHashedPath(pth: string, hash: string) { if (arguments.length !== 2) { throw new Error('`path` and `hash` required'); } - return modifyFilename(pth, function(filename, ext) { + return modifyFilename(pth, function(filename: string, ext: string) { return filename + '-' + hash + ext; }); }; -exports.revertHashedPath = function(pth, hash) { +export function revertHashedPath(pth: string, hash: string) { if (arguments.length !== 2) { throw new Error('`path` and `hash` required'); } - return modifyFilename(pth, function(filename, ext) { + return modifyFilename(pth, function(filename: string, ext: string) { return filename.replace(new RegExp('-' + hash + '$'), '') + ext; }); }; -exports.generateHash = function(bufOrStr) { +export function generateHash(bufOrStr: crypto.BinaryLike) { return crypto.createHash('md5').update(bufOrStr).digest('hex'); }; -exports.escapeForRegex = function(str) { - let matchers = /[|\\{}()[\]^$+*?.]/g; +export function escapeForRegex(str: string) { + const matchers = /[|\\{}()[\]^$+*?.]/g; return str.replace(matchers, '\\$&'); }; -exports.createBundleFileRegex = function(bundleName) { - return new RegExp(exports.escapeForRegex(bundleName) + '[^"\']*?\\.js', 'g'); +export function createBundleFileRegex(bundleName: string) { + return new RegExp(escapeForRegex(bundleName) + '[^"\']*?\\.js', 'g'); }; -function modifyFilename(pth, modifier) { +function modifyFilename(path: string, modifier: (filename: string, ext: string) => string): string; +function modifyFilename(path: string[], modifier: (filename: string, ext: string) => string): string[]; +function modifyFilename(pth: string | string[], modifier: (filename: string, ext: string) => string): string | string[] { if (arguments.length !== 2) { throw new Error('`path` and `modifier` required'); } if (Array.isArray(pth)) { - return pth.map(function(el) { + return pth.map(function(el: string) { return modifyFilename(el, modifier); }); } - let ext = path.extname(pth); + const ext = path.extname(pth); return path.posix.join(path.dirname(pth), modifier(path.basename(pth, ext), ext)); } // https://nodejs.org/dist/latest-v10.x/docs/api/modules.html // after "high-level algorithm in pseudocode of what require.resolve() does" -function nodejsLoadAsFile(resourcePath) { +function nodejsLoadAsFile(resourcePath: string) { if (fs.isFile(resourcePath)) { return resourcePath; } @@ -151,7 +155,7 @@ function nodejsLoadAsFile(resourcePath) { // skip .node file that nobody uses } -function nodejsLoadIndex(resourcePath) { +function nodejsLoadIndex(resourcePath: string) { if (!fs.isDirectory(resourcePath)) return; const indexJs = path.join(resourcePath, 'index.js'); @@ -166,7 +170,7 @@ function nodejsLoadIndex(resourcePath) { // skip index.node file that nobody uses } -function nodejsLoadAsDirectory(resourcePath) { +function nodejsLoadAsDirectory(resourcePath: string) { if (!fs.isDirectory(resourcePath)) return; const packageJson = path.join(resourcePath, 'package.json'); @@ -202,7 +206,7 @@ function nodejsLoadAsDirectory(resourcePath) { metaMain = metadata.main; } - let mainFile = metaMain || 'index'; + const mainFile = metaMain || 'index'; const mainResourcePath = path.resolve(resourcePath, mainFile); return nodejsLoadAsFile(mainResourcePath) || nodejsLoadIndex(mainResourcePath); } @@ -210,11 +214,11 @@ function nodejsLoadAsDirectory(resourcePath) { return nodejsLoadIndex(resourcePath); } -exports.nodejsLoad = function(resourcePath) { +export function nodejsLoad(resourcePath: string) { return nodejsLoadAsFile(resourcePath) || nodejsLoadAsDirectory(resourcePath); }; -exports.removeJsExtension = function(filePath) { +export function removeJsExtension(filePath: string) { if (path.extname(filePath).toLowerCase() === '.js') { return filePath.slice(0, -3); } diff --git a/lib/build/webpack-reporter.js b/src/build/webpack-reporter.ts similarity index 82% rename from lib/build/webpack-reporter.js rename to src/build/webpack-reporter.ts index 47d1dfa9d..a4233714a 100644 --- a/lib/build/webpack-reporter.js +++ b/src/build/webpack-reporter.ts @@ -1,8 +1,8 @@ -module.exports = function reportReadiness(options) { - const uri = createDomain(options); - const yargs = require('yargs'); +export async function reportReadiness(options) { + const uri = await createDomain(options); + const yargs = (await import('yargs')).default; const argv = yargs.argv; - argv.color = require('supports-color'); + argv.color = (await import('supports-color')).default; const useColor = argv.color; let startSentence = `Project is running at ${colorInfo(useColor, uri)}`; @@ -24,9 +24,9 @@ module.exports = function reportReadiness(options) { } }; -function createDomain(opts) { +async function createDomain(opts) { const protocol = opts.https ? 'https' : 'http'; - const url = require('url'); + const url = await import('node:url'); // the formatted domain (url without path) of the webpack server return opts.public ? `${protocol}://${opts.public}` : url.format({ diff --git a/lib/cli-options.js b/src/cli-options.ts similarity index 61% rename from lib/cli-options.js rename to src/cli-options.ts index 9c46d4c07..1e159b691 100644 --- a/lib/cli-options.js +++ b/src/cli-options.ts @@ -1,35 +1,50 @@ -const fs = require('./file-system'); +import * as fs from './file-system'; function definedEnvironments() { - let envs = []; + const envs: string[] = []; // read user defined environment files - let files; + let files: string[]; try { files = fs.readdirSync('aurelia_project/environments'); } catch { // ignore } - files && files.forEach(file => { - const m = file.match(/^(.+)\.(t|j)s$/); - if (m) envs.push(m[1]); - }); + if (Array.isArray(files)) { + files.forEach(file => { + const m = file.match(/^(.+)\.(t|j)s$/); + if (m) envs.push(m[1]); + }); + } return envs; } -exports.CLIOptions = class { +export class CLIOptions { + public taskPath: string | undefined; + public args: string[] | undefined; + public commandName: string | undefined; + /** accessed from `/bin/aurelia-cli.js` */ + public runningGlobally: boolean | undefined; + /** accessed from `/bin/aurelia-cli.js` */ + public runningLocally: boolean | undefined; + public originalBaseDir: string | undefined; + /** accessed from unit tests */ + public static instance: CLIOptions; + /** accessed from unit tests */ + public env: string | undefined; + constructor() { - exports.CLIOptions.instance = this; + CLIOptions.instance = this; } taskName() { - let name = this.taskPath.split(/[/\\]/).pop(); - let parts = name.split('.'); + const name = this.taskPath.split(/[/\\]/).pop(); + const parts = name.split('.'); parts.pop(); return parts.join('.'); } getEnvironment() { - if (this._env) return this._env; + if (this.env) return this.env; let env = this.getFlagValue('env') || process.env.NODE_ENV || 'dev'; const envs = definedEnvironments(); @@ -49,11 +64,11 @@ exports.CLIOptions = class { } } - this._env = env; + this.env = env; return env; } - hasFlag(name, shortcut) { + hasFlag(name: string, shortcut?:string) { if (this.args) { let lookup = '--' + name; let found = this.args.indexOf(lookup) !== -1; @@ -76,7 +91,7 @@ exports.CLIOptions = class { return false; } - getFlagValue(name, shortcut) { + getFlagValue(name:string, shortcut?:string) { if (this.args) { let lookup = '--' + name; let index = this.args.indexOf(lookup); @@ -106,18 +121,18 @@ exports.CLIOptions = class { } static taskName() { - return exports.CLIOptions.instance.taskName(); + return CLIOptions.instance.taskName(); } - static hasFlag(name, shortcut) { - return exports.CLIOptions.instance.hasFlag(name, shortcut); + static hasFlag(name: string, shortcut?: string) { + return CLIOptions.instance.hasFlag(name, shortcut); } - static getFlagValue(name, shortcut) { - return exports.CLIOptions.instance.getFlagValue(name, shortcut); + static getFlagValue(name: string, shortcut?: string) { + return CLIOptions.instance.getFlagValue(name, shortcut); } static getEnvironment() { - return exports.CLIOptions.instance.getEnvironment(); + return CLIOptions.instance.getEnvironment(); } }; diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 000000000..8386f432b --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,133 @@ +import * as path from 'node:path'; +import { Container } from 'aurelia-dependency-injection'; +import * as fs from './file-system'; +import * as ui from './ui'; +import { Project } from './project'; +import { CLIOptions } from './cli-options'; +import * as LogManager from 'aurelia-logging'; +import { Logger } from './logger'; + +export class CLI { + /** accessed from `/bin/aurelia-cli.js` */ + public options: CLIOptions; + private container: Container; + private ui: ui.ConsoleUI; + private logger: LogManager.Logger; + private project: Project | undefined; + + constructor(options: CLIOptions) { + this.options = options || new CLIOptions(); + this.container = new Container(); + this.ui = new ui.ConsoleUI(this.options); + this.configureContainer(); + this.logger = LogManager.getLogger('CLI'); + } + + // Note: cannot use this.logger.error inside run() + // because logger is not configured yet! + // this.logger.error prints nothing in run(), + // directly use this.ui.log. + async run(cmd: string, args: string[]): Promise { + const version = `${this.options.runningGlobally ? 'Global' : 'Local'} aurelia-cli v${((await import('../package.json')).default).version}`; + + if (cmd === '--version' || cmd === '-v') { + return this.ui.log(version); + } + + const project = await (cmd === 'new' ? Promise.resolve() : this._establishProject()); + void this.ui.log(version); + if (project && this.options.runningLocally) { + this.project = project; + this.container.registerInstance(Project, project); + } else if (project && this.options.runningGlobally) { + await this.ui.log('The current directory is likely an Aurelia-CLI project, but no local installation of Aurelia-CLI could be found. ' + + '(Do you need to restore node modules using npm install?)'); + return; + } else if (!project && this.options.runningLocally) { + await this.ui.log('It appears that the Aurelia CLI is running locally from ' + __dirname + '. However, no project directory could be found. ' + + 'The Aurelia CLI has to be installed globally (npm install -g aurelia-cli) and locally (npm install aurelia-cli) in an Aurelia CLI project directory'); + return; + } + const command = await this.createCommand(cmd, args); + return command.execute(args); + } + + configureLogger() { + LogManager.addAppender(this.container.get(Logger)); + const level = CLIOptions.hasFlag('debug') ? LogManager.logLevel.debug : LogManager.logLevel.info; + LogManager.setLevel(level); + } + + configureContainer() { + this.container.registerInstance(CLIOptions, this.options); + this.container.registerInstance(ui.UI, this.ui); + } + + async createCommand(commandText: string, commandArgs: string[]): Promise { + if (!commandText) { + return this.createHelpCommand(); + } + + const parts = commandText.split(':'); + const commandModule = parts[0]; + const commandName = parts[1] || 'default'; + try { + const alias = (await import('./commands/alias.json'))[commandModule]; + const found: ICommand = this.container.get((await import(`./commands/${alias || commandModule}/command`)).default); + Object.assign(this.options, { args: commandArgs }); + // need to configure logger after getting args + this.configureLogger(); + return found; + } catch { + if (this.project) { + const taskPath = await this.project.resolveTask(commandModule); + if (taskPath) { + Object.assign(this.options, { + taskPath: taskPath, + args: commandArgs, + commandName: commandName + }); + // need to configure logger after getting args + this.configureLogger(); + + return this.container.get((await import('./commands/gulp')).default); + } else { + void this.ui.log(`Invalid Command: ${commandText}`); + return this.createHelpCommand(); + } + } else { + void this.ui.log(`Invalid Command: ${commandText}`); + return this.createHelpCommand(); + } + } + } + + async createHelpCommand(): Promise { + return this.container.get((await import('./commands/help/command')).default); + } + + async _establishProject(): Promise { + const dir = await determineWorkingDirectory(process.cwd()); + return dir ? await Project.establish(dir) : this.ui.log('No Aurelia project found.'); + } +}; + +async function determineWorkingDirectory(dir: string): Promise { + const parent = path.join(dir, '..'); + + if (parent === dir) { + return; // resolve to nothing + } + + try { + await fs.stat(path.join(dir, 'aurelia_project')); + return dir; + } catch { + return determineWorkingDirectory(parent); + } +} + +process.on('unhandledRejection', (reason) => { + console.log('Uncaught promise rejection:'); + console.log(reason); +}); diff --git a/lib/commands/alias.json b/src/commands/alias.json similarity index 100% rename from lib/commands/alias.json rename to src/commands/alias.json diff --git a/lib/commands/config/command.json b/src/commands/config/command.json similarity index 100% rename from lib/commands/config/command.json rename to src/commands/config/command.json diff --git a/src/commands/config/command.ts b/src/commands/config/command.ts new file mode 100644 index 000000000..f87ea3add --- /dev/null +++ b/src/commands/config/command.ts @@ -0,0 +1,49 @@ +import * as os from 'node:os'; + +import { Configuration } from './configuration'; +import { ConfigurationUtilities } from './util'; +import { Container } from 'aurelia-dependency-injection'; +import { UI } from '../../ui'; +import { CLIOptions } from '../../cli-options'; + +export default class { + static inject() { return [Container, UI, CLIOptions]; } + + private container: Container; + private ui: UI; + private options: CLIOptions; + private config: Configuration; + private util: ConfigurationUtilities; + + constructor(container: Container, ui: UI, options: CLIOptions) { + this.container = container; + this.ui = ui; + this.options = options; + } + + async execute(args: string[]) { + this.config = new Configuration(this.options); + this.util = new ConfigurationUtilities(this.options, args); + const key = this.util.getArg(0) || ''; + const value = this.util.getValue(this.util.getArg(1)); + const save = !CLIOptions.hasFlag('no-save'); + const backup = !CLIOptions.hasFlag('no-backup'); + const action = this.util.getAction(value); + + await this.displayInfo(`Performing configuration action '${action}' on '${key}'${value ? ` with '${value}'` : ''}`); + await this.displayInfo(this.config.execute(action, key, value)); + + if (action !== 'get') { + if (save) { + const name = await this.config.save(backup); + await this.displayInfo('Configuration saved. ' + (backup ? `Backup file '${name}' created.` : 'No backup file was created.')); + } else { + await this.displayInfo(`Action was '${action}', but no save was performed!`); + } + } + } + + displayInfo(message: string) { + return this.ui.log(message + os.EOL); + } +}; diff --git a/src/commands/config/configuration.ts b/src/commands/config/configuration.ts new file mode 100644 index 000000000..2fba791cc --- /dev/null +++ b/src/commands/config/configuration.ts @@ -0,0 +1,151 @@ +import * as os from 'node:os'; +import { copySync, readFileSync, writeFile } from '../../file-system'; +import { CLIOptions } from '../../cli-options'; + +export class Configuration { + private options: CLIOptions; + private aureliaJsonPath: string; + private project: AureliaJson.IProject; + + constructor(options: CLIOptions) { + this.options = options; + this.aureliaJsonPath = options.originalBaseDir + '/aurelia_project/aurelia.json'; + this.project = JSON.parse(readFileSync(this.aureliaJsonPath)) as AureliaJson.IProject; + } + + configEntry(key: string, createKey?: string): object | null | undefined { + let entry: object | null | undefined = this.project; + const keys = key.split('.'); + + if (!keys[0]) { + return entry; + } + + while (entry && keys.length) { + const parsedKey = this.parsedKey(keys.shift()); + if (entry[parsedKey.value] === undefined || entry[parsedKey.value] === null) { + if (!createKey) { + return entry[parsedKey.value]; + } + const checkKey = this.parsedKey(keys.length ? keys[0] : createKey); + if (checkKey.index) { + entry[parsedKey.value] = []; + } else if (checkKey.key) { + entry[parsedKey.value] = {}; + } + } + entry = entry[parsedKey.value]; + + // TODO: Add support for finding objects based on input values? + // TODO: Add support for finding string in array? + } + + return entry; + } + + parsedKey(key: string) { + if (/\[(\d+)\]/.test(key)) { + return { index: true as const, key: false as const, value: +(RegExp.$1) }; + } + + return { index: false as const, key: true as const, value: key }; + } + + normalizeKey(key: string) { + const re = /([^.])\[/; + while (re.exec(key)) { + key = key.replace(re, RegExp.$1 + '.['); + } + + const keys = key.split('.'); + for (let i = 0; i < keys.length; i++) { + if (/\[(\d+)\]/.test(keys[i])) { + // console.log(`keys[${i}] is index: ${keys[i]}`); + } else if (/\[(.+)\]/.test(keys[i])) { + // console.log(`keys[${i}] is indexed name: ${keys[i]}`); + keys[i] = RegExp.$1; + } else { + // console.log(`keys[${i}] is name: ${keys[i]}`); + } + } + + return keys.join('.'); + } + + execute(action: string, key:string, value: unknown) { + const originalKey = key; + + key = this.normalizeKey(key); + + if (action === 'get') { + return `Configuration key '${key}' is:` + os.EOL + JSON.stringify(this.configEntry(key), null, 2); + } + + const keys = key.split('.'); + const parsedKey = this.parsedKey(keys.pop()); + const parent = keys.join('.'); + + if (action === 'set') { + const entry = this.configEntry(parent, parsedKey.value.toString()); + if (entry) { + entry[parsedKey.value] = value; + } else { + console.log('Failed to set property', this.normalizeKey(originalKey), '!'); + } + } else if (action === 'clear') { + const entry = this.configEntry(parent); + if (entry && (parsedKey.value in entry)) { + delete entry[parsedKey.value]; + } else { + console.log('No property', this.normalizeKey(originalKey), 'to clear!'); + } + } else if (action === 'add') { + const entry = this.configEntry(parent, parsedKey.value.toString()); + if (Array.isArray(entry[parsedKey.value]) && !Array.isArray(value)) { + value = [value]; + } if (Array.isArray(value) && !Array.isArray(entry[parsedKey.value])) { + entry[parsedKey.value] = (entry ? [entry[parsedKey.value]] : []); + } if (Array.isArray(value)) { + entry[parsedKey.value].push(...value); + } else if (Object(value) === value) { + if (Object(entry[parsedKey.value]) !== entry[parsedKey.value]) { + entry[parsedKey.value] = {}; + } + Object.assign(entry[parsedKey.value], value); + } else { + entry[parsedKey.value] = value; + } + } else if (action === 'remove') { + const entry = this.configEntry(parent); + + if (Array.isArray(entry) && parsedKey.index) { + entry.splice(parsedKey.value, 1); + } else if (Object(entry) === entry && parsedKey.key) { + delete entry[parsedKey.value]; + } else if (!entry) { + console.log('No property', this.normalizeKey(originalKey), 'to remove from!'); + } else { + console.log('Can\'t remove value from', entry[parsedKey.value], '!'); + } + } + key = this.normalizeKey(originalKey); + return `Configuration key '${key}' is now:` + os.EOL + JSON.stringify(this.configEntry(key), null, 2); + } + + save(backup: unknown) { + if (backup === undefined) backup = true; + + const unique = new Date().toISOString().replace(/[T\D]/g, ''); + const arr = this.aureliaJsonPath.split(/[\\/]/); + const name = arr.pop(); + const path = arr.join('/'); + const bak = `${name}.${unique}.bak`; + + if (backup) { + copySync(this.aureliaJsonPath, [path, bak].join('/')); + } + + return writeFile(this.aureliaJsonPath, JSON.stringify(this.project, null, 2), 'utf8') + .then(() => { return bak; }); + } +} diff --git a/lib/commands/config/util.js b/src/commands/config/util.ts similarity index 64% rename from lib/commands/config/util.js rename to src/commands/config/util.ts index 320b752d1..b3effe36a 100644 --- a/lib/commands/config/util.js +++ b/src/commands/config/util.ts @@ -1,11 +1,16 @@ -class ConfigurationUtilities { - constructor(options, args) { +import { CLIOptions } from '../../cli-options'; + +export class ConfigurationUtilities { + private options: CLIOptions; + private args: string[]; + + constructor(options: CLIOptions, args: string[]) { this.options = options; this.args = args; } - getArg(arg) { - let args = this.args; + getArg(arg: number) { + const args = this.args; if (args) { for (let i = 0; i < args.length; i++) { if (args[i].startsWith('--')) { @@ -18,7 +23,7 @@ class ConfigurationUtilities { } } - getValue(value) { + getValue(value: string) { if (value) { if (!value.startsWith('"') && !value.startsWith('[') && @@ -30,9 +35,9 @@ class ConfigurationUtilities { return value; } - getAction(value) { - let actions = ['add', 'remove', 'set', 'clear', 'get']; - for (let action of actions) { + getAction(value: unknown) { + const actions = ['add', 'remove', 'set', 'clear', 'get']; + for (const action of actions) { if (this.options.hasFlag(action)) { return action; } @@ -46,5 +51,3 @@ class ConfigurationUtilities { return 'set'; } } - -module.exports = ConfigurationUtilities; diff --git a/lib/commands/generate/command.json b/src/commands/generate/command.json similarity index 100% rename from lib/commands/generate/command.json rename to src/commands/generate/command.json diff --git a/src/commands/generate/command.ts b/src/commands/generate/command.ts new file mode 100644 index 000000000..3ed24f90a --- /dev/null +++ b/src/commands/generate/command.ts @@ -0,0 +1,57 @@ +import { Container } from 'aurelia-dependency-injection'; +import { UI } from '../../ui'; +import { CLIOptions } from '../../cli-options'; +import { Project } from '../../project'; + +import * as string from '../../string'; +import * as os from 'node:os'; + +export default class { + static inject() { return [Container, UI, CLIOptions, Project]; } + + private container: Container; + private ui: UI; + private options: CLIOptions; + private project: Project; + + constructor(container: Container, ui: UI, options: CLIOptions, project: Project) { + this.container = container; + this.ui = ui; + this.options = options; + this.project = project; + } + + async execute(args: string[]) { + if (args.length < 1) { + return this.displayGeneratorInfo('No Generator Specified. Available Generators:'); + } + + await this.project.installTranspiler(); + + const generatorPath = await this.project.resolveGenerator(args[0]); + Object.assign(this.options, { + generatorPath: generatorPath, + args: args.slice(1) + }); + if (generatorPath) { + const module = await import(generatorPath); + let generator = this.project.getExport(module); + + if (generator.inject) { + generator = this.container.get(generator); + generator = generator.execute.bind(generator); + } + + return generator(); + } + return this.displayGeneratorInfo(`Invalid Generator: ${args[0]}. Available Generators:`); + } + + async displayGeneratorInfo(message: string) { + await this.ui.displayLogo(); + await this.ui.log(message + os.EOL); + const metadata = await this.project.getGeneratorMetadata(); + const str = string.buildFromMetadata(metadata, this.ui.getWidth()); + return await this.ui.log(str); + } +}; diff --git a/src/commands/gulp.ts b/src/commands/gulp.ts new file mode 100644 index 000000000..25bc82146 --- /dev/null +++ b/src/commands/gulp.ts @@ -0,0 +1,86 @@ +import { Container } from 'aurelia-dependency-injection'; +import { UI } from '../ui'; +import { CLIOptions } from '../cli-options'; +import { Project } from '../project'; +import { type Gulp } from 'gulp'; +import type * as Undertaker from 'undertaker'; + +export default class { + static inject() { return [Container, UI, CLIOptions, Project]; } + + private container: Container; + private ui: UI; + private options: CLIOptions; + private project: Project; + + constructor(container: Container, ui: UI, options: CLIOptions, project: Project) { + this.container = container; + this.ui = ui; + this.options = options; + this.project = project; + } + + async execute(): Promise { + const { default: gulp } = await import('gulp'); + this.connectLogging(gulp); + + await this.project.installTranspiler(); + + makeInjectable(gulp, 'series', this.container); + makeInjectable(gulp, 'parallel', this.container); + + return new Promise((resolve, reject) => { + process.nextTick(async () => { + const task = this.project.getExport(await import(this.options.taskPath), this.options.commandName); + + gulp.series(task)(error => { + if (error) reject(error); + else resolve(); + }); + }); + }); + } + + connectLogging(gulp: Gulp) { + gulp.on('start', e => { + if (e.name[0] === '<') return; + void this.ui.log(`Starting '${e.name}'...`); + }); + + gulp.on('stop', e => { + if (e.name[0] === '<') return; + void this.ui.log(`Finished '${e.name}'`); + }); + + gulp.on('error', e => void this.ui.log(e)); + } +}; + +function makeInjectable(gulp: Gulp, name: 'series' | 'parallel', container: Container) { + const original = gulp[name]; + + gulp[name] = function() { + const args = Array.from({ length: arguments.length}); + + // `arguments` can be both spread tasks `(...tasks: Undertaker.Task[])` and an array `(tasks: Undertaker.Task[])` + // eslint-disable-next-line prefer-rest-params + const inputParams = arguments as unknown as Undertaker.Task[] | [Undertaker.Task[]]; + const tasks: Undertaker.Task[] = (inputParams.length === 1 && Array.isArray(inputParams[0]) ? inputParams[0] : inputParams) as Undertaker.Task[]; + + for (let i = 0, ii = tasks.length; i < ii; ++i) { + let task; + task = tasks[i]; + + if (task.inject) { + const taskName = task.name; + task = container.get(task); + task = task.execute.bind(task); + task.displayName = taskName; + } + + args[i] = task; + } + + return original.apply(gulp, args); + }; +} diff --git a/lib/commands/help/command.json b/src/commands/help/command.json similarity index 100% rename from lib/commands/help/command.json rename to src/commands/help/command.json diff --git a/src/commands/help/command.ts b/src/commands/help/command.ts new file mode 100644 index 000000000..dcc8c5c1f --- /dev/null +++ b/src/commands/help/command.ts @@ -0,0 +1,54 @@ +import { CLIOptions } from '../../cli-options'; +import * as ui from '../../ui'; +import { Optional } from 'aurelia-dependency-injection'; +import { Project } from '../../project'; +import * as string from '../../string'; + +export default class { + private options: CLIOptions; + private ui: ui.UI; + private project: Project; + static inject() { return [CLIOptions, ui.UI, Optional.of(Project)]; } + + constructor(options: CLIOptions, ui: ui.UI, project: Project) { + this.options = options; + this.ui = ui; + this.project = project; + } + + async execute(): Promise { + await this.ui.displayLogo(); + + let text: string; + if (this.options.runningGlobally) { + text = await this.getGlobalCommandText(); + } + else { + text = await this.getLocalCommandText(); + } + + void this.ui.log(text); + } + + private async getGlobalCommandText() { + const commands = await Promise.all([ + import('../new/command.json'), + import('./command.json') + ]); + return string.buildFromMetadata( + commands.map(c => c.default), + this.ui.getWidth()); + } + + private async getLocalCommandText() { + const commands = await Promise.all([ + import('../generate/command.json'), + import('../config/command.json'), + import('./command.json') + ]); + + const metadata = await this.project.getTaskMetadata(); + + return string.buildFromMetadata(metadata.concat(commands.map(c => c.default)), this.ui.getWidth()); + } +}; diff --git a/lib/commands/new/command.json b/src/commands/new/command.json similarity index 100% rename from lib/commands/new/command.json rename to src/commands/new/command.json diff --git a/lib/commands/new/command.js b/src/commands/new/command.ts similarity index 62% rename from lib/commands/new/command.js rename to src/commands/new/command.ts index 1e60c19d6..5c1b53b49 100644 --- a/lib/commands/new/command.js +++ b/src/commands/new/command.ts @@ -1,7 +1,7 @@ -const {spawn} = require('child_process'); +import {spawn} from 'node:child_process'; -module.exports = class { - async execute(args) { +export default class { + async execute(args: string[]) { // Calls "npx makes aurelia/v1" // https://github.com/aurelia/v1 spawn('npx', ['makes', 'aurelia/v1', ...args], {stdio: 'inherit', shell: true}); diff --git a/lib/configuration.js b/src/configuration.ts similarity index 61% rename from lib/configuration.js rename to src/configuration.ts index 20465a2f7..f66f27a96 100644 --- a/lib/configuration.js +++ b/src/configuration.ts @@ -1,7 +1,10 @@ -const CLIOptions = require('./cli-options').CLIOptions; +import { CLIOptions } from './cli-options'; -exports.Configuration = class { - constructor(options, defaultOptions, environment) { +export class Configuration { + private readonly options: AureliaJson.IBuildOptions; + private readonly environment: string; + + constructor(options: AureliaJson.IBuildOptions, defaultOptions: AureliaJson.IBuildOptions, environment?: string) { this.options = Object.assign({}, defaultOptions, options); this.environment = environment || CLIOptions.getEnvironment(); } @@ -10,9 +13,11 @@ exports.Configuration = class { return this.options; } - getValue(propPath) { - let split = propPath.split('.'); - let cur = this.options; + getValue(propPath: string) { + const split = propPath.split('.'); + // `cur` initially contains options, but then drills into child objects. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let cur: any = this.options; for (let i = 0; i < split.length; i++) { if (!cur) { @@ -23,7 +28,7 @@ exports.Configuration = class { } if (typeof cur === 'object') { - let keys = Object.keys(cur); + const keys = Object.keys(cur); let result = undefined; if (keys.indexOf('default') > -1 && typeof(cur.default) === 'object') { @@ -46,8 +51,8 @@ exports.Configuration = class { return cur; } - isApplicable(propPath) { - let value = this.getValue(propPath); + isApplicable(propPath: string) { + const value = this.getValue(propPath); if (!value) { return false; @@ -68,8 +73,8 @@ exports.Configuration = class { return false; } - matchesEnvironment(environment, value) { - let parts = value.split('&').map(x => x.trim().toLowerCase()); + matchesEnvironment(environment: string, value: string) { + const parts = value.split('&').map(x => x.trim().toLowerCase()); return parts.indexOf(environment) !== -1; } }; diff --git a/src/file-system.ts b/src/file-system.ts new file mode 100644 index 000000000..44def4fba --- /dev/null +++ b/src/file-system.ts @@ -0,0 +1,50 @@ +import { stat, mkdir, readdir, readFile as _readFile, writeFile as _writeFile } from 'node:fs/promises' +import { existsSync, readdirSync, readFileSync as _readFileSync, writeFileSync as _writeFileSync, statSync, mkdirSync as _mkdirSync } from 'node:fs'; +import { dirname as _dirname } from 'node:path'; + +export { stat, mkdir, existsSync, readdir, readdirSync, statSync }; + +/** Accessed by unit-tests */ +export function mkdirp(path: string) { + return mkdir(path, {recursive: true}); +}; + +export function readFile(path: string, encoding: BufferEncoding = 'utf8') { + return _readFile(path, encoding); +} + +export function readFileSync(path: string, encoding: BufferEncoding = 'utf8') { + return _readFileSync(path, encoding); +}; + +export function copySync(sourceFile: string, targetFile: string) { + _writeFileSync(targetFile, _readFileSync(sourceFile)); +}; + +export function isFile(path: string) { + try { + return statSync(path).isFile(); + } catch { + // ignore + return false; + } +}; + +export function isDirectory(path: string) { + try { + return statSync(path).isDirectory(); + } catch { + // ignore + return false; + } +}; + +export async function writeFile(path: string, content: string | Buffer, encoding: BufferEncoding = 'utf8') { + await mkdir(_dirname(path), { recursive: true }); + await _writeFile(path, content, encoding); +}; + +export function writeFileSync(path: string, content: string | Buffer, encoding: BufferEncoding = 'utf8') { + _mkdirSync(_dirname(path), { recursive: true }); + _writeFileSync(path, content, encoding); +}; diff --git a/lib/get-tty-size.js b/src/get-tty-size.ts similarity index 53% rename from lib/get-tty-size.js rename to src/get-tty-size.ts index a5f71dffd..2311251fc 100644 --- a/lib/get-tty-size.js +++ b/src/get-tty-size.ts @@ -1,21 +1,19 @@ -const tty = require('tty'); +import * as tty from 'node:tty'; -let size; +let size: { height: number; width: number }; -module.exports = function() { +export function getTtySize() { // Only run it once. if (size) return size; - let width; - let height; + let width: number; + let height: number; if (tty.isatty(1) && tty.isatty(2)) { if (process.stdout.getWindowSize) { - width = process.stdout.getWindowSize(1)[0]; - height = process.stdout.getWindowSize(1)[1]; - } else if (tty.getWindowSize) { - width = tty.getWindowSize()[1]; - height = tty.getWindowSize()[0]; + [width, height] = process.stdout.getWindowSize(); + } else if ('getWindowSize' in tty && typeof tty.getWindowSize === 'function') { + [height, width] = tty.getWindowSize(); } else if (process.stdout.columns && process.stdout.rows) { height = process.stdout.rows; width = process.stdout.columns; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..871c9f23a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,12 @@ +import 'aurelia-polyfills'; + +export { CLI } from './cli'; +export { CLIOptions } from './cli-options'; +export { UI } from './ui'; +export { Project } from './project'; +export { ProjectItem } from './project-item'; +export * as build from './build/index'; +export { Configuration } from './configuration'; +export { reportReadiness as reportWebpackReadiness } from './build/webpack-reporter'; +export { NPM } from './package-managers/npm'; +export { Yarn } from './package-managers/yarn'; diff --git a/src/interfaces.d.ts b/src/interfaces.d.ts new file mode 100644 index 000000000..f155f0d63 --- /dev/null +++ b/src/interfaces.d.ts @@ -0,0 +1,31 @@ +interface ILoaderConfig { + name: string; + main?: string; + path?: string; + packageRoot?: string; + lazyMain?: boolean; + resources?: string[]; + deps?: string[]; + exports?: string[]; + wrapShim?: boolean; +} + +interface IFile { + contents: string; + path?: string; + sourceMap?: string; + dependencyInclusion?: boolean; +} + +interface IBundleSourceContext { + pkgsMainMap: {[key: string]: string}; + config: { + shim: {[key: string]: { + deps: string[], + exports: string[] + }}} +} + +interface ICommand { + execute(args?: string[]): Promise +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 000000000..6a0e87dd4 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,41 @@ +import { UI } from './ui'; +import * as c from 'ansi-colors'; + +interface ILogger { + id: string; +} + +export class Logger { + static inject() { return [UI]; } + private ui: UI; + + constructor(ui: UI) { + this.ui = ui; + } + + debug(logger: ILogger, message: string, ...rest: unknown[]) { + this.log(logger, c.bold('DEBUG'), message, rest); + } + + info(logger: ILogger, message: string, ...rest: unknown[]) { + this.log(logger, c.bold('INFO'), message, rest); + } + + warn(logger: ILogger, message: string, ...rest: unknown[]) { + this.log(logger, c.bgYellow('WARN'), message, rest); + } + + error(logger: ILogger, message: string, ...rest: unknown[]) { + this.log(logger, c.bgRed('ERROR'), message, rest); + } + + log(logger: ILogger, level: string, message: string, rest: unknown[]) { + let msg = `${level} [${logger.id}] ${message}`; + + if (rest.length > 0) { + msg += ` ${rest.map(x => JSON.stringify(x)).join(' ')}`; + } + + void this.ui.log(msg); + } +}; diff --git a/src/package-managers/base-package-manager.ts b/src/package-managers/base-package-manager.ts new file mode 100644 index 000000000..62bfa942a --- /dev/null +++ b/src/package-managers/base-package-manager.ts @@ -0,0 +1,45 @@ +import { spawn, type ChildProcess } from 'node:child_process'; +import npmWhich from 'npm-which'; +const isWindows = process.platform === 'win32'; + +export abstract class BasePackageManager { + private executableName: string; + private proc: ChildProcess | undefined; + + constructor(executableName: string) { + this.executableName = executableName; + } + + install(packages: string[] = [], workingDirectory: string = process.cwd(), command: string = 'install') { + return this.run(command, packages, workingDirectory); + } + + run(command: string, args: string[] = [], workingDirectory: string = process.cwd()): Promise { + let executable = this.getExecutablePath(workingDirectory); + if (isWindows) { + executable = JSON.stringify(executable); // Add quotes around path + } + + return new Promise((resolve, reject) => { + this.proc = spawn( + executable, + [command, ...args], + { stdio: 'inherit', cwd: workingDirectory, shell: isWindows } + ) + .on('close', resolve) + .on('error', reject); + }); + } + + getExecutablePath(directory: string): string | null { + try { + return npmWhich(directory).sync(this.executableName) as string; + } catch { + return null; + } + } + + isAvailable(directory: string): boolean { + return !!this.getExecutablePath(directory); + } +}; diff --git a/src/package-managers/npm.ts b/src/package-managers/npm.ts new file mode 100644 index 000000000..3615cb1f1 --- /dev/null +++ b/src/package-managers/npm.ts @@ -0,0 +1,7 @@ +import { BasePackageManager } from './base-package-manager'; + +export class NPM extends BasePackageManager { + constructor() { + super('npm'); + } +}; diff --git a/src/package-managers/yarn.ts b/src/package-managers/yarn.ts new file mode 100644 index 000000000..e6d558cb6 --- /dev/null +++ b/src/package-managers/yarn.ts @@ -0,0 +1,11 @@ +import { BasePackageManager } from './base-package-manager'; + +export class Yarn extends BasePackageManager { + constructor() { + super('yarn'); + } + + install(packages: string[] = [], workingDirectory: string = process.cwd()) { + return super.install(packages, workingDirectory, !packages.length ? 'install' : 'add'); + } +}; diff --git a/lib/pretty-choices.js b/src/pretty-choices.ts similarity index 63% rename from lib/pretty-choices.js rename to src/pretty-choices.ts index 792f1a8e3..c506775d0 100644 --- a/lib/pretty-choices.js +++ b/src/pretty-choices.ts @@ -1,8 +1,31 @@ -const {wordWrap} = require('enquirer/lib/utils'); -const getTtySize = require('./get-tty-size'); +import { wordWrap } from 'enquirer/lib/utils.js'; +import { getTtySize } from './get-tty-size'; + +interface Choice { + name: string + message?: string + value?: unknown + hint?: string + role?: string + enabled?: boolean + disabled?: boolean | string +} + +/** + * displayName and description are for compatibility in lib/ui.js + */ +interface ChoiceEx extends Choice { + title?: string; + displayName?: string; + description?: string; + /** used by lib/workflow/run-questionnaire */ + if?: boolean; + initial?: string; + type?: string; +} // Check all values, indent hint line. -module.exports = function(...choices) { +export function prettyChoices(...choices: ChoiceEx[]) { if (choices.length && Array.isArray(choices[0])) { choices = choices[0]; } @@ -19,7 +42,7 @@ module.exports = function(...choices) { throw new Error(`Value type ${typeof value} is not supported. Only support string value.`); } - const choice = { + const choice: ChoiceEx = { value: value, // TODO after https://github.com/enquirer/enquirer/issues/115 // add ${idx + 1}. in front of message diff --git a/src/project-item.ts b/src/project-item.ts new file mode 100644 index 000000000..c449672cf --- /dev/null +++ b/src/project-item.ts @@ -0,0 +1,95 @@ +import * as path from 'node:path'; +import * as fs from './file-system'; +import * as Utils from './build/utils'; + +// Legacy code, kept only for supporting `au generate` +export class ProjectItem { + public parent: ProjectItem | undefined; + public text: string | undefined; + + /** Accessed from `aurelia_project/components.ts` */ + public readonly name: string; + private readonly isDirectory: boolean; + private _children: ProjectItem[] | undefined; + + constructor(name: string, isDirectory: boolean) { + this.name = name; + this.isDirectory = !!isDirectory; + } + + get children() { + if (!this._children) { + this._children = []; + } + + return this._children; + } + + add(...children: ProjectItem[]) { + if (!this.isDirectory) { + throw new Error('You cannot add items to a non-directory.'); + } + + for (let i = 0; i < children.length; ++i) { + const child = children[i]; + + if (this.children.indexOf(child) !== -1) { + continue; + } + + child.parent = this; + this.children.push(child); + } + + return this; + } + + calculateRelativePath(fromLocation: string | ProjectItem): string { + if (this === fromLocation) { + return ''; + } + + const parentRelativePath = (this.parent && this.parent !== fromLocation) + ? this.parent.calculateRelativePath(fromLocation) + : ''; + + return path.posix.join(parentRelativePath, this.name); + } + + async create(relativeTo: string): Promise { + const fullPath = relativeTo ? this.calculateRelativePath(relativeTo) : this.name; + + // Skip empty folder + if (this.isDirectory && this.children.length) { + try { + await fs.stat(fullPath); + } catch { + await fs.mkdir(fullPath); + } + await Utils.runSequentially(this.children, child => child.create(fullPath)); + return; + } + + if (this.text) { + await fs.writeFile(fullPath, this.text); + } +} + + + setText(text: string) { + this.text = text; + return this; + } + + getText() { + return this.text; + } + + static text(name: string, text: string) { + return new ProjectItem(name, false).setText(text); + } + + static directory(p: string) { + return new ProjectItem(p, true); + } +}; diff --git a/src/project.ts b/src/project.ts new file mode 100644 index 000000000..deca504d8 --- /dev/null +++ b/src/project.ts @@ -0,0 +1,176 @@ +import * as path from 'node:path'; +import * as fs from './file-system'; +import * as _ from 'lodash'; +import { ProjectItem } from './project-item'; + + +export class Project implements Record { + private directory: string; + private package: object; // package.json deserialized. + private taskDirectory: string; + private generatorDirectory: string; + private model: AureliaJson.IProject; + private aureliaJSONPath: string; + private locations: ProjectItem[]; + /** Accessed from `aurelia_project/generator.ts` */ + public generators: ProjectItem; + /** Accessed from `aurelia_project/task.ts` */ + public tasks: ProjectItem; + + // From AureliaJson.IPaths + public root: ProjectItem | undefined; + public resources: ProjectItem | undefined; + public elements: ProjectItem | undefined; + public attributes: ProjectItem | undefined; + public valueConverters: ProjectItem | undefined; + public bindingBehaviors: ProjectItem | undefined; + + + static async establish(dir: string) { + process.chdir(dir); + + const model = await fs.readFile(path.join(dir, 'aurelia_project', 'aurelia.json')); + const pack = await fs.readFile(path.join(dir, 'package.json')); + return new Project(dir, JSON.parse(model.toString()) as AureliaJson.IProject, JSON.parse(pack.toString())); + } + + constructor(directory: string, model: AureliaJson.IProject, pack: object) { + this.directory = directory; + this.model = model; + this.package = pack; + this.taskDirectory = path.join(directory, 'aurelia_project/tasks'); + this.generatorDirectory = path.join(directory, 'aurelia_project/generators'); + this.aureliaJSONPath = path.join(directory, 'aurelia_project', 'aurelia.json'); + + this.locations = Object.keys(model.paths).map(key => { + this[key] = ProjectItem.directory(model.paths[key]); + + if (key !== 'root') { + this[key] = ProjectItem.directory(model.paths[key]); + this[key].parent = this.root; + } + + return this[key]; + }); + this.locations.push(this.generators = ProjectItem.directory('aurelia_project/generators')); + this.locations.push(this.tasks = ProjectItem.directory('aurelia_project/tasks')); + } + + // Legacy code. This code and those ProjectItem.directory above, were kept only + // for supporting `au generate` + commitChanges() { + return Promise.all(this.locations.map(x => x.create(this.directory))); + } + + makeFileName(name: string) { + return _.kebabCase(name); + } + + makeClassName(name: string) { + const camel = _.camelCase(name); + return camel.slice(0, 1).toUpperCase() + camel.slice(1); + } + + makeFunctionName(name: string) { + return _.camelCase(name); + } + + async installTranspiler() { + switch (this.model.transpiler.id) { + case 'babel': + await installBabel.call(this); + break; + case 'typescript': + await installTypeScript(); + break; + default: + throw new Error(`${this.model.transpiler.id} is not a supported transpiler.`); + } + } + + getExport(m: { default }, name?: string) { + return name ? m[name] : m.default; + } + + getGeneratorMetadata() { + return getMetadata(this.generatorDirectory); + } + + getTaskMetadata() { + return getMetadata(this.taskDirectory); + } + + async resolveGenerator(name: string) { + const potential = path.join(this.generatorDirectory, `${name}${this.model.transpiler.fileExtension}`); + try { + await fs.stat(potential); + return potential; + } catch { + return null; + } + } + + async resolveTask(name: string) { + const potential = path.join(this.taskDirectory, `${name}${this.model.transpiler.fileExtension}`); + try { + await fs.stat(potential); + return potential; + } catch { + return null; + } + } +}; + +async function getMetadata(dir: string) { + const files = await fs.readdir(dir); + + const metadata = await Promise.all(files + .sort() + .map(file => path.join(dir, file)) + .filter(file_1 => path.extname(file_1) === '.json') + .map(async file_2 => { + const data = await fs.readFile(file_2); + return JSON.parse(data.toString()); + })); + + return metadata; +} + +async function installBabel(): Promise { + (await import('@babel/register'))({ + babelrc: false, + configFile: false, + plugins: [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-class-properties', { loose: true }], + ['@babel/plugin-transform-modules-commonjs', {loose: true}] + ], + only: [/aurelia_project/] + }); +} + +async function installTypeScript(): Promise { + const ts = await import('typescript'); + + const json = require.extensions['.json']; + delete require.extensions['.json']; + + require.extensions['.ts'] = function(module: NodeJS.Module, filename: string) { + const source = fs.readFileSync(filename); + const result = ts.transpile(source, { + module: ts.ModuleKind.CommonJS, + declaration: false, + noImplicitAny: false, + noResolve: true, + removeComments: true, + noLib: false, + emitDecoratorMetadata: true, + experimentalDecorators: true + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (module as any)._compile(result, filename); + }; + + require.extensions['.json'] = json; +} diff --git a/lib/resources/logo.txt b/src/resources/logo.txt similarity index 100% rename from lib/resources/logo.txt rename to src/resources/logo.txt diff --git a/lib/string.js b/src/string.ts similarity index 72% rename from lib/string.js rename to src/string.ts index 0b8f0390b..84e94c479 100644 --- a/lib/string.js +++ b/src/string.ts @@ -1,14 +1,29 @@ -const os = require('os'); -const c = require('ansi-colors'); -const {wordWrap} = require('enquirer/lib/utils'); +import * as os from 'node:os'; +import * as c from 'ansi-colors'; +import { wordWrap } from 'enquirer/lib/utils.js'; -exports.buildFromMetadata = function(metadata, width) { +export interface ITaskMetadata { + name: string, + description: string, + parameters?: { + optional: boolean; + name: string; + description: string; + }[]; + flags?: { + name: string; + type: string; + description: string; + }[] +} + +export function buildFromMetadata(metadata: ITaskMetadata[], width: number) { let text = ''; metadata.forEach(json => text += transformCommandToStyledText(json, width)); return text; }; -function transformCommandToStyledText(json, width) { +function transformCommandToStyledText(json: ITaskMetadata, width: number): string { const indent = ' '.repeat(4); let text = c.magenta.bold(json.name); diff --git a/lib/ui.js b/src/ui.ts similarity index 65% rename from lib/ui.js rename to src/ui.ts index 95c977c86..baa8aa4fa 100644 --- a/lib/ui.js +++ b/src/ui.ts @@ -1,30 +1,54 @@ -const os = require('os'); -const fs = require('./file-system'); -const {wordWrap} = require('enquirer/lib/utils'); -const getTtySize = require('./get-tty-size'); -const {Input, Select} = require('enquirer'); -const prettyChoices = require('./pretty-choices'); -const {Writable} = require('stream'); -const _ = require('lodash'); - -exports.UI = class { }; - -exports.ConsoleUI = class { - constructor(cliOptions) { +import { CLIOptions } from './cli-options'; + +import * as os from 'node:os'; +import { readFile } from './file-system'; +import { wordWrap } from 'enquirer/lib/utils.js'; +import { getTtySize } from './get-tty-size'; +import { prettyChoices } from './pretty-choices'; +import { Writable } from 'node:stream'; +import * as _ from 'lodash'; + +// type definitions for `enquirer` are very bad, we need to mock them. +declare module 'enquirer' { + /** [class Input extends StringPrompt] */ + export const Input; + /** [class SelectPrompt extends ArrayPrompt] */ + export const Select; +} +import { Input, Select } from 'enquirer'; + +/** Base class, used for DI registration of `ConsoleUI` */ +export abstract class UI { + public abstract displayLogo(): Promise; + public abstract log(text: string, indent?: number): Promise; + public abstract getWidth(): number; + public abstract getHeight(): number; + public abstract ensureAnswer(answer: string, question: string, suggestion?: string) +} + +export class ConsoleUI implements UI { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static UI(UI: unknown, ui: unknown) { + throw new Error('Method not implemented.'); + } + private readonly cliOptions: CLIOptions; + static ConsoleUI: ConsoleUI; + + constructor(cliOptions: CLIOptions) { this.cliOptions = cliOptions; } - log(text, indent) { + public log(text: string, indent?: number) { if (indent !== undefined) { text = wordWrap(text, {indent, width: this.getWidth()}); } - return new Promise(resolve => { + return new Promise(resolve => { console.log(text); resolve(); }); } - ensureAnswer(answer, question, suggestion) { + ensureAnswer(answer: string, question: string, suggestion?: string) { return this._ensureAnswer(answer, question, suggestion); } @@ -39,7 +63,7 @@ exports.ConsoleUI = class { } // _debug is used to pass in answers for prompts. - async _question(question, options, defaultValue, _debug = []) { + private async _question(question, options, defaultValue, _debug = []) { let opts; let PromptType; if (!options || typeof options === 'string') { @@ -84,7 +108,7 @@ exports.ConsoleUI = class { } // _debug is used to pass in answers for prompts. - async _multiselect(question, options, _debug = []) { + private async _multiselect(question, options, _debug = []) { options = options.filter(x => includeOption(this.cliOptions, x)); const opts = { @@ -95,7 +119,8 @@ exports.ConsoleUI = class { // https://github.com/enquirer/enquirer/issues/121#issuecomment-468413408 result(names) { return Object.values(this.map(names)); - } + }, + stdout: undefined }; if (_debug && _debug.length) { @@ -106,28 +131,27 @@ exports.ConsoleUI = class { return await _run(new Select(opts), _debug); } - getWidth() { + public getWidth() { return getTtySize().width; } - getHeight() { + public getHeight() { return getTtySize().height; } - displayLogo() { + async displayLogo() { if (this.getWidth() < 50) { return this.log('Aurelia CLI' + os.EOL); } - let logoLocation = require.resolve('./resources/logo.txt'); + const logoLocation = require.resolve('./resources/logo.txt'); - return fs.readFile(logoLocation).then(logo => { - this.log(logo.toString()); - }); + const logo = await readFile(logoLocation); + void this.log(logo.toString()); } }; -function includeOption(cliOptions, option) { +function includeOption(cliOptions: CLIOptions, option: { disabled?: boolean; flag?: string }) { if (option.disabled) { return false; } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..29faec687 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "target": "ES2020", // for Node 14.x or higher on client machine + "module": "CommonJS", // for backward compatibility of output JavaScript with Aurelia CLI 3.0.x + "moduleResolution": "node10", // max version for CommonJS output + "resolveJsonModule": true, + "esModuleInterop": true, + "typeRoots": [ "node_modules/@types"], + "types": ["node"], + "sourceMap": true, + "outDir": "./dist", + "declaration": true, + "strict": false + }, + "include": [ + "src" + ] +}