Skip to content

Commit 74a6afa

Browse files
authored
feat: support 'write-dts' mode in single and watch run (#708)
1 parent b48f98a commit 74a6afa

10 files changed

+256
-111
lines changed

README.md

Lines changed: 35 additions & 34 deletions
Large diffs are not rendered by default.

src/typescript/type-script-worker-config.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import path from 'path';
22

3-
import semver from 'semver';
43
import type webpack from 'webpack';
54

65
import type { TypeScriptVueExtensionConfig } from './extension/vue/type-script-vue-extension-config';
@@ -16,7 +15,7 @@ interface TypeScriptWorkerConfig {
1615
configOverwrite: TypeScriptConfigOverwrite;
1716
build: boolean;
1817
context: string;
19-
mode: 'readonly' | 'write-tsbuildinfo' | 'write-dts' | 'write-references';
18+
mode: 'readonly' | 'write-dts' | 'write-tsbuildinfo' | 'write-references';
2019
diagnosticOptions: TypeScriptDiagnosticsOptions;
2120
extensions: {
2221
vue: TypeScriptVueExtensionConfig;
@@ -44,31 +43,15 @@ function createTypeScriptWorkerConfig(
4443

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

47-
const defaultCompilerOptions: Record<string, unknown> = {
48-
skipLibCheck: true,
49-
sourceMap: false,
50-
inlineSourceMap: false,
51-
};
52-
// eslint-disable-next-line @typescript-eslint/no-var-requires
53-
if (semver.gte(require(typescriptPath).version, '2.9.0')) {
54-
defaultCompilerOptions.declarationMap = false;
55-
}
56-
5746
return {
5847
enabled: options !== false,
5948
memoryLimit: 2048,
6049
build: false,
61-
mode: 'write-tsbuildinfo',
50+
mode: optionsAsObject.build ? 'write-tsbuildinfo' : 'readonly',
6251
profile: false,
6352
...optionsAsObject,
6453
configFile: configFile,
65-
configOverwrite: {
66-
...(optionsAsObject.configOverwrite || {}),
67-
compilerOptions: {
68-
...defaultCompilerOptions,
69-
...((optionsAsObject.configOverwrite || {}).compilerOptions || {}),
70-
},
71-
},
54+
configOverwrite: optionsAsObject.configOverwrite || {},
7255
context: optionsAsObject.context || path.dirname(configFile),
7356
extensions: {
7457
vue: createTypeScriptVueExtensionConfig(

src/typescript/worker/lib/config.ts

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,83 @@ for (const extension of extensions) {
2929
}
3030
}
3131

32+
function getUserProvidedConfigOverwrite(): TypeScriptConfigOverwrite {
33+
return config.configOverwrite || {};
34+
}
35+
36+
function getImplicitConfigOverwrite(): TypeScriptConfigOverwrite {
37+
const baseCompilerOptionsOverwrite = {
38+
skipLibCheck: true,
39+
sourceMap: false,
40+
inlineSourceMap: false,
41+
};
42+
43+
switch (config.mode) {
44+
case 'write-dts':
45+
return {
46+
compilerOptions: {
47+
...baseCompilerOptionsOverwrite,
48+
declaration: true,
49+
emitDeclarationOnly: true,
50+
noEmit: false,
51+
},
52+
};
53+
case 'write-tsbuildinfo':
54+
case 'write-references':
55+
return {
56+
compilerOptions: {
57+
...baseCompilerOptionsOverwrite,
58+
declaration: true,
59+
emitDeclarationOnly: false,
60+
noEmit: false,
61+
},
62+
};
63+
}
64+
65+
return {
66+
compilerOptions: baseCompilerOptionsOverwrite,
67+
};
68+
}
69+
70+
function applyConfigOverwrite(
71+
baseConfig: TypeScriptConfigOverwrite,
72+
...overwriteConfigs: TypeScriptConfigOverwrite[]
73+
): TypeScriptConfigOverwrite {
74+
let config = baseConfig;
75+
76+
for (const overwriteConfig of overwriteConfigs) {
77+
config = {
78+
...(config || {}),
79+
...(overwriteConfig || {}),
80+
compilerOptions: {
81+
...(config?.compilerOptions || {}),
82+
...(overwriteConfig?.compilerOptions || {}),
83+
},
84+
};
85+
}
86+
87+
return config;
88+
}
89+
3290
export function parseConfig(
3391
configFileName: string,
34-
configFileContext: string,
35-
configOverwriteJSON: TypeScriptConfigOverwrite = {}
92+
configFileContext: string
3693
): ts.ParsedCommandLine {
3794
const configFilePath = forwardSlash(configFileName);
38-
const parsedConfigFileJSON = typescript.readConfigFile(
95+
96+
const { config: baseConfig, error: readConfigError } = typescript.readConfigFile(
3997
configFilePath,
4098
parseConfigFileHost.readFile
4199
);
42100

43-
const overwrittenConfigFileJSON = {
44-
...(parsedConfigFileJSON.config || {}),
45-
...configOverwriteJSON,
46-
compilerOptions: {
47-
...((parsedConfigFileJSON.config || {}).compilerOptions || {}),
48-
...(configOverwriteJSON.compilerOptions || {}),
49-
},
50-
};
101+
const overwrittenConfig = applyConfigOverwrite(
102+
baseConfig || {},
103+
getImplicitConfigOverwrite(),
104+
getUserProvidedConfigOverwrite()
105+
);
51106

52107
const parsedConfigFile = typescript.parseJsonConfigFileContent(
53-
overwrittenConfigFileJSON,
108+
overwrittenConfig,
54109
parseConfigFileHost,
55110
configFileContext
56111
);
@@ -61,7 +116,7 @@ export function parseConfig(
61116
...parsedConfigFile.options,
62117
configFilePath: configFilePath,
63118
},
64-
errors: parsedConfigFileJSON.error ? [parsedConfigFileJSON.error] : parsedConfigFile.errors,
119+
errors: readConfigError ? [readConfigError] : parsedConfigFile.errors,
65120
};
66121
}
67122

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

80135
export function getParsedConfig(force = false) {
81136
if (!parsedConfig || force) {
82-
parseConfigDiagnostics = [];
83-
84-
parsedConfig = parseConfig(config.configFile, config.context, config.configOverwrite);
85-
86-
const configFilePath = forwardSlash(config.configFile);
87-
const parsedConfigFileJSON = typescript.readConfigFile(
88-
configFilePath,
89-
parseConfigFileHost.readFile
90-
);
91-
const overwrittenConfigFileJSON = {
92-
...(parsedConfigFileJSON.config || {}),
93-
...config.configOverwrite,
94-
compilerOptions: {
95-
...((parsedConfigFileJSON.config || {}).compilerOptions || {}),
96-
...(config.configOverwrite.compilerOptions || {}),
97-
},
98-
};
99-
parsedConfig = typescript.parseJsonConfigFileContent(
100-
overwrittenConfigFileJSON,
101-
parseConfigFileHost,
102-
config.context
103-
);
104-
parsedConfig.options.configFilePath = configFilePath;
105-
parsedConfig.errors = parsedConfigFileJSON.error
106-
? [parsedConfigFileJSON.error]
107-
: parsedConfig.errors;
108-
109-
if (parsedConfig.errors) {
110-
parseConfigDiagnostics.push(...parsedConfig.errors);
111-
}
137+
parsedConfig = parseConfig(config.configFile, config.context);
138+
parseConfigDiagnostics = parsedConfig.errors || [];
112139
}
113140

114141
return parsedConfig;

src/typescript/worker/lib/emit.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type * as ts from 'typescript';
2+
3+
import { getParsedConfig } from './config';
4+
import { config } from './worker-config';
5+
6+
export function emitDtsIfNeeded(program: ts.Program | ts.BuilderProgram) {
7+
const parsedConfig = getParsedConfig();
8+
9+
if (config.mode === 'write-dts' && parsedConfig.options.declaration) {
10+
// emit .d.ts files only
11+
program.emit(undefined, undefined, undefined, true);
12+
}
13+
}

src/typescript/worker/lib/program/program.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type * as ts from 'typescript';
22

33
import { getConfigFilePathFromProgram, getParsedConfig } from '../config';
44
import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics';
5+
import { emitDtsIfNeeded } from '../emit';
56
import { createCompilerHost } from '../host/compiler-host';
67
import { typescript } from '../typescript';
78

@@ -24,6 +25,7 @@ export function useProgram() {
2425
}
2526

2627
updateDiagnostics(getConfigFilePathFromProgram(program), getDiagnosticsOfProgram(program));
28+
emitDtsIfNeeded(program);
2729
}
2830

2931
export function invalidateProgram(withHost = false) {

src/typescript/worker/lib/program/watch-program.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type * as ts from 'typescript';
33
import { getConfigFilePathFromBuilderProgram, getParsedConfig } from '../config';
44
import { getDependencies } from '../dependencies';
55
import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics';
6+
import { emitDtsIfNeeded } from '../emit';
67
import { createWatchCompilerHost } from '../host/watch-compiler-host';
78
import { startTracingIfNeeded, stopTracingIfNeeded } from '../tracing';
89
import { emitTsBuildInfoIfNeeded } from '../tsbuildinfo';
@@ -49,6 +50,7 @@ export function useWatchProgram() {
4950
getConfigFilePathFromBuilderProgram(builderProgram),
5051
getDiagnosticsOfProgram(builderProgram)
5152
);
53+
emitDtsIfNeeded(builderProgram);
5254
emitTsBuildInfoIfNeeded(builderProgram);
5355
stopTracingIfNeeded(builderProgram);
5456
}

test/e2e/type-script-solution-builder-api.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ describe('TypeScript SolutionBuilder API', () => {
129129
expect(await sandbox.exists('packages/client/lib/index.d.ts.map')).toEqual(true);
130130
expect(await sandbox.exists('packages/shared/lib/index.js')).toEqual(false);
131131
expect(await sandbox.exists('packages/client/lib/index.js')).toEqual(false);
132+
expect(await sandbox.exists('packages/client/lib/nested/additional.d.ts')).toEqual(true);
133+
expect(await sandbox.exists('packages/client/lib/nested/additional.d.ts.map')).toEqual(
134+
true
135+
);
136+
expect(await sandbox.exists('packages/client/lib/nested/additional.js')).toEqual(false);
132137

133138
expect(await sandbox.read('packages/shared/lib/tsconfig.tsbuildinfo')).not.toEqual('');
134139
expect(await sandbox.read('packages/client/lib/tsconfig.tsbuildinfo')).not.toEqual('');

test/e2e/type-script-watch-api.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,4 +412,91 @@ describe('TypeScript Watch API', () => {
412412
new Error('Exceeded time on waiting for errors to appear.')
413413
);
414414
});
415+
416+
it.each([{ async: false }, { async: true }])(
417+
'saves .d.ts files in watch mode with %p',
418+
async ({ async }) => {
419+
await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic'));
420+
await sandbox.install('yarn', {});
421+
await sandbox.patch(
422+
'webpack.config.js',
423+
'async: false,',
424+
`async: ${JSON.stringify(async)}, typescript: { mode: 'write-dts' },`
425+
);
426+
427+
const driver = createWebpackDevServerDriver(
428+
sandbox.spawn('yarn webpack serve --mode=development'),
429+
async
430+
);
431+
432+
// first compilation is successful
433+
await driver.waitForNoErrors();
434+
435+
// then we add a new file
436+
await sandbox.write(
437+
'src/model/Organization.ts',
438+
[
439+
'interface Organization {',
440+
' id: number;',
441+
' name: string;',
442+
'}',
443+
'',
444+
'export { Organization }',
445+
].join('\n')
446+
);
447+
448+
// this should not introduce an error - file is not used
449+
await driver.waitForNoErrors();
450+
451+
// add organization name to the getUserName function
452+
await sandbox.patch(
453+
'src/model/User.ts',
454+
'return [user.firstName, user.lastName]',
455+
'return [user.firstName, user.lastName, user.organization.name]'
456+
);
457+
458+
expect(await driver.waitForErrors()).toEqual([
459+
[
460+
'ERROR in ./src/model/User.ts 12:47-59',
461+
"TS2339: Property 'organization' does not exist on type 'User'.",
462+
' 10 |',
463+
' 11 | function getUserName(user: User): string {',
464+
" > 12 | return [user.firstName, user.lastName, user.organization.name].filter((name) => name !== undefined).join(' ');",
465+
' | ^^^^^^^^^^^^',
466+
' 13 | }',
467+
' 14 |',
468+
' 15 | export { User, getUserName };',
469+
].join('\n'),
470+
]);
471+
472+
// fix the error
473+
await sandbox.patch(
474+
'src/model/User.ts',
475+
"import { Role } from './Role';",
476+
["import { Role } from './Role';", "import { Organization } from './Organization';"].join(
477+
'\n'
478+
)
479+
);
480+
await sandbox.patch(
481+
'src/model/User.ts',
482+
' role: Role;',
483+
[' role: Role;', ' organization: Organization;'].join('\n')
484+
);
485+
486+
// there should be no errors
487+
await driver.waitForNoErrors();
488+
489+
// check if .d.ts files has been created
490+
expect(await sandbox.exists('dist')).toEqual(true);
491+
expect(await sandbox.exists('dist/index.d.ts')).toEqual(true);
492+
expect(await sandbox.exists('dist/index.js')).toEqual(false);
493+
expect(await sandbox.exists('dist/index.js.map')).toEqual(false);
494+
expect(await sandbox.exists('dist/authenticate.d.ts')).toEqual(true);
495+
expect(await sandbox.exists('dist/model/User.d.ts')).toEqual(true);
496+
expect(await sandbox.exists('dist/model/Role.d.ts')).toEqual(true);
497+
expect(await sandbox.exists('dist/model/Organization.d.ts')).toEqual(true);
498+
499+
await sandbox.remove('dist');
500+
}
501+
);
415502
});

test/e2e/webpack-production-build.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,45 @@ describe('Webpack Production Build', () => {
2222
const errors = extractWebpackErrors(result);
2323

2424
expect(errors).toEqual([]);
25+
26+
// check if files has been created
27+
expect(await sandbox.exists('dist')).toEqual(true);
28+
expect(await sandbox.exists('dist/index.d.ts')).toEqual(false);
29+
expect(await sandbox.exists('dist/index.js')).toEqual(true);
30+
expect(await sandbox.exists('dist/authenticate.d.ts')).toEqual(false);
31+
expect(await sandbox.exists('dist/model/User.d.ts')).toEqual(false);
32+
expect(await sandbox.exists('dist/model/Role.d.ts')).toEqual(false);
33+
34+
await sandbox.remove('dist');
2535
}
2636
);
2737

38+
it('generates .d.ts files in write-dts mode', async () => {
39+
await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic'));
40+
await sandbox.install('yarn', { webpack: '^5.11.0' });
41+
42+
await sandbox.patch(
43+
'webpack.config.js',
44+
'async: false,',
45+
'async: false, typescript: { mode: "write-dts" },'
46+
);
47+
48+
const result = await sandbox.exec('yarn webpack --mode=production');
49+
const errors = extractWebpackErrors(result);
50+
51+
expect(errors).toEqual([]);
52+
53+
// check if files has been created
54+
expect(await sandbox.exists('dist')).toEqual(true);
55+
expect(await sandbox.exists('dist/index.d.ts')).toEqual(true);
56+
expect(await sandbox.exists('dist/index.js')).toEqual(true);
57+
expect(await sandbox.exists('dist/authenticate.d.ts')).toEqual(true);
58+
expect(await sandbox.exists('dist/model/User.d.ts')).toEqual(true);
59+
expect(await sandbox.exists('dist/model/Role.d.ts')).toEqual(true);
60+
61+
await sandbox.remove('dist');
62+
});
63+
2864
it.each([{ webpack: '5.11.0' }, { webpack: '^5.11.0' }])(
2965
'exits with error on the project error with %p',
3066
async (dependencies) => {

0 commit comments

Comments
 (0)