From 79b0bd78d5c6973c78867776f03960c8a1baa736 Mon Sep 17 00:00:00 2001 From: crusader Date: Tue, 1 Dec 2020 20:35:56 +0900 Subject: [PATCH] project initialized --- .eslintignore | 2 + .eslintrc.json | 3 + .gitignore | 4 + .npmignore | 4 + .prettierrc.js | 3 + .vscode/settings.json | 14 + package.json | 33 + projects/notification-electron/package.json | 7 + projects/notification-electron/project.json | 6 + .../src/lib/services/notification.service.ts | 651 ++++++++++++++++++ .../src/lib/types/channel.type.ts | 11 + .../src/lib/utils/animation.queue.ts | 55 ++ .../notification-electron/src/public-api.ts | 5 + .../notification-electron/tsconfig.lib.json | 9 + .../tsconfig.lib.prod.json | 3 + .../notification-electron/tsconfig.spec.json | 9 + scripts/build.ts | 148 ++++ tsconfig.json | 8 + 18 files changed, 975 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .prettierrc.js create mode 100644 .vscode/settings.json create mode 100644 package.json create mode 100644 projects/notification-electron/package.json create mode 100644 projects/notification-electron/project.json create mode 100644 projects/notification-electron/src/lib/services/notification.service.ts create mode 100644 projects/notification-electron/src/lib/types/channel.type.ts create mode 100644 projects/notification-electron/src/lib/utils/animation.queue.ts create mode 100644 projects/notification-electron/src/public-api.ts create mode 100644 projects/notification-electron/tsconfig.lib.json create mode 100644 projects/notification-electron/tsconfig.lib.prod.json create mode 100644 projects/notification-electron/tsconfig.spec.json create mode 100644 scripts/build.ts create mode 100644 tsconfig.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..edd9d60 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +build/ +dist/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..f95bb33 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts/" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0023bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +build/ +dist/ +pack/ \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..07ffda5 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +node_modules +build/ +dist/ +pack/ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..c5166c2 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('gts/.prettierrc.json'), +}; diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..07a65de --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.autoClosingBrackets": "languageDefined", + "editor.trimAutoWhitespace": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.trimTrailingWhitespace": true, + "files.trimFinalNewlines": true, + "files.watcherExclude": { + "**/dist": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2b15848 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "notification", + "version": "0.0.0", + "private": true, + "description": "", + "license": "Apache-2.0", + "keywords": [], + "scripts": { + "lint": "gts lint", + "clean": "gts clean", + "fix": "gts fix", + "build:notification-electron": "ts-node ./scripts/build.ts notification-electron" + }, + "peerDependencies": { + "lodash": "^4.17.20", + "rxjs": "^6.6.3" + }, + "devDependencies": { + "@types/fs-extra": "^9.0.4", + "@types/lodash": "^4.14.165", + "@types/node": "^14.11.2", + "cross-env": "^7.0.2", + "electron": "^11.0.3", + "fs-extra": "^9.0.1", + "gts": "^3.0.2", + "husky": "^4.3.0", + "notification-electron": "file:pack/notification-electron-0.0.1.tgz", + "npm-run-all": "^4.1.5", + "rimraf": "^3.0.2", + "ts-node": "^9.0.0", + "typescript": "^4.0.3" + } +} diff --git a/projects/notification-electron/package.json b/projects/notification-electron/package.json new file mode 100644 index 0000000..5ac644d --- /dev/null +++ b/projects/notification-electron/package.json @@ -0,0 +1,7 @@ +{ + "name": "notification-electron", + "version": "0.0.1", + "peerDependencies": { + "electron": "^7.0.0" + } +} diff --git a/projects/notification-electron/project.json b/projects/notification-electron/project.json new file mode 100644 index 0000000..0869a78 --- /dev/null +++ b/projects/notification-electron/project.json @@ -0,0 +1,6 @@ +{ + "dest": "../../dist/notification-electron", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/projects/notification-electron/src/lib/services/notification.service.ts b/projects/notification-electron/src/lib/services/notification.service.ts new file mode 100644 index 0000000..b43ce33 --- /dev/null +++ b/projects/notification-electron/src/lib/services/notification.service.ts @@ -0,0 +1,651 @@ +import * as url from 'url'; +import * as _ from 'lodash'; +import * as Electron from 'electron'; + +import {AnimationQueue} from '../utils/animation.queue'; +import { + NotificationChannel, + NotificationWindowChannel, +} from '../types/channel.type'; + +export interface Template { + filePath?: string; + html?: string; + preload?: string; +} + +export interface DefaultOptions { + appIcon?: string; + optionsForBrowserWindow?: Electron.BrowserWindowConstructorOptions; + displayTime?: number; + template: Template; + padding?: { + item?: number; + bottom?: number; + }; + animation?: { + step: number; + timeForStep: number; + }; + windowPool?: { + min?: number; + max?: number; + }; +} + +export const defaultOptions: DefaultOptions = { + optionsForBrowserWindow: { + alwaysOnTop: true, + skipTaskbar: true, + resizable: false, + show: false, + frame: false, + transparent: true, + acceptFirstMouse: true, + }, + displayTime: 5000, + padding: { + item: 10, + bottom: 10, + }, + animation: { + step: 5, + timeForStep: 20, + }, + windowPool: { + min: 0, + max: 7, + }, + template: { + html: ` +\n +\n +\n +
\n + \n + \n +
\n + \n +

\n +
\n +
X
\n +
\n +\n + + `, + }, +}; + +export type OnShow4NotificationWindow = () => Promise; +export type OnClose4NotificationWindow = () => Promise; +export type OnClick4NotificationWindow = () => Promise; +export type OnUser4NotificationWindow = ( + channel: string, + ...args: any[] +) => Promise; + +export interface Notification { + id?: number; + optionsForBrowserWindow?: Omit< + Electron.BrowserWindowConstructorOptions, + 'width' + >; + displayTime?: number; + template: Template; + data?: any; + onShow?: OnShow4NotificationWindow; + onClose?: OnClose4NotificationWindow; + onClick?: OnClick4NotificationWindow; + onUser?: OnUser4NotificationWindow; +} + +class WindowPooler { + private readonly __windows: Electron.BrowserWindow[]; + private handle: any; + + constructor(private readonly __minSize: number) { + this.__minSize = 0 > this.__minSize ? 0 : this.__minSize; + this.__windows = []; + } + + get length() { + return this.__windows.length; + } + + push(...items: Electron.BrowserWindow[]): number { + const length = this.__windows.push(...items); + + if (this.__minSize < length) { + this.__start(); + } + return length; + } + + pop(): Electron.BrowserWindow | undefined { + if (!this.__windows || 0 === this.__windows.length) { + return undefined; + } + return this.__windows.pop(); + } + + closeAll() { + while (this.__windows.length > 0) { + const w = this.__windows.pop(); + if (!!w) { + w.close(); + } + } + } + + private __start() { + setTimeout(() => { + while (this.__minSize < this.__windows.length) { + const w = this.__windows.pop(); + if (!!w) { + w.close(); + } + } + }, 3000); + } +} + +type Activated = { + notification: Notification; + browserWindow: Electron.BrowserWindow; +}; + +export class NotificationService { + private readonly __defaultOptions: DefaultOptions; + private __animationQueue = new AnimationQueue(); + private __windowPooler: WindowPooler; + private __notificationId = 0; + private __width = 0; + private __availableHeight = 0; + private __paddingBottom: number; + private __paddingItem: number; + private __basePosition: Electron.Point; + private __activated: Activated[] = []; + private __delayQueue: Notification[] = []; + private __closed = new Map(); + + constructor(options?: DefaultOptions) { + this.__defaultOptions = _.assignIn(defaultOptions, options); + this.__windowPooler = new WindowPooler( + !!this.__defaultOptions.windowPool!.min + ? this.__defaultOptions.windowPool!.min + : 0 + ); + this.__basePosition = {x: 0, y: 0}; + this.__paddingBottom = !!this.__defaultOptions.padding?.bottom + ? this.__defaultOptions.padding?.bottom + : 0; + this.__paddingItem = !!this.__defaultOptions.padding?.item + ? this.__defaultOptions.padding?.item + : 0; + } + + init(): void { + this.__initDimension(); + this.__initListeners(); + } + + destroy(): void { + this.__destroyListeners(); + } + + push(notification: Notification): void { + notification.id = this.__nextNotificationId(); + this.__animationQueue.push({ + animate: this.__show, + context: this, + args: [notification], + }); + } + + private __show( + notification: Notification + ): Promise { + const __this = this; + return new Promise( + (resolve, reject) => { + const nextPosition = __this.__nextPosition(notification); + if (!!nextPosition) { + __this + .__getWindow(notification) + .then(browserWindow => { + browserWindow.setPosition(nextPosition.x, nextPosition.y); + __this.__activated.push({ + notification, + browserWindow, + }); + + const displayTime = !!notification.displayTime + ? notification.displayTime + : !!__this.__defaultOptions.displayTime + ? __this.__defaultOptions.displayTime + : 5000; + let timeoutId: any; + const onCloseFnc = __this.__onCloseFnc( + browserWindow, + notification, + () => timeoutId + ); + const onCloseFncGraceful = __this.__onCloseFnc4Graceful( + onCloseFnc + ); + timeoutId = setTimeout(() => { + if (browserWindow.isDestroyed()) { + return; + } + + onCloseFncGraceful('timeout'); + }, displayTime); + + if (!!notification.onShow) { + notification.onShow(); + } + + browserWindow.webContents.send( + NotificationChannel.ready4NotificationWindow + ); + + browserWindow.showInactive(); + return resolve(browserWindow); + }) + .catch(reason => {}); + } else { + __this.__delayQueue.push(notification); + return resolve(undefined); + } + } + ); + } + + private __getWindow( + notification: Notification + ): Promise { + const __this = this; + return new Promise((resolve, reject) => { + if (0 < __this.__windowPooler.length) { + const browserWindow = __this.__windowPooler.pop(); + if (!!browserWindow) { + resolve(browserWindow); + } + } else { + const constructorOptions = _.assignIn( + this.__defaultOptions.optionsForBrowserWindow, + notification.optionsForBrowserWindow, + { + width: this.__width, + title: 'Notification', + } + ); + + const browserWindow = new Electron.BrowserWindow(constructorOptions); + browserWindow.setVisibleOnAllWorkspaces(true); + browserWindow.loadURL(__this.__templateUrl(notification)); + browserWindow.webContents.once('did-finish-load', () => { + browserWindow.webContents.send( + NotificationChannel.init4NotificationWindow, + notification.data + ); + resolve(browserWindow); + }); + } + }); + } + + private __nextNotificationId(): number { + this.__notificationId++; + + if (Number.MAX_SAFE_INTEGER < this.__notificationId) { + this.__notificationId = 0; + } + + return this.__notificationId; + } + + private __nextPosition( + notification: Notification + ): Electron.Point | undefined { + const itemTotalHeight = this.__activated.reduce((a, b) => { + return a + b.browserWindow.getBounds().height; + }, 0); + + const itemWidth = this.__width; + const itemHeight = !!notification.optionsForBrowserWindow?.height + ? notification.optionsForBrowserWindow?.height + : !!this.__defaultOptions.optionsForBrowserWindow?.height + ? this.__defaultOptions.optionsForBrowserWindow?.height + : 10; + + const totalHeight = + itemTotalHeight + + this.__paddingBottom + + this.__paddingItem * + (1 < this.__activated.length ? this.__activated.length - 1 : 0); + const nextTotalHeight = totalHeight + itemHeight + this.__paddingItem; + + if (this.__availableHeight < nextTotalHeight) { + return undefined; + } + + if (!!notification.optionsForBrowserWindow) { + notification.optionsForBrowserWindow.height = itemHeight; + } + + return { + x: this.__basePosition.x - this.__width, + y: this.__basePosition.y - nextTotalHeight, + }; + } + + private __onCloseFnc4Graceful(onClose: OnClose4NotificationWindow) { + const __this = this; + return (reason: any): void => { + if (!reason) { + reason = 'closedGracefully'; + } + + __this.__animationQueue.push({ + animate: onClose, + context: __this, + args: [reason], + }); + }; + } + + private __onCloseFnc( + browserWindow: Electron.BrowserWindow, + notification: Notification, + timeoutIdFnc?: () => any + ) { + const __this = this; + return (): Promise => { + return new Promise((resolve, reject) => { + if (browserWindow.isDestroyed()) { + return resolve(); + } + const notificationId = !!notification.id ? notification.id : 0; + if (__this.__closed.has(notificationId)) { + __this.__closed.delete(notificationId); + return resolve(); + } else { + __this.__closed.set(notificationId, true); + } + + browserWindow.webContents.send( + NotificationChannel.reset4NotificationWindow + ); + + if (!!timeoutIdFnc) { + clearTimeout(timeoutIdFnc()); + } + const i = __this.__activated.findIndex( + w => w.browserWindow.id === browserWindow.id + ); + __this.__activated.splice(i, 1); + __this.__windowPooler.push(browserWindow); + + browserWindow.hide(); + __this.__check4Queued(); + + __this.__animate4Move(i); + }); + }; + } + + private __findActivatedByNotificationId(id: number): Activated | undefined { + const i = this.__activated.findIndex(a => a.notification.id === id); + if (-1 === i) { + return undefined; + } + return this.__activated[i]; + } + + private __findActivatedByBrowserWindowId(id: number): Activated | undefined { + const i = this.__activated.findIndex(a => a.browserWindow.id === id); + if (-1 === i) { + return undefined; + } + return this.__activated[i]; + } + + private __check4Queued(): void { + if ( + 0 < this.__delayQueue.length && + !!this.__nextPosition(this.__delayQueue[0]) + ) { + this.__animationQueue.push({ + animate: this.__show, + context: this, + args: [this.__delayQueue.shift()], + }); + } + } + + private __animate4Move(startIndex: number): Promise { + const __this = this; + return new Promise(async (resolve, reject) => { + if (startIndex >= __this.__activated.length || -1 === startIndex) { + return resolve(); + } + + const indexes: number[] = []; + for (let i = startIndex; i < __this.__activated.length; i++) { + indexes.push(i); + } + + await Promise.all( + indexes.map(async index => { + await __this.__animateFnc4Move(index); + }) + ); + return resolve(); + }); + } + + private __animateFnc4Move(index: number): Promise { + const __this = this; + + return new Promise((resolve, reject) => { + const activated = __this.__activated[index]; + const startY = activated.browserWindow.getPosition()[1]; + const endY = + startY + + (!!activated.notification.optionsForBrowserWindow?.height + ? activated.notification.optionsForBrowserWindow?.height + : 10); + const animationStep = !!__this.__defaultOptions.animation?.step + ? __this.__defaultOptions.animation?.step + : 5; + const animationTimeForStep = !!this.__defaultOptions.animation + ?.timeForStep + ? this.__defaultOptions.animation?.timeForStep + : 20; + const step = (endY - startY) / animationStep; + + let currentStep = 1; + const intervalId = setInterval(() => { + if (currentStep === animationStep) { + activated.browserWindow.setPosition(__this.__basePosition.x, endY); + clearInterval(intervalId); + return resolve(); + } + + activated.browserWindow.setPosition( + __this.__basePosition.x, + Math.trunc(startY + currentStep * step) + ); + currentStep++; + }, animationTimeForStep); + }); + } + + private __templateUrl(notification: Notification): string { + let u: string = ''; + + const filePath2Url = (filePath: string) => { + return url.format({ + protocol: 'file:', + pathname: filePath, + slashes: true, + }); + }; + + const html2Url = (html: string) => { + return 'data:text/html,' + encodeURIComponent(html); + }; + + if (!!notification.template.filePath) { + return filePath2Url(notification.template.filePath); + } + + if (!!notification.template.html) { + return html2Url(notification.template.html); + } + + if (!!this.__defaultOptions.template.filePath) { + return filePath2Url(this.__defaultOptions.template.filePath); + } + + if (!!this.__defaultOptions.template.html) { + return html2Url(this.__defaultOptions.template.html); + } + + return !!defaultOptions.template.html ? defaultOptions.template.html : ''; + } + + private __initDimension(): void { + const display = Electron.screen.getPrimaryDisplay(); + + this.__basePosition = { + x: display.bounds.x + display.workArea.x + display.workAreaSize.width, + y: display.bounds.y + display.workArea.y + display.workAreaSize.height, + }; + + this.__width = + !!this.__defaultOptions.optionsForBrowserWindow?.width && + this.__defaultOptions.optionsForBrowserWindow?.width < + display.workAreaSize.width + ? this.__defaultOptions.optionsForBrowserWindow?.width + : display.workAreaSize.width; + this.__availableHeight = display.workAreaSize.height; + } + + private __initListeners(): void { + Electron.ipcMain.on( + NotificationWindowChannel.onClose, + this.__onClose4NotificationWindow.bind(this) + ); + Electron.ipcMain.on( + NotificationWindowChannel.onClick, + this.__onClick4NotificationWindow.bind(this) + ); + Electron.ipcMain.on( + NotificationWindowChannel.onUser, + this.__onUser4NotificationWindow.bind(this) + ); + + Electron.screen.on( + 'display-added', + this.__onDisplayAdded4ElectronScreen.bind(this) + ); + Electron.screen.on( + 'display-removed', + this.__onDisplayRemoved4ElectronScreen.bind(this) + ); + Electron.screen.on( + 'display-metrics-changed', + this.__onDisplayMetricsChanged4ElectronScreen.bind(this) + ); + } + + private __destroyListeners(): void { + Electron.ipcMain.off( + NotificationWindowChannel.onClose, + this.__onClose4NotificationWindow.bind(this) + ); + Electron.ipcMain.off( + NotificationWindowChannel.onClick, + this.__onClick4NotificationWindow.bind(this) + ); + Electron.ipcMain.off( + NotificationWindowChannel.onUser, + this.__onUser4NotificationWindow.bind(this) + ); + + Electron.screen.off( + 'display-added', + this.__onDisplayAdded4ElectronScreen.bind(this) + ); + Electron.screen.off( + 'display-removed', + this.__onDisplayRemoved4ElectronScreen.bind(this) + ); + Electron.screen.off( + 'display-metrics-changed', + this.__onDisplayMetricsChanged4ElectronScreen.bind(this) + ); + } + + private __onClose4NotificationWindow( + event: Electron.IpcMainEvent, + notificationId: number + ): void { + const activated = this.__findActivatedByNotificationId(notificationId); + if (!activated || !activated.notification.onClose) { + return; + } + + activated.notification.onClose(); + } + + private __onClick4NotificationWindow( + event: Electron.IpcMainEvent, + notificationId: number + ): void { + const activated = this.__findActivatedByNotificationId(notificationId); + if (!activated || !activated.notification.onClick) { + return; + } + + activated.notification.onClick(); + } + + private __onUser4NotificationWindow( + event: Electron.IpcMainEvent, + notificationId: number, + channel: string, + ...args: any[] + ): void { + const activated = this.__findActivatedByNotificationId(notificationId); + if (!activated || !activated.notification.onUser) { + return; + } + + activated.notification.onUser(channel, ...args); + } + + private __onDisplayAdded4ElectronScreen( + event: Electron.Event, + newDisplay: Electron.Display + ): void { + this.__initDimension(); + } + + private __onDisplayRemoved4ElectronScreen( + event: Electron.Event, + oldDisplay: Electron.Display + ): void { + this.__initDimension(); + } + + private __onDisplayMetricsChanged4ElectronScreen( + event: Electron.Event, + display: Electron.Display, + changedMetrics: string[] // bounds, workArea, scaleFactor, rotation + ): void { + this.__initDimension(); + } +} diff --git a/projects/notification-electron/src/lib/types/channel.type.ts b/projects/notification-electron/src/lib/types/channel.type.ts new file mode 100644 index 0000000..167b65b --- /dev/null +++ b/projects/notification-electron/src/lib/types/channel.type.ts @@ -0,0 +1,11 @@ +export enum NotificationChannel { + init4NotificationWindow = 'notification-electron::NotificationChannel::init4NotificationWindow', + ready4NotificationWindow = 'notification-electron::NotificationChannel::ready4NotificationWindow', + reset4NotificationWindow = 'notification-electron::NotificationChannel::reset4NotificationWindow', +} + +export enum NotificationWindowChannel { + onClose = 'notification-electron::NotificationWindowChannel::onClose', + onClick = 'notification-electron::NotificationWindowChannel::onClick', + onUser = 'notification-electron::NotificationWindowChannel::onUser', +} diff --git a/projects/notification-electron/src/lib/utils/animation.queue.ts b/projects/notification-electron/src/lib/utils/animation.queue.ts new file mode 100644 index 0000000..97f30c0 --- /dev/null +++ b/projects/notification-electron/src/lib/utils/animation.queue.ts @@ -0,0 +1,55 @@ +export interface QueueObject { + animate: (...args: any[]) => Promise; + context: any; + args?: any[]; + onError?: (error: any) => void; +} + +export class AnimationQueue { + private __running = false; + private __queueObjects: QueueObject[] = []; + + push(o: QueueObject): void { + if (this.__running) { + this.__queueObjects.push(o); + } else { + this.__running = true; + this.__animate(o); + } + } + + clear(): void { + this.__queueObjects = []; + } + + private __animate(o: QueueObject): void { + const __this = this; + try { + if (!o.animate) { + return; + } + + o.animate + .apply(o.context, !!o.args ? o.args : []) + .then(() => { + if (0 < __this.__queueObjects.length) { + const __o = __this.__queueObjects.shift(); + if (!!__o) { + __this.__animate.call(__this, __o); + } else { + __this.__running = false; + } + } + }) + .catch(reason => { + if (!!o.onError) { + o.onError.apply(o.context, [reason]); + } + }); + } catch (e) { + if (!!o.onError) { + o.onError.apply(o.context, [e]); + } + } + } +} diff --git a/projects/notification-electron/src/public-api.ts b/projects/notification-electron/src/public-api.ts new file mode 100644 index 0000000..83d7e26 --- /dev/null +++ b/projects/notification-electron/src/public-api.ts @@ -0,0 +1,5 @@ +export * from './lib/services/notification.service'; + +export * from './lib/types/channel.type'; + +export * from './lib/utils/animation.queue'; diff --git a/projects/notification-electron/tsconfig.lib.json b/projects/notification-electron/tsconfig.lib.json new file mode 100644 index 0000000..d803fc0 --- /dev/null +++ b/projects/notification-electron/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "lib": ["dom"] + }, + "exclude": ["src/test.ts", "**/*.spec.ts"] +} diff --git a/projects/notification-electron/tsconfig.lib.prod.json b/projects/notification-electron/tsconfig.lib.prod.json new file mode 100644 index 0000000..136e7ee --- /dev/null +++ b/projects/notification-electron/tsconfig.lib.prod.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.lib.json" +} diff --git a/projects/notification-electron/tsconfig.spec.json b/projects/notification-electron/tsconfig.spec.json new file mode 100644 index 0000000..ec3528a --- /dev/null +++ b/projects/notification-electron/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": ["jasmine", "node"] + }, + "files": ["src/test.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..3c7fb23 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,148 @@ +import * as path from 'path'; +import {execSync} from 'child_process'; +import * as fse from 'fs-extra'; + +async function build(args: string[]): Promise { + const rootPath = path.join(__dirname, '..'); + const packPath = path.join(rootPath, 'pack'); + const projectName = args[0]; + + const projectPath = path.join(rootPath, 'projects', projectName); + const packageJson = require(path.join(projectPath, 'package.json')); + const projectJson = require(path.join(projectPath, 'project.json')); + + const tsconfigJsonPath = path.join(projectPath, 'tsconfig.lib.prod.json'); + const distPath = path.join(projectPath, projectJson.dest); + + const clean = () => + new Promise((resolve, reject) => { + try { + console.log(`Cleaning is started.`); + execSync(`rimraf ${distPath}`, {stdio: 'inherit'}); + console.log(`Cleaning is completed.`); + resolve(); + } catch (e) { + reject(e); + } + }); + const declaration = () => + new Promise((resolve, reject) => { + try { + const outDir = path.join(distPath, 'types'); + console.log(`Generating typescript declaration files is started.`); + execSync( + `tsc --project ${tsconfigJsonPath} --module commonjs --target es5 --declaration true --emitDeclarationOnly --outDir ${outDir}`, + {stdio: 'inherit'} + ); + console.log(`Generating typescript declaration files is completed.`); + resolve(); + } catch (e) { + reject(e); + } + }); + + const transpile = ( + name: string, + dirName: string, + moduleName: string, + targetName: string + ) => + new Promise((resolve, reject) => { + try { + let out = ''; + switch (moduleName.toLowerCase()) { + case 'amd': + case 'system': + out = `--outFile ${path.join(distPath, dirName, 'public-api.js')}`; + break; + default: + out = `--outDir ${path.join(distPath, dirName)}`; + break; + } + + const outDir = path.join(distPath, dirName); + console.log(`Generating typescript ${name} module is started.`); + execSync( + `tsc --project ${tsconfigJsonPath} --module ${moduleName} --target ${targetName} ${out}`, + {stdio: 'inherit'} + ); + console.log(`Generating typescript ${name} module is completed.`); + resolve(); + } catch (e) { + reject(e); + } + }); + + const packagejson = () => + new Promise((resolve, reject) => { + try { + console.log(`Generating project package.json is started.`); + const projectPackageJson = { + ...packageJson, + main: `cjs/public-api.js`, + module: `esm5/public-api.js`, + es2015: `es2015/public-api.js`, + types: `types/public-api.d.ts`, + sideEffects: false, + }; + + fse.writeFileSync( + path.join(distPath, 'package.json'), + JSON.stringify(projectPackageJson, null, 2) + ); + + console.log(`Generating project package.json is completed.`); + resolve(); + } catch (e) { + reject(e); + } + }); + + const install2Local = () => + new Promise((resolve, reject) => { + try { + console.log(`Installing to local is started.`); + + const distPackageJson = require(path.join(distPath, 'package.json')); + const packFileName = `${projectName}-${distPackageJson.version}.tgz`; + const distFilePath = path.join(distPath, packFileName); + const packFilePath = path.join(packPath, packFileName); + + process.chdir(path.join(distPath)); + execSync(`npm pack`, {stdio: 'inherit'}); + + fse.moveSync(distFilePath, packFilePath, { + overwrite: true, + }); + + process.chdir(path.join(rootPath)); + + execSync(`npm install -D ${packFilePath}`, { + stdio: 'inherit', + }); + + console.log(`Installing to local is completed.`); + resolve(); + } catch (e) { + reject(e); + } + }); + + return Promise.all([ + clean(), + declaration(), + transpile('commonjs', 'cjs', 'commonjs', 'es5'), + transpile('amd', 'amd', 'amd', 'es5'), + transpile('umd', 'umd', 'umd', 'es5'), + transpile('ESM5', 'esm5', 'es2015', 'es5'), + transpile('ES2015', 'es2015', 'es2015', 'es2015'), + packagejson(), + install2Local(), + ]); +} + +build(process.argv.slice(2)) + .then() + .catch(reason => { + console.error(reason); + }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..431a904 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "baseUrl": "./", + "declaration": false, + "moduleResolution": "node" + } +}