diff --git a/package.json b/package.json index 3a13ac383..6c8c7ee50 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,8 @@ "rimraf": "^2.4.2", "typescript": "^2.0.3", "typings": "^1.4.0", + "vue": "^2.0.3", + "vue-loader": "^9.7.0", "webpack": "^1.11.0" } } diff --git a/src/index.ts b/src/index.ts index a43f8c03f..b9423ec12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,9 +14,9 @@ const definitionFileRegex = /\.d\.ts$/; function loader(this: interfaces.Webpack, contents: string) { this.cacheable && this.cacheable(); const callback = this.async(); - const filePath = path.normalize(this.resourcePath); - const options = makeOptions(this); + const rawFilePath = path.normalize(this.resourcePath); + const filePath = utils.appendTsSuffixIfMatch(options.appendTsSuffixTo, rawFilePath); const { instance, error } = instances.ensureTypeScriptInstance(options, this); @@ -58,6 +58,7 @@ function makeOptions(loader: interfaces.Webpack) { configFileName: 'tsconfig.json', transpileOnly: false, compilerOptions: {}, + appendTsSuffixTo: [], }, configFileOptions, queryOptions); options.ignoreDiagnostics = arrify(options.ignoreDiagnostics).map(Number); options.logLevel = options.logLevel.toUpperCase(); diff --git a/src/instances.ts b/src/instances.ts index b1e0b799d..40bb24cf3 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -105,7 +105,7 @@ export function ensureTypeScriptInstance( modifiedFiles: null, }; - const servicesHost = makeServicesHost(scriptRegex, log, loader, instance); + const servicesHost = makeServicesHost(scriptRegex, log, loader, instance, loaderOptions.appendTsSuffixTo); instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry()); loader._compiler.plugin("after-compile", afterCompile(instance, configFilePath)); diff --git a/src/interfaces.ts b/src/interfaces.ts index b725a8cd7..0ba989d1c 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -85,7 +85,7 @@ export interface Resolve { /** * The directory (absolute path) that contains your modules. * May also be an array of directories. - * This setting should be used to add individual directories to the search path. + * This setting should be used to add individual directories to the search path. */ root?: string | string[]; /** @@ -153,6 +153,7 @@ export interface LoaderOptions { transpileOnly: boolean; ignoreDiagnostics: number[]; compilerOptions: typescript.CompilerOptions; + appendTsSuffixTo: RegExp[]; } export interface TSFile { diff --git a/src/servicesHost.ts b/src/servicesHost.ts index f2f9a8db9..8f5994ec6 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -13,7 +13,8 @@ function makeServicesHost( scriptRegex: RegExp, log: logger.Logger, loader: interfaces.Webpack, - instance: interfaces.TSInstance + instance: interfaces.TSInstance, + appendTsSuffixTo: RegExp[] ) { const { compiler, compilerOptions, files } = instance; @@ -77,6 +78,7 @@ function makeServicesHost( try { resolvedFileName = resolver.resolveSync(path.normalize(path.dirname(containingFile)), moduleName); + resolvedFileName = utils.appendTsSuffixIfMatch(appendTsSuffixTo, resolvedFileName); if (!resolvedFileName.match(scriptRegex)) { resolvedFileName = null; diff --git a/src/utils.ts b/src/utils.ts index 3861b64b2..8c7ba517e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -71,3 +71,12 @@ export function makeError({ rawMessage, message, location, file }: MakeError): i return objectAssign(error, { location, file }); } + +export function appendTsSuffixIfMatch(patterns: RegExp[], path: string): string { + for (let regexp of patterns) { + if (regexp.test(path)) { + return path + '.ts'; + } + } + return path; +} diff --git a/test/comparison-tests/vue/_FLAKY_ b/test/comparison-tests/vue/_FLAKY_ new file mode 100644 index 000000000..e69de29bb diff --git a/test/comparison-tests/vue/component.vue b/test/comparison-tests/vue/component.vue new file mode 100644 index 000000000..fffad9569 --- /dev/null +++ b/test/comparison-tests/vue/component.vue @@ -0,0 +1,10 @@ + + diff --git a/test/comparison-tests/vue/expectedOutput-2.0/bundle.js b/test/comparison-tests/vue/expectedOutput-2.0/bundle.js new file mode 100644 index 000000000..2a4e68326 --- /dev/null +++ b/test/comparison-tests/vue/expectedOutput-2.0/bundle.js @@ -0,0 +1,207 @@ +/******/ (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] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = 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; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + var __vue_exports__, __vue_options__ + + /* script */ + __vue_exports__ = __webpack_require__(1) + + /* template */ + var __vue_template__ = __webpack_require__(6) + __vue_options__ = __vue_exports__ = __vue_exports__ || {} + if ( + typeof __vue_exports__.default === "object" || + typeof __vue_exports__.default === "function" + ) { + if (Object.keys(__vue_exports__).some(function (key) { return key !== "default" && key !== "__esModule" })) {console.error("named exports are not supported in *.vue files.")} + __vue_options__ = __vue_exports__ = __vue_exports__.default + } + if (typeof __vue_options__ === "function") { + __vue_options__ = __vue_options__.options + } + __vue_options__.__file = "/ts-loader/.test/vue/index.vue" + __vue_options__.render = __vue_template__.render + __vue_options__.staticRenderFns = __vue_template__.staticRenderFns + + /* hot reload */ + if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-1e83055b", __vue_options__) + } else { + hotAPI.reload("data-v-1e83055b", __vue_options__) + } + })()} + if (__vue_options__.functional) {console.error("[vue-loader] index.vue: functional components are not supported and should be defined in plain js files using render functions.")} + + module.exports = __vue_exports__ + + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + // + "use strict"; + var component_vue_1 = __webpack_require__(2); + var helper_1 = __webpack_require__(5); + exports.__esModule = true; + exports["default"] = { + components: { component: component_vue_1["default"] }, + data: function () { + return { + msg: "world" + }; + }, + method: { + myMethod: helper_1.myMethod + } + }; + + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + var __vue_exports__, __vue_options__ + + /* script */ + __vue_exports__ = __webpack_require__(3) + + /* template */ + var __vue_template__ = __webpack_require__(4) + __vue_options__ = __vue_exports__ = __vue_exports__ || {} + if ( + typeof __vue_exports__.default === "object" || + typeof __vue_exports__.default === "function" + ) { + if (Object.keys(__vue_exports__).some(function (key) { return key !== "default" && key !== "__esModule" })) {console.error("named exports are not supported in *.vue files.")} + __vue_options__ = __vue_exports__ = __vue_exports__.default + } + if (typeof __vue_options__ === "function") { + __vue_options__ = __vue_options__.options + } + __vue_options__.__file = "/ts-loader/.test/vue/component.vue" + __vue_options__.render = __vue_template__.render + __vue_options__.staticRenderFns = __vue_template__.staticRenderFns + + /* hot reload */ + if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-6043e8f4", __vue_options__) + } else { + hotAPI.reload("data-v-6043e8f4", __vue_options__) + } + })()} + if (__vue_options__.functional) {console.error("[vue-loader] component.vue: functional components are not supported and should be defined in plain js files using render functions.")} + + module.exports = __vue_exports__ + + +/***/ }, +/* 3 */ +/***/ function(module, exports) { + + // + "use strict"; + exports.__esModule = true; + exports["default"] = { + data: function () { + return { + msg: "component" + }; + } + }; + + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports={render:function (){with(this) { + return _h('p', ["Hello from " + _s(msg)]) + }},staticRenderFns: []} + if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-6043e8f4", module.exports) + } + } + +/***/ }, +/* 5 */ +/***/ function(module, exports) { + + "use strict"; + function myMethod() { + console.log('from helper!'); + } + exports.myMethod = myMethod; + + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports={render:function (){with(this) { + return _h('p', ["hallo " + _s(msg)]) + }},staticRenderFns: []} + if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-1e83055b", module.exports) + } + } + +/***/ } +/******/ ]); diff --git a/test/comparison-tests/vue/expectedOutput-2.0/bundle.transpiled.js b/test/comparison-tests/vue/expectedOutput-2.0/bundle.transpiled.js new file mode 100644 index 000000000..768f978e9 --- /dev/null +++ b/test/comparison-tests/vue/expectedOutput-2.0/bundle.transpiled.js @@ -0,0 +1,207 @@ +/******/ (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] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = 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; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + var __vue_exports__, __vue_options__ + + /* script */ + __vue_exports__ = __webpack_require__(1) + + /* template */ + var __vue_template__ = __webpack_require__(6) + __vue_options__ = __vue_exports__ = __vue_exports__ || {} + if ( + typeof __vue_exports__.default === "object" || + typeof __vue_exports__.default === "function" + ) { + if (Object.keys(__vue_exports__).some(function (key) { return key !== "default" && key !== "__esModule" })) {console.error("named exports are not supported in *.vue files.")} + __vue_options__ = __vue_exports__ = __vue_exports__.default + } + if (typeof __vue_options__ === "function") { + __vue_options__ = __vue_options__.options + } + __vue_options__.__file = "ts-loader/.test/vue.transpile/index.vue" + __vue_options__.render = __vue_template__.render + __vue_options__.staticRenderFns = __vue_template__.staticRenderFns + + /* hot reload */ + if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-39ef11c7", __vue_options__) + } else { + hotAPI.reload("data-v-39ef11c7", __vue_options__) + } + })()} + if (__vue_options__.functional) {console.error("[vue-loader] index.vue: functional components are not supported and should be defined in plain js files using render functions.")} + + module.exports = __vue_exports__ + + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + // + "use strict"; + var component_vue_1 = __webpack_require__(2); + var helper_1 = __webpack_require__(5); + exports.__esModule = true; + exports["default"] = { + components: { component: component_vue_1["default"] }, + data: function () { + return { + msg: "world" + }; + }, + method: { + myMethod: helper_1.myMethod + } + }; + + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + var __vue_exports__, __vue_options__ + + /* script */ + __vue_exports__ = __webpack_require__(3) + + /* template */ + var __vue_template__ = __webpack_require__(4) + __vue_options__ = __vue_exports__ = __vue_exports__ || {} + if ( + typeof __vue_exports__.default === "object" || + typeof __vue_exports__.default === "function" + ) { + if (Object.keys(__vue_exports__).some(function (key) { return key !== "default" && key !== "__esModule" })) {console.error("named exports are not supported in *.vue files.")} + __vue_options__ = __vue_exports__ = __vue_exports__.default + } + if (typeof __vue_options__ === "function") { + __vue_options__ = __vue_options__.options + } + __vue_options__.__file = "/ts-loader/.test/vue.transpile/component.vue" + __vue_options__.render = __vue_template__.render + __vue_options__.staticRenderFns = __vue_template__.staticRenderFns + + /* hot reload */ + if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-045601f2", __vue_options__) + } else { + hotAPI.reload("data-v-045601f2", __vue_options__) + } + })()} + if (__vue_options__.functional) {console.error("[vue-loader] component.vue: functional components are not supported and should be defined in plain js files using render functions.")} + + module.exports = __vue_exports__ + + +/***/ }, +/* 3 */ +/***/ function(module, exports) { + + // + "use strict"; + exports.__esModule = true; + exports["default"] = { + data: function () { + return { + msg: "component" + }; + } + }; + + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports={render:function (){with(this) { + return _h('p', ["Hello from " + _s(msg)]) + }},staticRenderFns: []} + if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-045601f2", module.exports) + } + } + +/***/ }, +/* 5 */ +/***/ function(module, exports) { + + "use strict"; + function myMethod() { + console.log('from helper!'); + } + exports.myMethod = myMethod; + + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports={render:function (){with(this) { + return _h('p', ["hallo " + _s(msg)]) + }},staticRenderFns: []} + if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api").rerender("data-v-39ef11c7", module.exports) + } + } + +/***/ } +/******/ ]); diff --git a/test/comparison-tests/vue/expectedOutput-2.0/output.transpiled.txt b/test/comparison-tests/vue/expectedOutput-2.0/output.transpiled.txt new file mode 100644 index 000000000..af5da2c6b --- /dev/null +++ b/test/comparison-tests/vue/expectedOutput-2.0/output.transpiled.txt @@ -0,0 +1,10 @@ + Asset Size Chunks Chunk Names +bundle.js 5.9 kB 0 [emitted] main +chunk {0} bundle.js (main) 4.31 kB [rendered] + [0] ./.test/vue/index.vue 1.58 kB {0} [built] + [1] .!./~/vue-loader/lib/selector.js?type=script&index=0!./.test/vue/index.vue 355 bytes {0} [built] + [2] ./.test/vue/component.vue 1.59 kB {0} [built] + [3] .!./~/vue-loader/lib/selector.js?type=script&index=0!./.test/vue/component.vue 157 bytes {0} [built] + [4] ./~/vue-loader/lib/template-compiler.js?id=data-v-045601f2!./~/vue-loader/lib/selector.js?type=template&index=0!./.test/vue/component.vue 265 bytes {0} [built] + [5] ./.test/vue/helper.ts 100 bytes {0} [built] + [6] ./~/vue-loader/lib/template-compiler.js?id=data-v-39ef11c7!./~/vue-loader/lib/selector.js?type=template&index=0!./.test/vue/index.vue 260 bytes {0} [built] \ No newline at end of file diff --git a/test/comparison-tests/vue/expectedOutput-2.0/output.txt b/test/comparison-tests/vue/expectedOutput-2.0/output.txt new file mode 100644 index 000000000..c3deb0713 --- /dev/null +++ b/test/comparison-tests/vue/expectedOutput-2.0/output.txt @@ -0,0 +1,10 @@ + Asset Size Chunks Chunk Names +bundle.js 5.88 kB 0 [emitted] main +chunk {0} bundle.js (main) 4.29 kB [rendered] + [0] ./.test/vue/index.vue 1.57 kB {0} [built] + [1] .!./~/vue-loader/lib/selector.js?type=script&index=0!./.test/vue/index.vue 355 bytes {0} [built] + [2] ./.test/vue/component.vue 1.58 kB {0} [built] + [3] .!./~/vue-loader/lib/selector.js?type=script&index=0!./.test/vue/component.vue 157 bytes {0} [built] + [4] ./~/vue-loader/lib/template-compiler.js?id=data-v-6043e8f4!./~/vue-loader/lib/selector.js?type=template&index=0!./.test/vue/component.vue 265 bytes {0} [built] + [5] ./.test/vue/helper.ts 100 bytes {0} [built] + [6] ./~/vue-loader/lib/template-compiler.js?id=data-v-1e83055b!./~/vue-loader/lib/selector.js?type=template&index=0!./.test/vue/index.vue 260 bytes {0} [built] \ No newline at end of file diff --git a/test/comparison-tests/vue/helper.ts b/test/comparison-tests/vue/helper.ts new file mode 100644 index 000000000..dd9c09e32 --- /dev/null +++ b/test/comparison-tests/vue/helper.ts @@ -0,0 +1,3 @@ +export function myMethod() { + console.log('from helper!') +} diff --git a/test/comparison-tests/vue/index.vue b/test/comparison-tests/vue/index.vue new file mode 100644 index 000000000..dc6a05dd6 --- /dev/null +++ b/test/comparison-tests/vue/index.vue @@ -0,0 +1,16 @@ + + diff --git a/test/comparison-tests/vue/tsconfig.json b/test/comparison-tests/vue/tsconfig.json new file mode 100644 index 000000000..0508d5f17 --- /dev/null +++ b/test/comparison-tests/vue/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + }, + "files": [] +} diff --git a/test/comparison-tests/vue/webpack.config.js b/test/comparison-tests/vue/webpack.config.js new file mode 100644 index 000000000..186534c15 --- /dev/null +++ b/test/comparison-tests/vue/webpack.config.js @@ -0,0 +1,26 @@ +module.exports = { + entry: './index.vue', + output: { + filename: 'bundle.js' + }, + resolve: { + extensions: ['', '.ts', '.vue'] + }, + module: { + loaders: [ + { test: /\.vue$/, loader: 'vue' }, + { test: /\.ts$/, loader: 'ts-loader' } + ] + }, + vue: { + loaders: { + js: 'ts-loader' + } + }, + ts: { + appendTsSuffixTo: [/\.vue$/] + } +} + +// for test harness purposes only, you would not need this in a normal project +module.exports.resolveLoader = { alias: { 'ts-loader': require('path').join(__dirname, "../../index.js") } }