diff --git a/README.md b/README.md index 51b3f2872..d1f13d08f 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,6 @@ don't directly use them. Instead you require them at [split points](http://webpa [TypeScript 2.4 provides support for ECMAScript's new `import()` calls. These calls import a module and return a promise to that module.](https://blogs.msdn.microsoft.com/typescript/2017/06/12/announcing-typescript-2-4-rc/) This is also supported in webpack - details on usage can be found [here](https://webpack.js.org/guides/code-splitting-async/#dynamic-import-import-). Happy code splitting! -### Declarations (.d.ts) - -To output a built .d.ts file, you can set "declaration": true in your tsconfig, and use the [DeclarationBundlerPlugin](https://www.npmjs.com/package/declaration-bundler-webpack-plugin) in your webpack config. ### Compatibility @@ -269,6 +266,30 @@ of your code. To be used in concert with the `allowJs` compiler option. If your entry file is JS then you'll need to set this option to true. Please note that this is rather unusual and will generally not be necessary when using `allowJs`. +#### declarationBundle *(object)* + +If declarationBundle is set, the output .d.ts files will be combined into a single .d.ts file. + +Properties: +moduleName - the name of the internal module to generate +out - the path where the combined declaration file should be saved + +Set compilerOptions.declaration: true in your tsconfig, and ts-loader will output a .d.ts for every .ts file you had, and they will be linked. This may work fine! Or, you may want to bundle them together. To bundle them, you probably can't be using something like ES6 modules with named exports since the bundler only does simple concatenation. Also, your exported names do matter and will be used in the .d.ts. + +Example webpack config: +``` +{ + test: /\.ts$/, + loader: 'ts-loader', + options: { + declarationBundle: { + out: 'dist/bundle.d.ts', + moduleName: 'MyApp' + } + } + } +``` + #### appendTsSuffixTo *(RegExp[]) (default=[])* #### appendTsxSuffixTo *(RegExp[]) (default=[])* A list of regular expressions to be matched against filename. If filename matches one of the regular expressions, a `.ts` or `.tsx` suffix will be appended to that filename. diff --git a/src/DeclarationBundlerPlugin.ts b/src/DeclarationBundlerPlugin.ts new file mode 100644 index 000000000..a06dba1fb --- /dev/null +++ b/src/DeclarationBundlerPlugin.ts @@ -0,0 +1,79 @@ + +/** Based on https://www.npmjs.com/package/declaration-bundler-webpack-plugin - the original is broken due to a webpack api change and unmaintained. + * Typescript outputs a .d.ts file for every .ts file. This plugin just combines them. Typescript itself *can* combine them, but then you don't get the + * benefits of webpack. */ +import { + Compiler, WebpackCompilation, DeclarationBundleOptions +} from './interfaces'; + +class DeclarationBundlerPlugin { + constructor(options: DeclarationBundleOptions = {}) { + if (!options.moduleName) { + throw new Error('declarationBundle.moduleName is required'); + } + this.moduleName = options.moduleName; + this.out = options.out || this.moduleName + '.d.ts'; + } + + out: string; + moduleName: string; + + apply(compiler: Compiler) { + compiler.plugin('emit', (compilation: WebpackCompilation, callback: any) => { + var declarationFiles = {}; + + for (var filename in compilation.assets) { + if (filename.indexOf('.d.ts') !== -1) { + declarationFiles[filename] = compilation.assets[filename]; + delete compilation.assets[filename]; + } + } + + var combinedDeclaration = this.generateCombinedDeclaration(declarationFiles); + console.log(this.out); + compilation.assets[this.out] = { + source: function () { + return combinedDeclaration; + }, + size: function () { + return combinedDeclaration.length; + } + }; + + callback(); + }); + } + + generateCombinedDeclaration(declarationFiles: any) { + var declarations = ''; + for (var fileName in declarationFiles) { + var declarationFile = declarationFiles[fileName]; + var data = declarationFile._value || declarationFile.source(); + + var lines = data.split("\n"); + + var i = lines.length; + while (i--) { + var line = lines[i]; + var excludeLine = line === "" + || line.indexOf("export =") !== -1 + || (/import ([a-z0-9A-Z_-]+) = require\(/).test(line); + + if (excludeLine) { + lines.splice(i, 1); + } else { + if (line.indexOf("declare ") !== -1) { + lines[i] = line.replace("declare ", ""); + } + //add tab + lines[i] = "\t" + lines[i]; + } + } + declarations += lines.join("\n") + "\n\n"; + } + var output = "declare module " + this.moduleName + "\n{\n" + declarations + "}"; + return output; + }; +} + +export default DeclarationBundlerPlugin; diff --git a/src/index.ts b/src/index.ts index 8cd5d8c67..bc886e024 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,7 +109,12 @@ function getLoaderOptions(loader: Webpack) { } type ValidLoaderOptions = keyof LoaderOptions; -const validLoaderOptions: ValidLoaderOptions[] = ['silent', 'logLevel', 'logInfoToStdOut', 'instance', 'compiler', 'configFile', 'configFileName' /*DEPRECATED*/, 'transpileOnly', 'ignoreDiagnostics', 'visualStudioErrorFormat', 'compilerOptions', 'appendTsSuffixTo', 'appendTsxSuffixTo', 'entryFileIsJs', 'happyPackMode', 'getCustomTransformers']; +const validLoaderOptions: ValidLoaderOptions[] = [ + 'silent', 'logLevel', 'logInfoToStdOut', 'instance', 'compiler', 'configFile', + 'configFileName' /*DEPRECATED*/, 'transpileOnly', 'ignoreDiagnostics', 'visualStudioErrorFormat', + 'compilerOptions', 'appendTsSuffixTo', 'appendTsxSuffixTo', 'entryFileIsJs', 'happyPackMode', + 'getCustomTransformers', 'declarationBundle' +]; /** * Validate the supplied loader options. diff --git a/src/instances.ts b/src/instances.ts index a6abfbd07..4ae71e863 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -10,7 +10,8 @@ import { hasOwnProperty, makeError, formatErrors, registerWebpackErrors } from ' import * as logger from './logger'; import { makeServicesHost } from './servicesHost'; import { makeWatchRun } from './watch-run'; -import { +import DeclarationBundlerPlugin from './DeclarationBundlerPlugin'; +import { LoaderOptions, TSFiles, TSInstance, @@ -19,7 +20,7 @@ import { WebpackError } from './interfaces'; -const instances = {}; +const instances = {}; /** * The loader is executed once for each file seen by webpack. However, we need to keep @@ -48,7 +49,7 @@ export function getTypeScriptInstance( } return successfulTypeScriptInstance( - loaderOptions, loader, log, + loaderOptions, loader, log, compiler.compiler!, compiler.compilerCompatible!, compiler.compilerDetailsLogMessage! ); } @@ -94,7 +95,7 @@ function successfulTypeScriptInstance( if (!loaderOptions.happyPackMode) { registerWebpackErrors( loader._module.errors, - formatErrors(diagnostics, loaderOptions, compiler!, {file: configFilePath || 'tsconfig.json'})); + formatErrors(diagnostics, loaderOptions, compiler!, { file: configFilePath || 'tsconfig.json' })); } const instance = { compiler, compilerOptions, loaderOptions, files, dependencyGraph: {}, reverseDependencyGraph: {}, transformers: getCustomTransformers() }; @@ -114,11 +115,13 @@ function successfulTypeScriptInstance( text: fs.readFileSync(normalizedFilePath, 'utf-8'), version: 0 }; - }); + }); } catch (exc) { - return { error: makeError({ - rawMessage: `A file specified in tsconfig.json could not be found: ${ normalizedFilePath! }` - }) }; + return { + error: makeError({ + rawMessage: `A file specified in tsconfig.json could not be found: ${normalizedFilePath!}` + }) + }; } // if allowJs is set then we should accept js(x) files @@ -145,5 +148,10 @@ function successfulTypeScriptInstance( loader._compiler.plugin("after-compile", makeAfterCompile(instance, configFilePath)); loader._compiler.plugin("watch-run", makeWatchRun(instance)); + if (loaderOptions.declarationBundle && compilerOptions.declaration) { + var declarationBundler = new DeclarationBundlerPlugin(loaderOptions.declarationBundle); + declarationBundler.apply(loader._compiler); + } + return { instance }; } diff --git a/src/interfaces.ts b/src/interfaces.ts index 00280ef55..e48c34e9e 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -264,6 +264,12 @@ export interface LoaderOptions { entryFileIsJs: boolean; happyPackMode: boolean; getCustomTransformers?(): typescript.CustomTransformers | undefined; + declarationBundle?: DeclarationBundleOptions; +} + +export interface DeclarationBundleOptions { + moduleName: string; + out?: string; } export interface TSFile { diff --git a/test/comparison-tests/create-and-execute-test.js b/test/comparison-tests/create-and-execute-test.js index 4bbb52d4f..b7ab42460 100644 --- a/test/comparison-tests/create-and-execute-test.js +++ b/test/comparison-tests/create-and-execute-test.js @@ -50,6 +50,7 @@ if (fs.statSync(testPath).isDirectory() && if (testToRun === 'declarationOutput' || testToRun === 'importsWatch' || testToRun === 'declarationWatch' || + testToRun === 'declarationBundle' || // declarations can't be created with transpile testToRun === 'issue71' || testToRun === 'appendSuffixToWatch') { return; } @@ -120,7 +121,9 @@ function storeSavedOutputs(saveOutputMode, outputs, test, options, paths) { mkdirp.sync(paths.originalExpectedOutput); } else { - assert.ok(pathExists(paths.originalExpectedOutput), 'The expected output does not exist; there is nothing to compare against! Has the expected output been created?\nCould not find: ' + paths.originalExpectedOutput) + assert.ok(pathExists(paths.originalExpectedOutput), + 'The expected output does not exist; there is nothing to compare against! Has the expected output been created?\nCould not find: ' + + paths.originalExpectedOutput) } } @@ -380,7 +383,7 @@ function getNormalisedFileContent(file, location, test) { return 'at ' + remainingPathAndColon + 'irrelevant-line-number' + colon + 'irrelevant-column-number'; }); } catch (e) { - fileContent = '!!!' + filePath + ' doePsnt exist!!!'; + fileContent = '!!!' + filePath + ' does not exist!!!'; } return fileContent; } diff --git a/test/comparison-tests/declarationBundle/expectedOutput-2.5/out1/bundle.d.ts b/test/comparison-tests/declarationBundle/expectedOutput-2.5/out1/bundle.d.ts new file mode 100644 index 000000000..3083cad97 --- /dev/null +++ b/test/comparison-tests/declarationBundle/expectedOutput-2.5/out1/bundle.d.ts @@ -0,0 +1,29 @@ +declare module MyApp +{ + var App: { + Circle: typeof Circle; + Square: typeof Square; + }; + + class Circle extends Shape { + radius: number; + constructor(x: number, y: number, radius: number); + } + + class Shape { + x: number; + y: number; + /** x and y refer to the center of the shape */ + constructor(x: number, y: number); + moveTo(x: number, y: number): void; + fillColor: '#123456'; + borderColor: '#555555'; + borderWidth: 1; + } + + class Square extends Shape { + sideLength: number; + constructor(x: number, y: number, sideLength: number); + } + +} \ No newline at end of file diff --git a/test/comparison-tests/declarationBundle/expectedOutput-2.5/out1/bundle.js b/test/comparison-tests/declarationBundle/expectedOutput-2.5/out1/bundle.js new file mode 100644 index 000000000..057a7e176 --- /dev/null +++ b/test/comparison-tests/declarationBundle/expectedOutput-2.5/out1/bundle.js @@ -0,0 +1,167 @@ +/******/ (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; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // 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 = 3); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +var Shape = /** @class */ (function () { + /** x and y refer to the center of the shape */ + function Shape(x, y) { + this.x = x; + this.y = y; + } + Shape.prototype.moveTo = function (x, y) { + this.x = x; + this.y = y; + }; + return Shape; +}()); +module.exports = Shape; + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var Shape = __webpack_require__(0); +var Circle = /** @class */ (function (_super) { + __extends(Circle, _super); + function Circle(x, y, radius) { + var _this = _super.call(this, x, y) || this; + _this.radius = radius; + return _this; + } + return Circle; +}(Shape)); +module.exports = Circle; + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var Shape = __webpack_require__(0); +var Square = /** @class */ (function (_super) { + __extends(Square, _super); + function Square(x, y, sideLength) { + var _this = _super.call(this, x, y) || this; + _this.sideLength = sideLength; + return _this; + } + return Square; +}(Shape)); +module.exports = Square; + + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +var Circle = __webpack_require__(1); +var Square = __webpack_require__(2); +// const c = new Circle(100, 200, 60); +// c.moveTo(150, 250); +// const s = new Square(100, 200, 60); +// s.moveTo(150, 250); +var App = { + Circle: Circle, Square: Square +}; +module.exports = App; + + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/test/comparison-tests/declarationBundle/expectedOutput-2.5/output.txt b/test/comparison-tests/declarationBundle/expectedOutput-2.5/output.txt new file mode 100644 index 000000000..4a7f23d9f --- /dev/null +++ b/test/comparison-tests/declarationBundle/expectedOutput-2.5/output.txt @@ -0,0 +1,8 @@ + Asset Size Chunks Chunk Names + out1/bundle.js 5.16 kB 0 [emitted] main +out1/bundle.d.ts 589 bytes [emitted] +chunk {0} out1/bundle.js (main) 2.25 kB [entry] [rendered] + [0] ./.test/declarationBundle/src/Shape.ts 322 bytes {0} [built] + [1] ./.test/declarationBundle/src/Circle.ts 821 bytes {0} [built] + [2] ./.test/declarationBundle/src/Square.ts 833 bytes {0} [built] + [3] ./.test/declarationBundle/src/app.ts 278 bytes {0} [built] \ No newline at end of file diff --git a/test/comparison-tests/declarationBundle/src/Circle.ts b/test/comparison-tests/declarationBundle/src/Circle.ts new file mode 100644 index 000000000..60d3d1c71 --- /dev/null +++ b/test/comparison-tests/declarationBundle/src/Circle.ts @@ -0,0 +1,9 @@ +import Shape = require('./Shape'); + +class Circle extends Shape { + constructor(x: number, y: number, public radius: number) { + super(x, y); + } +} + +export = Circle; \ No newline at end of file diff --git a/test/comparison-tests/declarationBundle/src/Shape.ts b/test/comparison-tests/declarationBundle/src/Shape.ts new file mode 100644 index 000000000..13616da24 --- /dev/null +++ b/test/comparison-tests/declarationBundle/src/Shape.ts @@ -0,0 +1,18 @@ +class Shape { + /** x and y refer to the center of the shape */ + constructor(public x: number, public y: number) { + } + + moveTo(x: number, y: number) { + this.x = x; + this.y = y; + } + + fillColor: '#123456'; + + borderColor: '#555555'; + + borderWidth: 1; +} + +export = Shape; \ No newline at end of file diff --git a/test/comparison-tests/declarationBundle/src/Square.ts b/test/comparison-tests/declarationBundle/src/Square.ts new file mode 100644 index 000000000..f80e2d4f9 --- /dev/null +++ b/test/comparison-tests/declarationBundle/src/Square.ts @@ -0,0 +1,9 @@ +import Shape = require('./Shape'); + +class Square extends Shape { + constructor(x: number, y: number, public sideLength: number) { + super(x, y); + } +} + +export = Square; \ No newline at end of file diff --git a/test/comparison-tests/declarationBundle/src/app.ts b/test/comparison-tests/declarationBundle/src/app.ts new file mode 100644 index 000000000..a1ed29712 --- /dev/null +++ b/test/comparison-tests/declarationBundle/src/app.ts @@ -0,0 +1,8 @@ +import Circle = require('./Circle'); +import Square = require('./Square'); + +var App = { + Circle, Square +}; + +export = App; diff --git a/test/comparison-tests/declarationBundle/tsconfig.json b/test/comparison-tests/declarationBundle/tsconfig.json new file mode 100644 index 000000000..6c9058ec4 --- /dev/null +++ b/test/comparison-tests/declarationBundle/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "declaration": true + } +} diff --git a/test/comparison-tests/declarationBundle/webpack.config.js b/test/comparison-tests/declarationBundle/webpack.config.js new file mode 100644 index 000000000..48f06ef71 --- /dev/null +++ b/test/comparison-tests/declarationBundle/webpack.config.js @@ -0,0 +1,23 @@ +module.exports = { + entry: './src/app.ts', + output: { + filename: 'out1/bundle.js' + }, + resolve: { + extensions: ['.ts', '.js'] + }, + module: { + rules: [ + { + test: /\.ts$/, + loader: 'ts-loader', + options: { + declarationBundle: { + out: 'out1/bundle.d.ts', + moduleName: 'MyApp' + } + } + } + ] + } +}