Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,21 @@ export const enum GenericDialogType {
'success' = 'success',
}

export type EditorId = `${string}.${'js' | 'html' | 'css' | 'json'}`;
export type EditorId = `${string}.${
| 'cjs'
| 'js'
| 'mjs'
| 'html'
| 'css'
| 'json'}`;

export type EditorValues = Record<EditorId, string>;

// main.js gets special treatment: it is required as the entry point
// main.{cjs,js,mjs} gets special treatment: it is required as the entry point
// when we run fiddles or create a package.json to package fiddles.
export const MAIN_CJS = 'main.cjs';
export const MAIN_JS = 'main.js';
export const MAIN_MJS = 'main.mjs';

export const PACKAGE_NAME = 'package.json';

Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/sidebar-file-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { observer } from 'mobx-react';
import { EditorId, PACKAGE_NAME } from '../../interfaces';
import { EditorPresence } from '../editor-mosaic';
import { AppState } from '../state';
import { isRequiredFile, isSupportedFile } from '../utils/editor-utils';
import { isMainEntryPoint, isSupportedFile } from '../utils/editor-utils';

interface FileTreeProps {
appState: AppState;
Expand Down Expand Up @@ -68,7 +68,7 @@ export const SidebarFileTree = observer(
onClick={() => this.renameEditor(editorId)}
/>
<MenuItem
disabled={isRequiredFile(editorId)}
disabled={isMainEntryPoint(editorId)}
icon="remove"
text="Delete"
intent="danger"
Expand Down Expand Up @@ -201,7 +201,7 @@ export const SidebarFileTree = observer(

if (!isSupportedFile(id)) {
await appState.showErrorDialog(
`Invalid filename "${id}": Must be a file ending in .js, .html, .css, or .json`,
`Invalid filename "${id}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`,
);
return;
}
Expand Down
18 changes: 16 additions & 2 deletions src/renderer/editor-mosaic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MosaicDirection, MosaicNode, getLeaves } from 'react-mosaic-component';
import {
compareEditors,
getEmptyContent,
isMainEntryPoint,
isSupportedFile,
monacoLanguage,
} from './utils/editor-utils';
Expand Down Expand Up @@ -151,7 +152,7 @@ export class EditorMosaic {

if (!isSupportedFile(id)) {
throw new Error(
`Cannot add file "${id}": Must be .js, .html, .css, or .json`,
`Cannot add file "${id}": Must be .cjs, .js, .mjs, .html, .css, or .json`,
);
}

Expand Down Expand Up @@ -275,8 +276,17 @@ export class EditorMosaic {

/** Add a new file to the mosaic */
public addNewFile(id: EditorId, value: string = getEmptyContent(id)) {
if (this.files.has(id))
if (this.files.has(id)) {
throw new Error(`Cannot add file "${id}": File already exists`);
}

const entryPoint = this.mainEntryPointFile();

if (isMainEntryPoint(id) && entryPoint) {
throw new Error(
`Cannot add file "${id}": Main entry point ${entryPoint} exists`,
);
}

this.addFile(id, value);
}
Expand Down Expand Up @@ -318,6 +328,10 @@ export class EditorMosaic {
for (const editor of this.editors.values()) editor.updateOptions(options);
}

public mainEntryPointFile(): EditorId | undefined {
return Array.from(this.files.keys()).find((id) => isMainEntryPoint(id));
}

//=== Listen for user edits

private ignoreAllEdits() {
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/remote-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class RemoteLoader {
// contain any supported files. Throw an error to let the user know.
if (Object.keys(values).length === 0) {
throw new Error(
'This Gist did not contain any supported files. Supported files must have one of the following extensions: .js, .css, or .html.',
'This Gist did not contain any supported files. Supported files must have one of the following extensions: .cjs, .js, .mjs, .css, or .html.',
);
}

Expand Down
17 changes: 17 additions & 0 deletions src/renderer/runner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import semver from 'semver';

import { Bisector } from './bisect';
import { AppState } from './state';
import { maybePlural } from './utils/plural-maybe';
import {
FileTransformOperation,
InstallState,
MAIN_MJS,
PMOperationOptions,
PackageJsonOptions,
RunResult,
Expand Down Expand Up @@ -163,6 +166,20 @@ export class Runner {
return RunResult.INVALID;
}

if (
semver.lt(ver.version, '28.0.0') &&
!ver.version.startsWith('28.0.0-nightly')
) {
const entryPoint = appState.editorMosaic.mainEntryPointFile();

if (entryPoint === MAIN_MJS) {
appState.showErrorDialog(
'ESM main entry points are only supported starting in Electron 28',
);
return RunResult.INVALID;
}
}

if (appState.isClearingConsoleOnRun) {
appState.clearConsole();
}
Expand Down
45 changes: 33 additions & 12 deletions src/renderer/utils/editor-utils.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,65 @@
import { EditorId, MAIN_JS } from '../../interfaces';
import { EditorId, MAIN_CJS, MAIN_JS, MAIN_MJS } from '../../interfaces';
import {
ensureRequiredFiles,
getEmptyContent,
getSuffix,
isRequiredFile,
isMainEntryPoint,
isSupportedFile,
} from '../../utils/editor-utils';

export {
ensureRequiredFiles,
getEmptyContent,
getSuffix,
isRequiredFile,
isMainEntryPoint,
isSupportedFile,
};

// The order of these fields is the order that
// they'll be sorted in the mosaic
const KNOWN_FILES: string[] = [
MAIN_CJS,
MAIN_JS,
MAIN_MJS,
'renderer.cjs',
'renderer.js',
'renderer.mjs',
'index.html',
'preload.cjs',
'preload.js',
'preload.mjs',
'styles.css',
];

export function isKnownFile(filename: string): boolean {
return KNOWN_FILES.includes(filename);
}

const TITLE_MAP = new Map<EditorId, string>([
[MAIN_JS, `Main Process (${MAIN_JS})`],
['renderer.js', 'Renderer Process (renderer.js)'],
['index.html', 'HTML (index.html)'],
['preload.js', 'Preload (preload.js)'],
['styles.css', 'Stylesheet (styles.css)'],
]);

export function getEditorTitle(id: EditorId): string {
return TITLE_MAP.get(id) || id;
switch (id) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love how clean this entire section is, reads really well!

case 'index.html':
return 'HTML (index.html)';

case MAIN_CJS:
case MAIN_JS:
case MAIN_MJS:
return `Main Process (${id})`;

case 'preload.cjs':
case 'preload.js':
case 'preload.mjs':
return `Preload (${id})`;

case 'renderer.cjs':
case 'renderer.js':
case 'renderer.mjs':
return `Renderer Process (${id})`;

case 'styles.css':
return 'Stylesheet (styles.css)';
}

return id;
}

// the KNOWN_FILES, in the order of that array, go first.
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/utils/get-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ export async function getPackageJson(
}
}

const entryPoint = appState.editorMosaic.mainEntryPointFile() ?? MAIN_JS;

return JSON.stringify(
{
name,
productName: name,
description: 'My Electron application description',
keywords: [],
main: `./${MAIN_JS}`,
main: `./${entryPoint}`,
version: '1.0.0',
author: appState.packageAuthor,
scripts: {
Expand Down
46 changes: 29 additions & 17 deletions src/utils/editor-utils.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
import { EditorId, EditorValues, MAIN_JS } from '../interfaces';
import {
EditorId,
EditorValues,
MAIN_CJS,
MAIN_JS,
MAIN_MJS,
} from '../interfaces';

const requiredFiles = new Set<EditorId>([MAIN_JS]);
const mainEntryPointFiles = new Set<EditorId>([MAIN_CJS, MAIN_JS, MAIN_MJS]);

const EMPTY_EDITOR_CONTENT = {
css: '/* Empty */',
html: '<!-- Empty -->',
js: '// Empty',
json: '{}',
const EMPTY_EDITOR_CONTENT: Record<EditorId, string> = {
'.css': '/* Empty */',
'.html': '<!-- Empty -->',
'.cjs': '// Empty',
'.js': '// Empty',
'.mjs': '// Empty',
'.json': '{}',
} as const;

export function getEmptyContent(filename: string): string {
return (
EMPTY_EDITOR_CONTENT[
getSuffix(filename) as keyof typeof EMPTY_EDITOR_CONTENT
] || ''
);
return EMPTY_EDITOR_CONTENT[`.${getSuffix(filename)}` as EditorId] || '';
}

export function isRequiredFile(id: EditorId) {
return requiredFiles.has(id);
export function isMainEntryPoint(id: EditorId) {
return mainEntryPointFiles.has(id);
}

export function ensureRequiredFiles(values: EditorValues): EditorValues {
for (const file of requiredFiles) {
values[file] ||= getEmptyContent(file);
const mainEntryPoint = Object.keys(values).find((id: EditorId) =>
mainEntryPointFiles.has(id),
) as EditorId | undefined;

// If no entry point is found, default to main.js
if (!mainEntryPoint) {
values[MAIN_JS] = getEmptyContent(MAIN_JS);
} else {
values[mainEntryPoint] ||= getEmptyContent(mainEntryPoint);
}

return values;
}

Expand All @@ -33,5 +45,5 @@ export function getSuffix(filename: string) {
}

export function isSupportedFile(filename: string): filename is EditorId {
return /\.(css|html|js|json)$/i.test(filename);
return /\.(css|html|cjs|js|mjs|json)$/i.test(filename);
}
4 changes: 2 additions & 2 deletions tests/main/menu-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import * as electron from 'electron';
import { mocked } from 'jest-mock';

import { BlockableAccelerator } from '../../src/interfaces';
import { BlockableAccelerator, MAIN_JS } from '../../src/interfaces';
import { IpcEvents } from '../../src/ipc-events';
import {
saveFiddle,
Expand Down Expand Up @@ -271,7 +271,7 @@ describe('menu', () => {
});

it('attempts to open a template on click', async () => {
const editorValues = { 'main.js': 'foobar' };
const editorValues = { [MAIN_JS]: 'foobar' };
mocked(getTemplateValues).mockResolvedValue(editorValues);
await showMe.submenu[0].submenu[0].click();
expect(ipcMainManager.send).toHaveBeenCalledWith(
Expand Down
18 changes: 18 additions & 0 deletions tests/main/utils/read-fiddle-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { mocked } from 'jest-mock';
import {
EditorId,
EditorValues,
MAIN_CJS,
MAIN_JS,
MAIN_MJS,
PACKAGE_NAME,
} from '../../../src/interfaces';
import { readFiddle } from '../../../src/main/utils/read-fiddle';
Expand Down Expand Up @@ -46,6 +48,22 @@ describe('read-fiddle', () => {
expect(fiddle).toStrictEqual({ [MAIN_JS]: getEmptyContent(MAIN_JS) });
});

it('does not inject main.js if main.cjs or main.mjs present', async () => {
for (const entryPoint of [MAIN_CJS, MAIN_MJS]) {
const mockValues = {
[entryPoint]: getEmptyContent(entryPoint),
};
setupFSMocks(mockValues);

const fiddle = await readFiddle(folder);

expect(console.warn).not.toHaveBeenCalled();
expect(fiddle).toStrictEqual({
[entryPoint]: getEmptyContent(entryPoint),
});
}
});

it('reads supported files', async () => {
const content = 'hello';
const mockValues = { [MAIN_JS]: content };
Expand Down
2 changes: 1 addition & 1 deletion tests/renderer/app-spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('App component', () => {
mocked(openFiddle).mockResolvedValueOnce(undefined);

const filePath = '/fake/path';
const files = { MAIN_JS: 'foo' };
const files = { [MAIN_JS]: 'foo' };
await app.openFiddle({ localFiddle: { filePath, files } });
expect(openFiddle).toHaveBeenCalledWith(filePath, files);
});
Expand Down
2 changes: 1 addition & 1 deletion tests/renderer/editor-mosaic-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ describe('EditorMosaic', () => {
direction: 'row',
first: {
direction: 'column',
first: 'main.js',
first: MAIN_JS,
second: 'renderer.js',
},
second: {
Expand Down
Loading