diff --git a/HELP.md b/HELP.md index 3700dd5..85e73b8 100644 --- a/HELP.md +++ b/HELP.md @@ -11,25 +11,4 @@ back to Window Mode. (Thanks to @DisplacedGamers for that wisdom) On the app's home screen, select "Settings" in the lower menu. Then, delete your machine's state before starting it again - this time hopefully without issues. -## I want to install additional apps or games -If you are running macOS, or Linux, you can probably "mount" the -virtual hard drive used by `windows95` to add files. Hit the "Modify C: Drive" -button, which will take you to the disk image. - -On macOS, double-click the disk image to open it. - -On Windows 10, Windows will _think_ that it can open up the image, but will -actually fail to do so. Use a tool [like OSFMount][osfmount] to mount your -disk image. - -On Linux, search the Internet for instructions on how to mount an `img` disk -image on your distribution. It's likely that you'll be able to run `mount` -with the image as input. - -[osfmount]: https://www.osforensics.com/tools/mount-disk-images.html - -## What's the FrontPage Username and Password? -Username: windows95 - -Password: password diff --git a/src/constants.ts b/src/constants.ts index 55ea5f2..4ff64a5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,7 @@ import * as path from "path"; export const CONSTANTS = { - IMAGE_PATH: path.join(__dirname, "../../images/windows95.img"), + IMAGE_PATH: path.join(__dirname, "../../images/windows95_v4.raw"), IMAGE_DEFAULT_SIZE: 1073741824, // 1GB DEFAULT_STATE_PATH: path.join(__dirname, "../../images/default-state.bin"), }; diff --git a/src/main/fileserver/encoding.ts b/src/main/fileserver/encoding.ts new file mode 100644 index 0000000..68422cc --- /dev/null +++ b/src/main/fileserver/encoding.ts @@ -0,0 +1,15 @@ +export function encode(text: string) { + // Convert to windows-1252 compatible string by removing unsupported chars + let result = text.replaceAll(/[^\x00-\xFF]/g, ''); + + // If result would be empty, return original + if (!result.trim()) { + return text; + } + + return result; +} + +export function getEncoding() { + return ``; +} diff --git a/src/main/fileserver/fileserver.ts b/src/main/fileserver/fileserver.ts new file mode 100644 index 0000000..a11c2f7 --- /dev/null +++ b/src/main/fileserver/fileserver.ts @@ -0,0 +1,157 @@ +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'; + +export interface FileEntry { + name: string; + fullPath: string; + stats: fs.Stats; +} + +export const APP_INTERCEPT = 'http://windows95/'; +export const MY_COMPUTER_INTERCEPT = 'http://my-computer/'; + +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))) { + return fetch(request.url, { + headers: request.headers, + method: request.method, + body: request.body, + }); + } + + try { + const { fullPath, decodedPath } = getFilePath(request.url); + + 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' + } + }); + } + + // Check if it's a directory + const stats = await fs.promises.stat(fullPath); + 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'); + if (fs.existsSync(indexHtmlPath)) { + return serveFile(indexHtmlPath); + } + } + + // Generate directory listing + const files = await fs.promises.readdir(fullPath); + const listing = generateDirectoryListing(fullPath, files); + return new Response(listing, { + status: 200, + headers: { + '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' + } + }); + } + + // Re-throw other errors to be caught by outer try-catch + throw error; + } + } + } catch (error) { + const errorPage = generateErrorPage( + 'Internal Server Error', + `An error occurred while processing your request: ${error.message}` + ); + return new Response(errorPage, { + status: 500, + headers: { + 'Content-Type': 'text/html' + } + }); + } + }); +} + +function getFilePath(url: string) { + let urlPath: string; + let fullPath: string; + let decodedPath: string; + + if (url.startsWith(APP_INTERCEPT)) { + fullPath = path.resolve(__dirname, '../../../static/www', url.replace(APP_INTERCEPT, '')); + decodedPath = '.'; + } else if (url.startsWith(MY_COMPUTER_INTERCEPT)) { + urlPath = url.replace(MY_COMPUTER_INTERCEPT, ''); + decodedPath = decodeURIComponent(urlPath); + fullPath = path.join('/', decodedPath); + } else { + throw new Error('Invalid URL'); + } + + return { fullPath, decodedPath }; +} + +async function serveFile(fullPath: string): Promise { + const fileData = await fs.promises.readFile(fullPath); + + // Determine content type based on file extension + const ext = path.extname(fullPath).toLowerCase(); + let contentType = 'application/octet-stream'; + + // Common content types + const contentTypes: Record = { + '.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) { + contentType = contentTypes[ext]; + } + + return new Response(fileData, { + status: 200, + headers: { + 'Content-Type': contentType + } + }); +} + diff --git a/src/main/fileserver/hide-files.ts b/src/main/fileserver/hide-files.ts new file mode 100644 index 0000000..8b84cc2 --- /dev/null +++ b/src/main/fileserver/hide-files.ts @@ -0,0 +1,72 @@ +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', +]; + +const FILES_TO_HIDE_ON_WINDOWS: string[] = [ + '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')) { + return true; + } + + if (isSystemHiddenFile(file) && !settings.get('isFileServerShowingSystemHiddenFiles')) { + return true; + } + + return false; +} + +export function isHiddenFile(file: FileEntry) { + if (process.platform === 'win32') { + return (file.stats.mode & 0x2) === 0x2; + } else { + return file.name.startsWith('.'); + } +} + +export function isSystemHiddenFile(file: FileEntry) { + return getFilesToHide().some(hiddenFile => file.name.endsWith(hiddenFile)); +} + +let _filesToHide: string[]; + +function getFilesToHide() { + if (_filesToHide) { + return _filesToHide; + } + + if (process.platform === 'darwin') { + _filesToHide = FILES_TO_HIDE_ON_DARWIN; + } else if (process.platform === 'win32') { + _filesToHide = FILES_TO_HIDE_ON_WINDOWS; + } else { + _filesToHide = FILES_TO_HIDE_ON_LINUX; + } + + return _filesToHide; +} + + + diff --git a/src/main/fileserver/page-directory-listing.ts b/src/main/fileserver/page-directory-listing.ts new file mode 100644 index 0000000..ef96004 --- /dev/null +++ b/src/main/fileserver/page-directory-listing.ts @@ -0,0 +1,120 @@ +import path from "path"; +import fs from "fs"; + +import { APP_INTERCEPT, FileEntry, MY_COMPUTER_INTERCEPT } from "./fileserver"; +import { shouldHideFile } from "./hide-files"; +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)}`; + + // Get file info and sort (directories first, then alphabetically) + const items = files + .map(name => { + const fullPath = path.join(currentPath, name); + let stats: fs.Stats; + try { + stats = fs.statSync(fullPath); + } catch (error) { + log(`FileServer: Failed to get stats for ${fullPath}: ${error}`); + stats = new fs.Stats(); + } + + return { + name, + fullPath, + stats + } as FileEntry; + }) + .filter(entry => entry.stats && !shouldHideFile(entry)) + .sort((a, b) => { + if (a.stats.isDirectory() !== b.stats.isDirectory()) { + return a.stats.isDirectory() ? -1 : 1; + } + return a.name.localeCompare(b.name); + }) + .map(getFileLiHtml) + .join('') + + // Generate very simple HTML that works in IE 5.5 + return ` + + + ${getEncoding()} + ${title} + + +

${title}

+

${getParentFolderLinkHtml(parentPath)} | ${getDesktopLinkHtml()} | ${getDownloadsLinkHtml()}

+

+

+ + + `; +} + +function getParentFolderLinkHtml(parentPath: string) { + return ` + ${getIconHtml('folder.gif')} + + [Parent Directory] + + `; +} + +function getDesktopLinkHtml() { + const desktopPath = app.getPath('desktop'); + + return ` + ${getIconHtml('desktop.gif')} + + Desktop + + `; +} + +function getDownloadsLinkHtml() { + const downloadsPath = app.getPath('downloads'); + + return ` + ${getIconHtml('network.gif')} + + Downloads + + `; +} + +function getIconHtml(icon: string) { + return ``; +} + +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'); + + return `
  • + ${icon} + + ${getDisplayName(entry)} + + ${sizeDisplay} +
  • `; +} + +function getDisplayName(entry: FileEntry) { + return encode(entry.stats.isDirectory() ? `[${entry.name}]` : entry.name); +} + +function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + 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]; +} diff --git a/src/main/fileserver/page-error.ts b/src/main/fileserver/page-error.ts new file mode 100644 index 0000000..94cfe56 --- /dev/null +++ b/src/main/fileserver/page-error.ts @@ -0,0 +1,22 @@ +import { getEncoding } from "./encoding"; +import { MY_COMPUTER_INTERCEPT } from "./fileserver"; + +export function generateErrorPage(errorMessage: string, requestedPath: string): string { + return ` + + + ${getEncoding()} + Error - File Not Found + + +

    Error: ${errorMessage}

    +

    windows95 failed to find the file or directory on your host computer: ${requestedPath}

    +

    Options:

    + + + + `; +} diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 84d3501..6c6fc58 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -5,7 +5,7 @@ import { IPC_COMMANDS } from "../constants"; export function setupIpcListeners() { ipcMain.handle(IPC_COMMANDS.GET_STATE_PATH, () => { - return path.join(app.getPath("userData"), "state-v3.bin"); + return path.join(app.getPath("userData"), "state-v4.bin"); }); ipcMain.handle(IPC_COMMANDS.APP_QUIT, () => { diff --git a/src/main/logging.ts b/src/main/logging.ts new file mode 100644 index 0000000..0a71210 --- /dev/null +++ b/src/main/logging.ts @@ -0,0 +1,3 @@ +export function log(message: string, ...args: unknown[]) { + console.log(`[${new Date().toLocaleString()}] ${message}`, ...args); +} diff --git a/src/main/main.ts b/src/main/main.ts index 7960590..8d5d609 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -7,6 +7,8 @@ import { setupUpdates } from "./update"; import { getOrCreateWindow } from "./windows"; import { setupMenu } from "./menu"; import { setupIpcListeners } from "./ipc"; +import { setupSession } from "./session"; +import { setupFileServer } from './fileserver/fileserver'; /** * Handle the app's "ready" event. This is essentially @@ -15,11 +17,13 @@ import { setupIpcListeners } from "./ipc"; export async function onReady() { if (!isDevMode()) process.env.NODE_ENV = "production"; + setupSession(); setupIpcListeners(); getOrCreateWindow(); setupAboutPanel(); setupMenu(); setupUpdates(); + setupFileServer(); } /** diff --git a/src/main/menu.ts b/src/main/menu.ts index f990f54..09136aa 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -3,6 +3,7 @@ import { app, shell, Menu, BrowserWindow, ipcMain } from "electron"; import { clearCaches } from "../cache"; import { IPC_COMMANDS } from "../constants"; import { isDevMode } from "../utils/devmode"; +import { log } from "./logging"; const LINKS = { homepage: "https://www.twitter.com/felixrieseberg", @@ -26,10 +27,10 @@ function send(cmd: string) { const windows = BrowserWindow.getAllWindows(); if (windows[0]) { - console.log(`Sending "${cmd}"`); + log(`Sending "${cmd}"`); windows[0].webContents.send(cmd); } else { - console.log(`Tried to send "${cmd}", but could not find window`); + log(`Tried to send "${cmd}", but could not find window`); } } diff --git a/src/main/session.ts b/src/main/session.ts new file mode 100644 index 0000000..f91bb7a --- /dev/null +++ b/src/main/session.ts @@ -0,0 +1,20 @@ +import { session } from "electron"; + +export function setupSession() { + const s = session.defaultSession; + + s.webRequest.onBeforeSendHeaders( + (details, callback) => { + callback({ requestHeaders: { Origin: '*', ...details.requestHeaders } }); + }, + ); + + s.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + 'Access-Control-Allow-Origin': ['*'], + ...details.responseHeaders, + }, + }); + }); +} diff --git a/src/main/settings.ts b/src/main/settings.ts new file mode 100644 index 0000000..7be8f49 --- /dev/null +++ b/src/main/settings.ts @@ -0,0 +1,72 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { app } from 'electron'; + +export interface Settings { + isFileServerEnabled: boolean; + isFileServerShowingHiddenFiles: boolean; + isFileServerShowingSystemHiddenFiles: boolean; +} + +const DEFAULT_SETTINGS: Settings = { + isFileServerEnabled: true, + isFileServerShowingHiddenFiles: false, + isFileServerShowingSystemHiddenFiles: false, +}; + +class SettingsManager { + private filePath: string; + private data: Settings; + + constructor() { + 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 parsed = JSON.parse(fileContent); + + return { + ...DEFAULT_SETTINGS, + ...parsed, + }; + } + } catch (error) { + console.error('Error loading settings:', error); + } + + return DEFAULT_SETTINGS; + } + + private save(): void { + try { + fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2)); + } catch (error) { + console.error('Error saving settings:', error); + } + } + + get(key: keyof Settings): any { + return this.data[key]; + } + + set(key: keyof Settings, value: any): void { + this.data[key] = value; + this.save(); + } + + delete(key: keyof Settings): void { + delete this.data[key]; + this.save(); + } + + clear(): void { + this.data = DEFAULT_SETTINGS; + this.save(); + } +} + +export const settings = new SettingsManager(); diff --git a/src/main/windows.ts b/src/main/windows.ts index 53b52e9..d850056 100644 --- a/src/main/windows.ts +++ b/src/main/windows.ts @@ -24,9 +24,6 @@ export function getOrCreateWindow(): BrowserWindow { mainWindow.webContents.on("will-navigate", (event, url) => handleNavigation(event, url), ); - mainWindow.webContents.on("new-window", (event, url) => - handleNavigation(event, url), - ); mainWindow.on("closed", () => { mainWindow = null; diff --git a/src/renderer/emulator.tsx b/src/renderer/emulator.tsx index 7bcdff6..4102618 100644 --- a/src/renderer/emulator.tsx +++ b/src/renderer/emulator.tsx @@ -279,8 +279,13 @@ export class Emulator extends React.Component<{}, EmulatorState> { const options = { wasm_path: path.join(__dirname, "build/v86.wasm"), memory_size: 128 * 1024 * 1024, - vga_memory_size: 32 * 1024 * 1024, + vga_memory_size: 64 * 1024 * 1024, screen_container: document.getElementById("emulator"), + preserve_mac_from_state_image: true, + net_device: { + relay_url: "fetch", + type: "ne2k", + }, bios: { url: path.join(__dirname, "../../bios/seabios.bin"), }, @@ -367,7 +372,7 @@ export class Emulator extends React.Component<{}, EmulatorState> { } /** - * Reset the emulator by reloading the whole page (lol) + * Reset the emulator by reloading the whole page */ private async resetEmulator() { this.isResetting = true; diff --git a/static/www/apps.htm b/static/www/apps.htm new file mode 100644 index 0000000..e18d852 --- /dev/null +++ b/static/www/apps.htm @@ -0,0 +1,20 @@ + + + windows95 Help + + + + + + +
    + + windows95 Apps & Games +
    + +

    I've installed a few apps and games for you to try out. Check out the Games folder on the desktop!

    +

    If you want to try other games, I recommend trying to find them on the Internet Archive. On your host computer, visit https://archive.org, then find the "Classic PC Games" category. Once downloaded, you can import them into windows95 from your host's Download folder.

    +
    +
    + + diff --git a/static/www/buttons/macos.gif b/static/www/buttons/macos.gif new file mode 100644 index 0000000..52d3f72 Binary files /dev/null and b/static/www/buttons/macos.gif differ diff --git a/static/www/buttons/madewithelectron.gif b/static/www/buttons/madewithelectron.gif new file mode 100644 index 0000000..935f93a Binary files /dev/null and b/static/www/buttons/madewithelectron.gif differ diff --git a/static/www/buttons/msie.gif b/static/www/buttons/msie.gif new file mode 100644 index 0000000..3c7cd36 Binary files /dev/null and b/static/www/buttons/msie.gif differ diff --git a/static/www/credits.htm b/static/www/credits.htm new file mode 100644 index 0000000..7716724 --- /dev/null +++ b/static/www/credits.htm @@ -0,0 +1,31 @@ + + + Windows 95 Credits + + + + + + +
    + + windows95 Credits +
    + +

    Emulation Engine

    +

    + None of this would be possible without the people working on v86, in particular Fabian Hemmer aka copy. + You can visit his website at copy.sh. It also wouldn't be + possible without the QEMU project. If you enjoy running old systems, you probably want QEMU - windows95 + is merely a toy, QEMU lets you actually run old systems in a stable manner. +

    + +

    Electron

    +

    + Electron is a framework for building desktop applications using web technologies. It's what powers windows95. + You can visit the project's website at electronjs.org (in a modern browser, not in windows95). +

    +
    +
    + + diff --git a/static/www/help.htm b/static/www/help.htm new file mode 100644 index 0000000..86b98dc --- /dev/null +++ b/static/www/help.htm @@ -0,0 +1,34 @@ + + + windows95 Help + + + + + + +
    + + windows95 Help +
    + +

    MS-DOS Display Issues

    +

    If MS-DOS seems to mess up the screen:

    +
      +
    1. Hit Alt + Enter to make the command screen "Full Screen" (as far as Windows 95 is concerned)
    2. +
    3. This should restore the display from the garbled mess you see and allow you to access the Command Prompt
    4. +
    5. Press Alt-Enter again to leave Full Screen and go back to Window Mode
    6. +
    +

    (Thanks to @DisplacedGamers for that wisdom)

    + +

    windows95 Stuck in Bad State

    +

    If windows95 becomes unresponsive or stuck:

    +
      +
    1. On the app's home screen, select "Settings" in the lower menu
    2. +
    3. Delete your machine's state before starting it again
    4. +
    5. This should resolve the issue when you restart
    6. +
    +
    +
    + + diff --git a/static/www/home.htm b/static/www/home.htm new file mode 100644 index 0000000..4c13bf6 --- /dev/null +++ b/static/www/home.htm @@ -0,0 +1,37 @@ + + + Welcome to Windows 95! + + + + + + +
    +
    + + + Welcome to Windows 95! + + +
    + + +

    Hi, I'm Felix, the maker behind windows95. I hope you're having fun!

    + +

    Reach out to me in a modern browser (as in: not in windows95) on felixrieseberg.com or find me on Bluesky at @felixrieseberg.

    + +
    + + The Internet! +
    + +

    In a major update since the last version, windows95 now has working Internet! That said, most modern websites will not work, so brace yourself. I recommend using The Old Net to travel back in time.

    +
    + +
    + Last updated: 2025 +
    +
    + + diff --git a/static/www/index.htm b/static/www/index.htm new file mode 100644 index 0000000..3d201d1 --- /dev/null +++ b/static/www/index.htm @@ -0,0 +1,21 @@ + + + + Welcome to Windows 95! + + + + + + + <body bgcolor="#000080"> + <font face="Arial" color="#FFFFFF"> + <h2>Frame Alert!</h2> + <p>This page uses frames, but your browser doesn't support them.</p> + <p>Please upgrade to Netscape Navigator 2.0 or Internet Explorer 3.0!</p> + </font> + </body> + + + + diff --git a/static/www/navigation.htm b/static/www/navigation.htm new file mode 100644 index 0000000..126246e --- /dev/null +++ b/static/www/navigation.htm @@ -0,0 +1,46 @@ + + + Navigation + + + + + + + + +
    + + Navigation + +
    + + Home + +
    + + Apps & Games + +
    + + Help + +
    + + Credits + +
    +
    +
    +

    + + Best viewed with
    + Internet Explorer 5.5 and windows95 +
    +

    + + + +
    + + diff --git a/tools/parcel-build.js b/tools/parcel-build.js index 597dc9f..4190a80 100644 --- a/tools/parcel-build.js +++ b/tools/parcel-build.js @@ -18,6 +18,7 @@ async function copyLib() { let patchedLibv86 = libv86.replace('k.load_file="undefined"===typeof XMLHttpRequest?pa:qa', 'k.load_file=pa') patchedLibv86 = patchedLibv86.replace('H.exportSymbol=function(a,b){"undefined"!==typeof module&&"undefined"!==typeof module.exports?module.exports[a]=b:"undefined"!==typeof window?window[a]=b:"function"===typeof importScripts&&(self[a]=b)}', 'H.exportSymbol=function(a,b){"undefined"!==typeof window?window[a]=b:"undefined"!==typeof module&&"undefined"!==typeof module.exports?module.exports[a]=b:"function"===typeof importScripts&&(self[a]=b)}') + patchedLibv86 = patchedLibv86.replace('this.fetch=fetch;', 'this.fetch=(...args)=>fetch(...args);') fs.writeFileSync(libv86path, patchedLibv86) diff --git a/tsconfig.json b/tsconfig.json index 2fc3ca1..4fe8fd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,12 +8,11 @@ "preserveConstEnums": true, "sourceMap": true, "lib": [ - "es2017", + "es2021", "dom" ], "noImplicitAny": true, "noImplicitReturns": true, - "suppressImplicitAnyIndexErrors": true, "strictNullChecks": true, "noUnusedLocals": true, "noImplicitThis": true,