-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: move code to classes, add DI and unit tests (stage 2) (#3)
* refactor: add unit tests and DI (stage 2) * fix: lint * fix: adapt changes after linter * fix: cleanup * feat: add test helper to register injected dependencies * chore: remove unused package * feat: change DI library, add FileSystemHelper tests * refactor: no spy objects creation * refactor: move compile functionality to Compiler class * test: add unit tests for FileSystemHelper * refactor: move classes to lib sub-directory * refactor: remove spyOn substitution on Messages class in FileSystemHelper tests * test: add unit test for the Compiler class * fix: cleanup * refactor: move Compiler class to DI, read Config via Provider abstraction * feat: add config cache to Compiler class * feat: change ci/cd to check all aspects * feat: print skip logs, fix ro mode * feat: add Styler class, revert read-only build mode * docs: force update image * chore: move to mocha test engine * feat: print overall error message, cleanup * 1.1.0
- Loading branch information
Showing
28 changed files
with
4,605 additions
and
332 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
node_modules/ | ||
dist/ | ||
dist/ | ||
.nyc_output/ | ||
coverage/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,40 @@ | ||
import "reflect-metadata"; | ||
|
||
import { Container } from "inversify"; | ||
|
||
import { Compiler, ICompiler } from "./lib/classes/Compiler"; | ||
import { ChildProcessWrapper, IChildProcessWrapper } from "./lib/classes/ChildProcessWrapper"; | ||
import { FileSystemHelper, IFileSystemHelper } from "./lib/classes/FileSystemHelper"; | ||
import { IFileSystemWrapper, NodeFS } from "./lib/classes/FileSystemWrapper"; | ||
import { ILogger, Logger } from "./lib/classes/Logger"; | ||
import { Main } from "./lib/classes/Main"; | ||
import { IStyler, Styler } from "./lib/classes/Styler"; | ||
|
||
import { DI_TOKENS } from "./lib/tokens"; | ||
import { Config, ConfigProvider } from "./lib/interfaces"; | ||
import { DEFAULTS } from "./lib/constants"; | ||
import { copyAssets, flush, readConfig } from "./lib/fs"; | ||
import { compile } from "./lib/compile"; | ||
import { buildTailwind } from "./lib/style"; | ||
import { Messages } from "./lib/classes/Messages"; | ||
import { MessagesEnum } from "./messages"; | ||
|
||
readConfig(DEFAULTS.CONFIG) | ||
.then(async (config) => { | ||
await flush(DEFAULTS.DIST); | ||
await compile(config); | ||
await buildTailwind(config); | ||
await copyAssets(`${DEFAULTS.THEMES}/${config.theme}/${DEFAULTS.ASSETS}`, `${DEFAULTS.DIST}/${DEFAULTS.ASSETS}`); | ||
}) | ||
.catch((err) => { | ||
console.error(` | ||
An error happened during compile process. Please, check 'README.md' to get more details about calling this process. | ||
// Register DI | ||
const runContainer = new Container({ defaultScope: "Singleton" }); | ||
runContainer.bind<ICompiler>(DI_TOKENS.COMPILER).to(Compiler); | ||
runContainer.bind<IChildProcessWrapper>(DI_TOKENS.CHILD_PROCESS).to(ChildProcessWrapper); | ||
runContainer.bind<IFileSystemHelper>(DI_TOKENS.FS_HELPER).to(FileSystemHelper); | ||
runContainer.bind<IFileSystemWrapper>(DI_TOKENS.FS).to(NodeFS); | ||
runContainer.bind<ILogger>(DI_TOKENS.LOGGER).to(Logger); | ||
runContainer.bind<IStyler>(DI_TOKENS.STYLER).to(Styler); | ||
|
||
Error Message: | ||
${err.message} | ||
runContainer.bind<ConfigProvider>(DI_TOKENS.CONFIG_PROVIDER).toProvider<Config>((ctx) => { | ||
return () => { | ||
const fsHelper = ctx.container.get<IFileSystemHelper>(DI_TOKENS.FS_HELPER); | ||
return fsHelper.readConfig(DEFAULTS.CONFIG); | ||
}; | ||
}); | ||
|
||
Error Stack: | ||
${err.stack} | ||
`); | ||
runContainer | ||
.resolve(Main) | ||
.start() | ||
.catch((err) => { | ||
runContainer.get<ILogger>(DI_TOKENS.LOGGER).print(Messages.error(MessagesEnum.OVERALL, err)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { exec } from "child_process"; | ||
import { injectable } from "inversify"; | ||
import { promisify } from "util"; | ||
|
||
export interface IChildProcessWrapper { | ||
exec(cmd: string): Promise<{ stdout: string; stderr: string }>; | ||
} | ||
|
||
@injectable() | ||
export class ChildProcessWrapper implements IChildProcessWrapper { | ||
exec(cmd: string) { | ||
return promisify(exec)(cmd); | ||
} | ||
} | ||
|
||
@injectable() | ||
export class MockChildProcessWrapper implements IChildProcessWrapper { | ||
async exec() { | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { injectable, inject } from "inversify"; | ||
import { PackageId } from "typescript"; | ||
|
||
import { IFileSystemHelper } from "./FileSystemHelper"; | ||
import { ILogger } from "./Logger"; | ||
import { Messages } from "./Messages"; | ||
import { Renderer } from "./Renderer"; | ||
|
||
import { DEFAULTS } from "../constants"; | ||
import { Config, ConfigProvider, TemplateVariables } from "../interfaces"; | ||
import { DI_TOKENS } from "../tokens"; | ||
import { MessagesEnum } from "../../messages"; | ||
|
||
export const SRC_CODE_PATTERN = /^[0-9]{3}(?=\.json$)/i; | ||
|
||
export interface ICompiler { | ||
initTemplateVariables(): Promise<TemplateVariables>; | ||
getStatusList(): Promise<Set<number>>; | ||
makePages(): Promise<void>; | ||
makeConfigs(): Promise<void>; | ||
} | ||
|
||
@injectable() | ||
export class Compiler implements ICompiler { | ||
private config: Config; | ||
private statusList: Set<number> = new Set(); | ||
|
||
constructor( | ||
@inject(DI_TOKENS.CONFIG_PROVIDER) private configProvider: ConfigProvider, | ||
@inject(DI_TOKENS.FS_HELPER) private fsHelper: IFileSystemHelper, | ||
@inject(DI_TOKENS.LOGGER) private logger: ILogger | ||
) {} | ||
|
||
async initTemplateVariables(): Promise<TemplateVariables> { | ||
const config = await this.getConfig(); | ||
|
||
const pkg = await this.fsHelper.readJson<PackageId>(DEFAULTS.PACKAGE); | ||
return { | ||
locale: config.locale, | ||
version: pkg.version, | ||
}; | ||
} | ||
|
||
async getConfig(): Promise<Config> { | ||
if (!this.config) { | ||
this.config = await this.configProvider(); | ||
} | ||
return this.config; | ||
} | ||
|
||
async getStatusList(): Promise<Set<number>> { | ||
const config = await this.getConfig(); | ||
|
||
if (this.statusList.size === 0) { | ||
await this.fsHelper.readDir(`${DEFAULTS.SRC}/${config.locale}/`).then((files) => { | ||
files.forEach((file) => { | ||
const match = file.match(SRC_CODE_PATTERN); | ||
if (match) { | ||
this.statusList.add(Number(match[0])); | ||
} | ||
}); | ||
}); | ||
} | ||
return this.statusList; | ||
} | ||
|
||
async makePages(): Promise<void> { | ||
const config = await this.getConfig(); | ||
|
||
this.logger.print(Messages.info(MessagesEnum.COMPILE_PAGES)); | ||
const list = await this.getStatusList(); | ||
if (list.size > 0) { | ||
const initVars = await this.initTemplateVariables(); | ||
const commonVars = await this.fsHelper.readJson<TemplateVariables>(`${DEFAULTS.SRC}/${config.locale}/common.json`); | ||
const template = await this.fsHelper.readFile(`${DEFAULTS.THEMES}/${config.theme}/template.html`); | ||
|
||
await Promise.all( | ||
Array.from(list).map(async (code) => { | ||
const statusVars = await this.fsHelper.readJson<TemplateVariables>(`${DEFAULTS.SRC}/${config.locale}/${code}.json`); | ||
const path = `${DEFAULTS.DIST}/${code}.html`; | ||
|
||
this.logger.print(Messages.list(path)); | ||
|
||
await this.fsHelper.writeFile(path, Renderer.renderTemplate(template, { ...initVars, ...commonVars, ...statusVars, code })); | ||
}) | ||
); | ||
} else { | ||
throw new Error(Messages.text(MessagesEnum.NO_SOURCE_DATA)); | ||
} | ||
} | ||
|
||
async makeConfigs(): Promise<void> { | ||
this.logger.print(Messages.info(MessagesEnum.COMPILE_CONFIGS)); | ||
const list = await this.getStatusList(); | ||
if (list.size > 0) { | ||
const snippets = await this.fsHelper.readDir(`${DEFAULTS.SNIPPETS}/`); | ||
|
||
await Promise.all( | ||
snippets.map(async (snippet) => { | ||
const path = `${DEFAULTS.DIST}/${snippet}`; | ||
|
||
this.logger.print(Messages.list(path)); | ||
|
||
const template = await this.fsHelper.readFile(`${DEFAULTS.SNIPPETS}/${snippet}`); | ||
await this.fsHelper.writeFile(path, Renderer.renderTemplate(template, { codes: Array.from(list) })); | ||
}) | ||
); | ||
} else { | ||
throw new Error(Messages.text(MessagesEnum.NO_SOURCE_DATA)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { injectable, inject } from "inversify"; | ||
|
||
import { Config } from "../interfaces"; | ||
import { Messages } from "./Messages"; | ||
|
||
import { MessagesEnum } from "../../messages"; | ||
import { MANDATORY_CONFIG_PROPS } from "../constants"; | ||
import { ILogger } from "./Logger"; | ||
import { DI_TOKENS } from "../tokens"; | ||
import { IFileSystemWrapper } from "./FileSystemWrapper"; | ||
import { Styler } from "./Styler"; | ||
|
||
export interface IFileSystemHelper { | ||
copyAssets(src: string, dest: string): Promise<void>; | ||
ensure(path: string): Promise<boolean>; | ||
flush(path: string): Promise<void>; | ||
readDir(path: string): Promise<string[]>; | ||
readFile(path: string): Promise<string>; | ||
readJson<T>(path: string): Promise<T>; | ||
readConfig(path: string): Promise<Config>; | ||
writeFile(path: string, data: string): Promise<void>; | ||
} | ||
|
||
@injectable() | ||
export class FileSystemHelper implements IFileSystemHelper { | ||
constructor(@inject(DI_TOKENS.FS) private fs: IFileSystemWrapper, @inject(DI_TOKENS.LOGGER) private logger: ILogger) {} | ||
|
||
async copyAssets(src: string, dest: string): Promise<void> { | ||
if (await this.ensure(src)) { | ||
this.logger.print(Messages.info(MessagesEnum.COPYING_ASSETS)); | ||
await this.fs.cp(src, dest, { | ||
recursive: true, | ||
// TODO: add Logger.print() to filter function | ||
filter: Styler.sourceStyleFilter, | ||
}); | ||
// TODO: IMPROVEMENT: return list of copied files to add into web server configs as fixed locations | ||
} else { | ||
this.logger.print(Messages.warn(MessagesEnum.NO_ASSETS_TO_COPY)); | ||
} | ||
} | ||
|
||
async ensure(path: string): Promise<boolean> { | ||
try { | ||
await this.fs.access(path); | ||
return true; | ||
} catch (_) { | ||
return false; | ||
} | ||
} | ||
|
||
async flush(path: string): Promise<void> { | ||
this.logger.print(Messages.info(MessagesEnum.FLUSH_DESTINATION, { path })); | ||
|
||
await this.fs.rm(path, { force: true, recursive: true }); | ||
await this.fs.mkdir(path, { recursive: true }); | ||
} | ||
|
||
async readDir(path: string): Promise<string[]> { | ||
if (await this.ensure(path)) { | ||
return await this.fs.readDir(path); | ||
} else { | ||
throw new Error(Messages.text(MessagesEnum.NO_DIRECTORY, { path })); | ||
} | ||
} | ||
|
||
async readFile(path: string): Promise<string> { | ||
return await this.fs.readFile(path).then(String); | ||
} | ||
|
||
async readJson<T>(path: string): Promise<T> { | ||
if (await this.ensure(path)) { | ||
return await this.readFile(path).then(JSON.parse); | ||
} | ||
return {} as T; | ||
} | ||
|
||
async readConfig(path: string): Promise<Config> { | ||
const config = await this.readJson<Config>(path); | ||
|
||
// Check mandatory config properties | ||
MANDATORY_CONFIG_PROPS.forEach((prop) => { | ||
if (config[prop] === undefined) { | ||
throw new Error(Messages.text(MessagesEnum.NO_CONFIG_PROPERTY, { prop, path })); | ||
} | ||
}); | ||
|
||
return config; | ||
} | ||
|
||
async writeFile(path: string, data: string, opts = { flag: "w+" }): Promise<void> { | ||
return await this.fs.writeFile(path, data, opts); | ||
} | ||
} | ||
|
||
@injectable() | ||
export class MockFileSystemHelper implements IFileSystemHelper { | ||
async copyAssets() { | ||
return null; | ||
} | ||
|
||
async ensure() { | ||
return true; | ||
} | ||
|
||
async flush() { | ||
return null; | ||
} | ||
|
||
async readDir() { | ||
return [""]; | ||
} | ||
|
||
async readFile() { | ||
return ""; | ||
} | ||
|
||
async readJson<T>() { | ||
return {} as T; | ||
} | ||
|
||
async readConfig() { | ||
return {} as Config; | ||
} | ||
|
||
async writeFile() { | ||
return null; | ||
} | ||
} |
Oops, something went wrong.