Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/AppsCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,23 @@ import { AppPackager } from './packager';
import { TypescriptCompiler } from './compiler/TypescriptCompiler';
import { AppsEngineValidator } from './compiler/AppsEngineValidator';
import getBundler, { AvailableBundlers, BundlerFunction } from './bundler';
import logger from './misc/logger';

export type TypeScript = typeof fallbackTypescript;

export type AppCompilerOptions = {
/**
* Indicates whether the AppCompiler should take into
* account the .tsconfig file when compiling the app
*/
readTsProjectFile?: boolean;
};

const defaultOptions: AppCompilerOptions = {
// Default to false for compatibility
readTsProjectFile: false,
};

export class AppsCompiler {
private compilationResult?: ICompilerResult;

Expand All @@ -26,6 +40,7 @@ export class AppsCompiler {
private readonly compilerDesc: ICompilerDescriptor,
private readonly sourcePath: string,
ts: TypeScript = fallbackTypescript,
private readonly options = defaultOptions,
) {
this.validator = new AppsEngineValidator(createRequire(path.join(sourcePath, 'app.json')));

Expand All @@ -45,7 +60,11 @@ export class AppsCompiler {
}

public async compile(): Promise<ICompilerResult> {
const source = await getAppSource(this.sourcePath);
const source = await getAppSource(this.sourcePath, this.options);

if (source.compilerOptions) {
this.typescriptCompiler.setCompilerOptions(source.compilerOptions);
}

this.compilationResult = this.typescriptCompiler.transpileSource(source);

Expand All @@ -65,7 +84,7 @@ export class AppsCompiler {
// @NOTE this is important for generating the zip file with the correct name
await fd.readInfoFile();
} catch (e) {
console.error(e && e.message ? e.message : e);
logger.error(e && e.message ? e.message : e);
return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/compiler/TypescriptCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { AppsEngineValidator } from './AppsEngineValidator';
import logger from '../misc/logger';

export class TypescriptCompiler {
private readonly nonConfigurableCompilerOptions: CompilerOptions;

private readonly compilerOptions: CompilerOptions;

private libraryFiles: IMapCompilerFile;
Expand All @@ -29,10 +31,16 @@ export class TypescriptCompiler {
private readonly ts: TypeScript,
private readonly appValidator: AppsEngineValidator,
) {
this.compilerOptions = {
// Things we shouldn't allow the app dev changing
this.nonConfigurableCompilerOptions = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? Is there a reason for this options to be hardcoded?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because these options affect the output of the compilation, and that's up to the host system to decide, not the app developer

target: this.ts.ScriptTarget.ES2017,
module: this.ts.ModuleKind.CommonJS,
moduleResolution: this.ts.ModuleResolutionKind.NodeJs,
};

this.compilerOptions = {
...this.nonConfigurableCompilerOptions,
skipLibCheck: true,
declaration: false,
noImplicitAny: false,
removeComments: true,
Expand All @@ -48,6 +56,10 @@ export class TypescriptCompiler {
this.libraryFiles = {};
}

public setCompilerOptions(options: CompilerOptions): void {
Object.assign(this.compilerOptions, options, this.nonConfigurableCompilerOptions);
}

public transpileSource({ appInfo, sourceFiles: files }: IAppSource): ICompilerResult {
if (!appInfo.classFile || !files[appInfo.classFile] || !this.isValidFile(files[appInfo.classFile])) {
throw new Error(`Invalid App package. Could not find the classFile (${ appInfo.classFile }) file.`);
Expand Down
46 changes: 38 additions & 8 deletions src/compiler/getAppSource.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { promises as fs } from 'fs';
import { resolve, relative } from 'path';
import { resolve, relative, join } from 'path';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import { CompilerOptions } from 'typescript';

import { IAppSource, ICompilerFile, IMapCompilerFile } from '../definition';
import { AppCompilerOptions } from '../AppsCompiler';
import logger from '../misc/logger';

async function walkDirectory(directory: string): Promise<ICompilerFile[]> {
export type TSConfig = {
compilerOptions?: CompilerOptions;
exclude?: string[];
}

async function walkDirectory(directory: string, projectExcludes: string[] = []): Promise<ICompilerFile[]> {
const dirents = await fs.readdir(directory, { withFileTypes: true });
const dirsToIgnore = projectExcludes.concat(['node_modules', '.git']);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be included in app.json file? Also, can we add files here too?

Copy link
Member Author

@d-gubert d-gubert Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what the projectExcludes parameter is for

const files = await Promise.all(
dirents
.map(async (dirent) => {
const res = resolve(directory, dirent.name);

const dirsToIgnore = ['node_modules', '.git'];
if (dirsToIgnore.some((dir) => res.includes(dir))) {
return null;
}
Expand Down Expand Up @@ -48,30 +56,52 @@ function makeICompilerFileMap(compilerFiles: ICompilerFile[]): IMapCompilerFile
.reduce((acc: IMapCompilerFile, curr: IMapCompilerFile) => ({ ...acc, ...curr }), {});
}

async function getTSConfig(projectPath: string): Promise<TSConfig> {
const tsconfigFile = await fs.readFile(join(projectPath, 'tsconfig.json'));


if (!tsconfigFile) {
logger.debug('Project tsconfig.json file not found - ignoring');

return {};
}

try {
const a = JSON.parse(tsconfigFile.toString()) as TSConfig;
return a;
} catch {
logger.warn('Invalid tsconfig.json file - ignoring');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, let's not ignore the file if it throws any error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to ignore the tsconfig file if it is invalid, we can't risk breaking backward compatibility


return {};
}
}

function getAppInfo(projectFiles: ICompilerFile[]): IAppInfo {
const appJson = projectFiles.find((file: ICompilerFile) => file.name === 'app.json');

if (!appJson) {
throw new Error('There is no app.json file in the project');
throw new Error('There is no app.json file in the folder - is this a Rocket.Chat App project?');
}

try {
return JSON.parse(appJson.content) as IAppInfo;
} catch (error) {
throw new Error('app.json parsing fail');
throw new Error('Error attempting to parse app.json');
}
}

function getTypescriptFilesFromProject(projectFiles: ICompilerFile[]): ICompilerFile[] {
return projectFiles.filter((file: ICompilerFile) => file.name.endsWith('.ts'));
}

export async function getAppSource(path: string): Promise<IAppSource> {
const directoryWalkData: ICompilerFile[] = await walkDirectory(path);
export async function getAppSource(path: string, appCompilerOptions: AppCompilerOptions = {}): Promise<IAppSource> {
const { compilerOptions = null, exclude = [] } = appCompilerOptions.readTsProjectFile ? await getTSConfig(path) : {};

const directoryWalkData: ICompilerFile[] = await walkDirectory(path, exclude);
const projectFiles: ICompilerFile[] = filterProjectFiles(path, directoryWalkData);
const tsFiles: ICompilerFile[] = getTypescriptFilesFromProject(projectFiles);
const appInfo: IAppInfo = getAppInfo(projectFiles);
const files: IMapCompilerFile = makeICompilerFileMap(tsFiles);

return { appInfo, sourceFiles: files };
return { appInfo, sourceFiles: files, compilerOptions };
}
2 changes: 2 additions & 0 deletions src/definition/IAppSource.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { CompilerOptions } from 'typescript';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';

import { ICompilerFile } from './ICompilerFile';

export interface IAppSource {
appInfo: IAppInfo;
sourceFiles: { [filename: string]: ICompilerFile };
compilerOptions?: CompilerOptions;
}