Skip to content

Commit

Permalink
feat: add bare option to allow IIFE wrapper
Browse files Browse the repository at this point in the history
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
  • Loading branch information
eventualbuddha committed Jul 10, 2022
1 parent fbe41e2 commit 3621dfb
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 17 deletions.
51 changes: 39 additions & 12 deletions src/cli.ts
Expand Up @@ -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 {
Expand All @@ -18,7 +18,14 @@ export default async function run(
args: ReadonlyArray<string>,
io: IO = { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }
): Promise<number> {
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);
Expand All @@ -39,17 +46,25 @@ export default async function run(
return 0;
}

type ParseOptionsResult = CLIOptions | ParseOptionsError;

interface CLIOptions {
paths: Array<string>;
baseOptions: Options;
modernizeJS: boolean;
version: boolean;
help: boolean;
readonly kind: 'success';
readonly paths: Array<string>;
readonly baseOptions: Options;
readonly modernizeJS: boolean;
readonly version: boolean;
readonly help: boolean;
}

interface ParseOptionsError {
readonly kind: 'error';
readonly message: string;
}

function parseArguments(args: ReadonlyArray<string>, io: IO): CLIOptions {
function parseArguments(args: ReadonlyArray<string>, io: IO): ParseOptionsResult {
const paths = [];
const baseOptions: Options = {};
const baseOptions: Options = { ...DEFAULT_OPTIONS };
let modernizeJS = false;
let help = false;
let version = false;
Expand All @@ -75,6 +90,11 @@ function parseArguments(args: ReadonlyArray<string>, io: IO): CLIOptions {
modernizeJS = true;
break;

case '--bare':
case '--no-bare':
baseOptions.bare = arg === '--bare';
break;

case '--literate':
baseOptions.literate = true;
break;
Expand Down Expand Up @@ -166,15 +186,22 @@ function parseArguments(args: ReadonlyArray<string>, 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 };
}

/**
Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Expand Up @@ -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);
Expand Down Expand Up @@ -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);`,
};
}

Expand Down
5 changes: 5 additions & 0 deletions src/options.ts
Expand Up @@ -19,6 +19,7 @@ export interface Options {
optionalChaining?: boolean;
logicalAssignment?: boolean;
nullishCoalescing?: boolean;
bare?: boolean;
}

export const DEFAULT_OPTIONS: Options = {
Expand All @@ -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 {
Expand Down
33 changes: 31 additions & 2 deletions test/cli_test.ts
Expand Up @@ -30,7 +30,8 @@ async function runCli(
args: ReadonlyArray<string>,
stdin: string,
expectedStdout: string,
expectedStderr = ''
expectedStderr = '',
expectedExitCode = 0
): Promise<void> {
if (stdin[0] === '\n') {
stdin = stripSharedIndent(stdin);
Expand Down Expand Up @@ -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(),
});
Expand Down Expand Up @@ -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
);
});
});
15 changes: 15 additions & 0 deletions 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/
);
});
5 changes: 3 additions & 2 deletions 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';

Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 3621dfb

Please sign in to comment.