From 3621dfbeb312136295933582426de406de771ab6 Mon Sep 17 00:00:00 2001 From: Brian Donovan <1938+eventualbuddha@users.noreply.github.com> Date: Sun, 10 Jul 2022 15:33:52 -0700 Subject: [PATCH] feat: add `bare` option to allow IIFE wrapper Adds `bare` option, which is `true` by default, but when `false` will wrap the output in an IIFE similar to the official CoffeeScript compiler's default behavior. This can be triggered on the CLI with `--no-bare`, which mirrors `coffee`'s `--bare` flag. Closes #2412 --- src/cli.ts | 51 +++++++++++++++++++++++++++++++++---------- src/index.ts | 6 ++++- src/options.ts | 5 +++++ test/cli_test.ts | 33 ++++++++++++++++++++++++++-- test/iife_test.ts | 15 +++++++++++++ test/support/check.ts | 5 +++-- 6 files changed, 98 insertions(+), 17 deletions(-) create mode 100644 test/iife_test.ts diff --git a/src/cli.ts b/src/cli.ts index 4e8d5b5e9..8ac227869 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import { readdir, readFile, stat, writeFile } from 'mz/fs'; import { basename, dirname, extname, join } from 'path'; import { convert, modernizeJS } from './index'; -import { Options } from './options'; +import { DEFAULT_OPTIONS, Options } from './options'; import PatchError from './utils/PatchError'; export interface IO { @@ -18,7 +18,14 @@ export default async function run( args: ReadonlyArray, io: IO = { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr } ): Promise { - const options = parseArguments(args, io); + const parseResult = parseArguments(args, io); + + if (parseResult.kind === 'error') { + io.stderr.write(`${parseResult.message}\n`); + return 1; + } + + const options = parseResult; if (options.help) { usage(args[0], io.stdout); @@ -39,17 +46,25 @@ export default async function run( return 0; } +type ParseOptionsResult = CLIOptions | ParseOptionsError; + interface CLIOptions { - paths: Array; - baseOptions: Options; - modernizeJS: boolean; - version: boolean; - help: boolean; + readonly kind: 'success'; + readonly paths: Array; + readonly baseOptions: Options; + readonly modernizeJS: boolean; + readonly version: boolean; + readonly help: boolean; +} + +interface ParseOptionsError { + readonly kind: 'error'; + readonly message: string; } -function parseArguments(args: ReadonlyArray, io: IO): CLIOptions { +function parseArguments(args: ReadonlyArray, io: IO): ParseOptionsResult { const paths = []; - const baseOptions: Options = {}; + const baseOptions: Options = { ...DEFAULT_OPTIONS }; let modernizeJS = false; let help = false; let version = false; @@ -75,6 +90,11 @@ function parseArguments(args: ReadonlyArray, io: IO): CLIOptions { modernizeJS = true; break; + case '--bare': + case '--no-bare': + baseOptions.bare = arg === '--bare'; + break; + case '--literate': baseOptions.literate = true; break; @@ -166,15 +186,22 @@ function parseArguments(args: ReadonlyArray, io: IO): CLIOptions { default: if (arg.startsWith('-')) { - io.stderr.write(`Error: unrecognized option '${arg}'\n`); - process.exit(1); + return { kind: 'error', message: `unrecognized option: ${arg}` }; } paths.push(arg); break; } } - return { paths, baseOptions, modernizeJS, version, help }; + if (!baseOptions.bare && modernizeJS) { + return { kind: 'error', message: 'cannot use --modernize-js with --no-bare' }; + } + + if (!baseOptions.bare && baseOptions.useJSModules) { + return { kind: 'error', message: 'cannot use --use-js-modules with --no-bare' }; + } + + return { kind: 'success', paths, baseOptions, modernizeJS, version, help }; } /** diff --git a/src/index.ts b/src/index.ts index 31b132c29..02118d0f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,10 @@ interface Stage { * and formatting. */ export function convert(source: string, options: Options = {}): ConversionResult { + if (!options.bare && options.useJSModules) { + throw new Error('useJSModules requires bare output'); + } + source = removeUnicodeBOMIfNecessary(source); options = resolveOptions(options); const originalNewlineStr = detectNewlineStr(source); @@ -77,7 +81,7 @@ export function convert(source: string, options: Options = {}): ConversionResult } result.code = convertNewlines(result.code, originalNewlineStr); return { - code: result.code, + code: options.bare ? result.code : `(function() {\n${result.code}\n}).call(this);`, }; } diff --git a/src/options.ts b/src/options.ts index 45d913b2d..6aee8a918 100644 --- a/src/options.ts +++ b/src/options.ts @@ -19,6 +19,7 @@ export interface Options { optionalChaining?: boolean; logicalAssignment?: boolean; nullishCoalescing?: boolean; + bare?: boolean; } export const DEFAULT_OPTIONS: Options = { @@ -39,6 +40,10 @@ export const DEFAULT_OPTIONS: Options = { looseIncludes: false, looseComparisonNegation: false, disallowInvalidConstructors: false, + optionalChaining: false, + logicalAssignment: false, + nullishCoalescing: false, + bare: true, }; export function resolveOptions(options: Options): Options { diff --git a/test/cli_test.ts b/test/cli_test.ts index fcd78cf7b..4e3ee0ae7 100644 --- a/test/cli_test.ts +++ b/test/cli_test.ts @@ -30,7 +30,8 @@ async function runCli( args: ReadonlyArray, stdin: string, expectedStdout: string, - expectedStderr = '' + expectedStderr = '', + expectedExitCode = 0 ): Promise { if (stdin[0] === '\n') { stdin = stripSharedIndent(stdin); @@ -67,7 +68,7 @@ async function runCli( // Check the exit code and output streams. expect({ exitCode, stdout: stdout.trim(), stderr: stderr.trim() }).toEqual({ - exitCode: 0, + exitCode: expectedExitCode, stdout: expectedStdout.trim(), stderr: expectedStderr.trim(), }); @@ -561,4 +562,32 @@ describe('decaffeinate CLI', () => { `) ); }); + + it('can wrap output in an IIFE', async () => { + await runCli(['--no-bare'], '', `(function() {\n\n}).call(this);`); + }); + + it('cannot use --modernize-js with --no-bare', async () => { + await runCli( + ['--no-bare', '--modernize-js'], + '', + '', + ` + cannot use --modernize-js with --no-bare + `, + 1 + ); + }); + + it('cannot use --use-js-modules with --no-bare', async () => { + await runCli( + ['--no-bare', '--use-js-modules'], + '', + '', + ` + cannot use --use-js-modules with --no-bare + `, + 1 + ); + }); }); diff --git a/test/iife_test.ts b/test/iife_test.ts new file mode 100644 index 000000000..e4a80c7d7 --- /dev/null +++ b/test/iife_test.ts @@ -0,0 +1,15 @@ +import check from './support/check'; + +test('does not wrap in an IIFE by default', () => { + check(`a = 1`, `const a = 1;`); +}); + +test('wraps in an IIFE if requested', () => { + check(`a = 1`, `(function() {\nconst a = 1;\n}).call(this);`, { options: { bare: false } }); +}); + +test('cannot be used with useJSModules', () => { + expect(() => check(`a = 1`, `const a = 1;`, { options: { useJSModules: true, bare: false } })).toThrow( + /useJSModules requires bare output/ + ); +}); diff --git a/test/support/check.ts b/test/support/check.ts index c19f90c82..61af315ee 100644 --- a/test/support/check.ts +++ b/test/support/check.ts @@ -1,6 +1,6 @@ import assert from 'assert'; import { convert } from '../../src/index'; -import { Options } from '../../src/options'; +import { DEFAULT_OPTIONS, Options } from '../../src/options'; import PatchError from '../../src/utils/PatchError'; import stripSharedIndent from '../../src/utils/stripSharedIndent'; @@ -56,12 +56,13 @@ function maybeStripIndent( function checkOutput(source: string, expected: string, options: Options): void { try { const converted = convert(source, { + ...DEFAULT_OPTIONS, disableSuggestionComment: true, ...options, }); let actual = converted.code; if (actual.endsWith('\n') && !expected.endsWith('\n')) { - actual = actual.substr(0, actual.length - 1); + actual = actual.slice(0, -1); } expect(actual).toBe(expected); } catch (err: unknown) {