forked from stenciljs/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun-program.ts
More file actions
290 lines (254 loc) · 11.8 KB
/
run-program.ts
File metadata and controls
290 lines (254 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
import {
filterExcludedComponents,
getComponentsFromModules,
isOutputTargetDistTypes,
join,
loadTypeScriptDiagnostics,
normalizePath,
relative,
} from '@utils';
import { basename } from 'path';
import ts from 'typescript';
import type * as d from '../../declarations';
import { updateComponentBuildConditionals } from '../app-core/app-data';
import { resolveComponentDependencies } from '../entries/resolve-component-dependencies';
import { performAutomaticKeyInsertion } from '../transformers/automatic-key-insertion';
import { convertDecoratorsToStatic } from '../transformers/decorators-to-static/convert-decorators';
import { rewriteAliasedDTSImportPaths } from '../transformers/rewrite-aliased-paths';
import { updateModule } from '../transformers/static-to-meta/parse-static';
import { generateAppTypes } from '../types/generate-app-types';
import { updateStencilTypesImports } from '../types/stencil-types';
import { validateTranspiledComponents } from './validate-components';
export const runTsProgram = async (
config: d.ValidatedConfig,
compilerCtx: d.CompilerCtx,
buildCtx: d.BuildCtx,
tsBuilder: ts.BuilderProgram,
): Promise<string[]> => {
const tsSyntactic = loadTypeScriptDiagnostics(tsBuilder.getSyntacticDiagnostics());
const tsGlobal = loadTypeScriptDiagnostics(tsBuilder.getGlobalDiagnostics());
const tsOptions = loadTypeScriptDiagnostics(tsBuilder.getOptionsDiagnostics());
buildCtx.diagnostics.push(...tsSyntactic);
buildCtx.diagnostics.push(...tsGlobal);
buildCtx.diagnostics.push(...tsOptions);
if (buildCtx.hasError) {
return [];
}
const tsProgram = tsBuilder.getProgram();
const tsTypeChecker = tsProgram.getTypeChecker();
const typesOutputTarget = config.outputTargets.filter(isOutputTargetDistTypes);
const emittedDts: string[] = [];
const emitCallback: ts.WriteFileCallback = (emitFilePath, data, _w, _e, tsSourceFiles) => {
if (
emitFilePath.includes('e2e.') ||
emitFilePath.includes('spec.') ||
emitFilePath.endsWith('e2e.d.ts') ||
emitFilePath.endsWith('spec.d.ts')
) {
// we don't want to write these to disk!
return;
}
if (emitFilePath.endsWith('.js') || emitFilePath.endsWith('js.map')) {
updateModule(config, compilerCtx, buildCtx, tsSourceFiles[0], data, emitFilePath, tsTypeChecker, null);
} else if (emitFilePath.endsWith('.d.ts')) {
const srcDtsPath = normalizePath(tsSourceFiles[0].fileName);
const relativeEmitFilepath = getRelativeDts(config, srcDtsPath, emitFilePath);
emittedDts.push(srcDtsPath);
typesOutputTarget.forEach((o) => {
const distPath = normalizePath(join(normalizePath(o.typesDir), normalizePath(relativeEmitFilepath)));
data = updateStencilTypesImports(o.typesDir, distPath, data);
compilerCtx.fs.writeFile(distPath, data);
});
}
};
const transformers: ts.CustomTransformers = {
before: [
convertDecoratorsToStatic(config, buildCtx.diagnostics, tsTypeChecker, tsProgram),
performAutomaticKeyInsertion,
],
afterDeclarations: [],
};
if (config.transformAliasedImportPaths) {
/**
* Generate a collection of transformations that are to be applied as a part of the `afterDeclarations` step in the
* TypeScript compilation process.
*
* TypeScript handles the generation of JS and `.d.ts` files through different pipelines. One (possibly surprising)
* consequence of this is that if you modify a source file using a transformer, it will not automatically result in
* changes to the corresponding `.d.ts` file. Instead, if you want to, for instance, rewrite some import specifiers
* in both the source file _and_ its typedef you'll need to run a transformer for both of them.
*
* See here: https://github.com/itsdouges/typescript-transformer-handbook#transforms
* and here: https://github.com/microsoft/TypeScript/pull/23946
*
* This quirk is not terribly well documented, unfortunately.
*/
transformers.afterDeclarations.push(rewriteAliasedDTSImportPaths);
}
// Emit files that changed
const emitResult = tsBuilder.emit(undefined, emitCallback, undefined, false, transformers);
// Check for emit diagnostics
if (emitResult.diagnostics.length > 0) {
const emitDiagnostics = loadTypeScriptDiagnostics(emitResult.diagnostics);
// Enhance error messages for TS4094 to be more helpful for mixin users;
// These occur when mixins return classes with private/protected members that TypeScript cannot emit
emitDiagnostics.forEach((diagnostic) => {
if (diagnostic.code === '4094') {
diagnostic.level = 'warn';
diagnostic.messageText =
`${diagnostic.messageText}\n\n` +
`This commonly occurs when using mixins that return classes with private or protected members. ` +
`TypeScript cannot emit declaration files for anonymous classes with non-public members.\n\n` +
`Possible solutions:\n` +
` 1. Add explicit type annotations to your mixin's return type\n` +
` 2. Use public members in your mixin classes\n` +
` 3. Use JavaScript private fields (#field) instead of TypeScript's private keyword`;
}
});
buildCtx.diagnostics.push(...emitDiagnostics);
}
const changedmodules = Array.from(compilerCtx.changedModules.keys());
buildCtx.debug('Transpiled modules: ' + JSON.stringify(changedmodules, null, '\n'));
// Finalize components metadata
buildCtx.moduleFiles = Array.from(compilerCtx.moduleMap.values());
const allComponents = getComponentsFromModules(buildCtx.moduleFiles);
// Filter out excluded components based on config patterns
const { components: filteredComponents, excludedComponents } = filterExcludedComponents(allComponents, config);
buildCtx.components = filteredComponents;
// Queue deletion of .d.ts files for excluded components in the in-memory FS
// These deletions will be committed when compilerCtx.fs.commit() is called during writeBuild
if (excludedComponents.length > 0 && typesOutputTarget.length > 0) {
excludedComponents.forEach((cmp) => {
const srcPath = normalizePath(cmp.sourceFilePath);
const relativeToSrc = relative(config.srcDir, srcPath);
const dtsRelativePath = relativeToSrc.replace(/\.tsx?$/, '.d.ts');
typesOutputTarget.forEach((outputTarget) => {
const outputDtsPath = join(outputTarget.typesDir, dtsRelativePath);
// The file may have been queued for writing during emit
// We need to cancel the write and queue it for deletion instead
const item = compilerCtx.fs.getItem(outputDtsPath);
item.queueWriteToDisk = false;
item.queueDeleteFromDisk = true;
});
});
}
updateComponentBuildConditionals(compilerCtx.moduleMap, buildCtx.components);
resolveComponentDependencies(buildCtx.components);
validateTranspiledComponents(config, buildCtx);
if (buildCtx.hasError) {
return [];
}
return emittedDts;
};
export interface ValidateTypesResult {
hasTypesChanged: boolean;
needsRebuild: boolean;
}
/**
* Generate types and run semantic validation AFTER components.d.ts exists on disk
*/
export const validateTypesAfterGeneration = async (
config: d.ValidatedConfig,
compilerCtx: d.CompilerCtx,
buildCtx: d.BuildCtx,
tsBuilder: ts.BuilderProgram,
emittedDts: string[],
): Promise<ValidateTypesResult> => {
const tsProgram = tsBuilder.getProgram();
const typesOutputTarget = config.outputTargets.filter(isOutputTargetDistTypes);
// Check if components.d.ts already exists
const componentsDtsPath = join(config.srcDir, 'components.d.ts');
const componentsDtsExistedBefore = await compilerCtx.fs.access(componentsDtsPath);
// If components.d.ts doesn't exist yet, generate it and signal that a rebuild is needed.
// The current TS program was created without components.d.ts, so it can't provide
// accurate type checking. We need a fresh TS program that includes components.d.ts.
if (!componentsDtsExistedBefore) {
await generateAppTypes(config, compilerCtx, buildCtx, 'src');
// Signal that we need to rebuild with a fresh TS program
return { hasTypesChanged: true, needsRebuild: true };
}
// components.d.ts existed, so the TS program has full type information.
// Run semantic validation on user source files.
if (config.validateTypes) {
const sourceFiles = tsProgram.getSourceFiles().filter((sf) => {
const fileName = normalizePath(sf.fileName);
return (
!fileName.includes('node_modules') &&
!fileName.endsWith('.d.ts') &&
fileName.startsWith(normalizePath(config.srcDir))
);
});
for (const sourceFile of sourceFiles) {
const sourceSemanticDiagnostics = tsProgram.getSemanticDiagnostics(sourceFile);
const tsSemantic = loadTypeScriptDiagnostics(sourceSemanticDiagnostics);
if (config.devMode) {
tsSemantic.forEach((semanticDiagnostic) => {
if (semanticDiagnostic.code === '6133' || semanticDiagnostic.code === '6192') {
semanticDiagnostic.level = 'warn';
}
});
}
buildCtx.diagnostics.push(...tsSemantic);
}
}
// Update components.d.ts in case components changed
const hasTypesChanged = await generateAppTypes(config, compilerCtx, buildCtx, 'src');
if (typesOutputTarget.length > 0) {
// copy src dts files that do not get emitted by the compiler
// but we still want to ship them in the dist directory
const srcRootDtsFiles = tsProgram
.getRootFileNames()
.filter((f) => f.endsWith('.d.ts') && !f.endsWith('components.d.ts'))
.map((s) => normalizePath(s))
.filter((f) => !emittedDts.includes(f))
.map((srcRootDtsFilePath) => {
const relativeEmitFilepath = relative(config.srcDir, srcRootDtsFilePath);
return Promise.all(
typesOutputTarget.map(async (o) => {
const distPath = join(o.typesDir, relativeEmitFilepath);
let dtsContent = await compilerCtx.fs.readFile(srcRootDtsFilePath);
dtsContent = updateStencilTypesImports(o.typesDir, distPath, dtsContent);
await compilerCtx.fs.writeFile(distPath, dtsContent);
}),
);
});
await Promise.all(srcRootDtsFiles);
}
return { hasTypesChanged, needsRebuild: false };
};
/**
* Calculate a relative path for a `.d.ts` file, giving the location within
* the typedef output directory where we'd like to write it to disk.
*
* The correct relative path for a `.d.ts` file is basically given by the
* relative location of the _source_ file associated with the `.d.ts` file
* within the Stencil project's source directory.
*
* Thus, in order to calculate this, we take the path to the source file, the
* emit path calculated by typescript (which is going to be right next to the
* emit location for the JavaScript that the compiler emits for the source file)
* and we do a pairwise walk up the two paths, assembling path components as
* we go, until the source file path is equal to the configured source
* directory. Then the path components from the `emitDtsPath` can be reversed
* and re-assembled into a suitable relative path.
*
* @param config a Stencil configuration object
* @param srcPath the path to the source file for the `.d.ts` file of interest
* @param emitDtsPath the emit path for the `.d.ts` file calculated by
* TypeScript
* @returns a relative path to a suitable location where the typedef file can be
* written
*/
export const getRelativeDts = (config: d.ValidatedConfig, srcPath: string, emitDtsPath: string): string => {
const parts: string[] = [];
for (let i = 0; i < 30; i++) {
if (normalizePath(config.srcDir) === srcPath) {
break;
}
const b = basename(emitDtsPath);
parts.push(b);
emitDtsPath = normalizePath(join(emitDtsPath, '..'));
srcPath = normalizePath(join(normalizePath(srcPath), '..'));
}
return normalizePath(join(...parts.reverse()));
};