From e7622c48751c801b5f1131fd04554676dd83f08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Fri, 26 Jul 2024 15:08:49 -0700 Subject: [PATCH] Rewrite legacy API using containingUrl --- .eslintrc | 14 ++ lib/src/compiler-path.ts | 2 +- lib/src/compiler/utils.ts | 25 +-- lib/src/legacy/importer.ts | 348 ++++++++++++++-------------------- lib/src/legacy/index.ts | 88 ++++----- lib/src/legacy/utils.ts | 69 +++---- lib/src/sync-process/index.ts | 2 +- 7 files changed, 220 insertions(+), 328 deletions(-) diff --git a/.eslintrc b/.eslintrc index f135f8dd..525910e8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,5 +5,19 @@ // It would be nice to sort import declaration order as well, but that's not // autofixable and it's not worth the effort of handling manually. "sort-imports": ["error", {"ignoreDeclarationSort": true}], + // Match TypeScript style of exempting names starting with `_`. + // See: https://typescript-eslint.io/rules/no-unused-vars/ + "@typescript-eslint/no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ] } } diff --git a/lib/src/compiler-path.ts b/lib/src/compiler-path.ts index 60675e5b..351abbb4 100644 --- a/lib/src/compiler-path.ts +++ b/lib/src/compiler-path.ts @@ -44,7 +44,7 @@ export const compilerCommand = (() => { `sass-embedded-${platform}-${arch}/dart-sass/src/sass.snapshot` ), ]; - } catch (ignored) { + } catch (_ignored) { // ignored } diff --git a/lib/src/compiler/utils.ts b/lib/src/compiler/utils.ts index 80e1e0fc..2eac405e 100644 --- a/lib/src/compiler/utils.ts +++ b/lib/src/compiler/utils.ts @@ -9,11 +9,6 @@ import {deprotofySourceSpan} from '../deprotofy-span'; import {Dispatcher, DispatcherHandlers} from '../dispatcher'; import {Exception} from '../exception'; import {ImporterRegistry} from '../importer-registry'; -import { - legacyImporterProtocol, - removeLegacyImporter, - removeLegacyImporterFromSpan, -} from '../legacy/utils'; import {Logger} from '../logger'; import {MessageTransformer} from '../message-transformer'; import * as utils from '../utils'; @@ -121,19 +116,12 @@ export function newCompileStringRequest( }); const url = options?.url?.toString(); - if (url && url !== legacyImporterProtocol) { + if (url) { input.url = url; } if (options && 'importer' in options && options.importer) { input.importer = importers.register(options.importer); - } else if (url === legacyImporterProtocol) { - input.importer = new proto.InboundMessage_CompileRequest_Importer({ - importer: {case: 'path', value: p.resolve('.')}, - }); - } else { - // When importer is not set on the host, the compiler will set a - // FileSystemImporter if `url` is set to a file: url or a NoOpImporter. } const request = newCompileRequest(importers, options); @@ -153,12 +141,9 @@ export function handleLogEvent( options: OptionsWithLegacy<'sync' | 'async'> | undefined, event: proto.OutboundMessage_LogEvent ): void { - let span = event.span ? deprotofySourceSpan(event.span) : null; - if (span && options?.legacy) span = removeLegacyImporterFromSpan(span); - let message = event.message; - if (options?.legacy) message = removeLegacyImporter(message); - let formatted = event.formatted; - if (options?.legacy) formatted = removeLegacyImporter(formatted); + const span = event.span ? deprotofySourceSpan(event.span) : null; + const message = event.message; + const formatted = event.formatted; const deprecationType = validDeprecationId(event.deprecationType) ? deprecations[event.deprecationType] : null; @@ -189,7 +174,7 @@ export function handleLogEvent( const stack = event.stackTrace; if (stack) { - params.stack = options?.legacy ? removeLegacyImporter(stack) : stack; + params.stack = stack; } options.logger.warn(message, params); diff --git a/lib/src/legacy/importer.ts b/lib/src/legacy/importer.ts index 68b79e11..b3ac161f 100644 --- a/lib/src/legacy/importer.ts +++ b/lib/src/legacy/importer.ts @@ -3,19 +3,19 @@ // https://opensource.org/licenses/MIT. import {strict as assert} from 'assert'; -import * as fs from 'fs'; import * as p from 'path'; import * as util from 'util'; +import {pathToFileURL} from 'url'; import {resolvePath} from './resolve-path'; import { PromiseOr, SyncBoolean, fileUrlToPathCrossPlatform, - isErrnoException, thenOr, } from '../utils'; import { + FileImporter, Importer, ImporterResult, LegacyAsyncImporter, @@ -25,247 +25,190 @@ import { LegacyPluginThis, LegacySyncImporter, } from '../vendor/sass'; -import { - legacyFileUrlToPath, - legacyImporterProtocol, - legacyImporterProtocolPrefix, - pathToLegacyFileUrl, -} from './utils'; +import {legacyFileUrlToPath, pathToLegacyFileURL} from './utils'; /** - * A special URL protocol we use to signal when a stylesheet has finished - * loading. This allows us to determine which stylesheet is "current" when - * resolving a new load, which in turn allows us to pass in an accurate `prev` - * parameter to the legacy callback. - */ -export const endOfLoadProtocol = 'sass-embedded-legacy-load-done:'; - -/** - * The `file:` URL protocol with [legacyImporterProtocolPrefix] at the beginning. + * A wrapper around a `LegacyImporter` callback that exposes it as a new-API + * `Importer`, delegating to `LegacyImportersWrapper`. */ -export const legacyImporterFileProtocol = 'legacy-importer-file:'; - -// A count of `endOfLoadProtocol` imports that have been generated. Each one -// must be a different URL to ensure that the importer results aren't cached. -let endOfLoadCount = 0; +export class LegacyImporterWrapper + implements Importer +{ + constructor(private readonly wrapper: LegacyImportersWrapper) {} -// The interface for previous URLs that were passed to -// `LegacyImporterWrapper.callbacks`. -interface PreviousUrl { - // The URL itself. This is actually an absolute path if `path` is true. - url: string; + canonicalize( + url: string, + options: {fromImport: boolean; containingUrl: URL | null} + ): PromiseOr { + return this.wrapper.canonicalize(url, options) as PromiseOr< + URL | null, + sync + >; + } - // Whether `url` is an absolute path. - path: boolean; + load(canonicalUrl: URL): ImporterResult | null { + return this.wrapper.load(canonicalUrl); + } } /** * A wrapper around a `LegacyImporter` callback that exposes it as a new-API - * `Importer`. + * `FileImporter`, delegating to `LegacyImportersWrapper`. */ -export class LegacyImporterWrapper - implements Importer +export class LegacyFileImporterWrapper + implements FileImporter { - // A stack of previous URLs passed to `this.callbacks`. - private readonly prev: PreviousUrl[] = []; + constructor(private readonly wrapper: LegacyImportersWrapper) {} - // The `contents` field returned by the last successful invocation of - // `this.callbacks`, if it returned one. - private lastContents: string | undefined; + findFileUrl( + url: string, + options: {fromImport: boolean; containingUrl: URL | null} + ): PromiseOr { + return this.wrapper.findFileUrl(url, options) as PromiseOr< + URL | null, + sync + >; + } +} + +/** + * A wrapper around a `LegacyImporter` callback that exposes it as a pair of + * new-API `Importer` and `FileImporter`. + */ +export class LegacyImportersWrapper { + private importerResult?: ImporterResult; + private fileUrl?: URL; constructor( private readonly self: LegacyPluginThis, private readonly callbacks: Array>, private readonly loadPaths: string[], - initialPrev: string, private readonly sync: SyncBoolean - ) { - const path = initialPrev !== 'stdin'; - this.prev.push({url: path ? p.resolve(initialPrev) : 'stdin', path}); + ) {} + + importers() { + return [ + new LegacyImporterWrapper(this), + new LegacyFileImporterWrapper(this), + ]; } canonicalize( url: string, options: {fromImport: boolean; containingUrl: URL | null} ): PromiseOr { - if (url.startsWith(endOfLoadProtocol)) return new URL(url); - - // Emulate a base importer instead of using a real base importer, - // because we want to mark containingUrl as used, which is impossible - // in a real base importer. - if (options.containingUrl !== null) { - try { - const absoluteUrl = new URL(url, options.containingUrl).toString(); - const resolved = this.canonicalize(absoluteUrl, { - fromImport: options.fromImport, - containingUrl: null, - }); - if (resolved !== null) return resolved; - } catch (error: unknown) { - if ( - error instanceof TypeError && - isErrnoException(error) && - error.code === 'ERR_INVALID_URL' - ) { - // ignore - } else { - throw error; - } - } - } - if ( - url.startsWith(legacyImporterProtocolPrefix) || - url.startsWith(legacyImporterProtocol) + (options.containingUrl !== null && + options.containingUrl.protocol !== 'file:') || + (options.containingUrl === null && url.startsWith('file:')) ) { - // A load starts with `legacyImporterProtocolPrefix` if and only if it's a - // relative load for the current importer rather than an absolute load. - // For the most part, we want to ignore these, but for `file:` URLs - // specifically we want to resolve them on the filesystem to ensure - // locality. - const urlWithoutPrefix = url.substring( - legacyImporterProtocolPrefix.length - ); - if (urlWithoutPrefix.startsWith('file:')) { - let resolved: string | null = null; - - try { - const path = fileUrlToPathCrossPlatform(urlWithoutPrefix); - resolved = resolvePath(path, options.fromImport); - } catch (error: unknown) { - if ( - error instanceof TypeError && - isErrnoException(error) && - (error.code === 'ERR_INVALID_URL' || - error.code === 'ERR_INVALID_FILE_URL_PATH') - ) { - // It's possible for `url` to represent an invalid path for the - // current platform. For example, `@import "/foo/bar/baz"` will - // resolve to `file:///foo/bar/baz` which is an invalid URL on - // Windows. In that case, we treat it as though the file doesn't - // exist so that the user's custom importer can still handle the - // URL. - } else { - throw error; - } - } - - if (resolved !== null) { - this.prev.push({url: resolved, path: true}); - return pathToLegacyFileUrl(resolved); - } - } - return null; } - const prev = this.prev[this.prev.length - 1]; - return thenOr( - thenOr(this.invokeCallbacks(url, prev.url, options), result => { - if (result instanceof Error) throw result; - if (result === null) return null; - - if (typeof result !== 'object') { - throw ( - 'Expected importer to return an object, got ' + - `${util.inspect(result)}.` - ); - } + const path = /^[A-Za-z][+\-.0-9A-Za-z]+:/.test(url) + ? decodeURI(url) + : decodeURIComponent(url); + const parentPathOrUndefined = + options.containingUrl === null + ? undefined + : legacyFileUrlToPath(options.containingUrl); + const parentPath = parentPathOrUndefined ?? 'stdin'; + + const absolutePath = url.startsWith('file:') + ? fileUrlToPathCrossPlatform(url) + : p.resolve(p.dirname(parentPath), path); + if (parentPathOrUndefined !== undefined) { + const resolved = resolvePath(absolutePath, options.fromImport); + if (resolved !== null) { + this.fileUrl = pathToFileURL(resolved); + return null; + } + } - if ('contents' in result || !('file' in result)) { - this.lastContents = result.contents ?? ''; + return thenOr(this.invokeCallbacks(path, parentPath, options), result => { + if (result instanceof Error) throw result; + if (result === null) return null; - if ('file' in result) { - return new URL( - legacyImporterProtocol + - encodeURI((result as {file: string}).file) - ); - } else if (/^[A-Za-z+.-]+:/.test(url)) { - return new URL(`${legacyImporterProtocolPrefix}${url}`); - } else { - return new URL(legacyImporterProtocol + encodeURI(url)); - } - } else { - if (p.isAbsolute(result.file)) { - const resolved = resolvePath(result.file, options.fromImport); - return resolved ? pathToLegacyFileUrl(resolved) : null; - } + if (typeof result !== 'object') { + throw ( + 'Expected importer to return an object, got ' + + `${util.inspect(result)}.` + ); + } - const prefixes = [...this.loadPaths, '.']; - if (prev.path) prefixes.unshift(p.dirname(prev.url)); + if ('contents' in result || !('file' in result)) { + const canonicalUrl = + 'file' in result + ? pathToLegacyFileURL( + p.resolve( + p.dirname(parentPath), + (result as {file: string}).file + ), + (result as {file: string}).file + ) + : pathToLegacyFileURL( + absolutePath, + p.join(p.dirname(parentPath), path) + ); + this.importerResult = { + contents: result.contents || '', + syntax: canonicalUrl.pathname.endsWith('.sass') + ? 'indented' + : canonicalUrl.pathname.endsWith('.css') + ? 'css' + : 'scss', + sourceMapUrl: canonicalUrl, + }; + return canonicalUrl; + } - for (const prefix of prefixes) { - const resolved = resolvePath( - p.join(prefix, result.file), - options.fromImport - ); - if (resolved !== null) return pathToLegacyFileUrl(resolved); + if ('file' in result) { + if (p.isAbsolute(result.file)) { + const resolved = resolvePath(result.file, options.fromImport); + if (resolved !== null) { + this.fileUrl = pathToFileURL(resolved); + return null; } - return null; } - }), - result => { - if (result !== null) { - const path = result.protocol === legacyImporterFileProtocol; - this.prev.push({ - url: path ? legacyFileUrlToPath(result) : url, - path, - }); - return result; - } else { - for (const loadPath of this.loadPaths) { - const resolved = resolvePath( - p.join(loadPath, url), - options.fromImport - ); - if (resolved !== null) return pathToLegacyFileUrl(resolved); + + const prefixes = [p.dirname(parentPath), ...this.loadPaths, '.']; + for (const prefix of prefixes) { + const resolved = resolvePath( + p.join(prefix, result.file), + options.fromImport + ); + if (resolved !== null) { + this.fileUrl = pathToFileURL(resolved); + return null; } - return null; } } - ); + + return null; + }); } - load(canonicalUrl: URL): ImporterResult | null { - if (canonicalUrl.protocol === endOfLoadProtocol) { - this.prev.pop(); - return { - contents: '', - syntax: 'scss', - sourceMapUrl: new URL(endOfLoadProtocol), - }; + load(_canonicalUrl: URL): ImporterResult | null { + if (this.importerResult) { + const importerResult = this.importerResult; + delete this.importerResult; + return importerResult; } - if (canonicalUrl.protocol === legacyImporterFileProtocol) { - const syntax = canonicalUrl.pathname.endsWith('.sass') - ? 'indented' - : canonicalUrl.pathname.endsWith('.css') - ? 'css' - : 'scss'; - - let contents = - this.lastContents ?? - fs.readFileSync(legacyFileUrlToPath(canonicalUrl), 'utf-8'); - this.lastContents = undefined; - if (syntax === 'scss') { - contents += this.endOfLoadImport; - } else if (syntax === 'indented') { - contents += `\n@import "${endOfLoadProtocol}${endOfLoadCount++}"`; - } else { - this.prev.pop(); - } + return null; + } - return {contents, syntax, sourceMapUrl: canonicalUrl}; + findFileUrl( + _url: string, + options: {fromImport: boolean; containingUrl: URL | null} + ): URL | null { + options.containingUrl; + if (this.fileUrl) { + const fileUrl = this.fileUrl; + delete this.fileUrl; + return fileUrl; } - - const lastContents = this.lastContents; - assert.notEqual(lastContents, undefined); - this.lastContents = undefined; - return { - contents: lastContents + this.endOfLoadImport, - syntax: 'scss', - sourceMapUrl: canonicalUrl, - }; + return null; } // Invokes each callback in `this.callbacks` until one returns a non-null @@ -332,11 +275,4 @@ export class LegacyImporterWrapper if (syncResult !== undefined) resolve(syncResult); }) as PromiseOr; } - - // The `@import` statement to inject after the contents of files to ensure - // that we know when a load has completed so we can pass the correct `prev` - // argument to callbacks. - private get endOfLoadImport(): string { - return `\n;@import "${endOfLoadProtocol}${endOfLoadCount++}";`; - } } diff --git a/lib/src/legacy/index.ts b/lib/src/legacy/index.ts index f5467565..ce342699 100644 --- a/lib/src/legacy/index.ts +++ b/lib/src/legacy/index.ts @@ -34,13 +34,8 @@ import { StringOptions, } from '../vendor/sass'; import {wrapFunction} from './value/wrap'; -import {LegacyImporterWrapper, endOfLoadProtocol} from './importer'; -import { - legacyImporterProtocol, - pathToLegacyFileUrl, - removeLegacyImporter, - removeLegacyImporterFromSpan, -} from './utils'; +import {LegacyImporterWrapper, LegacyImportersWrapper} from './importer'; +import {legacyFileUrlToPath, pathToLegacyFileURL} from './utils'; export function render( options: LegacyOptions<'async'>, @@ -144,17 +139,14 @@ function convertOptions( const importers = options.importer && (!(options.importer instanceof Array) || options.importer.length > 0) - ? [ - new LegacyImporterWrapper( - self, - options.importer instanceof Array - ? options.importer - : [options.importer], - options.includePaths ?? [], - options.file ?? 'stdin', - sync - ), - ] + ? new LegacyImportersWrapper( + self, + options.importer instanceof Array + ? options.importer + : [options.importer], + options.includePaths ?? [], + sync + ).importers() : undefined; return { @@ -165,7 +157,7 @@ function convertOptions( : importers, sourceMap: wasSourceMapRequested(options), sourceMapIncludeSources: options.sourceMapContents, - loadPaths: importers ? undefined : options.includePaths, + loadPaths: options.includePaths, style: options.outputStyle as 'compressed' | 'expanded' | undefined, quietDeps: options.quietDeps, verbose: options.verbose, @@ -200,10 +192,8 @@ function convertStringOptions( return { ...modernOptions, url: options.file - ? options.importer - ? pathToLegacyFileUrl(options.file) - : pathToFileURL(options.file) - : new URL(legacyImporterProtocol), + ? pathToFileURL(options.file) + : pathToLegacyFileURL('stdin'), importer, syntax: options.indentedSyntax ? 'indented' : 'scss', }; @@ -277,20 +267,17 @@ function newLegacyResult( sourceMap.file = 'stdin.css'; } - sourceMap.sources = sourceMap.sources - .filter(source => !source.startsWith(endOfLoadProtocol)) - .map(source => { - source = removeLegacyImporter(source); - if (source.startsWith('file://')) { - return pathToUrlString( - p.relative(sourceMapDir, fileUrlToPathCrossPlatform(source)) - ); - } else if (source.startsWith('data:')) { - return 'stdin'; - } else { - return source; - } - }); + sourceMap.sources = sourceMap.sources.map(source => { + if (source.startsWith('file://')) { + return pathToUrlString( + p.relative(sourceMapDir, fileUrlToPathCrossPlatform(source)) + ); + } else if (source.startsWith('data:')) { + return 'stdin'; + } else { + return source; + } + }); sourceMapBytes = Buffer.from(JSON.stringify(sourceMap)); @@ -319,18 +306,13 @@ function newLegacyResult( start, end, duration: end - start, - includedFiles: result.loadedUrls - .filter(url => url.protocol !== endOfLoadProtocol) - .map(url => { - if (url.protocol === legacyImporterProtocol) { - return decodeURI(url.pathname); - } - - const urlString = removeLegacyImporter(url.toString()); - return urlString.startsWith('file:') - ? fileUrlToPathCrossPlatform(urlString) - : urlString; - }), + includedFiles: result.loadedUrls.flatMap(url => { + if (url.protocol === 'file:') { + return legacyFileUrlToPath(url) ?? []; + } + + return url.toString(); + }), }, }; } @@ -345,7 +327,7 @@ function newLegacyException(error: Error): LegacyException { }); } - const span = error.span ? removeLegacyImporterFromSpan(error.span) : null; + const span = error.span ? error.span : null; let file: string; if (!span?.url) { file = 'stdin'; @@ -354,18 +336,18 @@ function newLegacyException(error: Error): LegacyException { // standard URL type which is slightly less featureful. `fileURLToPath()` // does work with standard URL objects in practice, but we know that we // generate Node URLs here regardless. - file = fileUrlToPathCrossPlatform(span.url as URL); + file = legacyFileUrlToPath(span.url as URL) || 'stdin'; } else { file = span.url.toString(); } - const errorString = removeLegacyImporter(error.toString()); + const errorString = error.toString(); return Object.assign(new Error(), { status: 1, message: errorString.replace(/^Error: /, ''), formatted: errorString, toString: () => errorString, - stack: error.stack ? removeLegacyImporter(error.stack) : undefined, + stack: error.stack, line: isNullOrUndefined(error.span?.start.line) ? undefined : error.span!.start.line + 1, diff --git a/lib/src/legacy/utils.ts b/lib/src/legacy/utils.ts index 7a72472f..3ac8d262 100644 --- a/lib/src/legacy/utils.ts +++ b/lib/src/legacy/utils.ts @@ -2,58 +2,33 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import {strict as assert} from 'assert'; -import {pathToFileURL} from 'url'; - +import {URL, pathToFileURL} from 'url'; import {fileUrlToPathCrossPlatform} from '../utils'; -import {SourceSpan} from '../vendor/sass'; -import {legacyImporterFileProtocol} from './importer'; +import * as p from 'path'; -/** - * The URL protocol to use for URLs canonicalized using `LegacyImporterWrapper`. - */ -export const legacyImporterProtocol = 'legacy-importer:'; +const legacyFileUrlAnnotaion = 'legacy_importer_file'; /** - * The prefix for absolute URLs canonicalized using `LegacyImporterWrapper`. - * - * This is used to distinguish imports resolved relative to URLs returned by a - * legacy importer from manually-specified absolute URLs. + * Returns a file: URL with a querystring annotating relative file */ -export const legacyImporterProtocolPrefix = 'legacy-importer-'; - -// A regular expression that matches legacy importer protocol syntax that -// should be removed from human-readable messages. -const removeLegacyImporterRegExp = new RegExp( - `${legacyImporterProtocol}|${legacyImporterProtocolPrefix}`, - 'g' -); - -// Returns `string` with all instances of legacy importer syntax removed. -export function removeLegacyImporter(string: string): string { - return string.replace(removeLegacyImporterRegExp, ''); +export function pathToLegacyFileURL(path: string, file?: string) { + const url = pathToFileURL(path); + if (file === undefined) { + url.searchParams.set(legacyFileUrlAnnotaion, ''); + } else if (!p.isAbsolute(file)) { + url.searchParams.set(legacyFileUrlAnnotaion, file); + } + return url; } -// Returns a copy of [span] with the URL updated to remove legacy importer -// syntax. -export function removeLegacyImporterFromSpan(span: SourceSpan): SourceSpan { - if (!span.url) return span; - return {...span, url: new URL(removeLegacyImporter(span.url.toString()))}; -} - -// Converts [path] to a `file:` URL and adds the [legacyImporterProtocolPrefix] -// to the beginning so we can distinguish it from manually-specified absolute -// `file:` URLs. -export function pathToLegacyFileUrl(path: string): URL { - return new URL(`${legacyImporterProtocolPrefix}${pathToFileURL(path)}`); -} - -// Converts a `file:` URL with [legacyImporterProtocolPrefix] to the filesystem -// path which it represents. -export function legacyFileUrlToPath(url: URL): string { - assert.equal(url.protocol, legacyImporterFileProtocol); - const originalUrl = url - .toString() - .substring(legacyImporterProtocolPrefix.length); - return fileUrlToPathCrossPlatform(originalUrl); +/** + * Returns the annotated relative file or absolute path from a file: URL + */ +export function legacyFileUrlToPath(fileUrl: URL | string): string | undefined { + const url = new URL(fileUrl); + if (url.searchParams.has(legacyFileUrlAnnotaion)) { + const path = url.searchParams.get(legacyFileUrlAnnotaion)!; + return path === '' ? undefined : path; + } + return fileUrlToPathCrossPlatform(url); } diff --git a/lib/src/sync-process/index.ts b/lib/src/sync-process/index.ts index 426dbb1c..c92ad8e1 100644 --- a/lib/src/sync-process/index.ts +++ b/lib/src/sync-process/index.ts @@ -57,7 +57,7 @@ export class SyncProcess { this.worker.on('error', console.error); this.stdin = new stream.Writable({ - write: (chunk: Buffer, encoding, callback) => { + write: (chunk: Buffer, _encoding, callback) => { this.port.postMessage( { type: 'stdin',