Skip to content

Commit

Permalink
feat: support 'write-dts' mode in single and watch run (#708)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-oles committed Feb 6, 2022
1 parent b48f98a commit 74a6afa
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 111 deletions.
69 changes: 35 additions & 34 deletions README.md

Large diffs are not rendered by default.

23 changes: 3 additions & 20 deletions src/typescript/type-script-worker-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from 'path';

import semver from 'semver';
import type webpack from 'webpack';

import type { TypeScriptVueExtensionConfig } from './extension/vue/type-script-vue-extension-config';
Expand All @@ -16,7 +15,7 @@ interface TypeScriptWorkerConfig {
configOverwrite: TypeScriptConfigOverwrite;
build: boolean;
context: string;
mode: 'readonly' | 'write-tsbuildinfo' | 'write-dts' | 'write-references';
mode: 'readonly' | 'write-dts' | 'write-tsbuildinfo' | 'write-references';
diagnosticOptions: TypeScriptDiagnosticsOptions;
extensions: {
vue: TypeScriptVueExtensionConfig;
Expand Down Expand Up @@ -44,31 +43,15 @@ function createTypeScriptWorkerConfig(

const typescriptPath = optionsAsObject.typescriptPath || require.resolve('typescript');

const defaultCompilerOptions: Record<string, unknown> = {
skipLibCheck: true,
sourceMap: false,
inlineSourceMap: false,
};
// eslint-disable-next-line @typescript-eslint/no-var-requires
if (semver.gte(require(typescriptPath).version, '2.9.0')) {
defaultCompilerOptions.declarationMap = false;
}

return {
enabled: options !== false,
memoryLimit: 2048,
build: false,
mode: 'write-tsbuildinfo',
mode: optionsAsObject.build ? 'write-tsbuildinfo' : 'readonly',
profile: false,
...optionsAsObject,
configFile: configFile,
configOverwrite: {
...(optionsAsObject.configOverwrite || {}),
compilerOptions: {
...defaultCompilerOptions,
...((optionsAsObject.configOverwrite || {}).compilerOptions || {}),
},
},
configOverwrite: optionsAsObject.configOverwrite || {},
context: optionsAsObject.context || path.dirname(configFile),
extensions: {
vue: createTypeScriptVueExtensionConfig(
Expand Down
113 changes: 70 additions & 43 deletions src/typescript/worker/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,83 @@ for (const extension of extensions) {
}
}

function getUserProvidedConfigOverwrite(): TypeScriptConfigOverwrite {
return config.configOverwrite || {};
}

function getImplicitConfigOverwrite(): TypeScriptConfigOverwrite {
const baseCompilerOptionsOverwrite = {
skipLibCheck: true,
sourceMap: false,
inlineSourceMap: false,
};

switch (config.mode) {
case 'write-dts':
return {
compilerOptions: {
...baseCompilerOptionsOverwrite,
declaration: true,
emitDeclarationOnly: true,
noEmit: false,
},
};
case 'write-tsbuildinfo':
case 'write-references':
return {
compilerOptions: {
...baseCompilerOptionsOverwrite,
declaration: true,
emitDeclarationOnly: false,
noEmit: false,
},
};
}

return {
compilerOptions: baseCompilerOptionsOverwrite,
};
}

function applyConfigOverwrite(
baseConfig: TypeScriptConfigOverwrite,
...overwriteConfigs: TypeScriptConfigOverwrite[]
): TypeScriptConfigOverwrite {
let config = baseConfig;

for (const overwriteConfig of overwriteConfigs) {
config = {
...(config || {}),
...(overwriteConfig || {}),
compilerOptions: {
...(config?.compilerOptions || {}),
...(overwriteConfig?.compilerOptions || {}),
},
};
}

return config;
}

export function parseConfig(
configFileName: string,
configFileContext: string,
configOverwriteJSON: TypeScriptConfigOverwrite = {}
configFileContext: string
): ts.ParsedCommandLine {
const configFilePath = forwardSlash(configFileName);
const parsedConfigFileJSON = typescript.readConfigFile(

const { config: baseConfig, error: readConfigError } = typescript.readConfigFile(
configFilePath,
parseConfigFileHost.readFile
);

const overwrittenConfigFileJSON = {
...(parsedConfigFileJSON.config || {}),
...configOverwriteJSON,
compilerOptions: {
...((parsedConfigFileJSON.config || {}).compilerOptions || {}),
...(configOverwriteJSON.compilerOptions || {}),
},
};
const overwrittenConfig = applyConfigOverwrite(
baseConfig || {},
getImplicitConfigOverwrite(),
getUserProvidedConfigOverwrite()
);

const parsedConfigFile = typescript.parseJsonConfigFileContent(
overwrittenConfigFileJSON,
overwrittenConfig,
parseConfigFileHost,
configFileContext
);
Expand All @@ -61,7 +116,7 @@ export function parseConfig(
...parsedConfigFile.options,
configFilePath: configFilePath,
},
errors: parsedConfigFileJSON.error ? [parsedConfigFileJSON.error] : parsedConfigFile.errors,
errors: readConfigError ? [readConfigError] : parsedConfigFile.errors,
};
}

Expand All @@ -79,36 +134,8 @@ export function getParseConfigIssues(): Issue[] {

export function getParsedConfig(force = false) {
if (!parsedConfig || force) {
parseConfigDiagnostics = [];

parsedConfig = parseConfig(config.configFile, config.context, config.configOverwrite);

const configFilePath = forwardSlash(config.configFile);
const parsedConfigFileJSON = typescript.readConfigFile(
configFilePath,
parseConfigFileHost.readFile
);
const overwrittenConfigFileJSON = {
...(parsedConfigFileJSON.config || {}),
...config.configOverwrite,
compilerOptions: {
...((parsedConfigFileJSON.config || {}).compilerOptions || {}),
...(config.configOverwrite.compilerOptions || {}),
},
};
parsedConfig = typescript.parseJsonConfigFileContent(
overwrittenConfigFileJSON,
parseConfigFileHost,
config.context
);
parsedConfig.options.configFilePath = configFilePath;
parsedConfig.errors = parsedConfigFileJSON.error
? [parsedConfigFileJSON.error]
: parsedConfig.errors;

if (parsedConfig.errors) {
parseConfigDiagnostics.push(...parsedConfig.errors);
}
parsedConfig = parseConfig(config.configFile, config.context);
parseConfigDiagnostics = parsedConfig.errors || [];
}

return parsedConfig;
Expand Down
13 changes: 13 additions & 0 deletions src/typescript/worker/lib/emit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type * as ts from 'typescript';

import { getParsedConfig } from './config';
import { config } from './worker-config';

export function emitDtsIfNeeded(program: ts.Program | ts.BuilderProgram) {
const parsedConfig = getParsedConfig();

if (config.mode === 'write-dts' && parsedConfig.options.declaration) {
// emit .d.ts files only
program.emit(undefined, undefined, undefined, true);
}
}
2 changes: 2 additions & 0 deletions src/typescript/worker/lib/program/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type * as ts from 'typescript';

import { getConfigFilePathFromProgram, getParsedConfig } from '../config';
import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics';
import { emitDtsIfNeeded } from '../emit';
import { createCompilerHost } from '../host/compiler-host';
import { typescript } from '../typescript';

Expand All @@ -24,6 +25,7 @@ export function useProgram() {
}

updateDiagnostics(getConfigFilePathFromProgram(program), getDiagnosticsOfProgram(program));
emitDtsIfNeeded(program);
}

export function invalidateProgram(withHost = false) {
Expand Down
2 changes: 2 additions & 0 deletions src/typescript/worker/lib/program/watch-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type * as ts from 'typescript';
import { getConfigFilePathFromBuilderProgram, getParsedConfig } from '../config';
import { getDependencies } from '../dependencies';
import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics';
import { emitDtsIfNeeded } from '../emit';
import { createWatchCompilerHost } from '../host/watch-compiler-host';
import { startTracingIfNeeded, stopTracingIfNeeded } from '../tracing';
import { emitTsBuildInfoIfNeeded } from '../tsbuildinfo';
Expand Down Expand Up @@ -49,6 +50,7 @@ export function useWatchProgram() {
getConfigFilePathFromBuilderProgram(builderProgram),
getDiagnosticsOfProgram(builderProgram)
);
emitDtsIfNeeded(builderProgram);
emitTsBuildInfoIfNeeded(builderProgram);
stopTracingIfNeeded(builderProgram);
}
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/type-script-solution-builder-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ describe('TypeScript SolutionBuilder API', () => {
expect(await sandbox.exists('packages/client/lib/index.d.ts.map')).toEqual(true);
expect(await sandbox.exists('packages/shared/lib/index.js')).toEqual(false);
expect(await sandbox.exists('packages/client/lib/index.js')).toEqual(false);
expect(await sandbox.exists('packages/client/lib/nested/additional.d.ts')).toEqual(true);
expect(await sandbox.exists('packages/client/lib/nested/additional.d.ts.map')).toEqual(
true
);
expect(await sandbox.exists('packages/client/lib/nested/additional.js')).toEqual(false);

expect(await sandbox.read('packages/shared/lib/tsconfig.tsbuildinfo')).not.toEqual('');
expect(await sandbox.read('packages/client/lib/tsconfig.tsbuildinfo')).not.toEqual('');
Expand Down
87 changes: 87 additions & 0 deletions test/e2e/type-script-watch-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,91 @@ describe('TypeScript Watch API', () => {
new Error('Exceeded time on waiting for errors to appear.')
);
});

it.each([{ async: false }, { async: true }])(
'saves .d.ts files in watch mode with %p',
async ({ async }) => {
await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic'));
await sandbox.install('yarn', {});
await sandbox.patch(
'webpack.config.js',
'async: false,',
`async: ${JSON.stringify(async)}, typescript: { mode: 'write-dts' },`
);

const driver = createWebpackDevServerDriver(
sandbox.spawn('yarn webpack serve --mode=development'),
async
);

// first compilation is successful
await driver.waitForNoErrors();

// then we add a new file
await sandbox.write(
'src/model/Organization.ts',
[
'interface Organization {',
' id: number;',
' name: string;',
'}',
'',
'export { Organization }',
].join('\n')
);

// this should not introduce an error - file is not used
await driver.waitForNoErrors();

// add organization name to the getUserName function
await sandbox.patch(
'src/model/User.ts',
'return [user.firstName, user.lastName]',
'return [user.firstName, user.lastName, user.organization.name]'
);

expect(await driver.waitForErrors()).toEqual([
[
'ERROR in ./src/model/User.ts 12:47-59',
"TS2339: Property 'organization' does not exist on type 'User'.",
' 10 |',
' 11 | function getUserName(user: User): string {',
" > 12 | return [user.firstName, user.lastName, user.organization.name].filter((name) => name !== undefined).join(' ');",
' | ^^^^^^^^^^^^',
' 13 | }',
' 14 |',
' 15 | export { User, getUserName };',
].join('\n'),
]);

// fix the error
await sandbox.patch(
'src/model/User.ts',
"import { Role } from './Role';",
["import { Role } from './Role';", "import { Organization } from './Organization';"].join(
'\n'
)
);
await sandbox.patch(
'src/model/User.ts',
' role: Role;',
[' role: Role;', ' organization: Organization;'].join('\n')
);

// there should be no errors
await driver.waitForNoErrors();

// check if .d.ts files has been created
expect(await sandbox.exists('dist')).toEqual(true);
expect(await sandbox.exists('dist/index.d.ts')).toEqual(true);
expect(await sandbox.exists('dist/index.js')).toEqual(false);
expect(await sandbox.exists('dist/index.js.map')).toEqual(false);
expect(await sandbox.exists('dist/authenticate.d.ts')).toEqual(true);
expect(await sandbox.exists('dist/model/User.d.ts')).toEqual(true);
expect(await sandbox.exists('dist/model/Role.d.ts')).toEqual(true);
expect(await sandbox.exists('dist/model/Organization.d.ts')).toEqual(true);

await sandbox.remove('dist');
}
);
});
36 changes: 36 additions & 0 deletions test/e2e/webpack-production-build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,45 @@ describe('Webpack Production Build', () => {
const errors = extractWebpackErrors(result);

expect(errors).toEqual([]);

// check if files has been created
expect(await sandbox.exists('dist')).toEqual(true);
expect(await sandbox.exists('dist/index.d.ts')).toEqual(false);
expect(await sandbox.exists('dist/index.js')).toEqual(true);
expect(await sandbox.exists('dist/authenticate.d.ts')).toEqual(false);
expect(await sandbox.exists('dist/model/User.d.ts')).toEqual(false);
expect(await sandbox.exists('dist/model/Role.d.ts')).toEqual(false);

await sandbox.remove('dist');
}
);

it('generates .d.ts files in write-dts mode', async () => {
await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic'));
await sandbox.install('yarn', { webpack: '^5.11.0' });

await sandbox.patch(
'webpack.config.js',
'async: false,',
'async: false, typescript: { mode: "write-dts" },'
);

const result = await sandbox.exec('yarn webpack --mode=production');
const errors = extractWebpackErrors(result);

expect(errors).toEqual([]);

// check if files has been created
expect(await sandbox.exists('dist')).toEqual(true);
expect(await sandbox.exists('dist/index.d.ts')).toEqual(true);
expect(await sandbox.exists('dist/index.js')).toEqual(true);
expect(await sandbox.exists('dist/authenticate.d.ts')).toEqual(true);
expect(await sandbox.exists('dist/model/User.d.ts')).toEqual(true);
expect(await sandbox.exists('dist/model/Role.d.ts')).toEqual(true);

await sandbox.remove('dist');
});

it.each([{ webpack: '5.11.0' }, { webpack: '^5.11.0' }])(
'exits with error on the project error with %p',
async (dependencies) => {
Expand Down

0 comments on commit 74a6afa

Please sign in to comment.