Update all dependencies

React 19, Electron 41, TypeScript 6, electron-forge 7.8, plus everything
else to latest. Migrated to React 19 createRoot API, updated for Electron 41
session/window type changes, and adjusted tsconfig for TS 6 deprecations.
Regenerated @electron/packager patch for 18.4.4.
This commit is contained in:
Felix Rieseberg
2026-04-10 20:34:28 -07:00
parent a6d57c6538
commit 00943ae4da
19 changed files with 3791 additions and 2756 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ Microsoft.Trusted.Signing.Client*
trusted-signing-metadata.json
.env
electron-windows-sign.log
.npmrc

6191
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,28 +23,28 @@
},
"dependencies": {
"electron-squirrel-startup": "^1.0.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"update-electron-app": "^2.0.1"
"react": "^19.2.4",
"react-dom": "^19.2.4",
"update-electron-app": "^3.1.2"
},
"devDependencies": {
"@electron-forge/cli": "7.6.1",
"@electron-forge/maker-deb": "7.6.1",
"@electron-forge/maker-flatpak": "^7.6.1",
"@electron-forge/maker-rpm": "^7.6.1",
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/publisher-github": "^7.6.1",
"@types/node": "^20",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"dotenv": "^16.4.7",
"electron": "34.2.0",
"less": "^3.13.0",
"@electron-forge/cli": "7.8.3",
"@electron-forge/maker-deb": "7.8.3",
"@electron-forge/maker-flatpak": "^7.8.3",
"@electron-forge/maker-rpm": "^7.8.3",
"@electron-forge/maker-squirrel": "^7.8.3",
"@electron-forge/maker-zip": "^7.8.3",
"@electron-forge/publisher-github": "^7.8.3",
"@types/node": "^22.19.17",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"dotenv": "^17.3.1",
"electron": "41.2.0",
"less": "^4.6.4",
"parcel-bundler": "^1.12.5",
"patch-package": "^8.0.0",
"prettier": "^3.5.1",
"rimraf": "^6.0.1",
"typescript": "^5.7.3"
"patch-package": "^8.0.1",
"prettier": "^3.8.1",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
}
}

View File

@@ -1,8 +1,8 @@
diff --git a/node_modules/@electron/packager/dist/win32.js b/node_modules/@electron/packager/dist/win32.js
index 5399b3e..f3b6e88 100644
index d318f6c..bfde740 100644
--- a/node_modules/@electron/packager/dist/win32.js
+++ b/node_modules/@electron/packager/dist/win32.js
@@ -57,7 +57,26 @@ class WindowsApp extends platform_1.App {
@@ -65,7 +65,26 @@ class WindowsApp extends platform_1.App {
resOpts.iconPath = icon;
}
(0, common_1.debug)(`Running resedit with the options ${JSON.stringify(resOpts)}`);

View File

@@ -18,7 +18,6 @@ export async function clearStorageData() {
await session.defaultSession.clearStorageData({
storages: [
"appcache",
"cookies",
"filesystem",
"indexdb",
@@ -27,6 +26,6 @@ export async function clearStorageData() {
"websql",
"serviceworkers",
],
quotas: ["temporary", "persistent", "syncable"],
quotas: ["temporary"],
});
}

View File

@@ -1,6 +1,6 @@
export function encode(text: string) {
// Convert to windows-1252 compatible string by removing unsupported chars
let result = text.replaceAll(/[^\x00-\xFF]/g, '');
let result = text.replaceAll(/[^\x00-\xFF]/g, "");
// If result would be empty, return original
if (!result.trim()) {

View File

@@ -1,9 +1,9 @@
import { protocol, net } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { generateDirectoryListing } from './page-directory-listing';
import { generateErrorPage } from './page-error';
import { log } from '../logging';
import { protocol } from "electron";
import * as fs from "fs";
import * as path from "path";
import { generateDirectoryListing } from "./page-directory-listing";
import { generateErrorPage } from "./page-error";
import { log } from "../logging";
export interface FileEntry {
name: string;
@@ -11,18 +11,15 @@ export interface FileEntry {
stats: fs.Stats;
}
export const APP_INTERCEPT = 'http://windows95/';
export const MY_COMPUTER_INTERCEPT = 'http://my-computer/';
export const APP_INTERCEPT = "http://windows95/";
export const MY_COMPUTER_INTERCEPT = "http://my-computer/";
const interceptedUrls = [
MY_COMPUTER_INTERCEPT,
APP_INTERCEPT
];
const interceptedUrls = [MY_COMPUTER_INTERCEPT, APP_INTERCEPT];
export function setupFileServer() {
// Register protocol handler for our custom schema
protocol.handle('http', async (request) => {
if (!interceptedUrls.some(url => request.url.startsWith(url))) {
protocol.handle("http", async (request) => {
if (!interceptedUrls.some((url) => request.url.startsWith(url))) {
return fetch(request.url, {
headers: request.headers,
method: request.method,
@@ -33,19 +30,22 @@ export function setupFileServer() {
try {
const { fullPath, decodedPath } = getFilePath(request.url);
log(`FileServer: Handling request for ${request.url}`, { fullPath, decodedPath });
log(`FileServer: Handling request for ${request.url}`, {
fullPath,
decodedPath,
});
// Check if path exists
if (!fs.existsSync(fullPath)) {
return new Response(generateErrorPage(
'File or Directory Not Found',
decodedPath
), {
status: 404,
headers: {
'Content-Type': 'text/html'
}
});
return new Response(
generateErrorPage("File or Directory Not Found", decodedPath),
{
status: 404,
headers: {
"Content-Type": "text/html",
},
},
);
}
// Check if it's a directory
@@ -53,7 +53,7 @@ export function setupFileServer() {
if (stats.isDirectory()) {
// If we're in an app-intercept, check if there's an index.htm file in the directory
if (request.url.startsWith(APP_INTERCEPT)) {
const indexHtmlPath = path.join(fullPath, 'index.htm');
const indexHtmlPath = path.join(fullPath, "index.htm");
if (fs.existsSync(indexHtmlPath)) {
return serveFile(indexHtmlPath);
}
@@ -65,24 +65,27 @@ export function setupFileServer() {
return new Response(listing, {
status: 200,
headers: {
'Content-Type': 'text/html'
}
"Content-Type": "text/html",
},
});
} else {
try {
return await serveFile(fullPath);
} catch (error) {
// Handle specific file read errors
if (error.code === 'EACCES') {
return new Response(generateErrorPage(
'Access Denied',
'You do not have permission to access this file'
), {
status: 403,
headers: {
'Content-Type': 'text/html'
}
});
if ((error as NodeJS.ErrnoException).code === "EACCES") {
return new Response(
generateErrorPage(
"Access Denied",
"You do not have permission to access this file",
),
{
status: 403,
headers: {
"Content-Type": "text/html",
},
},
);
}
// Re-throw other errors to be caught by outer try-catch
@@ -90,15 +93,16 @@ export function setupFileServer() {
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const errorPage = generateErrorPage(
'Internal Server Error',
`An error occurred while processing your request: ${error.message}`
"Internal Server Error",
`An error occurred while processing your request: ${message}`,
);
return new Response(errorPage, {
status: 500,
headers: {
'Content-Type': 'text/html'
}
"Content-Type": "text/html",
},
});
}
});
@@ -110,14 +114,18 @@ function getFilePath(url: string) {
let decodedPath: string;
if (url.startsWith(APP_INTERCEPT)) {
fullPath = path.resolve(__dirname, '../../../static/www', url.replace(APP_INTERCEPT, ''));
decodedPath = '.';
fullPath = path.resolve(
__dirname,
"../../../static/www",
url.replace(APP_INTERCEPT, ""),
);
decodedPath = ".";
} else if (url.startsWith(MY_COMPUTER_INTERCEPT)) {
urlPath = url.replace(MY_COMPUTER_INTERCEPT, '');
urlPath = url.replace(MY_COMPUTER_INTERCEPT, "");
decodedPath = decodeURIComponent(urlPath);
fullPath = path.join('/', decodedPath);
fullPath = path.join("/", decodedPath);
} else {
throw new Error('Invalid URL');
throw new Error("Invalid URL");
}
return { fullPath, decodedPath };
@@ -128,19 +136,19 @@ async function serveFile(fullPath: string): Promise<Response> {
// Determine content type based on file extension
const ext = path.extname(fullPath).toLowerCase();
let contentType = 'application/octet-stream';
let contentType = "application/octet-stream";
// Common content types
const contentTypes: Record<string, string> = {
'.htm': 'text/html',
'.html': 'text/html',
'.txt': 'text/plain',
'.css': 'text/css',
'.js': 'text/javascript',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif'
".htm": "text/html",
".html": "text/html",
".txt": "text/plain",
".css": "text/css",
".js": "text/javascript",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
};
if (ext in contentTypes) {
@@ -150,8 +158,7 @@ async function serveFile(fullPath: string): Promise<Response> {
return new Response(fileData, {
status: 200,
headers: {
'Content-Type': contentType
}
"Content-Type": contentType,
},
});
}

View File

@@ -1,37 +1,39 @@
import { Stats } from "fs";
import { settings } from "../settings";
import { FileEntry } from "./fileserver";
const FILES_TO_HIDE_ON_DARWIN: string[] = [
'.DS_Store',
'.localized',
'.Trashes',
'.fseventsd',
'.Spotlight-V100',
'.file',
'.hotfiles.btree',
'.DocumentRevisions-V100',
'.TemporaryItems',
'.file (resource fork files)',
'.VolumeIcon.icns',
".DS_Store",
".localized",
".Trashes",
".fseventsd",
".Spotlight-V100",
".file",
".hotfiles.btree",
".DocumentRevisions-V100",
".TemporaryItems",
".file (resource fork files)",
".VolumeIcon.icns",
];
const FILES_TO_HIDE_ON_WINDOWS: string[] = [
'desktop.ini',
'Thumbs.db',
'ehthumbs.db',
'ehthumbs.db-shm',
'ehthumbs.db-wal',
"desktop.ini",
"Thumbs.db",
"ehthumbs.db",
"ehthumbs.db-shm",
"ehthumbs.db-wal",
];
const FILES_TO_HIDE_ON_LINUX: string[] = [];
export function shouldHideFile(file: FileEntry) {
if (isHiddenFile(file) && !settings.get('isFileServerShowingHiddenFiles')) {
if (isHiddenFile(file) && !settings.get("isFileServerShowingHiddenFiles")) {
return true;
}
if (isSystemHiddenFile(file) && !settings.get('isFileServerShowingSystemHiddenFiles')) {
if (
isSystemHiddenFile(file) &&
!settings.get("isFileServerShowingSystemHiddenFiles")
) {
return true;
}
@@ -39,15 +41,15 @@ export function shouldHideFile(file: FileEntry) {
}
export function isHiddenFile(file: FileEntry) {
if (process.platform === 'win32') {
if (process.platform === "win32") {
return (file.stats.mode & 0x2) === 0x2;
} else {
return file.name.startsWith('.');
return file.name.startsWith(".");
}
}
export function isSystemHiddenFile(file: FileEntry) {
return getFilesToHide().some(hiddenFile => file.name.endsWith(hiddenFile));
return getFilesToHide().some((hiddenFile) => file.name.endsWith(hiddenFile));
}
let _filesToHide: string[];
@@ -57,9 +59,9 @@ function getFilesToHide() {
return _filesToHide;
}
if (process.platform === 'darwin') {
if (process.platform === "darwin") {
_filesToHide = FILES_TO_HIDE_ON_DARWIN;
} else if (process.platform === 'win32') {
} else if (process.platform === "win32") {
_filesToHide = FILES_TO_HIDE_ON_WINDOWS;
} else {
_filesToHide = FILES_TO_HIDE_ON_LINUX;
@@ -67,6 +69,3 @@ function getFilesToHide() {
return _filesToHide;
}

View File

@@ -7,29 +7,31 @@ import { encode, getEncoding } from "./encoding";
import { log } from "console";
import { app } from "electron";
export function generateDirectoryListing(currentPath: string, files: string[]): string {
const parentPath = path.dirname(currentPath || '/');
const title = currentPath === '/' ? 'My Host Computer' : `Directory: ${encode(currentPath)}`;
export function generateDirectoryListing(
currentPath: string,
files: string[],
): string {
const parentPath = path.dirname(currentPath || "/");
const title =
currentPath === "/"
? "My Host Computer"
: `Directory: ${encode(currentPath)}`;
// Get file info and sort (directories first, then alphabetically)
const items = files
.map(name => {
.map((name) => {
const fullPath = path.join(currentPath, name);
let stats: fs.Stats;
try {
stats = fs.statSync(fullPath);
const stats = fs.statSync(fullPath);
return { name, fullPath, stats } as FileEntry;
} catch (error) {
log(`FileServer: Failed to get stats for ${fullPath}: ${error}`);
stats = new fs.Stats();
return null;
}
return {
name,
fullPath,
stats
} as FileEntry;
})
.filter(entry => entry.stats && !shouldHideFile(entry))
.filter(
(entry): entry is FileEntry => entry !== null && !shouldHideFile(entry),
)
.sort((a, b) => {
if (a.stats.isDirectory() !== b.stats.isDirectory()) {
return a.stats.isDirectory() ? -1 : 1;
@@ -37,7 +39,7 @@ export function generateDirectoryListing(currentPath: string, files: string[]):
return a.name.localeCompare(b.name);
})
.map(getFileLiHtml)
.join('')
.join("");
// Generate very simple HTML that works in IE 5.5
return `
@@ -60,7 +62,7 @@ export function generateDirectoryListing(currentPath: string, files: string[]):
function getParentFolderLinkHtml(parentPath: string) {
return `
${getIconHtml('folder.gif')}
${getIconHtml("folder.gif")}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(parentPath)}">
[Parent Directory]
</a>
@@ -68,10 +70,10 @@ function getParentFolderLinkHtml(parentPath: string) {
}
function getDesktopLinkHtml() {
const desktopPath = app.getPath('desktop');
const desktopPath = app.getPath("desktop");
return `
${getIconHtml('desktop.gif')}
${getIconHtml("desktop.gif")}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(desktopPath)}">
Desktop
</a>
@@ -79,10 +81,10 @@ function getDesktopLinkHtml() {
}
function getDownloadsLinkHtml() {
const downloadsPath = app.getPath('downloads');
const downloadsPath = app.getPath("downloads");
return `
${getIconHtml('network.gif')}
${getIconHtml("network.gif")}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(downloadsPath)}">
Downloads
</a>
@@ -95,8 +97,12 @@ function getIconHtml(icon: string) {
function getFileLiHtml(entry: FileEntry) {
const encodedPath = encodeURI(entry.fullPath);
const sizeDisplay = entry.stats.isDirectory() ? '' : ` (${formatFileSize(entry.stats.size)})`;
const icon = entry.stats.isDirectory() ? getIconHtml('folder.gif') : getIconHtml('doc.gif');
const sizeDisplay = entry.stats.isDirectory()
? ""
: ` (${formatFileSize(entry.stats.size)})`;
const icon = entry.stats.isDirectory()
? getIconHtml("folder.gif")
: getIconHtml("doc.gif");
return `<li>
${icon}
@@ -112,9 +118,9 @@ function getDisplayName(entry: FileEntry) {
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}

View File

@@ -1,7 +1,10 @@
import { getEncoding } from "./encoding";
import { MY_COMPUTER_INTERCEPT } from "./fileserver";
export function generateErrorPage(errorMessage: string, requestedPath: string): string {
export function generateErrorPage(
errorMessage: string,
requestedPath: string,
): string {
return `
<html>
<head>

View File

@@ -8,7 +8,7 @@ import { getOrCreateWindow } from "./windows";
import { setupMenu } from "./menu";
import { setupIpcListeners } from "./ipc";
import { setupSession } from "./session";
import { setupFileServer } from './fileserver/fileserver';
import { setupFileServer } from "./fileserver/fileserver";
/**
* Handle the app's "ready" event. This is essentially

View File

@@ -64,7 +64,7 @@ async function createMenu({ isRunning } = { isRunning: false }) {
}
})(),
click: function (_item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow instanceof BrowserWindow) {
focusedWindow.webContents.toggleDevTools();
}
},
@@ -166,12 +166,13 @@ async function createMenu({ isRunning } = { isRunning: false }) {
label: "Reset",
click: async () => {
const result = await dialog.showMessageBox({
type: 'warning',
buttons: ['Reset', 'Cancel'],
type: "warning",
buttons: ["Reset", "Cancel"],
defaultId: 1,
title: 'Reset Machine',
message: 'Are you sure you want to reset the machine?',
detail: 'This will delete the machine state, including all changes you have made.',
title: "Reset Machine",
message: "Are you sure you want to reset the machine?",
detail:
"This will delete the machine state, including all changes you have made.",
});
if (result.response === 0) {

View File

@@ -3,16 +3,14 @@ import { session } from "electron";
export function setupSession() {
const s = session.defaultSession;
s.webRequest.onBeforeSendHeaders(
(details, callback) => {
callback({ requestHeaders: { Origin: '*', ...details.requestHeaders } });
},
);
s.webRequest.onBeforeSendHeaders((details, callback) => {
callback({ requestHeaders: { Origin: "*", ...details.requestHeaders } });
});
s.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
'Access-Control-Allow-Origin': ['*'],
"Access-Control-Allow-Origin": ["*"],
...details.responseHeaders,
},
});

View File

@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { app } from 'electron';
import * as fs from "fs";
import * as path from "path";
import { app } from "electron";
export interface Settings {
isFileServerEnabled: boolean;
@@ -19,14 +19,14 @@ class SettingsManager {
private data: Settings;
constructor() {
this.filePath = path.join(app.getPath('userData'), 'settings.json');
this.filePath = path.join(app.getPath("userData"), "settings.json");
this.data = this.load();
}
private load(): Settings {
try {
if (fs.existsSync(this.filePath)) {
const fileContent = fs.readFileSync(this.filePath, 'utf8');
const fileContent = fs.readFileSync(this.filePath, "utf8");
const parsed = JSON.parse(fileContent);
return {
@@ -35,7 +35,7 @@ class SettingsManager {
};
}
} catch (error) {
console.error('Error loading settings:', error);
console.error("Error loading settings:", error);
}
return DEFAULT_SETTINGS;
@@ -45,7 +45,7 @@ class SettingsManager {
try {
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
} catch (error) {
console.error('Error saving settings:', error);
console.error("Error saving settings:", error);
}
}

View File

@@ -2,7 +2,8 @@ import { app } from "electron";
export function setupUpdates() {
if (app.isPackaged) {
require("update-electron-app")({
const { updateElectronApp } = require("update-electron-app");
updateElectronApp({
repo: "felixrieseberg/windows95",
updateInterval: "1 hour",
});

View File

@@ -18,9 +18,9 @@ export class App {
* Initial setup call, loading Monaco and kicking off the React
* render process.
*/
public async setup(): Promise<void | Element> {
public async setup(): Promise<void> {
const React = await import("react");
const { render } = await import("react-dom");
const { createRoot } = await import("react-dom/client");
const { Emulator } = await import("./emulator");
const className = `${process.platform}`;
@@ -30,9 +30,8 @@ export class App {
</div>
);
const rendered = render(app, document.getElementById("app"));
return rendered;
const root = createRoot(document.getElementById("app")!);
root.render(app);
}
}

View File

@@ -75,7 +75,7 @@ export class CardSettings extends React.Component<
"iso" format.
</p>
<p id="floppy-path">
{cdrom ? `Inserted CD: ${cdrom?.path}` : `No CD mounted`}
{cdrom ? `Inserted CD: ${cdrom?.name}` : `No CD mounted`}
</p>
<button
className="btn"

View File

@@ -207,7 +207,9 @@ export class Emulator extends React.Component<{}, EmulatorState> {
<>
{card}
<StartMenu
navigate={(target) => this.setState({ currentUiCard: target as "start" | "settings" })}
navigate={(target) =>
this.setState({ currentUiCard: target as "start" | "settings" })
}
/>
</>
);
@@ -281,7 +283,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
vga_memory_size: 64 * 1024 * 1024,
screen: {
container: document.getElementById("emulator"),
scale: 0
scale: 0,
},
preserve_mac_from_state_image: true,
net_device: {
@@ -402,7 +404,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
try {
const newState = await emulator.save_state();
await fs.promises.writeFile(statePath, Buffer.from(newState), {
flush: true
flush: true,
});
} catch (error) {
console.warn(`saveState: Could not save state`, error);
@@ -524,7 +526,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
const canvas = document.getElementById("emulator-canvas");
if (canvas instanceof HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
ctx?.clearRect(0, 0, canvas.width, canvas.height);
}
}

View File

@@ -20,13 +20,13 @@
"noEmitHelpers": false,
"module": "commonjs",
"moduleResolution": "node",
"ignoreDeprecations": "6.0",
"pretty": true,
"target": "es2023",
"jsx": "react",
"typeRoots": [
"./node_modules/@types"
],
"baseUrl": "."
]
},
"include": [
"src/**/*"