feat: Move to TypeScript
This commit is contained in:
parent
b7aa6a760d
commit
241606d097
|
@ -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": [],
|
||||
|
|
25
src/cache.js
25
src/cache.js
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
36
src/es6.js
36
src/es6.js
|
@ -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
|
||||
}
|
55
src/index.js
55
src/index.js
|
@ -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()
|
||||
}
|
||||
})
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
|
@ -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));
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export function shouldQuit() {
|
||||
return require("electron-squirrel-startup");
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { app } from "electron";
|
||||
|
||||
export function setupUpdates() {
|
||||
if (app.isPackaged) {
|
||||
require("update-electron-app")({
|
||||
repo: "felixrieseberg/windows95",
|
||||
updateInterval: "1 hour"
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
182
src/menu.js
182
src/menu.js
|
@ -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
|
||||
}
|
|
@ -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()
|
|
@ -1,9 +0,0 @@
|
|||
export function setupState () {
|
||||
window.appState = {
|
||||
isResetting: false,
|
||||
isQuitting: false,
|
||||
cursorCaptured: false,
|
||||
floppyFile: null,
|
||||
bootFresh: false
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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')
|
||||
}
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
declare const V86Starter: any;
|
||||
declare const win95: any;
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
81
src/state.js
81
src/state.js
|
@ -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
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Are we currently running in development mode?
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDevMode() {
|
||||
return !!process.defaultApp;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
@ -0,0 +1,7 @@
|
|||
/* tslint:disable */
|
||||
|
||||
const { compileParcel } = require('./parcel-build')
|
||||
|
||||
module.exports = async () => {
|
||||
await Promise.all([compileParcel()])
|
||||
}
|
|
@ -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()
|
|
@ -0,0 +1,11 @@
|
|||
const { compileParcel } = require('./parcel-build')
|
||||
|
||||
async function watchParcel () {
|
||||
return compileParcel({ watch: true })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
watchParcel
|
||||
}
|
||||
|
||||
if (require.main === module) watchParcel()
|
|
@ -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
|
||||
}
|
|
@ -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()
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue