Skip to content

Commit

Permalink
feat: bundle type definitions into a single file per module (#12345)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Feb 9, 2022
1 parent e9eced4 commit 819c2be
Show file tree
Hide file tree
Showing 6 changed files with 456 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -20,6 +20,7 @@

- `[*]` [**BREAKING**] Drop support for Node v10 and v15 and target first LTS `16.13.0` ([#12220](https://github.com/facebook/jest/pull/12220))
- `[*]` [**BREAKING**] Drop support for `typescript@3.8`, minimum version is now `4.2` ([#11142](https://github.com/facebook/jest/pull/11142))
- `[*]` Bundle all `.d.ts` files into a single `index.d.ts` per module ([#12345](https://github.com/facebook/jest/pull/12345))
- `[expect]` [**BREAKING**] Remove support for importing `build/utils` ([#12323](https://github.com/facebook/jest/pull/12323))
- `[@jest/core]` Use `index.ts` instead of `jest.ts` as main export ([#12329](https://github.com/facebook/jest/pull/12329))
- `[jest]` Use `index.ts` instead of `jest.ts` as main export ([#12329](https://github.com/facebook/jest/pull/12329))
Expand Down
5 changes: 4 additions & 1 deletion package.json
Expand Up @@ -12,6 +12,7 @@
"@crowdin/cli": "^3.5.2",
"@jest/globals": "workspace:*",
"@jest/test-utils": "workspace:*",
"@microsoft/api-extractor": "^7.19.4",
"@tsconfig/node12": "^1.0.9",
"@tsd/typescript": "~4.5.5",
"@types/babel__core": "^7.0.0",
Expand Down Expand Up @@ -64,6 +65,7 @@
"mock-fs": "^4.4.1",
"netlify-plugin-cache": "^1.0.3",
"node-notifier": "^10.0.0",
"pkg-dir": "^5.0.0",
"prettier": "^2.1.1",
"progress": "^2.0.0",
"promise": "^8.0.2",
Expand All @@ -84,9 +86,10 @@
},
"scripts": {
"build-clean": "rimraf './packages/*/build' './packages/*/dist' './packages/*/tsconfig.tsbuildinfo'",
"build": "yarn build:js && yarn build:ts",
"build": "yarn build:js && yarn build:ts && yarn bundle:ts",
"build:js": "node ./scripts/build.js",
"build:ts": "node ./scripts/buildTs.js",
"bundle:ts": "node ./scripts/bundleTs.js",
"check-copyright-headers": "node ./scripts/checkCopyrightHeaders.js",
"clean-all": "yarn clean-e2e && yarn build-clean && rimraf './packages/*/node_modules' && rimraf './node_modules'",
"clean-e2e": "node ./scripts/cleanE2e.js",
Expand Down
8 changes: 6 additions & 2 deletions packages/jest-util/src/index.ts
Expand Up @@ -5,6 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/

// need to do this for api-extractor: https://github.com/microsoft/rushstack/issues/2780
import * as preRunMessage from './preRunMessage';
import * as specialChars from './specialChars';

export {default as clearLine} from './clearLine';
export {default as createDirectory} from './createDirectory';
export {default as ErrorWithStack} from './ErrorWithStack';
Expand All @@ -15,11 +19,11 @@ export {default as isPromise} from './isPromise';
export {default as setGlobal} from './setGlobal';
export {default as deepCyclicCopy} from './deepCyclicCopy';
export {default as convertDescriptorToString} from './convertDescriptorToString';
export * as specialChars from './specialChars';
export {specialChars};
export {default as replacePathSepForGlob} from './replacePathSepForGlob';
export {default as testPathPatternToRegExp} from './testPathPatternToRegExp';
export {default as globsToMatcher} from './globsToMatcher';
export * as preRunMessage from './preRunMessage';
export {preRunMessage};
export {default as pluralize} from './pluralize';
export {default as formatTime} from './formatTime';
export {default as tryRealpath} from './tryRealpath';
Expand Down
15 changes: 8 additions & 7 deletions scripts/buildTs.js
Expand Up @@ -11,16 +11,13 @@ const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const util = require('util');
const chalk = require('chalk');
const execa = require('execa');
const globby = require('globby');
const stripJsonComments = require('strip-json-comments');
const throat = require('throat');
const {getPackages} = require('./buildUtils');

const readFilePromise = util.promisify(fs.readFile);

(async () => {
const packages = getPackages();

Expand Down Expand Up @@ -124,6 +121,9 @@ const readFilePromise = util.promisify(fs.readFile);
// we want to limit the number of processes we spawn
const cpus = Math.max(1, os.cpus().length - 1);

const typesReferenceDirective = '/// <reference types';
const typesNodeReferenceDirective = `${typesReferenceDirective}="node" />`;

try {
await Promise.all(
packagesWithTs.map(
Expand All @@ -134,21 +134,22 @@ const readFilePromise = util.promisify(fs.readFile);

const files = await Promise.all(
globbed.map(file =>
Promise.all([file, readFilePromise(file, 'utf8')]),
Promise.all([file, fs.promises.readFile(file, 'utf8')]),
),
);

const filesWithTypeReferences = files
.filter(([, content]) => content.includes('/// <reference types'))
.filter(([, content]) => content.includes(typesReferenceDirective))
.filter(hit => hit.length > 0);

const filesWithReferences = filesWithTypeReferences
.map(([name, content]) => [
name,
content
.split('\n')
.filter(line => line !== '/// <reference types="node" />')
.filter(line => line.includes('/// <reference types'))
.map(line => line.trim())
.filter(line => line.includes(typesReferenceDirective))
.filter(line => line !== typesNodeReferenceDirective)
.join('\n'),
])
.filter(([, content]) => content.length > 0)
Expand Down
201 changes: 201 additions & 0 deletions scripts/bundleTs.js
@@ -0,0 +1,201 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

const fs = require('fs');
const path = require('path');
const {
CompilerState,
Extractor,
ExtractorConfig,
} = require('@microsoft/api-extractor');
const chalk = require('chalk');
const {sync: pkgDir} = require('pkg-dir');
const prettier = require('prettier');
const rimraf = require('rimraf');
const {getPackages} = require('./buildUtils');

const prettierConfig = prettier.resolveConfig.sync(
__filename.replace(/\.js$/, '.d.ts'),
);

const typescriptCompilerFolder = pkgDir(require.resolve('typescript'));

const copyrightSnippet = `
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
`.trim();

(async () => {
const packages = getPackages();

const packagesWithTs = packages.filter(p =>
fs.existsSync(path.resolve(p.packageDir, 'tsconfig.json')),
);

const typesNodeReferenceDirective = '/// <reference types="node" />';

console.log(chalk.inverse(' Extracting TypeScript definition files '));

const sharedExtractorConfig = {
$schema:
'https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json',

apiReport: {
enabled: false,
},

/**
* A list of NPM package names whose exports should be treated as part of this package.
*/
bundledPackages: [],

compiler: {
skipLibCheck: true,
},

docModel: {
enabled: false,
},

dtsRollup: {
enabled: true,
untrimmedFilePath: '<projectFolder>/dist/index.d.ts',
},

messages: {
compilerMessageReporting: {
default: {
logLevel: 'warning',
},
},

extractorMessageReporting: {
'ae-forgotten-export': {
logLevel: 'none',
},
'ae-missing-release-tag': {
logLevel: 'none',
},
default: {
logLevel: 'warning',
},
},

tsdocMessageReporting: {
default: {
logLevel: 'none',
},
},
},

tsdocMetadata: {
enabled: false,
},
};

await fs.promises.writeFile(
path.resolve(__dirname, '../api-extractor.json'),
JSON.stringify(sharedExtractorConfig, null, 2),
);

let compilerState;

await Promise.all(
packagesWithTs.map(async ({packageDir, pkg}) => {
const configFile = path.resolve(packageDir, 'api-extractor.json');

await fs.promises.writeFile(
configFile,
JSON.stringify(
{
extends: '../../api-extractor.json',
mainEntryPointFilePath: path.resolve(packageDir, pkg.types),
projectFolder: packageDir,
},
null,
2,
),
);
const extractorConfig = ExtractorConfig.loadFileAndPrepare(configFile);

if (!compilerState) {
compilerState = CompilerState.create(extractorConfig, {
additionalEntryPoints: packagesWithTs.map(({pkg, packageDir}) =>
path.resolve(packageDir, pkg.types),
),
typescriptCompilerFolder,
});
}

const extractorResult = Extractor.invoke(extractorConfig, {
compilerState,
localBuild: true,
showVerboseMessages: true,
typescriptCompilerFolder,
});

if (!extractorResult.succeeded || extractorResult.warningCount > 0) {
console.error(
chalk.inverse.red(' Unable to extract TypeScript definition files '),
);
throw new Error(
`API Extractor completed with ${extractorResult.errorCount} errors and ${extractorResult.warningCount} warnings`,
);
}

const filepath = extractorResult.extractorConfig.untrimmedFilePath;

let definitionFile = await fs.promises.readFile(filepath, 'utf8');

rimraf.sync(path.resolve(packageDir, 'build/**/*.d.ts'));
rimraf.sync(path.resolve(packageDir, 'dist/'));
// this is invalid now, so remove it to not confuse `tsc`
rimraf.sync(path.resolve(packageDir, 'tsconfig.tsbuildinfo'));

definitionFile = definitionFile.replace(/\r\n/g, '\n');

const hasNodeTypesReference = definitionFile.includes(
typesNodeReferenceDirective,
);

if (hasNodeTypesReference) {
definitionFile = [
typesNodeReferenceDirective,
...definitionFile.split(typesNodeReferenceDirective),
].join('\n');
}

definitionFile = [
copyrightSnippet,
...definitionFile.split(copyrightSnippet),
].join('\n');

const formattedContent = prettier.format(definitionFile, {
...prettierConfig,
filepath,
});

await fs.promises.writeFile(
filepath.replace('/dist/', '/build/'),
formattedContent,
);
}),
);

console.log(
chalk.inverse.green(' Successfully extracted TypeScript definition files '),
);
})().catch(error => {
console.error('Got error', error.stack);
process.exitCode = 1;
});

0 comments on commit 819c2be

Please sign in to comment.