@teambit/compiler 1.0.107 → 1.0.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/types.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { BuildContext, BuildTask, BuiltTaskResult, TaskResultsList } from '@teambit/builder';
2
+ import type { Component } from '@teambit/component';
3
+ import { ServiceHandler } from '@teambit/envs';
4
+
5
+ export type TranspileFileParams = {
6
+ componentDir: string; // absolute path of the component's root directory
7
+ filePath: string; // relative path of the file inside the component directory
8
+ };
9
+
10
+ export enum CompilationInitiator {
11
+ CmdReport,
12
+ CmdJson,
13
+ PreStart,
14
+ PreWatch,
15
+ Start,
16
+ ComponentChanged,
17
+ AspectLoadFail,
18
+ ComponentAdded,
19
+ Install,
20
+ }
21
+
22
+ export type TranspileComponentParams = {
23
+ component: Component;
24
+ componentDir: string; // absolute path of the component's root directory
25
+ outputDir: string; // absolute path of the component's output directory
26
+ initiator: CompilationInitiator; // origin of the compilation's request
27
+ };
28
+
29
+ export type TranspileFileOutputOneFile = {
30
+ outputText: string;
31
+ outputPath: string;
32
+ };
33
+
34
+ export type TranspileFileOutput = TranspileFileOutputOneFile[] | null;
35
+
36
+ export interface CompilerOptions {
37
+ /**
38
+ * name of the compiler.
39
+ */
40
+ name?: string;
41
+
42
+ /**
43
+ * relative path of the dist directory inside the capsule. e.g. "dist".
44
+ */
45
+ distDir: string;
46
+
47
+ /**
48
+ * determines which ones of the generated files will be saved in the bit objects when tagging.
49
+ * e.g. distGlobPatterns = [`${this.distDir}/**`, `!${this.distDir}/tsconfig.tsbuildinfo`];
50
+ * see https://github.com/mrmlnc/fast-glob for the supported glob patters syntax.
51
+ */
52
+ distGlobPatterns?: string[];
53
+
54
+ /**
55
+ * whether or not unsupported files (such as assets) should be copied into the dist directory
56
+ */
57
+ shouldCopyNonSupportedFiles?: boolean;
58
+
59
+ /**
60
+ * optional. default to "dist".
61
+ * useful when the build pipeline has multiple compiler tasks of the same compiler.
62
+ * e.g. using the same Babel compiler for two different tasks, one for creating "es5" files, and
63
+ * the second for creating "esm". the artifact names would be "es5" and "esm" accordingly.
64
+ */
65
+ artifactName?: string;
66
+ }
67
+
68
+ export interface Compiler extends CompilerOptions, ServiceHandler {
69
+ /**
70
+ * id of the compiler.
71
+ */
72
+ id: string;
73
+
74
+ /**
75
+ * Delete dist folder before writing the new compiled files
76
+ */
77
+ deleteDistDir?: boolean;
78
+
79
+ /**
80
+ * serialized config of the compiler.
81
+ */
82
+ displayConfig?(): string;
83
+
84
+ /**
85
+ * transpile a single file that gets saved into the workspace, used by `bit compile` and during
86
+ * development
87
+ */
88
+ transpileFile?: (
89
+ fileContent: string,
90
+ params: TranspileFileParams
91
+ ) => TranspileFileOutput | Promise<TranspileFileOutput>;
92
+
93
+ /**
94
+ * transpile all the files of a component, use this when you can't use `transpileFile`
95
+ */
96
+ transpileComponent?: (params: TranspileComponentParams) => Promise<void>;
97
+
98
+ /**
99
+ * compile components inside isolated capsules. this being used during tag for the release.
100
+ * meaning, the final package of the component has the dists generated by this method.
101
+ *
102
+ * no need to handle non-supported-files (e.g. assets) in this method because the compiler-aspect already copies them
103
+ * into the compiler.dist in the preBuild stage.
104
+ */
105
+ build(context: BuildContext): Promise<BuiltTaskResult>;
106
+
107
+ /**
108
+ * return the dist dir of the compiled files (relative path from the component root dir)
109
+ */
110
+ getDistDir?(): string;
111
+
112
+ /**
113
+ * given a source file, return its parallel in the dists. e.g. "index.ts" => "dist/index.js"
114
+ * both, the return path and the given path are relative paths.
115
+ */
116
+ getDistPathBySrcPath(srcPath: string): string;
117
+
118
+ /**
119
+ * given a component, returns the path to the source folder to use for the preview, uses the one
120
+ * in node_modules by default
121
+ */
122
+ getPreviewComponentRootPath?(component: Component): string;
123
+
124
+ /**
125
+ * only supported files matching get compiled. others, are copied to the dist dir.
126
+ */
127
+ isFileSupported(filePath: string): boolean;
128
+
129
+ /**
130
+ * sugar to create a Compiler task via the concrete compiler
131
+ */
132
+ createTask?(name?: string): BuildTask;
133
+
134
+ /**
135
+ * run before the build pipeline has started. this is useful when some preparation are needed to
136
+ * be done on all envs before the build starts.
137
+ */
138
+ preBuild?(context: BuildContext): Promise<void>;
139
+
140
+ /**
141
+ * run after the build pipeline completed for all envs. useful for some cleanups
142
+ */
143
+ postBuild?(context: BuildContext, tasksResults: TaskResultsList): Promise<void>;
144
+
145
+ /**
146
+ * returns the version of the current compiler instance (e.g. '4.0.1').
147
+ */
148
+ version(): string;
149
+
150
+ /**
151
+ * returns the display name of the current compiler instance (e.g. 'TypeScript')
152
+ */
153
+ displayName: string;
154
+ }
@@ -0,0 +1,398 @@
1
+ /* eslint-disable max-classes-per-file */
2
+ import mapSeries from 'p-map-series';
3
+ import { Component } from '@teambit/component';
4
+ import { EnvsMain } from '@teambit/envs';
5
+ import type { PubsubMain } from '@teambit/pubsub';
6
+ import { SerializableResults, Workspace, OutsideWorkspaceError } from '@teambit/workspace';
7
+ import { WatcherMain, WatchOptions } from '@teambit/watcher';
8
+ import path from 'path';
9
+ import { ComponentID } from '@teambit/component-id';
10
+ import { Logger } from '@teambit/logger';
11
+ import loader from '@teambit/legacy/dist/cli/loader';
12
+ import { DEFAULT_DIST_DIRNAME } from '@teambit/legacy/dist/constants';
13
+ import { AbstractVinyl, Dist } from '@teambit/legacy/dist/consumer/component/sources';
14
+ import DataToPersist from '@teambit/legacy/dist/consumer/component/sources/data-to-persist';
15
+ import {
16
+ linkToNodeModulesByComponents,
17
+ removeLinksFromNodeModules,
18
+ } from '@teambit/workspace.modules.node-modules-linker';
19
+ import { AspectLoaderMain } from '@teambit/aspect-loader';
20
+ import { DependencyResolverMain } from '@teambit/dependency-resolver';
21
+ import componentIdToPackageName from '@teambit/legacy/dist/utils/bit/component-id-to-package-name';
22
+ import RemovePath from '@teambit/legacy/dist/consumer/component/sources/remove-path';
23
+ import { UiMain } from '@teambit/ui';
24
+ import { readBitRootsDir } from '@teambit/bit-roots';
25
+ import { groupBy, uniq } from 'lodash';
26
+ import type { PreStartOpts } from '@teambit/ui';
27
+ import { PathOsBasedAbsolute, PathOsBasedRelative } from '@teambit/legacy/dist/utils/path';
28
+ import { MultiCompiler } from '@teambit/multi-compiler';
29
+ import { CompilerAspect } from './compiler.aspect';
30
+ import { CompilerErrorEvent, ComponentCompilationOnDoneEvent } from './events';
31
+ import { Compiler, CompilationInitiator } from './types';
32
+
33
+ export type BuildResult = { component: string; buildResults: string[] | null | undefined };
34
+
35
+ export type CompileOptions = {
36
+ changed?: boolean; // compile only new and modified components
37
+ verbose?: boolean; // show more data, such as, dist paths
38
+ /**
39
+ * whether the dist root dir should be deleted before writing new dists.
40
+ * defaults to true for `bit compile` and false everywhere else, such as `bit watch` and `bit
41
+ * start` to avoid webpack "EINTR" error.
42
+ */
43
+ deleteDistDir?: boolean;
44
+ initiator: CompilationInitiator; // describes where the compilation is coming from
45
+ // should we create links in node_modules for the compiled components (default = true)
46
+ // this will link the source files, and create the package.json
47
+ linkComponents?: boolean;
48
+ };
49
+
50
+ export type CompileError = { path: string; error: Error };
51
+
52
+ export class ComponentCompiler {
53
+ constructor(
54
+ private pubsub: PubsubMain,
55
+ private workspace: Workspace,
56
+ private component: Component,
57
+ private compilerInstance: Compiler,
58
+ private compilerId: string,
59
+ private logger: Logger,
60
+ private dependencyResolver: DependencyResolverMain,
61
+ private dists: Dist[] = [],
62
+ private compileErrors: CompileError[] = []
63
+ ) {}
64
+
65
+ async compile(noThrow = true, options: CompileOptions): Promise<BuildResult> {
66
+ let dataToPersist;
67
+ const deleteDistDir = options.deleteDistDir ?? this.compilerInstance.deleteDistDir;
68
+ // delete dist folder before transpilation (because some compilers (like ngPackagr) can generate files there during the compilation process)
69
+ if (deleteDistDir) {
70
+ dataToPersist = new DataToPersist();
71
+ for (const distDir of await this.distDirs()) {
72
+ dataToPersist.removePath(new RemovePath(distDir));
73
+ }
74
+ dataToPersist.addBasePath(this.workspace.path);
75
+ await dataToPersist.persistAllToFS();
76
+ }
77
+
78
+ const compilers: Compiler[] = (this.compilerInstance as MultiCompiler).compilers
79
+ ? (this.compilerInstance as MultiCompiler).compilers
80
+ : [this.compilerInstance];
81
+ const canTranspileFile = compilers.find((c) => c.transpileFile);
82
+ const canTranspileComponent = compilers.find((c) => c.transpileComponent);
83
+
84
+ if (canTranspileFile) {
85
+ await Promise.all(
86
+ this.component.filesystem.files.map((file: AbstractVinyl) => this.compileOneFile(file, options.initiator))
87
+ );
88
+ }
89
+
90
+ if (canTranspileComponent) {
91
+ await this.compileAllFiles(this.component, options.initiator);
92
+ }
93
+
94
+ if (!canTranspileFile && !canTranspileComponent) {
95
+ throw new Error(
96
+ `compiler ${this.compilerId.toString()} doesn't implement either "transpileFile" or "transpileComponent" methods`
97
+ );
98
+ }
99
+ this.throwOnCompileErrors(noThrow);
100
+
101
+ // writing the dists with `component.setDists(dists); component.dists.writeDists` is tricky
102
+ // as it uses other base-paths and doesn't respect the new node-modules base path.
103
+ dataToPersist = new DataToPersist();
104
+ dataToPersist.addManyFiles(this.dists);
105
+ dataToPersist.addBasePath(this.workspace.path);
106
+ await dataToPersist.persistAllToFS();
107
+ const linkComponents = options.linkComponents ?? true;
108
+ if (linkComponents) {
109
+ await linkToNodeModulesByComponents([this.component], this.workspace);
110
+ }
111
+ const buildResults = this.dists.map((distFile) => distFile.path);
112
+ if (this.component.state._consumer.compiler) loader.succeed();
113
+ this.pubsub.pub(
114
+ CompilerAspect.id,
115
+ new ComponentCompilationOnDoneEvent(this.compileErrors, this.component, buildResults)
116
+ );
117
+ return { component: this.component.id.toString(), buildResults };
118
+ }
119
+
120
+ private throwOnCompileErrors(noThrow = true) {
121
+ if (this.compileErrors.length) {
122
+ this.compileErrors.forEach((errorItem) => {
123
+ this.logger.error(`compilation error at ${errorItem.path}`, errorItem.error);
124
+ });
125
+ const formatError = (errorItem) => `${errorItem.path}\n${errorItem.error}`;
126
+ const err = new Error(`compilation failed. see the following errors from the compiler
127
+ ${this.compileErrors.map(formatError).join('\n')}`);
128
+
129
+ this.pubsub.pub(CompilerAspect.id, new CompilerErrorEvent(err));
130
+
131
+ if (!noThrow) {
132
+ throw err;
133
+ }
134
+
135
+ this.logger.console(err.message);
136
+ }
137
+ }
138
+
139
+ private async distDirs(): Promise<PathOsBasedRelative[]> {
140
+ const packageName = componentIdToPackageName(this.component.state._consumer);
141
+ const packageDir = path.join('node_modules', packageName);
142
+ const distDirName = this.compilerInstance.getDistDir?.() || DEFAULT_DIST_DIRNAME;
143
+ const injectedDirs = await this.getInjectedDirs(packageName);
144
+ return [packageDir, ...injectedDirs].map((dist) => path.join(dist, distDirName));
145
+ }
146
+
147
+ private async getInjectedDirs(packageName: string): Promise<string[]> {
148
+ const injectedDirs = await this.workspace.getInjectedDirs(this.component);
149
+ if (injectedDirs.length > 0) return injectedDirs;
150
+
151
+ const rootDirs = await readBitRootsDir(this.workspace.path);
152
+ return rootDirs.map((rootDir) => path.relative(this.workspace.path, path.join(rootDir, packageName)));
153
+ }
154
+
155
+ private get componentDir(): PathOsBasedAbsolute {
156
+ return this.workspace.componentDir(this.component.id);
157
+ }
158
+
159
+ private async compileOneFile(file: AbstractVinyl, initiator: CompilationInitiator): Promise<void> {
160
+ const options = { componentDir: this.componentDir, filePath: file.relative, initiator };
161
+ const isFileSupported = this.compilerInstance.isFileSupported(file.path);
162
+ let compileResults;
163
+ if (isFileSupported) {
164
+ try {
165
+ compileResults = await this.compilerInstance.transpileFile?.(file.contents.toString(), options);
166
+ } catch (error: any) {
167
+ this.compileErrors.push({ path: file.path, error });
168
+ return;
169
+ }
170
+ }
171
+ for (const base of await this.distDirs()) {
172
+ if (isFileSupported && compileResults) {
173
+ this.dists.push(
174
+ ...compileResults.map(
175
+ (result) =>
176
+ new Dist({
177
+ base,
178
+ path: path.join(base, result.outputPath),
179
+ contents: Buffer.from(result.outputText),
180
+ })
181
+ )
182
+ );
183
+ } else if (this.compilerInstance.shouldCopyNonSupportedFiles) {
184
+ // compiler doesn't support this file type. copy the file as is to the dist dir.
185
+ this.dists.push(new Dist({ base, path: path.join(base, file.relative), contents: file.contents }));
186
+ }
187
+ }
188
+ }
189
+
190
+ private async compileAllFiles(component: Component, initiator: CompilationInitiator): Promise<void> {
191
+ const filesToCompile: AbstractVinyl[] = [];
192
+ for (const base of await this.distDirs()) {
193
+ component.filesystem.files.forEach((file: AbstractVinyl) => {
194
+ const isFileSupported = this.compilerInstance.isFileSupported(file.path);
195
+ if (isFileSupported) {
196
+ filesToCompile.push(file);
197
+ } else if (this.compilerInstance.shouldCopyNonSupportedFiles) {
198
+ // compiler doesn't support this file type. copy the file as is to the dist dir.
199
+ this.dists.push(
200
+ new Dist({
201
+ base,
202
+ path: path.join(base, file.relative),
203
+ contents: file.contents,
204
+ })
205
+ );
206
+ }
207
+ });
208
+ }
209
+
210
+ if (filesToCompile.length) {
211
+ try {
212
+ await this.compilerInstance.transpileComponent?.({
213
+ component,
214
+ componentDir: this.componentDir,
215
+ outputDir: await this.workspace.getComponentPackagePath(component),
216
+ initiator,
217
+ });
218
+ } catch (error: any) {
219
+ this.compileErrors.push({ path: this.componentDir, error });
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ export class WorkspaceCompiler {
226
+ constructor(
227
+ private workspace: Workspace,
228
+ private envs: EnvsMain,
229
+ private pubsub: PubsubMain,
230
+ private aspectLoader: AspectLoaderMain,
231
+ private ui: UiMain,
232
+ private logger: Logger,
233
+ private dependencyResolver: DependencyResolverMain,
234
+ private watcher: WatcherMain
235
+ ) {
236
+ if (this.workspace) {
237
+ this.workspace.registerOnComponentChange(this.onComponentChange.bind(this));
238
+ this.workspace.registerOnComponentAdd(this.onComponentAdd.bind(this));
239
+ this.watcher.registerOnPreWatch(this.onPreWatch.bind(this));
240
+ }
241
+ this.ui.registerPreStart(this.onPreStart.bind(this));
242
+ if (this.aspectLoader) {
243
+ this.aspectLoader.registerOnAspectLoadErrorSlot(this.onAspectLoadFail.bind(this));
244
+ }
245
+ }
246
+
247
+ async onPreStart(preStartOpts: PreStartOpts): Promise<void> {
248
+ if (this.workspace) {
249
+ if (preStartOpts.skipCompilation) {
250
+ return;
251
+ }
252
+ await this.compileComponents([], {
253
+ changed: true,
254
+ verbose: false,
255
+ deleteDistDir: false,
256
+ initiator: CompilationInitiator.PreStart,
257
+ });
258
+ } else {
259
+ await this.watcher.watchScopeInternalFiles();
260
+ }
261
+ }
262
+
263
+ async onAspectLoadFail(err: Error & { code?: string }, id: ComponentID): Promise<boolean> {
264
+ if (err.code && err.code === 'MODULE_NOT_FOUND' && this.workspace) {
265
+ await this.compileComponents([id.toString()], { initiator: CompilationInitiator.AspectLoadFail }, true);
266
+ return true;
267
+ }
268
+ return false;
269
+ }
270
+
271
+ async onComponentAdd(component: Component, files: string[], watchOpts: WatchOptions) {
272
+ return this.onComponentChange(component, files, undefined, watchOpts);
273
+ }
274
+
275
+ async onComponentChange(
276
+ component: Component,
277
+ files: string[],
278
+ removedFiles: string[] = [],
279
+ watchOpts: WatchOptions
280
+ ): Promise<SerializableResults | void> {
281
+ if (!watchOpts.compile) return undefined;
282
+ // when files are removed, we need to remove the dist directories and the old symlinks, otherwise, it has
283
+ // symlinks to non-exist files and the dist has stale files
284
+ const deleteDistDir = Boolean(removedFiles?.length);
285
+ if (removedFiles?.length) {
286
+ await removeLinksFromNodeModules(component, this.workspace, removedFiles);
287
+ }
288
+ const buildResults = await this.compileComponents(
289
+ [component.id.toString()],
290
+ { initiator: watchOpts.initiator || CompilationInitiator.ComponentChanged, deleteDistDir },
291
+ true
292
+ );
293
+ // await linkToNodeModulesByComponents([component], this.workspace);
294
+ return {
295
+ results: buildResults,
296
+ toString() {
297
+ return `${buildResults[0]?.buildResults?.join('\n\t')}`;
298
+ },
299
+ };
300
+ }
301
+
302
+ async onPreWatch(componentIds: ComponentID[], watchOpts: WatchOptions) {
303
+ if (watchOpts.preCompile) {
304
+ const start = Date.now();
305
+ this.logger.console(`compiling ${componentIds.length} components`);
306
+ await this.compileComponents(
307
+ componentIds.map((id) => id),
308
+ { initiator: CompilationInitiator.PreWatch }
309
+ );
310
+ const end = Date.now() - start;
311
+ this.logger.consoleSuccess(`compiled ${componentIds.length} components successfully (${end / 1000} sec)`);
312
+ }
313
+ }
314
+
315
+ async compileComponents(
316
+ componentsIds: string[] | ComponentID[] | ComponentID[], // when empty, it compiles new+modified (unless options.all is set),
317
+ options: CompileOptions,
318
+ noThrow?: boolean
319
+ ): Promise<BuildResult[]> {
320
+ if (!this.workspace) throw new OutsideWorkspaceError();
321
+ const componentIds = await this.getIdsToCompile(componentsIds, options.changed);
322
+ // In case the aspect failed to load, we want to compile it without try to re-load it again
323
+ const getManyOpts =
324
+ options.initiator === CompilationInitiator.AspectLoadFail ? { loadSeedersAsAspects: false } : undefined;
325
+ const components = await this.workspace.getMany(componentIds, getManyOpts);
326
+
327
+ const grouped = this.groupByIsEnv(components);
328
+ const envsResults = grouped.envs ? await this.runCompileComponents(grouped.envs, options, noThrow) : [];
329
+ const otherResults = grouped.other ? await this.runCompileComponents(grouped.other, options, noThrow) : [];
330
+ return [...envsResults, ...otherResults];
331
+ }
332
+
333
+ private async runCompileComponents(
334
+ components: Component[],
335
+ options: CompileOptions,
336
+ noThrow?: boolean
337
+ ): Promise<BuildResult[]> {
338
+ const componentsCompilers: ComponentCompiler[] = [];
339
+
340
+ components.forEach((c) => {
341
+ const environment = this.envs.getEnv(c).env;
342
+ const compilerInstance = environment.getCompiler?.();
343
+
344
+ if (compilerInstance) {
345
+ const compilerName = compilerInstance.constructor.name || 'compiler';
346
+ componentsCompilers.push(
347
+ new ComponentCompiler(
348
+ this.pubsub,
349
+ this.workspace,
350
+ c,
351
+ compilerInstance,
352
+ compilerName,
353
+ this.logger,
354
+ this.dependencyResolver
355
+ )
356
+ );
357
+ } else {
358
+ this.logger.warn(`unable to find a compiler instance for ${c.id.toString()}`);
359
+ }
360
+ });
361
+ const resultOnWorkspace = await mapSeries(componentsCompilers, (componentCompiler) =>
362
+ componentCompiler.compile(noThrow, options)
363
+ );
364
+
365
+ return resultOnWorkspace;
366
+ }
367
+
368
+ /**
369
+ * This function get's a list of aspect ids and return them grouped by whether any of them is the env of other from the list
370
+ * @param ids
371
+ */
372
+ groupByIsEnv(components: Component[]): { envs?: Component[]; other?: Component[] } {
373
+ const envsIds = uniq(
374
+ components
375
+ .map((component) => this.envs.getEnvId(component))
376
+ .filter((envId) => !this.aspectLoader.isCoreEnv(envId))
377
+ );
378
+ const grouped = groupBy(components, (component) => {
379
+ if (envsIds.includes(component.id.toString())) return 'envs';
380
+ return 'other';
381
+ });
382
+ return grouped as { envs: Component[]; other: Component[] };
383
+ }
384
+
385
+ private async getIdsToCompile(
386
+ componentsIds: Array<string | ComponentID | ComponentID>,
387
+ changed = false
388
+ ): Promise<ComponentID[]> {
389
+ if (componentsIds.length) {
390
+ const componentIds = await this.workspace.resolveMultipleComponentIds(componentsIds);
391
+ return this.workspace.filterIds(componentIds);
392
+ }
393
+ if (changed) {
394
+ return this.workspace.getNewAndModifiedIds();
395
+ }
396
+ return this.workspace.listIds();
397
+ }
398
+ }