feat: Move to TypeScript

This commit is contained in:
Felix Rieseberg 2019-08-21 09:48:49 +02:00
parent b7aa6a760d
commit 241606d097
42 changed files with 1294 additions and 856 deletions

View File

@ -9,7 +9,7 @@
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "standard \"src/**/*.js\"",
"lint": "prettier --write src/**/*.{ts,tsx}",
"less": "node ./tools/lessc.js"
},
"keywords": [],

View File

@ -1,25 +0,0 @@
const { session } = require('electron')
const clearCaches = async () => {
await clearCache()
await clearStorageData()
}
const clearCache = () => {
return new Promise((resolve) => {
session.defaultSession.clearCache(resolve)
})
}
const clearStorageData = () => {
return new Promise((resolve) => {
session.defaultSession.clearStorageData({
storages: 'appcache, cookies, filesystem, indexdb, localstorage, shadercache, websql, serviceworkers',
quotas: 'temporary, persistent, syncable'
}, resolve)
})
}
module.exports = {
clearCaches
}

25
src/cache.ts Normal file
View File

@ -0,0 +1,25 @@
import { session } from 'electron';
export async function clearCaches() {
await clearCache()
await clearStorageData()
}
export async function clearCache() {
if (session.defaultSession) {
await session.defaultSession.clearCache();
}
}
export function clearStorageData() {
return new Promise((resolve) => {
if (!session.defaultSession) {
return resolve();
}
session.defaultSession.clearStorageData({
storages: [ 'appcache', 'cookies', 'filesystem', 'indexdb', 'localstorage', 'shadercache', 'websql', 'serviceworkers' ],
quotas: [ 'temporary', 'persistent', 'syncable' ]
}, resolve)
})
}

View File

@ -1,24 +1,19 @@
const { remote, app } = require('electron')
const path = require('path')
import { remote, app } from 'electron';
import * as path from 'path';
const _app = app || remote.app
const CONSTANTS = {
IMAGE_PATH: path.join(__dirname, 'images/windows95.img'),
export const CONSTANTS = {
IMAGE_PATH: path.join(__dirname, '../../images/windows95.img'),
IMAGE_DEFAULT_SIZE: 1073741824, // 1GB
DEFAULT_STATE_PATH: path.join(__dirname, 'images/default-state.bin'),
DEFAULT_STATE_PATH: path.join(__dirname, '../../images/default-state.bin'),
STATE_PATH: path.join(_app.getPath('userData'), 'state-v2.bin')
}
const IPC_COMMANDS = {
export const IPC_COMMANDS = {
TOGGLE_INFO: 'TOGGLE_INFO',
MACHINE_RESTART: 'MACHINE_RESTART',
MACHINE_RESET: 'MACHINE_RESET',
MACHINE_CTRL_ALT_DEL: 'MACHINE_CTRL_ALT_DEL',
SHOW_DISK_IMAGE: 'SHOW_DISK_IMAGE'
}
module.exports = {
CONSTANTS,
IPC_COMMANDS
}

View File

@ -1,36 +0,0 @@
const { protocol } = require('electron')
const fs = require('fs-extra')
const path = require('path')
const ES6_PATH = path.join(__dirname, 'renderer')
protocol.registerSchemesAsPrivileged([
{
scheme: 'es6',
privileges: {
standard: true
}
}
])
async function setupProtocol () {
protocol.registerBufferProtocol('es6', async (req, cb) => {
console.log(req)
try {
const filePath = path.join(ES6_PATH, req.url.replace('es6://', ''))
.replace('.js/', '.js')
.replace('.js\\', '.js')
const fileContent = await fs.readFile(filePath)
cb({ mimeType: 'text/javascript', data: fileContent }) // eslint-disable-line
} catch (error) {
console.warn(error)
}
})
}
module.exports = {
setupProtocol
}

View File

@ -1,55 +0,0 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
const { createMenu } = require('./menu')
const { setupProtocol } = require('./es6')
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
app.quit()
}
if (app.isPackaged) {
require('update-electron-app')({
repo: 'felixrieseberg/windows95',
updateInterval: '1 hour'
})
}
let mainWindow
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
useContentSize: true,
webPreferences: {
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadURL(`file://${__dirname}/renderer/index.html`)
mainWindow.on('closed', () => {
mainWindow = null
})
}
app.on('ready', async () => {
await setupProtocol()
await createMenu()
createWindow()
})
// Quit when all windows are closed.
app.on('window-all-closed', () => {
app.quit()
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})

28
src/main/about-panel.ts Normal file
View File

@ -0,0 +1,28 @@
import { AboutPanelOptionsOptions, app } from "electron";
/**
* Sets Fiddle's About panel options on Linux and macOS
*
* @returns
*/
export function setupAboutPanel(): void {
if (process.platform === "win32") return;
const options: AboutPanelOptionsOptions = {
applicationName: "windows95",
applicationVersion: app.getVersion(),
version: process.versions.electron,
copyright: "Felix Rieseberg"
};
switch (process.platform) {
case "linux":
options.website = "https://github.com/felixrieseberg/windows95";
case "darwin":
options.credits = "https://github.com/felixrieseberg/windows95";
default:
// fallthrough
}
app.setAboutPanelOptions(options);
}

67
src/main/main.ts Normal file
View File

@ -0,0 +1,67 @@
import { app } from "electron";
import { isDevMode } from "../utils/devmode";
import { setupAboutPanel } from "./about-panel";
import { shouldQuit } from "./squirrel";
import { setupUpdates } from "./update";
import { createWindow } from "./windows";
import { setupMenu } from "./menu";
/**
* Handle the app's "ready" event. This is essentially
* the method that takes care of booting the application.
*/
export async function onReady() {
if (!isDevMode()) process.env.NODE_ENV = "production";
createWindow();
setupAboutPanel();
setupMenu();
setupUpdates();
}
/**
* Handle the "before-quit" event
*
* @export
*/
export function onBeforeQuit() {
(global as any).isQuitting = true;
}
/**
* All windows have been closed, quit on anything but
* macOS.
*/
export function onWindowsAllClosed() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== "darwin") {
app.quit();
}
}
/**
* The main method - and the first function to run
* when Fiddle is launched.
*
* Exported for testing purposes.
*/
export function main() {
// Handle creating/removing shortcuts on Windows when
// installing/uninstalling.
if (shouldQuit()) {
app.quit();
return;
}
// Set the app's name
app.setName("windows95");
// Launch
app.on("ready", onReady);
app.on("before-quit", onBeforeQuit);
app.on("window-all-closed", onWindowsAllClosed);
}
main();

197
src/main/menu.ts Normal file
View File

@ -0,0 +1,197 @@
import { app, shell, Menu, BrowserWindow } from "electron";
import { clearCaches } from "../cache";
import { IPC_COMMANDS } from "../constants";
import { isDevMode } from "../utils/devmode";
const LINKS = {
homepage: "https://www.twitter.com/felixrieseberg",
repo: "https://github.com/felixrieseberg/windows95",
credits: "https://github.com/felixrieseberg/windows95/blob/master/CREDITS.md",
help: "https://github.com/felixrieseberg/windows95/blob/master/HELP.md"
};
function send(cmd: string) {
const windows = BrowserWindow.getAllWindows();
if (windows[0]) {
windows[0].webContents.send(cmd);
}
}
export async function setupMenu() {
const template: Array<Electron.MenuItemConstructorOptions> = [
{
label: "View",
submenu: [
{
label: "Toggle Full Screen",
accelerator: (function() {
if (process.platform === "darwin") {
return "Ctrl+Command+F";
} else {
return "F11";
}
})(),
click: function(_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
}
},
{
label: "Toggle Developer Tools",
accelerator: (function() {
if (process.platform === "darwin") {
return "Alt+Command+I";
} else {
return "Ctrl+Shift+I";
}
})(),
click: function(_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools();
}
}
},
{
type: "separator"
},
{
label: "Toggle Emulator Info",
click: () => send(IPC_COMMANDS.TOGGLE_INFO)
},
{
type: "separator"
},
{
role: "reload"
}
]
},
{
role: "editMenu",
visible: isDevMode()
},
{
label: "Window",
role: "window",
submenu: [
{
label: "Minimize",
accelerator: "CmdOrCtrl+M",
role: "minimize"
},
{
label: "Close",
accelerator: "CmdOrCtrl+W",
role: "close"
}
]
},
{
label: "Machine",
submenu: [
{
label: "Send Ctrl+Alt+Del",
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL)
},
{
label: "Restart",
click: () => send(IPC_COMMANDS.MACHINE_RESTART)
},
{
label: "Reset",
click: () => send(IPC_COMMANDS.MACHINE_RESET)
},
{
type: "separator"
},
{
label: "Go to Disk Image",
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE)
}
]
},
{
label: "Help",
role: "help",
submenu: [
{
label: "Author",
click: () => shell.openExternal(LINKS.homepage)
},
{
label: "windows95 on GitHub",
click: () => shell.openExternal(LINKS.repo)
},
{
label: "Help",
click: () => shell.openExternal(LINKS.help)
},
{
type: "separator"
},
{
label: "Troubleshooting",
submenu: [
{
label: "Clear Cache and Restart",
async click() {
await clearCaches();
app.relaunch();
app.quit();
}
}
]
}
]
}
];
if (process.platform === "darwin") {
template.unshift({
label: "windows95",
submenu: [
{
role: "about"
},
{
type: "separator"
},
{
role: "services"
},
{
type: "separator"
},
{
label: "Hide windows95",
accelerator: "Command+H",
role: "hide"
},
{
label: "Hide Others",
accelerator: "Command+Shift+H",
role: "hideothers"
},
{
role: "unhide"
},
{
type: "separator"
},
{
label: "Quit",
accelerator: "Command+Q",
click() {
app.quit();
}
}
]
} as any);
}
Menu.setApplicationMenu(Menu.buildFromTemplate(template as any));
}

3
src/main/squirrel.ts Normal file
View File

@ -0,0 +1,3 @@
export function shouldQuit() {
return require("electron-squirrel-startup");
}

10
src/main/update.ts Normal file
View File

@ -0,0 +1,10 @@
import { app } from "electron";
export function setupUpdates() {
if (app.isPackaged) {
require("update-electron-app")({
repo: "felixrieseberg/windows95",
updateInterval: "1 hour"
});
}
}

21
src/main/windows.ts Normal file
View File

@ -0,0 +1,21 @@
import { BrowserWindow } from "electron";
let mainWindow;
export function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
useContentSize: true,
webPreferences: {
nodeIntegration: true
}
});
mainWindow.loadFile("./dist/static/index.html");
mainWindow.on("closed", () => {
mainWindow = null;
});
}

View File

@ -1,182 +0,0 @@
const { app, shell, Menu, BrowserWindow } = require('electron')
const { clearCaches } = require('./cache')
const { IPC_COMMANDS } = require('./constants')
const LINKS = {
homepage: 'https://www.twitter.com/felixrieseberg',
repo: 'https://github.com/felixrieseberg/windows95',
credits: 'https://github.com/felixrieseberg/windows95/blob/master/CREDITS.md',
help: 'https://github.com/felixrieseberg/windows95/blob/master/HELP.md'
}
function send (cmd) {
const windows = BrowserWindow.getAllWindows()
if (windows[0]) {
windows[0].webContents.send(cmd)
}
}
async function createMenu () {
const template = [
{
label: 'View',
submenu: [
{
label: 'Toggle Full Screen',
accelerator: (function () {
if (process.platform === 'darwin') { return 'Ctrl+Command+F' } else { return 'F11' }
})(),
click: function (_item, focusedWindow) {
if (focusedWindow) { focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) }
}
},
{
label: 'Toggle Developer Tools',
accelerator: (function () {
if (process.platform === 'darwin') { return 'Alt+Command+I' } else { return 'Ctrl+Shift+I' }
})(),
click: function (_item, focusedWindow) {
if (focusedWindow) { focusedWindow.toggleDevTools() }
}
},
{
type: 'separator'
},
{
label: 'Toggle Emulator Info',
click: () => send(IPC_COMMANDS.TOGGLE_INFO)
}
]
},
{
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},
{
label: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
},
{
label: 'Machine',
submenu: [
{
label: 'Send Ctrl+Alt+Del',
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL)
},
{
label: 'Restart',
click: () => send(IPC_COMMANDS.MACHINE_RESTART)
},
{
label: 'Reset',
click: () => send(IPC_COMMANDS.MACHINE_RESET)
},
{
type: 'separator'
},
{
label: 'Go to Disk Image',
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE)
}
]
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Author',
click: () => shell.openExternal(LINKS.homepage)
},
{
label: 'windows95 on GitHub',
click: () => shell.openExternal(LINKS.repo)
},
{
label: 'Help',
click: () => shell.openExternal(LINKS.help)
},
{
type: 'separator'
},
{
label: 'Troubleshooting',
submenu: [
{
label: 'Clear Cache and Restart',
async click () {
await clearCaches()
app.relaunch()
app.quit()
}
}
]
}
]
}
]
if (process.platform === 'darwin') {
template.unshift({
label: 'windows95',
submenu: [
{
label: 'About windows95',
role: 'about'
},
{
type: 'separator'
},
{
label: 'Services',
role: 'services',
submenu: []
},
{
type: 'separator'
},
{
label: 'Hide windows95',
accelerator: 'Command+H',
role: 'hide'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
role: 'hideothers'
},
{
label: 'Show All',
role: 'unhide'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
click () {
app.quit()
}
}
]
})
}
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}
module.exports = {
createMenu
}

View File

@ -1,42 +0,0 @@
const { remote, shell, ipcRenderer } = require('electron')
const path = require('path')
const EventEmitter = require('events')
const { resetState, restoreState, saveState } = require('./state')
const { getDiskImageSize } = require('./utils/disk-image-size')
const { IPC_COMMANDS, CONSTANTS } = require('./constants')
class Windows95 extends EventEmitter {
constructor () {
super()
// Constants
this.CONSTANTS = CONSTANTS
this.IPC_COMMANDS = IPC_COMMANDS
// Methods
this.getDiskImageSize = getDiskImageSize
this.restoreState = restoreState
this.resetState = resetState
this.saveState = saveState
Object.keys(IPC_COMMANDS).forEach((command) => {
ipcRenderer.on(command, (...args) => {
this.emit(command, args)
})
})
}
showDiskImage () {
const imagePath = path.join(__dirname, 'images/windows95.img')
.replace('app.asar', 'app.asar.unpacked')
shell.showItemInFolder(imagePath)
}
quit () {
remote.app.quit()
}
}
window.windows95 = new Windows95()

View File

@ -1,9 +0,0 @@
export function setupState () {
window.appState = {
isResetting: false,
isQuitting: false,
cursorCaptured: false,
floppyFile: null,
bootFresh: false
}
}

34
src/renderer/app.tsx Normal file
View File

@ -0,0 +1,34 @@
/**
* The top-level class controlling the whole app. This is *not* a React component,
* but it does eventually render all components.
*
* @class App
*/
export class App {
/**
* Initial setup call, loading Monaco and kicking off the React
* render process.
*/
public async setup(): Promise<void | Element> {
const React = await import("react");
const { render } = await import("react-dom");
const { Emulator } = await import("./emulator");
const className = `${process.platform}`;
const app = (
<div className={className}>
<Emulator />
</div>
);
const rendered = render(app, document.getElementById("app"));
return rendered;
}
}
window["win95"] = window["win95"] || {
app: new App()
};
window["win95"].app.setup();

View File

@ -1,56 +0,0 @@
const $ = document.querySelector.bind(document)
const $$ = document.querySelectorAll.bind(document)
export function setupButtons (start) {
// Sections
$('a#start').addEventListener('click', () => setVisibleSection('start'))
$('a#floppy').addEventListener('click', () => setVisibleSection('floppy'))
$('a#state').addEventListener('click', () => setVisibleSection('state'))
$('a#disk').addEventListener('click', () => setVisibleSection('disk'))
// Start
$('.btn-start').addEventListener('click', start)
// Disk Image
$('#disk-image-show').addEventListener('click', () => windows95.showDiskImage())
// Reset
$('#reset').addEventListener('click', () => windows95.resetState())
$('#discard-state').addEventListener('click', () => {
window.appState.bootFresh = true
start()
})
// Floppy
$('#floppy-select').addEventListener('click', () => {
$('#floppy-input').click()
})
// Floppy (Hidden Input)
$('#floppy-input').addEventListener('change', (event) => {
window.appState.floppyFile = event.target.files && event.target.files.length > 0
? event.target.files[0]
: null
if (window.appState.floppyFile) {
$('#floppy-path').innerHTML = `Inserted Floppy Disk: ${window.appState.floppyFile.path}`
}
})
}
export function toggleSetup (forceTo) {
const buttonElements = $('#setup')
if (buttonElements.style.display !== 'none' || forceTo === false) {
buttonElements.style.display = 'none'
} else {
buttonElements.style.display = undefined
}
}
function setVisibleSection(id = '') {
$$(`section`).forEach((s) => s.classList.remove('visible'))
$(`section#section-${id}`).classList.add('visible')
}

View File

@ -0,0 +1,78 @@
import * as React from "react";
export interface CardFloppyProps {
setFloppyPath: (path: string) => void;
floppyPath?: string;
}
export class CardFloppy extends React.Component<CardFloppyProps, {}> {
constructor(props: CardFloppyProps) {
super(props);
this.onChange = this.onChange.bind(this);
}
public render() {
return (
<section>
<div className="card">
<div className="card-header">
<h2 className="card-title">Floppy Drive</h2>
</div>
<div className="card-body">
<input
id="floppy-input"
type="file"
onChange={this.onChange}
style={{ display: "none" }}
/>
<p>
windows95 comes with a virtual floppy drive. If you have floppy
disk images in the "img" format, you can mount them here.
</p>
<p>
Back in the 90s and before CD-ROM became a popular format,
software was typically distributed on floppy disks. Some
developers have since released their apps or games for free,
usually on virtual floppy disks using the "img" format.
</p>
<p>
Once you've mounted a disk image, you might have to reboot your
virtual windows95 machine from scratch.
</p>
<p id="floppy-path">
{this.props.floppyPath
? `Inserted Floppy Disk: ${this.props.floppyPath}`
: `No floppy mounted`}
</p>
<button
id="floppy-select"
className="btn"
onClick={() =>
(document.querySelector("#floppy-input") as any).click()
}
>
Mount floppy disk
</button>
<button id="floppy-reboot" className="btn">
Reboot from scratch
</button>
</div>
</div>
</section>
);
}
public onChange(event: React.ChangeEvent<HTMLInputElement>) {
const floppyFile =
event.target.files && event.target.files.length > 0
? event.target.files[0]
: null;
if (floppyFile) {
this.props.setFloppyPath(floppyFile.path);
} else {
console.log(`Floppy: Input changed but no file selected`);
}
}
}

View File

@ -0,0 +1,23 @@
import * as React from "react";
export interface CardStartProps {
startEmulator: () => void;
}
export class CardStart extends React.Component<CardStartProps, {}> {
public render() {
return (
<section id="section-start" className="visible">
<div
className="btn btn-start"
id="win95"
onClick={this.props.startEmulator}
>
Start Windows 95
<br />
<small>Hit ESC to lock or unlock your mouse</small>
</div>
</section>
);
}
}

View File

@ -0,0 +1,47 @@
import * as React from "react";
import * as fs from "fs-extra";
import { CONSTANTS } from "../constants";
export interface CardStateProps {
bootFromScratch: () => void;
}
export class CardState extends React.Component<CardStateProps, {}> {
constructor(props: CardStateProps) {
super(props);
this.onReset = this.onReset.bind(this);
}
public render() {
return (
<section>
<div className="card">
<div className="card-header">
<h2 className="card-title">Machine State</h2>
</div>
<div className="card-body">
<p>
windows95 stores any changes to your machine (like saved files) in
a state file. If you encounter any trouble, you can either reset
your state or boot Windows 95 from scratch.
</p>
<button className="btn" onClick={this.onReset}>
Reset state
</button>
<button className="btn" onClick={this.props.bootFromScratch}>
Reboot from scratch
</button>
</div>
</div>
</section>
);
}
public async onReset() {
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
await fs.remove(CONSTANTS.STATE_PATH);
}
}
}

View File

@ -0,0 +1,166 @@
import * as React from "react";
interface EmulatorInfoProps {
toggleInfo: () => void;
emulator: any;
}
interface EmulatorInfoState {
cpu: number;
disk: string;
lastCounter: number;
lastTick: number;
}
export class EmulatorInfo extends React.Component<
EmulatorInfoProps,
EmulatorInfoState
> {
private cpuInterval = -1;
constructor(props: EmulatorInfoProps) {
super(props);
this.cpuCount = this.cpuCount.bind(this);
this.onIDEReadStart = this.onIDEReadStart.bind(this);
this.onIDEReadWriteEnd = this.onIDEReadWriteEnd.bind(this);
this.state = {
cpu: 0,
disk: "Idle",
lastCounter: 0,
lastTick: 0
};
}
public render() {
const { cpu, disk } = this.state;
return (
<div id="status">
Disk: <span>{disk}</span>| CPU Speed: <span>{cpu}</span>|{" "}
<a href="#" onClick={this.props.toggleInfo}>
Hide
</a>
</div>
);
}
public componentWillUnmount() {
this.uninstallListeners();
}
/**
* The emulator starts whenever, so install or uninstall listeners
* at the right time
*
* @param newProps
*/
public componentDidUpdate(prevProps: EmulatorInfoProps) {
if (prevProps.emulator !== this.props.emulator) {
if (this.props.emulator) {
this.installListeners();
} else {
this.uninstallListeners();
}
}
}
/**
* Let's start listening to what the emulator is up to.
*/
private installListeners() {
const { emulator } = this.props;
if (!emulator) {
console.log(
`Emulator info: Tried to install listeners, but emulator not defined yet.`
);
return;
}
// CPU
if (this.cpuInterval > -1) {
clearInterval(this.cpuInterval);
}
// TypeScript think's we're using a Node.js setInterval. We're not.
this.cpuInterval = (setInterval(this.cpuCount, 500) as unknown) as number;
// Disk
emulator.add_listener("ide-read-start", this.onIDEReadStart);
emulator.add_listener("ide-read-end", this.onIDEReadWriteEnd);
emulator.add_listener("ide-write-end", this.onIDEReadWriteEnd);
// Screen
emulator.add_listener("screen-set-size-graphical", console.log);
}
/**
* Stop listening to the emulator.
*/
private uninstallListeners() {
const { emulator } = this.props;
if (!emulator) {
console.log(
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`
);
return;
}
// CPU
if (this.cpuInterval > -1) {
clearInterval(this.cpuInterval);
}
// Disk
emulator.remove_listener("ide-read-start", this.onIDEReadStart);
emulator.remove_listener("ide-read-end", this.onIDEReadWriteEnd);
emulator.remove_listener("ide-write-end", this.onIDEReadWriteEnd);
// Screen
emulator.remove_listener("screen-set-size-graphical", console.log);
}
/**
* The virtual IDE is handling read (start).
*/
private onIDEReadStart() {
this.requestIdle(() => this.setState({ disk: "Read" }));
}
/**
* The virtual IDE is handling read/write (end).
*/
private onIDEReadWriteEnd() {
this.requestIdle(() => this.setState({ disk: "Idle" }));
}
/**
* Request an idle callback with a 3s timeout.
*
* @param fn
*/
private requestIdle(fn: () => void) {
(window as any).requestIdleCallback(fn, { timeout: 3000 });
}
/**
* Calculates what's up with the virtual cpu.
*/
private cpuCount() {
const { lastCounter, lastTick } = this.state;
const now = Date.now();
const instructionCounter = this.props.emulator.get_instruction_counter();
const ips = instructionCounter - lastCounter;
const deltaTime = now - lastTick;
this.setState({
lastTick: now,
lastCounter: instructionCounter,
cpu: Math.round(ips / deltaTime)
});
}
}

350
src/renderer/emulator.tsx Normal file
View File

@ -0,0 +1,350 @@
import * as React from "react";
import * as fs from "fs-extra";
import * as path from "path";
import { ipcRenderer, remote, shell } from "electron";
import { CONSTANTS, IPC_COMMANDS } from "../constants";
import { getDiskImageSize } from "../utils/disk-image-size";
import { CardStart } from "./card-start";
import { StartMenu } from "./start-menu";
import { CardFloppy } from "./card-floppy";
import { CardState } from "./card-state";
import { EmulatorInfo } from "./emulator-info";
export interface EmulatorState {
currentUiCard: string;
emulator?: any;
floppyFile?: string;
isBootingFresh: boolean;
isCursorCaptured: boolean;
isInfoDisplayed: boolean;
isRunning: boolean;
}
export class Emulator extends React.Component<{}, EmulatorState> {
private isQuitting = false;
private isResetting = false;
constructor(props: {}) {
super(props);
this.startEmulator = this.startEmulator.bind(this);
this.state = {
isBootingFresh: false,
isCursorCaptured: false,
isRunning: false,
currentUiCard: "start",
isInfoDisplayed: true
};
this.setupInputListeners();
this.setupIpcListeners();
this.setupUnloadListeners();
if (document.location.hash.includes("AUTO_START")) {
this.startEmulator();
}
}
/**
* We want to capture and release the mouse at appropriate times.
*/
public setupInputListeners() {
// ESC
document.onkeydown = evt => {
const { emulator, isCursorCaptured } = this.state;
evt = evt || window.event;
if (evt.keyCode === 27) {
if (isCursorCaptured) {
this.setState({ isCursorCaptured: false });
if (emulator) {
emulator.mouse_set_status(false);
}
document.exitPointerLock();
} else {
this.setState({ isCursorCaptured: true });
if (emulator) {
emulator.lock_mouse();
}
}
}
};
// Click
document.addEventListener("click", () => {
if (!this.state.isCursorCaptured) {
this.setState({ isCursorCaptured: true });
this.state.emulator.mouse_set_status(true);
this.state.emulator.lock_mouse();
}
});
}
/**
* Save the emulator's state to disk during exit.
*/
public setupUnloadListeners() {
const handleClose = async () => {
await this.saveState();
this.isQuitting = true;
remote.app.quit();
};
window.onbeforeunload = event => {
if (this.isQuitting) return;
if (this.isResetting) return;
handleClose();
event.preventDefault();
event.returnValue = false;
};
}
/**
* Setup the various IPC messages sent to the renderer
* from the main process
*/
public setupIpcListeners() {
ipcRenderer.on(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL, () => {
if (this.state.emulator && this.state.isRunning) {
this.state.emulator.keyboard_send_scancodes([
0x1d, // ctrl
0x38, // alt
0x53, // delete
// break codes
0x1d | 0x80,
0x38 | 0x80,
0x53 | 0x80
]);
}
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_RESTART, () => {
if (this.state.emulator && this.state.isRunning) {
this.state.emulator.restart();
}
});
ipcRenderer.on(IPC_COMMANDS.TOGGLE_INFO, () => {
this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
});
ipcRenderer.on(IPC_COMMANDS.SHOW_DISK_IMAGE, () => {
this.showDiskImage();
});
}
/**
* If the emulator isn't running, this is rendering the, erm, UI.
*
* 🤡
*/
public renderUI() {
const { isRunning, currentUiCard } = this.state;
if (isRunning) {
return null;
}
let card;
if (currentUiCard === "floppy") {
card = (
<CardFloppy
setFloppyPath={floppyFile => this.setState({ floppyFile })}
floppyPath={this.state.floppyFile}
/>
);
} else if (currentUiCard === "state") {
card = <CardState bootFromScratch={this.bootFromScratch} />;
} else {
card = <CardStart startEmulator={this.startEmulator} />;
}
return (
<>
{card}
<StartMenu
navigate={target => this.setState({ currentUiCard: target })}
/>
</>
);
}
/**
* Yaknow, render things and stuff.
*/
public render() {
return (
<>
<EmulatorInfo
emulator={this.state.emulator}
toggleInfo={() => {
this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
}}
/>
{this.renderUI()}
<div id="emulator">
<div></div>
<canvas></canvas>
</div>
</>
);
}
/**
* Boot the emulator without restoring state
*/
public bootFromScratch() {
this.setState({ isBootingFresh: true });
this.startEmulator();
}
/**
* Show the disk image on disk
*/
public showDiskImage() {
const imagePath = path
.join(__dirname, "images/windows95.img")
.replace("app.asar", "app.asar.unpacked");
shell.showItemInFolder(imagePath);
}
/**
* Start the actual emulator
*/
public async startEmulator() {
document.body.classList.remove("paused");
const imageSize = await getDiskImageSize();
const options = {
memory_size: 128 * 1024 * 1024,
video_memory_size: 32 * 1024 * 1024,
screen_container: document.getElementById("emulator"),
bios: {
url: "../../bios/seabios.bin"
},
vga_bios: {
url: "../../bios/vgabios.bin"
},
hda: {
url: "../../images/windows95.img",
async: true,
size: imageSize
},
fda: {
buffer: this.state.floppyFile
},
boot_order: 0x132
};
console.log(`Starting emulator with options`, options);
// New v86 instance
this.setState({
emulator: new V86Starter(options),
isRunning: true
});
// Restore state. We can't do this right away
// and randomly chose 500ms as the appropriate
// wait time (lol)
setTimeout(async () => {
if (!this.state.isBootingFresh) {
this.restoreState();
}
this.state.emulator.lock_mouse();
this.state.emulator.run();
}, 500);
}
/**
* Reset the emulator by reloading the whole page (lol)
*/
public async resetEmulator() {
this.isResetting = true;
document.location.hash = `#AUTO_START`;
document.location.reload();
}
/**
* Take the emulators state and write it to disk. This is possibly
* a fairly big file.
*/
public async saveState(): Promise<void> {
const { emulator } = this.state;
if (!emulator || !emulator.save_state) {
console.log(`restoreState: No emulator present`);
return;
}
return new Promise(resolve => {
emulator.save_state(async (error: Error, newState: ArrayBuffer) => {
if (error) {
console.warn(`saveState: Could not save state`, error);
return;
}
await fs.outputFile(CONSTANTS.STATE_PATH, Buffer.from(newState));
console.log(`saveState: Saved state to ${CONSTANTS.STATE_PATH}`);
resolve();
});
});
}
/**
* Restores state to the emulator.
*/
public restoreState() {
const { emulator } = this.state;
const state = this.getState();
// Nothing to do with if we don't have a state
if (!state) {
console.log(`restoreState: No state present, not restoring.`);
}
if (!emulator) {
console.log(`restoreState: No emulator present`);
}
try {
this.state.emulator.restore_state(state);
} catch (error) {
console.log(
`State: Could not read state file. Maybe none exists?`,
error
);
}
}
/**
* Returns the current machine's state - either what
* we have saved or alternatively the default state.
*
* @returns {ArrayBuffer}
*/
public getState(): ArrayBuffer | null {
const statePath = fs.existsSync(CONSTANTS.STATE_PATH)
? CONSTANTS.STATE_PATH
: CONSTANTS.DEFAULT_STATE_PATH;
if (fs.existsSync(statePath)) {
return fs.readFileSync(statePath).buffer;
}
return null;
}
}

2
src/renderer/global.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare const V86Starter: any;
declare const win95: any;

View File

@ -1,75 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="windows95">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Windows</title>
<script src="./lib/libv86.js"></script>
<link rel="stylesheet" href="style/vendor/95css.css">
<link rel="stylesheet" href="style/style.css">
</head>
<body class="paused">
<div id="status">
Disk: <span id="disk-status">Idle</span>
| CPU Speed: <span id="cpu-status">0</span>
| <a href="#" id="toggle-status">Hide</a>
</div>
<div id="setup">
<section id="section-start" class="visible">
<div class="btn btn-start" id="win95">
Start Windows 95
<br />
<small>Hit ESC to lock or unlock your mouse</small>
</div>
</section>
<section id="section-floppy">
<div class="card">
<div class="card-header">
<h2 class="card-title">Floppy Drive</h2>
</div class="card-header">
<div class="card-body">
<input id="floppy-input" type='file' style="display: none">
<p>windows95 comes with a virtual floppy drive. If you have floppy disk images in the "img" format, you can
mount them here.</p>
<p>Back in the 90s and before CD-ROM became a popular format, software was typically distributed on floppy
disks. Some developers have since released their apps or games for free, usually on virtual floppy disks
using the "img" format.</p>
<p>Once you've mounted a disk image, you might have to reboot your virtual windows95 machine from scratch.</p>
<p id="floppy-path">No floppy mounted</p>
<button id="floppy-select" class="btn">Mount floppy disk</button>
<button id="floppy-reboot" class="btn">Reboot from scratch</button>
</div>
</div>
</section>
<section id="section-state">
</section>
<section id="section-disk">
</section>
<nav class="nav nav-bottom">
<a href="#" id="start" class="nav-logo">
<img src="style/start.png" alt=""><span>Start</span>
</a>
<div class="nav-menu">
<a href="#" id="floppy" class="nav-link">Floppy Disk</a>
<a href="#" id="state" class="nav-link">Reset Machine</a>
<a href="#" id="disk" class="nav-link">Modify C: Drive</a>
</div>
</nav>
</div>
<div id="emulator" style="height: 100vh; width: 100vw">
<div style="white-space: pre; font: 14px monospace; line-height: 14px"></div>
<canvas style="display: none"></canvas>
</div>
<script type="module">
import("es6://renderer.js")
</script>
</body>
</html>

View File

@ -1,94 +0,0 @@
const $ = document.querySelector.bind(document)
const status = $('#status')
const diskStatus = $('#disk-status')
const cpuStatus = $('#cpu-status')
const toggleStatus = $('#toggle-status')
let lastCounter = 0
let lastTick = 0
let infoInterval = null
const onIDEReadStart = () => {
diskStatus.innerHTML = 'Read'
}
const onIDEReadWriteEnd = () => {
diskStatus.innerHTML = 'Idle'
}
toggleStatus.onclick = toggleInfo
/**
* Toggle the information display
*/
export function toggleInfo () {
if (status.style.display !== 'none') {
disableInfo()
} else {
enableInfo()
}
}
/**
* Start information gathering, but only if the panel is visible
*/
export function startInfoMaybe () {
if (status.style.display !== 'none') {
enableInfo()
}
}
/**
* Enable the gathering of information (and hide the little information tab)
*/
export function enableInfo () {
// Show the info thingy
status.style.display = 'block'
// We can only do the rest with an emulator
if (!window.emulator.add_listener) {
return
}
// Set listeners
window.emulator.add_listener('ide-read-start', onIDEReadStart)
window.emulator.add_listener('ide-read-end', onIDEReadWriteEnd)
window.emulator.add_listener('ide-write-end', onIDEReadWriteEnd)
window.emulator.add_listener('screen-set-size-graphical', console.log)
// Set an interval
infoInterval = setInterval(() => {
const now = Date.now()
const instructionCounter = window.emulator.get_instruction_counter()
const ips = instructionCounter - lastCounter
const deltaTime = now - lastTick
lastTick = now
lastCounter = instructionCounter
cpuStatus.innerHTML = Math.round(ips / deltaTime)
}, 500)
}
/**
* Disable the gathering of information (and hide the little information tab)
*/
export function disableInfo () {
// Hide the info thingy
status.style.display = 'none'
// Clear the interval
clearInterval(infoInterval)
infoInterval = null
// We can only do the rest with an emulator
if (!window.emulator.remove_listener) {
return
}
// Unset the listeners
window.emulator.remove_listener('ide-read-start', onIDEReadStart)
window.emulator.remove_listener('ide-read-end', onIDEReadWriteEnd)
window.emulator.remove_listener('ide-write-end', onIDEReadWriteEnd)
window.emulator.remove_listener('screen-set-size-graphical', console.log)
}

View File

@ -1,44 +0,0 @@
import { toggleInfo } from 'es6://info.js'
export function setupIpcListeners (start) {
const { windows95 } = window
windows95.addListener(windows95.IPC_COMMANDS.TOGGLE_INFO, () => {
toggleInfo()
})
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_RESTART, () => {
console.log(`Restarting machine`)
if (!window.emulator || !window.emulator.is_running) return
window.emulator.restart()
})
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_RESET, () => {
console.log(`Resetting machine`)
window.appState.isResetting = true
document.location.hash = `#AUTO_START`
document.location.reload()
})
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_CTRL_ALT_DEL, () => {
if (!window.emulator || !window.emulator.is_running) return
window.emulator.keyboard_send_scancodes([
0x1D, // ctrl
0x38, // alt
0x53, // delete
// break codes
0x1D | 0x80,
0x38 | 0x80,
0x53 | 0x80
])
})
windows95.addListener(windows95.IPC_COMMANDS.SHOW_DISK_IMAGE, () => {
windows95.showDiskImage()
})
}

View File

@ -1,47 +0,0 @@
export function setupCloseListener () {
window.appState.isQuitting = false
const handleClose = async () => {
await windows95.saveState()
window.appState.isQuitting = true
windows95.quit()
}
window.onbeforeunload = (event) => {
if (window.appState.isQuitting) return
if (window.appState.isResetting) return
handleClose()
event.preventDefault()
event.returnValue = false
}
}
export function setupEscListener () {
document.onkeydown = function (evt) {
evt = evt || window.event
if (evt.keyCode === 27) {
if (window.appState.cursorCaptured) {
window.appState.cursorCaptured = false
window.emulator.mouse_set_status(false)
document.exitPointerLock()
} else {
window.appState.cursorCaptured = true
window.emulator.lock_mouse()
}
}
}
}
function onDocumentClick () {
if (!window.appState.cursorCaptured) {
window.appState.cursorCaptured = true
window.emulator.mouse_set_status(true)
window.emulator.lock_mouse()
}
}
export function setupClickListener () {
document.removeEventListener('click', onDocumentClick)
document.addEventListener('click', onDocumentClick)
}

View File

@ -1,72 +0,0 @@
/* We're using modern esm imports here */
import { setupState } from 'es6://app-state.js'
import { setupClickListener, setupEscListener, setupCloseListener } from 'es6://listeners.js'
import { toggleSetup, setupButtons } from 'es6://buttons.js'
import { startInfoMaybe } from 'es6://info.js'
import { setupIpcListeners } from 'es6://ipc.js'
setupState()
/**
* The main method executing the VM.
*/
async function main () {
const imageSize = await window.windows95.getDiskImageSize()
const options = {
memory_size: 128 * 1024 * 1024,
video_memory_size: 32 * 1024 * 1024,
screen_container: document.getElementById('emulator'),
bios: {
url: './bios/seabios.bin'
},
vga_bios: {
url: './bios/vgabios.bin'
},
hda: {
url: '../images/windows95.img',
async: true,
size: imageSize
},
fda: {
buffer: window.appState.floppyFile || undefined
},
boot_order: 0x132
}
console.log(`Starting emulator with options`, options)
// New v86 instance
window.emulator = new V86Starter(options)
// Restore state. We can't do this right away
// and randomly chose 500ms as the appropriate
// wait time (lol)
setTimeout(async () => {
if (!window.appState.bootFresh) {
windows95.restoreState()
}
startInfoMaybe()
window.appState.cursorCaptured = true
window.emulator.lock_mouse()
window.emulator.run()
}, 500)
}
function start () {
document.body.className = ''
toggleSetup(false)
setupClickListener()
main()
}
setupIpcListeners(start)
setupEscListener()
setupCloseListener()
setupButtons(start)
if (document.location.hash.includes('AUTO_START')) {
start()
}

View File

@ -0,0 +1,39 @@
import * as React from "react";
export interface StartMenuProps {
navigate: (to: string) => void;
}
export class StartMenu extends React.Component<StartMenuProps, {}> {
constructor(props: StartMenuProps) {
super(props);
this.navigate = this.navigate.bind(this);
}
public render() {
return (
<nav className="nav nav-bottom">
<a onClick={this.navigate} href="#" id="start" className="nav-logo">
<img src="../../static/start.png" alt="" />
<span>Start</span>
</a>
<div className="nav-menu">
<a onClick={this.navigate} href="#" id="floppy" className="nav-link">
Floppy Disk
</a>
<a onClick={this.navigate} href="#" id="state" className="nav-link">
Reset Machine
</a>
<a onClick={this.navigate} href="#" id="disk" className="nav-link">
Modify C: Drive
</a>
</div>
</nav>
);
}
private navigate(event: React.SyntheticEvent<HTMLAnchorElement>) {
this.props.navigate(event.currentTarget.id);
}
}

0
src/renderer/status.tsx Normal file
View File

View File

@ -1,81 +0,0 @@
const fs = require('fs-extra')
const { CONSTANTS } = require('./constants')
/**
* Returns the current machine's state - either what
* we have saved or alternatively the default state.
*
* @returns {ArrayBuffer}
*/
function getState () {
const statePath = fs.existsSync(CONSTANTS.STATE_PATH)
? CONSTANTS.STATE_PATH
: CONSTANTS.DEFAULT_STATE_PATH
if (fs.existsSync(statePath)) {
return fs.readFileSync(statePath).buffer
}
}
/**
* Resets a saved state by simply deleting it.
*
* @returns {Promise<void>}
*/
async function resetState () {
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
return fs.remove(CONSTANTS.STATE_PATH)
}
}
/**
* Saves the current VM's state.
*
* @returns {Promise<void>}
*/
async function saveState () {
return new Promise((resolve) => {
if (!window.emulator || !window.emulator.save_state) {
return resolve()
}
window.emulator.save_state(async (error, newState) => {
if (error) {
console.warn(`State: Could not save state`, error)
return
}
await fs.outputFile(CONSTANTS.STATE_PATH, Buffer.from(newState))
console.log(`State: Saved state to ${CONSTANTS.STATE_PATH}`)
resolve()
})
})
}
/**
* Restores the VM's state.
*/
function restoreState () {
const state = getState()
// Nothing to do with if we don't have a state
if (!state) {
console.log(`State: No state present, not restoring.`)
}
try {
window.emulator.restore_state(state)
} catch (error) {
console.log(`State: Could not read state file. Maybe none exists?`, error)
}
}
module.exports = {
saveState,
restoreState,
resetState,
getState
}

8
src/utils/devmode.ts Normal file
View File

@ -0,0 +1,8 @@
/**
* Are we currently running in development mode?
*
* @returns {boolean}
*/
export function isDevMode() {
return !!process.defaultApp;
}

View File

@ -1,26 +0,0 @@
const fs = require('fs-extra')
const { CONSTANTS } = require('../constants')
/**
* Get the size of the disk image
*
* @returns {number}
*/
async function getDiskImageSize () {
try {
const stats = await fs.stat(CONSTANTS.IMAGE_PATH)
if (stats) {
return stats.size
}
} catch (error) {
console.warn(`Could not determine image size`, error)
}
return CONSTANTS.IMAGE_DEFAULT_SIZE
}
module.exports = {
getDiskImageSize
}

View File

@ -0,0 +1,22 @@
import * as fs from "fs-extra";
import { CONSTANTS } from "../constants";
/**
* Get the size of the disk image
*
* @returns {number}
*/
export async function getDiskImageSize() {
try {
const stats = await fs.stat(CONSTANTS.IMAGE_PATH);
if (stats) {
return stats.size;
}
} catch (error) {
console.warn(`Could not determine image size`, error);
}
return CONSTANTS.IMAGE_DEFAULT_SIZE;
}

16
static/index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>windows95</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../src/less/vendor/95css.css">
<link rel="stylesheet" href="../src/less/root.less">
<script src="../src/renderer/lib/libv86.js"></script>
</head>
<body class="paused windows95">
<div id="app"></div>
<script src="../src/renderer/app.tsx"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

7
tools/generateAssets.js Normal file
View File

@ -0,0 +1,7 @@
/* tslint:disable */
const { compileParcel } = require('./parcel-build')
module.exports = async () => {
await Promise.all([compileParcel()])
}

47
tools/parcel-build.js Normal file
View File

@ -0,0 +1,47 @@
/* tslint:disable */
const Bundler = require('parcel-bundler')
const path = require('path')
async function compileParcel (options = {}) {
const entryFiles = [
path.join(__dirname, '../static/index.html'),
path.join(__dirname, '../src/main/main.ts')
]
const bundlerOptions = {
outDir: './dist', // The out directory to put the build files in, defaults to dist
outFile: undefined, // The name of the outputFile
publicUrl: '../', // The url to server on, defaults to dist
watch: false, // whether to watch the files and rebuild them on change, defaults to process.env.NODE_ENV !== 'production'
cache: false, // Enabled or disables caching, defaults to true
cacheDir: '.cache', // The directory cache gets put in, defaults to .cache
contentHash: false, // Disable content hash from being included on the filename
minify: false, // Minify files, enabled if process.env.NODE_ENV === 'production'
scopeHoist: false, // turn on experimental scope hoisting/tree shaking flag, for smaller production bundles
target: 'electron', // browser/node/electron, defaults to browser
// https: { // Define a custom {key, cert} pair, use true to generate one or false to use http
// cert: './ssl/c.crt', // path to custom certificate
// key: './ssl/k.key' // path to custom key
// },
logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
hmr: true, // Enable or disable HMR while watching
hmrPort: 0, // The port the HMR socket runs on, defaults to a random free port (0 in node.js resolves to a random free port)
sourceMaps: true, // Enable or disable sourcemaps, defaults to enabled (minified builds currently always create sourcemaps)
hmrHostname: '', // A hostname for hot module reload, default to ''
detailedReport: false, // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled,
...options
}
const bundler = new Bundler(entryFiles, bundlerOptions)
// Run the bundler, this returns the main bundle
// Use the events if you're using watch mode as this promise will only trigger once and not for every rebuild
await bundler.bundle()
}
module.exports = {
compileParcel
}
if (require.main === module) compileParcel()

11
tools/parcel-watch.js Normal file
View File

@ -0,0 +1,11 @@
const { compileParcel } = require('./parcel-build')
async function watchParcel () {
return compileParcel({ watch: true })
}
module.exports = {
watchParcel
}
if (require.main === module) watchParcel()

30
tools/run-bin.js Normal file
View File

@ -0,0 +1,30 @@
/* tslint:disable */
const childProcess = require('child_process')
const path = require('path')
async function run (name, bin, args = []) {
await new Promise((resolve, reject) => {
console.info(`Running ${name}`)
const cmd = process.platform === 'win32' ? `${bin}.cmd` : bin
const child = childProcess.spawn(
path.resolve(__dirname, '..', 'node_modules', '.bin', cmd),
args,
{
cwd: path.resolve(__dirname, '..'),
stdio: 'inherit'
}
)
child.on('exit', (code) => {
console.log('')
if (code === 0) return resolve()
reject(new Error(`${name} failed`))
})
})
};
module.exports = {
run
}

13
tools/tsc.js Normal file
View File

@ -0,0 +1,13 @@
/* tslint:disable */
const { run } = require('./run-bin')
async function compileTypeScript () {
await run('TypeScript', 'tsc', ['-p', 'tsconfig.json'])
};
module.exports = {
compileTypeScript
}
if (require.main === module) compileTypeScript()

43
tsconfig.json Normal file
View File

@ -0,0 +1,43 @@
{
"compilerOptions": {
"outDir": "./dist",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"lib": [
"es2017",
"dom"
],
"noImplicitAny": true,
"noImplicitReturns": true,
"suppressImplicitAnyIndexErrors": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noImplicitThis": true,
"noUnusedParameters": true,
"importHelpers": true,
"noEmitHelpers": false,
"module": "commonjs",
"moduleResolution": "node",
"pretty": true,
"target": "es2017",
"jsx": "react",
"typeRoots": [
"./node_modules/@types"
],
"baseUrl": "."
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
],
"formatCodeOptions": {
"indentSize": 2,
"tabSize": 2
}
}