esm-spec.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { expect } from 'chai';
  2. import * as cp from 'node:child_process';
  3. import { BrowserWindow } from 'electron';
  4. import * as fs from 'node:fs';
  5. import * as os from 'node:os';
  6. import * as path from 'node:path';
  7. import { pathToFileURL } from 'node:url';
  8. const runFixture = async (appPath: string, args: string[] = []) => {
  9. const result = cp.spawn(process.execPath, [appPath, ...args], {
  10. stdio: 'pipe'
  11. });
  12. const stdout: Buffer[] = [];
  13. const stderr: Buffer[] = [];
  14. result.stdout.on('data', (chunk) => stdout.push(chunk));
  15. result.stderr.on('data', (chunk) => stderr.push(chunk));
  16. const [code, signal] = await new Promise<[number | null, NodeJS.Signals | null]>((resolve) => {
  17. result.on('close', (code, signal) => {
  18. resolve([code, signal]);
  19. });
  20. });
  21. return {
  22. code,
  23. signal,
  24. stdout: Buffer.concat(stdout).toString().trim(),
  25. stderr: Buffer.concat(stderr).toString().trim()
  26. };
  27. };
  28. const fixturePath = path.resolve(__dirname, 'fixtures', 'esm');
  29. describe('esm', () => {
  30. describe('main process', () => {
  31. it('should load an esm entrypoint', async () => {
  32. const result = await runFixture(path.resolve(fixturePath, 'entrypoint.mjs'));
  33. expect(result.code).to.equal(0);
  34. expect(result.stdout).to.equal('ESM Launch, ready: false');
  35. });
  36. it('should load an esm entrypoint based on type=module', async () => {
  37. const result = await runFixture(path.resolve(fixturePath, 'package'));
  38. expect(result.code).to.equal(0);
  39. expect(result.stdout).to.equal('ESM Package Launch, ready: false');
  40. });
  41. it('should wait for a top-level await before declaring the app ready', async () => {
  42. const result = await runFixture(path.resolve(fixturePath, 'top-level-await.mjs'));
  43. expect(result.code).to.equal(0);
  44. expect(result.stdout).to.equal('Top level await, ready: false');
  45. });
  46. it('should allow usage of pre-app-ready apis in top-level await', async () => {
  47. const result = await runFixture(path.resolve(fixturePath, 'pre-app-ready-apis.mjs'));
  48. expect(result.code).to.equal(0);
  49. });
  50. it('should allow use of dynamic import', async () => {
  51. const result = await runFixture(path.resolve(fixturePath, 'dynamic.mjs'));
  52. expect(result.code).to.equal(0);
  53. expect(result.stdout).to.equal('Exit with app, ready: false');
  54. });
  55. });
  56. describe('renderer process', () => {
  57. let w: BrowserWindow | null = null;
  58. const tempDirs: string[] = [];
  59. afterEach(async () => {
  60. if (w) w.close();
  61. w = null;
  62. while (tempDirs.length) {
  63. await fs.promises.rm(tempDirs.pop()!, { force: true, recursive: true });
  64. }
  65. });
  66. async function loadWindowWithPreload (preload: string, webPreferences: Electron.WebPreferences) {
  67. const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'e-spec-preload-'));
  68. tempDirs.push(tmpDir);
  69. const preloadPath = path.resolve(tmpDir, 'preload.mjs');
  70. await fs.promises.writeFile(preloadPath, preload);
  71. w = new BrowserWindow({
  72. show: false,
  73. webPreferences: {
  74. ...webPreferences,
  75. preload: preloadPath
  76. }
  77. });
  78. let error: Error | null = null;
  79. w.webContents.on('preload-error', (_, __, err) => {
  80. error = err;
  81. });
  82. await w.loadFile(path.resolve(fixturePath, 'empty.html'));
  83. return [w.webContents, error] as [Electron.WebContents, Error | null];
  84. }
  85. describe('nodeIntegration', () => {
  86. it('should support an esm entrypoint', async () => {
  87. const [webContents] = await loadWindowWithPreload('import { resolve } from "path"; window.resolvePath = resolve;', {
  88. nodeIntegration: true,
  89. sandbox: false,
  90. contextIsolation: false
  91. });
  92. const exposedType = await webContents.executeJavaScript('typeof window.resolvePath');
  93. expect(exposedType).to.equal('function');
  94. });
  95. it('should delay load until the ESM import chain is complete', async () => {
  96. const [webContents] = await loadWindowWithPreload(`import { resolve } from "path";
  97. await new Promise(r => setTimeout(r, 500));
  98. window.resolvePath = resolve;`, {
  99. nodeIntegration: true,
  100. sandbox: false,
  101. contextIsolation: false
  102. });
  103. const exposedType = await webContents.executeJavaScript('typeof window.resolvePath');
  104. expect(exposedType).to.equal('function');
  105. });
  106. it('should support a top-level await fetch blocking the page load', async () => {
  107. const [webContents] = await loadWindowWithPreload(`
  108. const r = await fetch("package/package.json");
  109. window.packageJson = await r.json();`, {
  110. nodeIntegration: true,
  111. sandbox: false,
  112. contextIsolation: false
  113. });
  114. const packageJson = await webContents.executeJavaScript('window.packageJson');
  115. expect(packageJson).to.deep.equal(require('./fixtures/esm/package/package.json'));
  116. });
  117. const hostsUrl = pathToFileURL(process.platform === 'win32' ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/hosts');
  118. describe('without context isolation', () => {
  119. it('should use Blinks dynamic loader in the main world', async () => {
  120. const [webContents] = await loadWindowWithPreload('', {
  121. nodeIntegration: true,
  122. sandbox: false,
  123. contextIsolation: false
  124. });
  125. let error: Error | null = null;
  126. try {
  127. await webContents.executeJavaScript(`import(${JSON.stringify(hostsUrl)})`);
  128. } catch (err) {
  129. error = err as Error;
  130. }
  131. expect(error).to.not.equal(null);
  132. // This is a blink specific error message
  133. expect(error?.message).to.include('Failed to fetch dynamically imported module');
  134. });
  135. it('should use import.meta callback handling from Node.js for Node.js modules', async () => {
  136. const result = await runFixture(path.resolve(fixturePath, 'import-meta'));
  137. expect(result.code).to.equal(0);
  138. });
  139. });
  140. describe('with context isolation', () => {
  141. let badFilePath = '';
  142. beforeEach(async () => {
  143. badFilePath = path.resolve(path.resolve(os.tmpdir(), 'bad-file.badjs'));
  144. await fs.promises.writeFile(badFilePath, 'const foo = "bar";');
  145. });
  146. afterEach(async () => {
  147. await fs.promises.unlink(badFilePath);
  148. });
  149. it('should use Node.js ESM dynamic loader in the isolated context', async () => {
  150. const [, preloadError] = await loadWindowWithPreload(`await import(${JSON.stringify((pathToFileURL(badFilePath)))})`, {
  151. nodeIntegration: true,
  152. sandbox: false,
  153. contextIsolation: true
  154. });
  155. expect(preloadError).to.not.equal(null);
  156. // This is a node.js specific error message
  157. expect(preloadError!.toString()).to.include('Unknown file extension');
  158. });
  159. it('should use Blinks dynamic loader in the main world', async () => {
  160. const [webContents] = await loadWindowWithPreload('', {
  161. nodeIntegration: true,
  162. sandbox: false,
  163. contextIsolation: true
  164. });
  165. let error: Error | null = null;
  166. try {
  167. await webContents.executeJavaScript(`import(${JSON.stringify(hostsUrl)})`);
  168. } catch (err) {
  169. error = err as Error;
  170. }
  171. expect(error).to.not.equal(null);
  172. // This is a blink specific error message
  173. expect(error?.message).to.include('Failed to fetch dynamically imported module');
  174. });
  175. });
  176. });
  177. });
  178. });