Skip to content

Declaration bundler plugin #634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
79 changes: 79 additions & 0 deletions src/DeclarationBundlerPlugin.ts
Original file line number Diff line number Diff line change
@@ -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 = <any>{}) {
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;
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 16 additions & 8 deletions src/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,7 +20,7 @@ import {
WebpackError
} from './interfaces';

const instances = <TSInstances> {};
const instances = <TSInstances>{};

/**
* The loader is executed once for each file seen by webpack. However, we need to keep
Expand Down Expand Up @@ -48,7 +49,7 @@ export function getTypeScriptInstance(
}

return successfulTypeScriptInstance(
loaderOptions, loader, log,
loaderOptions, loader, log,
compiler.compiler!, compiler.compilerCompatible!, compiler.compilerDetailsLogMessage!
);
}
Expand Down Expand Up @@ -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() };
Expand All @@ -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
Expand All @@ -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 };
}
6 changes: 6 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions test/comparison-tests/create-and-execute-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading