UNPKG

18.8 kBJavaScriptView Raw
1"use strict";
2const crypto = require("crypto");
3const loaderUtils = require("loader-utils");
4const path = require("path");
5const constants = require("./constants");
6const instances_1 = require("./instances");
7const utils_1 = require("./utils");
8const loaderOptionsCache = {};
9/**
10 * The entry point for ts-loader
11 */
12function loader(contents) {
13 this.cacheable && this.cacheable();
14 const callback = this.async();
15 const options = getLoaderOptions(this);
16 const instanceOrError = instances_1.getTypeScriptInstance(options, this);
17 if (instanceOrError.error !== undefined) {
18 callback(new Error(instanceOrError.error.message));
19 return;
20 }
21 const instance = instanceOrError.instance;
22 instances_1.buildSolutionReferences(instance, this);
23 successLoader(this, contents, callback, instance);
24}
25function successLoader(loaderContext, contents, callback, instance) {
26 instances_1.initializeInstance(loaderContext, instance);
27 instances_1.reportTranspileErrors(instance, loaderContext);
28 const rawFilePath = path.normalize(loaderContext.resourcePath);
29 const filePath = instance.loaderOptions.appendTsSuffixTo.length > 0 ||
30 instance.loaderOptions.appendTsxSuffixTo.length > 0
31 ? utils_1.appendSuffixesIfMatch({
32 '.ts': instance.loaderOptions.appendTsSuffixTo,
33 '.tsx': instance.loaderOptions.appendTsxSuffixTo,
34 }, rawFilePath)
35 : rawFilePath;
36 const fileVersion = updateFileInCache(instance.loaderOptions, filePath, contents, instance);
37 const { outputText, sourceMapText } = instance.loaderOptions.transpileOnly
38 ? getTranspilationEmit(filePath, contents, instance, loaderContext)
39 : getEmit(rawFilePath, filePath, instance, loaderContext);
40 makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, fileVersion, callback, instance);
41}
42function makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, fileVersion, callback, instance) {
43 if (outputText === null || outputText === undefined) {
44 setModuleMeta(loaderContext, instance, fileVersion);
45 const additionalGuidance = utils_1.isReferencedFile(instance, filePath)
46 ? ' The most common cause for this is having errors when building referenced projects.'
47 : !instance.loaderOptions.allowTsInNodeModules &&
48 filePath.indexOf('node_modules') !== -1
49 ? ' By default, ts-loader will not compile .ts files in node_modules.\n' +
50 'You should not need to recompile .ts files there, but if you really want to, use the allowTsInNodeModules option.\n' +
51 'See: https://github.com/Microsoft/TypeScript/issues/12358'
52 : '';
53 callback(new Error(`TypeScript emitted no output for ${filePath}.${additionalGuidance}`), outputText, undefined);
54 return;
55 }
56 const { sourceMap, output } = makeSourceMap(sourceMapText, outputText, filePath, contents, loaderContext);
57 setModuleMeta(loaderContext, instance, fileVersion);
58 callback(null, output, sourceMap);
59}
60function setModuleMeta(loaderContext, instance, fileVersion) {
61 // _module.meta is not available inside happypack
62 if (!instance.loaderOptions.happyPackMode &&
63 loaderContext._module.buildMeta !== undefined) {
64 // Make sure webpack is aware that even though the emitted JavaScript may be the same as
65 // a previously cached version the TypeScript may be different and therefore should be
66 // treated as new
67 loaderContext._module.buildMeta.tsLoaderFileVersion = fileVersion;
68 }
69}
70/**
71 * Get a unique hash based on the contents of the options
72 * Hash is created from the values converted to strings
73 * Values which are functions (such as getCustomTransformers) are
74 * converted to strings by this code, which JSON.stringify would not do.
75 */
76function getOptionsHash(loaderOptions) {
77 const hash = crypto.createHash('sha256');
78 Object.keys(loaderOptions).forEach(key => {
79 const value = loaderOptions[key];
80 if (value) {
81 hash.update(key + value.toString());
82 }
83 });
84 return hash.digest('hex').substring(0, 16);
85}
86/**
87 * either retrieves loader options from the cache
88 * or creates them, adds them to the cache and returns
89 */
90function getLoaderOptions(loaderContext) {
91 const loaderOptions = loaderUtils.getOptions(loaderContext) ||
92 {};
93 // If no instance name is given in the options, use the hash of the loader options
94 // In this way, if different options are given the instances will be different
95 const instanceName = loaderOptions.instance || 'default_' + getOptionsHash(loaderOptions);
96 if (!loaderOptionsCache.hasOwnProperty(instanceName)) {
97 loaderOptionsCache[instanceName] = new WeakMap();
98 }
99 const cache = loaderOptionsCache[instanceName];
100 if (cache.has(loaderOptions)) {
101 return cache.get(loaderOptions);
102 }
103 validateLoaderOptions(loaderOptions);
104 const options = makeLoaderOptions(instanceName, loaderOptions);
105 cache.set(loaderOptions, options);
106 return options;
107}
108const validLoaderOptions = [
109 'silent',
110 'logLevel',
111 'logInfoToStdOut',
112 'instance',
113 'compiler',
114 'context',
115 'configFile',
116 'transpileOnly',
117 'ignoreDiagnostics',
118 'errorFormatter',
119 'colors',
120 'compilerOptions',
121 'appendTsSuffixTo',
122 'appendTsxSuffixTo',
123 'onlyCompileBundledFiles',
124 'happyPackMode',
125 'getCustomTransformers',
126 'reportFiles',
127 'experimentalWatchApi',
128 'allowTsInNodeModules',
129 'experimentalFileCaching',
130 'projectReferences',
131 'resolveModuleName',
132 'resolveTypeReferenceDirective',
133 'useCaseSensitiveFileNames',
134];
135/**
136 * Validate the supplied loader options.
137 * At present this validates the option names only; in future we may look at validating the values too
138 * @param loaderOptions
139 */
140function validateLoaderOptions(loaderOptions) {
141 const loaderOptionKeys = Object.keys(loaderOptions);
142 for (let i = 0; i < loaderOptionKeys.length; i++) {
143 const option = loaderOptionKeys[i];
144 const isUnexpectedOption = validLoaderOptions.indexOf(option) === -1;
145 if (isUnexpectedOption) {
146 throw new Error(`ts-loader was supplied with an unexpected loader option: ${option}
147
148Please take a look at the options you are supplying; the following are valid options:
149${validLoaderOptions.join(' / ')}
150`);
151 }
152 }
153 if (loaderOptions.context !== undefined &&
154 !path.isAbsolute(loaderOptions.context)) {
155 throw new Error(`Option 'context' has to be an absolute path. Given '${loaderOptions.context}'.`);
156 }
157}
158function makeLoaderOptions(instanceName, loaderOptions) {
159 const options = Object.assign({}, {
160 silent: false,
161 logLevel: 'WARN',
162 logInfoToStdOut: false,
163 compiler: 'typescript',
164 configFile: 'tsconfig.json',
165 context: undefined,
166 transpileOnly: false,
167 compilerOptions: {},
168 appendTsSuffixTo: [],
169 appendTsxSuffixTo: [],
170 transformers: {},
171 happyPackMode: false,
172 colors: true,
173 onlyCompileBundledFiles: false,
174 reportFiles: [],
175 // When the watch API usage stabilises look to remove this option and make watch usage the default behaviour when available
176 experimentalWatchApi: false,
177 allowTsInNodeModules: false,
178 experimentalFileCaching: true,
179 }, loaderOptions);
180 options.ignoreDiagnostics = utils_1.arrify(options.ignoreDiagnostics).map(Number);
181 options.logLevel = options.logLevel.toUpperCase();
182 options.instance = instanceName;
183 // happypack can be used only together with transpileOnly mode
184 options.transpileOnly = options.happyPackMode ? true : options.transpileOnly;
185 return options;
186}
187/**
188 * Either add file to the overall files cache or update it in the cache when the file contents have changed
189 * Also add the file to the modified files
190 */
191function updateFileInCache(options, filePath, contents, instance) {
192 let fileWatcherEventKind;
193 // Update file contents
194 const key = instance.filePathKeyMapper(filePath);
195 let file = instance.files.get(key);
196 if (file === undefined) {
197 file = instance.otherFiles.get(key);
198 if (file !== undefined) {
199 if (!utils_1.isReferencedFile(instance, filePath)) {
200 instance.otherFiles.delete(key);
201 instance.files.set(key, file);
202 instance.changedFilesList = true;
203 }
204 }
205 else {
206 if (instance.watchHost !== undefined) {
207 fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Created;
208 }
209 file = { fileName: filePath, version: 0 };
210 if (!utils_1.isReferencedFile(instance, filePath)) {
211 instance.files.set(key, file);
212 instance.changedFilesList = true;
213 }
214 }
215 }
216 if (instance.watchHost !== undefined && contents === undefined) {
217 fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Deleted;
218 }
219 // filePath is a root file as it was passed to the loader. But it
220 // could have been found earlier as a dependency of another file. If
221 // that is the case, compiling this file changes the structure of
222 // the program and we need to increase the instance version.
223 //
224 // See https://github.com/TypeStrong/ts-loader/issues/943
225 if (!utils_1.isReferencedFile(instance, filePath) &&
226 !instance.rootFileNames.has(filePath) &&
227 // however, be careful not to add files from node_modules unless
228 // it is allowed by the options.
229 (options.allowTsInNodeModules || filePath.indexOf('node_modules') === -1)) {
230 instance.version++;
231 instance.rootFileNames.add(filePath);
232 }
233 if (file.text !== contents) {
234 file.version++;
235 file.text = contents;
236 file.modifiedTime = new Date();
237 instance.version++;
238 if (instance.watchHost !== undefined &&
239 fileWatcherEventKind === undefined) {
240 fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Changed;
241 }
242 }
243 // Added in case the files were already updated by the watch API
244 if (instance.modifiedFiles && instance.modifiedFiles.get(key)) {
245 fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Changed;
246 }
247 if (instance.watchHost !== undefined && fileWatcherEventKind !== undefined) {
248 instance.hasUnaccountedModifiedFiles =
249 instance.watchHost.invokeFileWatcher(filePath, fileWatcherEventKind) ||
250 instance.hasUnaccountedModifiedFiles;
251 }
252 // push this file to modified files hash.
253 if (!instance.modifiedFiles) {
254 instance.modifiedFiles = new Map();
255 }
256 instance.modifiedFiles.set(key, true);
257 return file.version;
258}
259function getEmit(rawFilePath, filePath, instance, loaderContext) {
260 var _a;
261 const outputFiles = instances_1.getEmitOutput(instance, filePath);
262 loaderContext.clearDependencies();
263 loaderContext.addDependency(rawFilePath);
264 const dependencies = [];
265 const addDependency = (file) => {
266 file = path.resolve(file);
267 loaderContext.addDependency(file);
268 dependencies.push(file);
269 };
270 // Make this file dependent on *all* definition files in the program
271 if (!utils_1.isReferencedFile(instance, filePath)) {
272 for (const { fileName: defFilePath } of instance.files.values()) {
273 if (defFilePath.match(constants.dtsDtsxOrDtsDtsxMapRegex) &&
274 // Remove the project reference d.ts as we are adding dependency for .ts later
275 // This removed extra build pass (resulting in new stats object in initial build)
276 !((_a = instance.solutionBuilderHost) === null || _a === void 0 ? void 0 : _a.getOutputFileKeyFromReferencedProject(defFilePath))) {
277 addDependency(defFilePath);
278 }
279 }
280 }
281 // Additionally make this file dependent on all imported files
282 const fileDependencies = instance.dependencyGraph.get(instance.filePathKeyMapper(filePath));
283 if (fileDependencies) {
284 for (const { resolvedFileName, originalFileName } of fileDependencies) {
285 // In the case of dependencies that are part of a project reference,
286 // the real dependency that webpack should watch is the JS output file.
287 addDependency(instances_1.getInputFileNameFromOutput(instance, path.resolve(resolvedFileName)) ||
288 originalFileName);
289 }
290 }
291 addDependenciesFromSolutionBuilder(instance, filePath, addDependency);
292 loaderContext._module.buildMeta.tsLoaderDefinitionFileVersions = dependencies.map(defFilePath => path.relative(loaderContext.rootContext, defFilePath) +
293 '@' +
294 (utils_1.isReferencedFile(instance, defFilePath)
295 ? instance
296 .solutionBuilderHost.getInputFileStamp(defFilePath)
297 .toString()
298 : (instance.files.get(instance.filePathKeyMapper(defFilePath)) ||
299 instance.otherFiles.get(instance.filePathKeyMapper(defFilePath)) || {
300 version: '?',
301 }).version));
302 return getOutputAndSourceMapFromOutputFiles(outputFiles);
303}
304function getOutputAndSourceMapFromOutputFiles(outputFiles) {
305 const outputFile = outputFiles
306 .filter(file => file.name.match(constants.jsJsx))
307 .pop();
308 const outputText = outputFile === undefined ? undefined : outputFile.text;
309 const sourceMapFile = outputFiles
310 .filter(file => file.name.match(constants.jsJsxMap))
311 .pop();
312 const sourceMapText = sourceMapFile === undefined ? undefined : sourceMapFile.text;
313 return { outputText, sourceMapText };
314}
315function addDependenciesFromSolutionBuilder(instance, filePath, addDependency) {
316 if (!instance.solutionBuilderHost) {
317 return;
318 }
319 // Add all the input files from the references as
320 const resolvedFilePath = instance.filePathKeyMapper(filePath);
321 if (!utils_1.isReferencedFile(instance, filePath)) {
322 if (instance.configParseResult.fileNames.some(f => instance.filePathKeyMapper(f) === resolvedFilePath)) {
323 addDependenciesFromProjectReferences(instance, instance.configFilePath, instance.configParseResult.projectReferences, addDependency);
324 }
325 return;
326 }
327 // Referenced file find the config for it
328 for (const [configFile, configInfo,] of instance.solutionBuilderHost.configFileInfo.entries()) {
329 if (!configInfo.config ||
330 !configInfo.config.projectReferences ||
331 !configInfo.config.projectReferences.length) {
332 continue;
333 }
334 if (configInfo.outputFileNames) {
335 if (!configInfo.outputFileNames.has(resolvedFilePath)) {
336 continue;
337 }
338 }
339 else if (!configInfo.config.fileNames.some(f => instance.filePathKeyMapper(f) === resolvedFilePath)) {
340 continue;
341 }
342 // Depend on all the dts files from the program
343 if (configInfo.dtsFiles) {
344 configInfo.dtsFiles.forEach(addDependency);
345 }
346 addDependenciesFromProjectReferences(instance, configFile, configInfo.config.projectReferences, addDependency);
347 break;
348 }
349}
350function addDependenciesFromProjectReferences(instance, configFile, projectReferences, addDependency) {
351 if (!projectReferences || !projectReferences.length) {
352 return;
353 }
354 // This is the config for the input file
355 const seenMap = new Map();
356 seenMap.set(instance.filePathKeyMapper(configFile), true);
357 // Add dependencies to all the input files from the project reference files since building them
358 const queue = projectReferences.slice();
359 while (true) {
360 const currentRef = queue.pop();
361 if (!currentRef) {
362 break;
363 }
364 const refConfigFile = instance.filePathKeyMapper(instance.compiler.resolveProjectReferencePath(currentRef));
365 if (seenMap.has(refConfigFile)) {
366 continue;
367 }
368 const refConfigInfo = instance.solutionBuilderHost.configFileInfo.get(refConfigFile);
369 if (!refConfigInfo) {
370 continue;
371 }
372 seenMap.set(refConfigFile, true);
373 if (refConfigInfo.config) {
374 refConfigInfo.config.fileNames.forEach(addDependency);
375 if (refConfigInfo.config.projectReferences) {
376 queue.push(...refConfigInfo.config.projectReferences);
377 }
378 }
379 }
380}
381/**
382 * Transpile file
383 */
384function getTranspilationEmit(fileName, contents, instance, loaderContext) {
385 if (utils_1.isReferencedFile(instance, fileName)) {
386 const outputFiles = instance.solutionBuilderHost.getOutputFilesFromReferencedProjectInput(fileName);
387 addDependenciesFromSolutionBuilder(instance, fileName, file => loaderContext.addDependency(path.resolve(file)));
388 return getOutputAndSourceMapFromOutputFiles(outputFiles);
389 }
390 const { outputText, sourceMapText, diagnostics, } = instance.compiler.transpileModule(contents, {
391 compilerOptions: Object.assign(Object.assign({}, instance.compilerOptions), { rootDir: undefined }),
392 transformers: instance.transformers,
393 reportDiagnostics: true,
394 fileName,
395 });
396 const module = loaderContext._module;
397 addDependenciesFromSolutionBuilder(instance, fileName, file => loaderContext.addDependency(path.resolve(file)));
398 // _module.errors is not available inside happypack - see https://github.com/TypeStrong/ts-loader/issues/336
399 if (!instance.loaderOptions.happyPackMode) {
400 const errors = utils_1.formatErrors(diagnostics, instance.loaderOptions, instance.colors, instance.compiler, { module }, loaderContext.context);
401 /**
402 * Since webpack 5, the `errors` property is deprecated,
403 * so we can check if some methods for reporting errors exist.
404 */
405 if (module.addError) {
406 errors.forEach(error => module.addError(error));
407 }
408 else {
409 module.errors.push(...errors);
410 }
411 }
412 return { outputText, sourceMapText };
413}
414function makeSourceMap(sourceMapText, outputText, filePath, contents, loaderContext) {
415 if (sourceMapText === undefined) {
416 return { output: outputText, sourceMap: undefined };
417 }
418 return {
419 output: outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
420 sourceMap: Object.assign(JSON.parse(sourceMapText), {
421 sources: [loaderUtils.getRemainingRequest(loaderContext)],
422 file: filePath,
423 sourcesContent: [contents],
424 }),
425 };
426}
427module.exports = loader;
428//# sourceMappingURL=index.js.map
\No newline at end of file