@rocket.chat/apps-compiler
Advanced tools
Comparing version 0.1.7 to 0.2.0
import * as fallbackTypescript from 'typescript'; | ||
import { ModuleResolutionHost, ResolvedModule } from 'typescript'; | ||
import { Omit } from './misc/util'; | ||
import { ICompilerDescriptor, ICompilerFile, ICompilerResult, IFiles } from './definition'; | ||
declare type TypeScript = typeof fallbackTypescript; | ||
import { IBundledCompilerResult, ICompilerDescriptor, ICompilerResult } from './definition'; | ||
export declare type TypeScript = typeof fallbackTypescript; | ||
export declare class AppsCompiler { | ||
private readonly compilerDesc; | ||
private readonly ts; | ||
private readonly compilerOptions; | ||
private libraryFiles; | ||
private compiled; | ||
private implemented; | ||
private wd; | ||
private _appRequire; | ||
constructor(compilerDesc: ICompilerDescriptor, ts?: TypeScript); | ||
readonly appRequire: NodeRequire; | ||
compile(path: string): Promise<ICompilerResult>; | ||
output(): IFiles; | ||
getImplemented(): string[]; | ||
private readonly sourcePath; | ||
private compilationResult?; | ||
private readonly bundler; | ||
private readonly validator; | ||
private readonly typescriptCompiler; | ||
constructor(compilerDesc: ICompilerDescriptor, sourcePath: string, ts?: TypeScript); | ||
getLatestCompilationResult(): ICompilerResult; | ||
run(outputPath: string): Promise<Buffer>; | ||
compile(): Promise<ICompilerResult>; | ||
bundle(): Promise<IBundledCompilerResult>; | ||
outputZip(outputPath: string): Promise<Buffer>; | ||
private validateAppPermissionsSchema; | ||
private toJs; | ||
private normalizeDiagnostics; | ||
resolvePath(containingFile: string, moduleName: string, cwd: string): string; | ||
resolver(moduleName: string, resolvedModules: Array<ResolvedModule>, containingFile: string, result: Omit<ICompilerResult, 'permissions'>, cwd: string, moduleResHost: ModuleResolutionHost): number; | ||
getLibraryFile(fileName: string): ICompilerFile; | ||
private checkInheritance; | ||
private requireCompiled; | ||
private isValidFile; | ||
} | ||
export {}; |
@@ -11,58 +11,37 @@ "use strict"; | ||
const fs = __importStar(require("fs")); | ||
const vm = __importStar(require("vm")); | ||
const path = __importStar(require("path")); | ||
const fallbackTypescript = __importStar(require("typescript")); | ||
const module_1 = require("module"); | ||
const getAppSource_1 = require("./compiler/getAppSource"); | ||
const Utilities_1 = require("./misc/Utilities"); | ||
const folderDetails_1 = require("./misc/folderDetails"); | ||
const appPackager_1 = require("./misc/appPackager"); | ||
const getAvailablePermissions_1 = require("./misc/getAvailablePermissions"); | ||
const packager_1 = require("./packager"); | ||
const TypescriptCompiler_1 = require("./compiler/TypescriptCompiler"); | ||
const AppsEngineValidator_1 = require("./compiler/AppsEngineValidator"); | ||
const bundler_1 = __importStar(require("./bundler")); | ||
class AppsCompiler { | ||
constructor(compilerDesc, ts = fallbackTypescript) { | ||
constructor(compilerDesc, sourcePath, ts = fallbackTypescript) { | ||
this.compilerDesc = compilerDesc; | ||
this.ts = ts; | ||
this.compilerOptions = { | ||
target: this.ts.ScriptTarget.ES2017, | ||
module: this.ts.ModuleKind.CommonJS, | ||
moduleResolution: this.ts.ModuleResolutionKind.NodeJs, | ||
declaration: false, | ||
noImplicitAny: false, | ||
removeComments: true, | ||
strictNullChecks: true, | ||
noImplicitReturns: true, | ||
emitDecoratorMetadata: true, | ||
experimentalDecorators: true, | ||
types: ['node'], | ||
traceResolution: false, | ||
}; | ||
this.libraryFiles = {}; | ||
this.sourcePath = sourcePath; | ||
this.validator = new AppsEngineValidator_1.AppsEngineValidator(module_1.createRequire(`${sourcePath}/app.json`)); | ||
this.typescriptCompiler = new TypescriptCompiler_1.TypescriptCompiler(sourcePath, ts, this.validator); | ||
this.bundler = bundler_1.default(bundler_1.AvailableBundlers.esbuild); | ||
} | ||
get appRequire() { | ||
return this._appRequire; | ||
getLatestCompilationResult() { | ||
return this.compilationResult; | ||
} | ||
async compile(path) { | ||
this.wd = path; | ||
this._appRequire = module_1.createRequire(`${path}/app.json`); | ||
const source = await getAppSource_1.getAppSource(path); | ||
this.validateAppPermissionsSchema(source.appInfo.permissions); | ||
const compilerResult = this.toJs(source); | ||
const { files, implemented } = compilerResult; | ||
const { permissions } = source.appInfo; | ||
this.validateAppPermissionsSchema(permissions); | ||
this.compiled = Object.entries(files) | ||
.map(([, { name, compiled }]) => ({ [name]: compiled })) | ||
.reduce((acc, cur) => Object.assign(acc, cur), {}); | ||
this.implemented = implemented; | ||
this.checkInheritance(source.appInfo.classFile.replace(/\.ts$/, '')); | ||
return Object.assign(compilerResult, { permissions }); | ||
async run(outputPath) { | ||
await this.compile(); | ||
await this.bundle(); | ||
return this.outputZip(outputPath); | ||
} | ||
output() { | ||
return this.compiled; | ||
async compile() { | ||
const source = await getAppSource_1.getAppSource(this.sourcePath); | ||
this.compilationResult = this.typescriptCompiler.transpileSource(source); | ||
return this.getLatestCompilationResult(); | ||
} | ||
getImplemented() { | ||
return this.implemented; | ||
async bundle() { | ||
this.compilationResult = await this.bundler(this.getLatestCompilationResult(), this.validator); | ||
return this.getLatestCompilationResult(); | ||
} | ||
async outputZip(outputPath) { | ||
const fd = new folderDetails_1.FolderDetails(this.wd); | ||
const fd = new folderDetails_1.FolderDetails(this.sourcePath); | ||
try { | ||
@@ -75,251 +54,10 @@ await fd.readInfoFile(); | ||
} | ||
const packager = new appPackager_1.AppPackager(this.compilerDesc, fd, this, outputPath); | ||
const compilationResult = this.getLatestCompilationResult(); | ||
if (!compilationResult) { | ||
throw new Error('No compilation data found'); | ||
} | ||
const packager = new packager_1.AppPackager(this.compilerDesc, fd, compilationResult, outputPath); | ||
return fs.promises.readFile(await packager.zipItUp()); | ||
} | ||
validateAppPermissionsSchema(permissions) { | ||
if (!permissions) { | ||
return; | ||
} | ||
if (!Array.isArray(permissions)) { | ||
throw new Error('Invalid permission definition. Check your manifest file.'); | ||
} | ||
const permissionsRequire = this.appRequire('@rocket.chat/apps-engine/server/permissions/AppPermissions'); | ||
if (!permissionsRequire || !permissionsRequire.AppPermissions) { | ||
return; | ||
} | ||
const availablePermissions = getAvailablePermissions_1.getAvailablePermissions(permissionsRequire.AppPermissions); | ||
permissions.forEach((permission) => { | ||
if (permission && !availablePermissions.includes(permission.name)) { | ||
throw new Error(`Invalid permission "${String(permission.name)}" defined. Check your manifest file`); | ||
} | ||
}); | ||
} | ||
toJs({ appInfo, sourceFiles: files }) { | ||
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.`); | ||
} | ||
const startTime = Date.now(); | ||
const result = { | ||
files, | ||
implemented: [], | ||
diagnostics: [], | ||
duration: NaN, | ||
name: appInfo.name, | ||
version: appInfo.version, | ||
typeScriptVersion: this.ts.version, | ||
}; | ||
Object.keys(result.files).forEach((key) => { | ||
if (!this.isValidFile(result.files[key])) { | ||
throw new Error(`Invalid TypeScript file: "${key}".`); | ||
} | ||
result.files[key].name = path.normalize(result.files[key].name); | ||
}); | ||
const modulesNotFound = []; | ||
const host = { | ||
getScriptFileNames: () => Object.keys(result.files), | ||
getScriptVersion: (fileName) => { | ||
fileName = path.normalize(fileName); | ||
const file = result.files[fileName] || this.getLibraryFile(fileName); | ||
return file && file.version.toString(); | ||
}, | ||
getScriptSnapshot: (fileName) => { | ||
fileName = path.normalize(fileName); | ||
const file = result.files[fileName] || this.getLibraryFile(fileName); | ||
if (!file || !file.content) { | ||
return; | ||
} | ||
return this.ts.ScriptSnapshot.fromString(file.content); | ||
}, | ||
getCompilationSettings: () => this.compilerOptions, | ||
getCurrentDirectory: () => this.wd, | ||
getDefaultLibFileName: () => this.ts.getDefaultLibFilePath(this.compilerOptions), | ||
fileExists: (fileName) => this.ts.sys.fileExists(fileName), | ||
readFile: (fileName) => this.ts.sys.readFile(fileName), | ||
resolveModuleNames: (moduleNames, containingFile) => { | ||
const resolvedModules = []; | ||
const moduleResHost = { | ||
fileExists: host.fileExists, readFile: host.readFile, trace: (traceDetail) => console.log(traceDetail), | ||
}; | ||
for (const moduleName of moduleNames) { | ||
const index = this.resolver(moduleName, resolvedModules, containingFile, result, this.wd, moduleResHost); | ||
if (index === -1) { | ||
modulesNotFound.push({ | ||
filename: containingFile, | ||
line: 0, | ||
character: 0, | ||
lineText: '', | ||
message: `Failed to resolve module: ${moduleName}`, | ||
originalMessage: `Module not found: ${moduleName}`, | ||
originalDiagnostic: undefined, | ||
}); | ||
} | ||
} | ||
return resolvedModules; | ||
}, | ||
}; | ||
const languageService = this.ts.createLanguageService(host, this.ts.createDocumentRegistry()); | ||
try { | ||
const coDiag = languageService.getCompilerOptionsDiagnostics(); | ||
if (coDiag.length !== 0) { | ||
console.log(coDiag); | ||
console.error('A VERY UNEXPECTED ERROR HAPPENED THAT SHOULD NOT!'); | ||
throw new Error(`Language Service's Compiler Options Diagnostics contains ${coDiag.length} diagnostics.`); | ||
} | ||
} | ||
catch (e) { | ||
if (modulesNotFound.length !== 0) { | ||
result.diagnostics = modulesNotFound; | ||
result.duration = Date.now() - startTime; | ||
return result; | ||
} | ||
throw e; | ||
} | ||
const src = languageService.getProgram().getSourceFile(appInfo.classFile); | ||
this.ts.forEachChild(src, (n) => { | ||
if (!this.ts.isClassDeclaration(n)) | ||
return; | ||
this.ts.forEachChild(n, (node) => { | ||
if (this.ts.isHeritageClause(node)) { | ||
const e = node; | ||
this.ts.forEachChild(node, (nn) => { | ||
if (e.token === this.ts.SyntaxKind.ImplementsKeyword) { | ||
result.implemented.push(nn.getText()); | ||
} | ||
}); | ||
} | ||
}); | ||
}); | ||
Object.defineProperty(result, 'diagnostics', { | ||
value: this.normalizeDiagnostics(this.ts.getPreEmitDiagnostics(languageService.getProgram())), | ||
configurable: false, | ||
writable: false, | ||
}); | ||
Object.keys(result.files).forEach((key) => { | ||
const file = result.files[key]; | ||
const output = languageService.getEmitOutput(file.name); | ||
file.name = key.replace(/\.ts/g, '.js'); | ||
delete result.files[key]; | ||
result.files[file.name] = file; | ||
file.compiled = output.outputFiles[0].text; | ||
}); | ||
result.duration = Date.now() - startTime; | ||
return result; | ||
} | ||
normalizeDiagnostics(diagnostics) { | ||
return diagnostics.map((diag) => { | ||
const message = this.ts.flattenDiagnosticMessageText(diag.messageText, '\n'); | ||
const norm = { | ||
originalDiagnostic: diag, | ||
originalMessage: message, | ||
message, | ||
}; | ||
Object.defineProperties(norm, { | ||
originalDiagnostic: { enumerable: false }, | ||
}); | ||
if (diag.file) { | ||
const { line, character } = diag.file.getLineAndCharacterOfPosition(diag.start); | ||
const lineStart = diag.file.getPositionOfLineAndCharacter(line, 0); | ||
Object.assign(norm, { | ||
filename: diag.file.fileName, | ||
line, | ||
character, | ||
lineText: diag.file.getText().substring(lineStart, diag.file.getLineEndOfPosition(lineStart)), | ||
message: `Error ${diag.file.fileName} (${line + 1},${character + 1}): ${message}`, | ||
}); | ||
} | ||
return norm; | ||
}); | ||
} | ||
resolvePath(containingFile, moduleName, cwd) { | ||
const currentFolderPath = path.dirname(containingFile).replace(cwd.replace(/\/$/, ''), ''); | ||
const modulePath = path.join(currentFolderPath, moduleName); | ||
const transformedModule = Utilities_1.Utilities.transformModuleForCustomRequire(modulePath); | ||
if (transformedModule) { | ||
return transformedModule; | ||
} | ||
} | ||
resolver(moduleName, resolvedModules, containingFile, result, cwd, moduleResHost) { | ||
moduleName = moduleName.replace(/@rocket.chat\/apps-ts-definition\//, '@rocket.chat/apps-engine/definition/'); | ||
if (/node_modules\/@types\/node\/\S+\.d\.ts$/.test(containingFile)) { | ||
return resolvedModules.push(undefined); | ||
} | ||
if (Utilities_1.Utilities.allowedInternalModuleRequire(moduleName)) { | ||
return resolvedModules.push({ resolvedFileName: `${moduleName}.js` }); | ||
} | ||
const resolvedPath = this.resolvePath(containingFile, moduleName, cwd); | ||
if (result.files[resolvedPath]) { | ||
return resolvedModules.push({ resolvedFileName: resolvedPath }); | ||
} | ||
const rs = this.ts.resolveModuleName(moduleName, containingFile, this.compilerOptions, moduleResHost); | ||
if (rs.resolvedModule) { | ||
return resolvedModules.push(rs.resolvedModule); | ||
} | ||
return -1; | ||
} | ||
getLibraryFile(fileName) { | ||
if (!fileName.endsWith('.d.ts')) { | ||
return undefined; | ||
} | ||
const norm = path.normalize(fileName); | ||
if (this.libraryFiles[norm]) { | ||
return this.libraryFiles[norm]; | ||
} | ||
if (!fs.existsSync(fileName)) { | ||
return undefined; | ||
} | ||
this.libraryFiles[norm] = { | ||
name: norm, | ||
content: fs.readFileSync(fileName).toString(), | ||
version: 0, | ||
}; | ||
return this.libraryFiles[norm]; | ||
} | ||
checkInheritance(mainClassFile) { | ||
const { App: EngineBaseApp } = this.appRequire('@rocket.chat/apps-engine/definition/App'); | ||
const mainClassModule = this.requireCompiled(mainClassFile); | ||
if (!mainClassModule.default && !mainClassModule[mainClassFile]) { | ||
throw new Error(`There must be an exported class "${mainClassFile}" or a default export in the main class file.`); | ||
} | ||
const RealApp = mainClassModule.default ? mainClassModule.default : mainClassModule[mainClassFile]; | ||
const mockInfo = { name: '', requiredApiVersion: '', author: { name: '' } }; | ||
const mockLogger = { debug: () => { } }; | ||
const realApp = new RealApp(mockInfo, mockLogger); | ||
if (!(realApp instanceof EngineBaseApp)) { | ||
throw new Error('App must extend apps-engine\'s "App" abstract class.' | ||
+ ' Maybe you forgot to install dependencies? Try running `npm install`' | ||
+ ' in your app folder to fix it.'); | ||
} | ||
} | ||
requireCompiled(filename) { | ||
const exports = {}; | ||
const context = vm.createContext({ | ||
require: (filepath) => { | ||
if (filepath.startsWith('@rocket.chat/apps-engine/definition/')) { | ||
return require(`${this.wd}/node_modules/${filepath}`); | ||
} | ||
if (Utilities_1.Utilities.allowedInternalModuleRequire(filepath)) { | ||
return require(filepath); | ||
} | ||
if (!filepath.startsWith('.')) { | ||
return undefined; | ||
} | ||
filepath = path.normalize(`${path.dirname(filename)}/${filepath}`); | ||
if (this.compiled[filepath.endsWith('.js') ? filepath : `${filepath}.js`]) { | ||
return this.requireCompiled(filepath); | ||
} | ||
}, | ||
exports, | ||
}); | ||
vm.runInContext(this.compiled[`${filename}.js`], context); | ||
return exports; | ||
} | ||
isValidFile(file) { | ||
if (!file || !file.name || !file.content) { | ||
return false; | ||
} | ||
return file.name.trim() !== '' | ||
&& path.normalize(file.name) | ||
&& file.content.trim() !== ''; | ||
} | ||
} | ||
exports.AppsCompiler = AppsCompiler; |
@@ -40,5 +40,5 @@ "use strict"; | ||
try { | ||
const compiler = new _1.AppsCompiler(compilerDesc, appTs); | ||
const compiler = new _1.AppsCompiler(compilerDesc, sourceDir, appTs); | ||
log.debug('Starting compilation...'); | ||
const result = await compiler.compile(sourceDir); | ||
const result = await compiler.compile(); | ||
if (result.diagnostics.length) { | ||
@@ -48,6 +48,9 @@ return result; | ||
log.debug('Compilation complete, inspection \n', util_1.inspect(result)); | ||
log.debug('Starting bundling...'); | ||
await compiler.bundle(); | ||
log.debug('Compilation complete, inspection \n', util_1.inspect(compiler.getLatestCompilationResult())); | ||
log.debug('Starting packaging...'); | ||
await compiler.outputZip(outputFile); | ||
log.info(`Compilation successful! Took ${result.duration / 1000}s. Package saved at `, outputFile); | ||
return result; | ||
return compiler.getLatestCompilationResult(); | ||
} | ||
@@ -54,0 +57,0 @@ catch (error) { |
@@ -5,7 +5,4 @@ "use strict"; | ||
const path_1 = require("path"); | ||
const util_1 = require("util"); | ||
const readdirPromise = util_1.promisify(fs_1.readdir); | ||
const readfilePromise = util_1.promisify(fs_1.readFile); | ||
async function walkDirectory(directory) { | ||
const dirents = await readdirPromise(directory, { withFileTypes: true }); | ||
const dirents = await fs_1.promises.readdir(directory, { withFileTypes: true }); | ||
const files = await Promise.all(dirents | ||
@@ -21,3 +18,3 @@ .map(async (dirent) => { | ||
} | ||
const content = await readfilePromise(res, 'utf8'); | ||
const content = await fs_1.promises.readFile(res, 'utf8'); | ||
return { | ||
@@ -32,10 +29,6 @@ content, | ||
} | ||
function truncateFilename(fileName, projectDirectory) { | ||
return path_1.normalize(fileName).substring(projectDirectory.length + 1); | ||
} | ||
function filterProjectFiles(projectDirectory, directoryWalkData) { | ||
return directoryWalkData | ||
.filter((file) => file) | ||
.map((file) => ({ ...file, name: truncateFilename(file.name, projectDirectory) })) | ||
.filter((file) => !file.name.startsWith('.')); | ||
.filter((file) => file && !file.name.startsWith('.')) | ||
.map((file) => ({ ...file, name: path_1.relative(projectDirectory, file.name) })); | ||
} | ||
@@ -48,3 +41,3 @@ function makeICompilerFileMap(compilerFiles) { | ||
function getAppInfo(projectFiles) { | ||
const appJson = projectFiles.filter((file) => file.name === 'app.json')[0]; | ||
const appJson = projectFiles.find((file) => file.name === 'app.json'); | ||
if (!appJson) { | ||
@@ -51,0 +44,0 @@ throw new Error('There is no app.json file in the project'); |
@@ -8,2 +8,3 @@ import { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; | ||
}; | ||
mainFile?: ICompilerFile; | ||
implemented: Array<string>; | ||
@@ -15,3 +16,6 @@ diagnostics: Array<ICompilerDiagnostic>; | ||
typeScriptVersion: string; | ||
permissions: Array<IPermission>; | ||
permissions?: Array<IPermission>; | ||
} | ||
export interface IBundledCompilerResult extends ICompilerResult { | ||
bundle: string; | ||
} |
@@ -6,6 +6,6 @@ import { IAppSource } from './IAppSource'; | ||
import { ICompilerFile } from './ICompilerFile'; | ||
import { ICompilerResult } from './ICompilerResult'; | ||
import { IFiles } from './IFiles'; | ||
import { IMapCompilerFile } from './IMapCompilerFile'; | ||
import { CompilerFileNotFoundError } from './CompilerFileNotFoundError'; | ||
export { CompilerFileNotFoundError, IAppSource, ICompilerDescriptor, ICompilerDiagnostic, ICompilerError, ICompilerFile, IFiles, IMapCompilerFile, ICompilerResult, }; | ||
export { CompilerFileNotFoundError, IAppSource, ICompilerDescriptor, ICompilerDiagnostic, ICompilerError, ICompilerFile, IFiles, IMapCompilerFile, }; | ||
export * from './ICompilerResult'; |
{ | ||
"name": "@rocket.chat/apps-compiler", | ||
"version": "0.1.7", | ||
"version": "0.2.0", | ||
"description": "The Rocket.Chat apps compiler", | ||
@@ -53,2 +53,3 @@ "main": "index.js", | ||
"@rocket.chat/apps-engine": "^1.22.2", | ||
"esbuild": "^0.12.16", | ||
"figures": "^3.0.0", | ||
@@ -55,0 +56,0 @@ "fs-extra": "^8.1.0", |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
54646
55
1266
11
10
+ Addedesbuild@^0.12.16
+ Addedesbuild@0.12.29(transitive)