From 35400b6cdb3c88601d9111db93f6c33d0811df43 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 17 Dec 2018 14:43:16 -0800 Subject: [PATCH] Revert "Revert "Add support for tsconfig `paths`. (fixes #150)" (#171)" This reverts commit d26fce1fc9e8057e0f7900106ffaa8cf3253c696. --- .gitignore | 3 + package.json | 2 + src/index.js | 46 +++++++- test/index.test.js | 12 +- .../input.ts | 6 + .../module.ts | 1 + .../output.js | 110 ++++++++++++++++++ .../tsconfig.json | 10 ++ test/unit/tsconfig-paths/input.ts | 3 + test/unit/tsconfig-paths/module.ts | 1 + test/unit/tsconfig-paths/output.js | 110 ++++++++++++++++++ test/unit/tsconfig-paths/tsconfig.json | 10 ++ yarn.lock | 39 ++++++- 13 files changed, 345 insertions(+), 8 deletions(-) create mode 100644 test/unit/tsconfig-paths-conflicting-external/input.ts create mode 100644 test/unit/tsconfig-paths-conflicting-external/module.ts create mode 100644 test/unit/tsconfig-paths-conflicting-external/output.js create mode 100644 test/unit/tsconfig-paths-conflicting-external/tsconfig.json create mode 100644 test/unit/tsconfig-paths/input.ts create mode 100644 test/unit/tsconfig-paths/module.ts create mode 100644 test/unit/tsconfig-paths/output.js create mode 100644 test/unit/tsconfig-paths/tsconfig.json diff --git a/.gitignore b/.gitignore index a28edb18..55b4a716 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ dist/**/*.js !test/integration !test/integration/*.json !test/integration/*.js +!test/integration/*.ts +!test/unit +!test/unit/** !dist/ !dist/ncc/ !dist/buildin/ diff --git a/package.json b/package.json index 66dd844d..29ea57b0 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,8 @@ "terser": "^3.11.0", "the-answer": "^1.0.0", "ts-loader": "^5.3.1", + "tsconfig-paths": "^3.7.0", + "tsconfig-paths-webpack-plugin": "^3.2.0", "twilio": "^3.23.2", "typescript": "^3.2.2", "vue": "^2.5.17", diff --git a/src/index.js b/src/index.js index e3be1052..3cae6211 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,8 @@ const MemoryFS = require("memory-fs"); const WebpackParser = require("webpack/lib/Parser"); const webpackParse = WebpackParser.parse; const terser = require("terser"); +const tsconfigPaths = require("tsconfig-paths"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const shebangRegEx = require('./utils/shebang'); // overload the webpack parser so that we can make @@ -21,7 +23,7 @@ WebpackParser.parse = function(source, opts = {}) { const SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".json", ".node"]; -function resolveModule(context, request, callback, forcedExternals = []) { +function resolveModule(matchPath, context, request, callback, forcedExternals = []) { const resolveOptions = { basedir: context, preserveSymlinks: true, @@ -35,6 +37,12 @@ function resolveModule(context, request, callback, forcedExternals = []) { resolve(request, resolveOptions, err => { if (err) { + // check tsconfig paths before erroring + if (matchPath && matchPath(request, undefined, undefined, SUPPORTED_EXTENSIONS)) { + callback(); + return; + } + console.error( `ncc: Module directory "${context}" attempted to require "${request}" but could not be resolved, assuming external.` ); @@ -58,6 +66,20 @@ module.exports = async ( const mfs = new MemoryFS(); const assetNames = Object.create(null); const assets = Object.create(null); + const resolvePlugins = []; + let tsconfigMatchPath; + // add TsconfigPathsPlugin to support `paths` resolution in tsconfig + // we need to catch here because the plugin will + // error if there's no tsconfig in the working directory + try { + resolvePlugins.push(new TsconfigPathsPlugin({ silent: true })); + + const tsconfig = tsconfigPaths.loadConfig(); + if (tsconfig.resultType === "success") { + tsconfigMatchPath = tsconfigPaths.createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths); + } + } catch (e) {} + const compiler = webpack({ entry, optimization: { @@ -76,11 +98,12 @@ module.exports = async ( extensions: SUPPORTED_EXTENSIONS, // webpack defaults to `module` and `main`, but that's // not really what node.js supports, so we reset it - mainFields: ["main"] + mainFields: ["main"], + plugins: resolvePlugins }, // https://github.com/zeit/ncc/pull/29#pullrequestreview-177152175 node: false, - externals: (...args) => resolveModule(...[...args, externals]), + externals: (...args) => resolveModule(tsconfigMatchPath, ...[...args, externals]), module: { rules: [ { @@ -140,7 +163,7 @@ module.exports = async ( "var e = new Error", `if (typeof req === 'number' && __webpack_require__.m[req])\n` + ` return __webpack_require__(req);\n` + - `try { return require(req) }\n` + + `try { return require(req) }\n` + `catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }\n` + `var e = new Error` ); @@ -176,6 +199,21 @@ module.exports = async ( }); compiler.inputFileSystem = fs; compiler.outputFileSystem = mfs; + // tsconfig-paths-webpack-plugin requires a readJson method on the filesystem + compiler.inputFileSystem.readJson = (path, callback) => { + compiler.inputFileSystem.readFile(path, (err, data) => { + if (err) { + callback(err); + return; + } + + try { + callback(null, JSON.parse(data)); + } catch (e) { + callback(e); + } + }); + }; compiler.resolvers.normal.fileSystem = mfs; return new Promise((resolve, reject) => { compiler.run((err, stats) => { diff --git a/test/index.test.js b/test/index.test.js index 9da768e8..75247983 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -6,13 +6,19 @@ const { dirname } = require("path"); for (const unitTest of fs.readdirSync(`${__dirname}/unit`)) { it(`should generate correct output for ${unitTest}`, async () => { + const testDir = `${__dirname}/unit/${unitTest}`; const expected = fs - .readFileSync(`${__dirname}/unit/${unitTest}/output.js`) + .readFileSync(`${testDir}/output.js`) .toString() .trim() // Windows support .replace(/\r/g, ""); - await ncc(`${__dirname}/unit/${unitTest}/input.js`, { minify: false }).then( + + // set env variable so tsconfig-paths can find the config + process.env.TS_NODE_PROJECT = `${testDir}/tsconfig.json`; + // find the name of the input file (e.g input.ts) + const inputFile = fs.readdirSync(testDir).find(file => file.includes("input")); + await ncc(`${testDir}/${inputFile}`, { minify: false }).then( async ({ code, assets }) => { // very simple asset validation in unit tests if (unitTest.startsWith("asset-")) { @@ -27,7 +33,7 @@ for (const unitTest of fs.readdirSync(`${__dirname}/unit`)) { expect(actual).toBe(expected); } catch (e) { // useful for updating fixtures - fs.writeFileSync(`${__dirname}/unit/${unitTest}/actual.js`, actual); + fs.writeFileSync(`${testDir}/actual.js`, actual); throw e; } } diff --git a/test/unit/tsconfig-paths-conflicting-external/input.ts b/test/unit/tsconfig-paths-conflicting-external/input.ts new file mode 100644 index 00000000..12d2def5 --- /dev/null +++ b/test/unit/tsconfig-paths-conflicting-external/input.ts @@ -0,0 +1,6 @@ +import module from "@module"; +// this matches the pattern specified in tsconfig, +// but it should use the external module instead of erroring +import * as _ from "@sentry/node"; + +console.log(module); diff --git a/test/unit/tsconfig-paths-conflicting-external/module.ts b/test/unit/tsconfig-paths-conflicting-external/module.ts new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/test/unit/tsconfig-paths-conflicting-external/module.ts @@ -0,0 +1 @@ +export default {}; diff --git a/test/unit/tsconfig-paths-conflicting-external/output.js b/test/unit/tsconfig-paths-conflicting-external/output.js new file mode 100644 index 00000000..a56b54a6 --- /dev/null +++ b/test/unit/tsconfig-paths-conflicting-external/output.js @@ -0,0 +1,110 @@ +module.exports = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +exports.__esModule = true; +var _module_1 = __webpack_require__(1); +console.log(_module_1["default"]); + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +exports.__esModule = true; +exports["default"] = {}; + + +/***/ }) +/******/ ]); diff --git a/test/unit/tsconfig-paths-conflicting-external/tsconfig.json b/test/unit/tsconfig-paths-conflicting-external/tsconfig.json new file mode 100644 index 00000000..a44f3b75 --- /dev/null +++ b/test/unit/tsconfig-paths-conflicting-external/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "baseUrl": ".", + "paths": { + "@*": ["./*"] + } + } +} diff --git a/test/unit/tsconfig-paths/input.ts b/test/unit/tsconfig-paths/input.ts new file mode 100644 index 00000000..01a9bd7e --- /dev/null +++ b/test/unit/tsconfig-paths/input.ts @@ -0,0 +1,3 @@ +import module from "@module"; + +console.log(module); diff --git a/test/unit/tsconfig-paths/module.ts b/test/unit/tsconfig-paths/module.ts new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/test/unit/tsconfig-paths/module.ts @@ -0,0 +1 @@ +export default {}; diff --git a/test/unit/tsconfig-paths/output.js b/test/unit/tsconfig-paths/output.js new file mode 100644 index 00000000..a56b54a6 --- /dev/null +++ b/test/unit/tsconfig-paths/output.js @@ -0,0 +1,110 @@ +module.exports = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +exports.__esModule = true; +var _module_1 = __webpack_require__(1); +console.log(_module_1["default"]); + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +exports.__esModule = true; +exports["default"] = {}; + + +/***/ }) +/******/ ]); diff --git a/test/unit/tsconfig-paths/tsconfig.json b/test/unit/tsconfig-paths/tsconfig.json new file mode 100644 index 00000000..a44f3b75 --- /dev/null +++ b/test/unit/tsconfig-paths/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "baseUrl": ".", + "paths": { + "@*": ["./*"] + } + } +} diff --git a/yarn.lock b/yarn.lock index a19c50fe..5cd21fca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -842,6 +842,11 @@ "@types/node" "*" "@types/request" "*" +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/lodash@^4.14.104": version "4.14.118" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.118.tgz#247bab39bfcc6d910d4927c6e06cbc70ec376f27" @@ -3155,6 +3160,11 @@ deepmerge@^1.5.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== +deepmerge@^2.0.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + default-require-extensions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" @@ -6100,6 +6110,13 @@ json5@^0.5.0, json5@^0.5.1: resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -10443,7 +10460,7 @@ strip-bom-string@^0.1.2: resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-0.1.2.tgz#9c6e720a313ba9836589518405ccfb88a5f41b9c" integrity sha1-nG5yCjE7qYNliVGEBcz7iKX0G5w= -strip-bom@3.0.0: +strip-bom@3.0.0, strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= @@ -10937,6 +10954,26 @@ ts-loader@^5.3.1: micromatch "^3.1.4" semver "^5.0.1" +tsconfig-paths-webpack-plugin@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz#6e70bd42915ad0efb64d3385163f0c1270f3e04d" + integrity sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + tsconfig-paths "^3.4.0" + +tsconfig-paths@^3.4.0, tsconfig-paths@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.7.0.tgz#02ae978db447b22e09dafcd4198be95c4885ceb2" + integrity sha512-7iE+Q/2E1lgvxD+c0Ot+GFFmgmfIjt/zCayyruXkXQ84BLT85gHXy0WSoQSiuFX9+d+keE/jiON7notV74ZY+A== + dependencies: + "@types/json5" "^0.0.29" + deepmerge "^2.0.1" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tslib@1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8"