Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement stdin input with optional "-" as the file name. #3290

Merged
merged 2 commits into from Jan 4, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions cli/run/index.ts
Expand Up @@ -3,6 +3,7 @@ import relative from 'require-relative';
import { WarningHandler } from '../../src/rollup/types';
import mergeOptions, { GenericConfigObject } from '../../src/utils/mergeOptions';
import { getAliasName } from '../../src/utils/relativeId';
import { stdinName } from '../../src/utils/stdin';
import { handleError } from '../logging';
import batchWarnings from './batchWarnings';
import build from './build';
Expand Down Expand Up @@ -101,6 +102,7 @@ function execute (configFile: string, configs: GenericConfigObject[], command: a
for (const config of configs) {
promise = promise.then(() => {
const warnings = batchWarnings();
handleMissingInput(command, config);
const { inputOptions, outputOptions, optionError } = mergeOptions({
command,
config,
Expand All @@ -115,3 +117,9 @@ function execute (configFile: string, configs: GenericConfigObject[], command: a
return promise;
}
}

function handleMissingInput(command: any, config: GenericConfigObject) {
if (!(command.input || config.input || config.input === '' || process.stdin.isTTY)) {
command.input = stdinName;
}
}
5 changes: 4 additions & 1 deletion src/utils/defaultPlugin.ts
Expand Up @@ -2,13 +2,14 @@ import { Plugin, ResolveIdHook } from '../rollup/types';
import { error } from './error';
import { lstatSync, readdirSync, readFile, realpathSync } from './fs';
import { basename, dirname, isAbsolute, resolve } from './path';
import { readStdin, stdinName } from './stdin';

export function getRollupDefaultPlugin(preserveSymlinks: boolean): Plugin {
return {
name: 'Rollup Core',
resolveId: createResolveId(preserveSymlinks) as ResolveIdHook,
load(id) {
return readFile(id);
return id === stdinName ? readStdin() : readFile(id);
},
resolveFileUrl({ relativePath, format }) {
return relativeUrlMechanisms[format](relativePath);
Expand Down Expand Up @@ -58,6 +59,8 @@ function createResolveId(preserveSymlinks: boolean) {
});
}

if (source === stdinName) return source;

// external modules (non-entry modules that start with neither '.' or '/')
// are skipped at this stage.
if (importer !== undefined && !isAbsolute(source) && source[0] !== '.') return null;
Expand Down
15 changes: 13 additions & 2 deletions src/utils/mergeOptions.ts
Expand Up @@ -5,6 +5,8 @@ import {
WarningHandlerWithDefault
} from '../rollup/types';

import { stdinName } from './stdin';

export interface GenericConfigObject {
[key: string]: unknown;
}
Expand Down Expand Up @@ -208,6 +210,7 @@ function getInputOptions(
defaultOnWarnHandler: WarningHandler
): InputOptions {
const getOption = createGetOption(config, command);
const input = getOption('input', []);

const inputOptions: InputOptions = {
acorn: config.acorn,
Expand All @@ -220,7 +223,7 @@ function getInputOptions(
experimentalTopLevelAwait: getOption('experimentalTopLevelAwait'),
external: getExternal(config, command) as any,
inlineDynamicImports: getOption('inlineDynamicImports', false),
input: getOption('input', []),
input,
manualChunks: getOption('manualChunks'),
moduleContext: config.moduleContext as any,
onwarn: getOnWarn(config, defaultOnWarnHandler),
Expand All @@ -234,6 +237,13 @@ function getInputOptions(
watch: config.watch as any
};

if (
config.watch &&
(input === stdinName || (Array.isArray(input) && input.indexOf(stdinName) >= 0))
) {
throw new Error('watch mode is incompatible with stdin input');
}

// support rollup({ cache: prevBuildObject })
if (inputOptions.cache && (inputOptions.cache as any).cache)
inputOptions.cache = (inputOptions.cache as any).cache;
Expand All @@ -250,6 +260,7 @@ function getOutputOptions(

// Handle format aliases
switch (format) {
case undefined:
case 'esm':
case 'module':
format = 'es';
Expand All @@ -273,7 +284,7 @@ function getOutputOptions(
externalLiveBindings: getOption('externalLiveBindings', true),
file: getOption('file'),
footer: getOption('footer'),
format: format === 'esm' ? 'es' : format,
format,
freeze: getOption('freeze', true),
globals: getOption('globals'),
indent: getOption('indent', true),
Expand Down
54 changes: 54 additions & 0 deletions src/utils/stdin.ts
@@ -0,0 +1,54 @@
export const stdinName = '-';

let stdinResult: string | Error | null = null;
const pending: { reject: Function; resolve: Function }[] = [];

export function readStdin() {
return new Promise<string>((resolve, reject) => {
if (typeof process == 'undefined' || typeof process.stdin == 'undefined') {
reject(new Error('stdin input is invalid in browser'));
return;
}
pending.push({ resolve, reject });
processPending();
if (pending.length === 1) {
// stdin is read once - all callers will get the same result

const chunks: Buffer[] = [];
process.stdin.setEncoding('utf8');
process.stdin
.on('data', chunk => {
if (stdinResult === null) {
chunks.push(chunk);
}
})
.on('end', () => {
if (stdinResult === null) {
stdinResult = chunks.join('');
chunks.length = 0;
}
processPending();
})
.on('error', err => {
if (stdinResult === null) {
stdinResult = err instanceof Error ? err : new Error(err);
chunks.length = 0;
}
processPending();
});
process.stdin.resume();
}
});

function processPending() {
if (stdinResult !== null) {
for (let it; (it = pending.shift()); ) {
if (typeof stdinResult == 'string') {
it.resolve(stdinResult);
} else {
it.reject(stdinResult);
}
}
}
}
}
11 changes: 9 additions & 2 deletions test/cli/index.js
Expand Up @@ -23,8 +23,15 @@ runTestSuiteWithSamples(
done => {
process.chdir(config.cwd || dir);

const command =
'node ' + path.resolve(__dirname, '../../dist/bin') + path.sep + config.command;
const command = config.command
.replace(
/(^| )rollup /g,
`node ${path.resolve(__dirname, '../../dist/bin')}${path.sep}rollup `
)
.replace(
/(^| )shx /g,
`node ${path.resolve(__dirname, '../../node_modules/.bin')}${path.sep}shx `
);

const childProcess = exec(
command,
Expand Down
4 changes: 4 additions & 0 deletions test/cli/samples/stdin-multiple-targets/_config.js
@@ -0,0 +1,4 @@
module.exports = {
description: 'uses stdin in multiple targets',
command: `shx echo "import {PRINT as p} from './a'; import C from './b'; 0 && fail() || p(C); export {C as value, p as print}" | rollup -c`
};
12 changes: 12 additions & 0 deletions test/cli/samples/stdin-multiple-targets/_expected/cjs.js
@@ -0,0 +1,12 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const PRINT = x => console.log(x);

var C = 123;

PRINT(C);

exports.print = PRINT;
exports.value = C;
7 changes: 7 additions & 0 deletions test/cli/samples/stdin-multiple-targets/_expected/es.js
@@ -0,0 +1,7 @@
const PRINT = x => console.log(x);

var C = 123;

PRINT(C);

export { PRINT as print, C as value };
1 change: 1 addition & 0 deletions test/cli/samples/stdin-multiple-targets/a.mjs
@@ -0,0 +1 @@
export const PRINT = x => console.log(x);
1 change: 1 addition & 0 deletions test/cli/samples/stdin-multiple-targets/b.mjs
@@ -0,0 +1 @@
export default 123;
16 changes: 16 additions & 0 deletions test/cli/samples/stdin-multiple-targets/rollup.config.js
@@ -0,0 +1,16 @@
export default [
{
input: '-',
output: {
file: '_actual/cjs.js',
format: 'cjs'
}
},
{
input: '-',
output: {
file: '_actual/es.js',
format: 'es'
}
}
];
4 changes: 4 additions & 0 deletions test/cli/samples/stdin-no-dash/_config.js
@@ -0,0 +1,4 @@
module.exports = {
description: 'stdin input with no dash on CLI',
command: `shx mkdir -p _actual && shx echo "0 && fail() || console.log('PASS');" | rollup > _actual/out.js`
};
1 change: 1 addition & 0 deletions test/cli/samples/stdin-no-dash/_expected/out.js
@@ -0,0 +1 @@
console.log('PASS');
4 changes: 4 additions & 0 deletions test/cli/samples/stdin-self-import/_config.js
@@ -0,0 +1,4 @@
module.exports = {
description: 'stdin input of code that imports a copy of itself',
command: `shx mkdir -p _actual && shx cat input.txt | rollup -f cjs --silent > _actual/out.js`
};
11 changes: 11 additions & 0 deletions test/cli/samples/stdin-self-import/_expected/out.js
@@ -0,0 +1,11 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

let b = 2;
var a = 4;

console.log(a, b);

exports.b = b;
exports.default = a;
10 changes: 10 additions & 0 deletions test/cli/samples/stdin-self-import/input.txt
@@ -0,0 +1,10 @@
import a from "-";
import { b as c } from "-";

export let b = 2;
export default 4;

if (a > c)
console.log(a, c);
else
console.log(c, a);
4 changes: 4 additions & 0 deletions test/cli/samples/stdin-with-dash/_config.js
@@ -0,0 +1,4 @@
module.exports = {
description: 'stdin input with dash on CLI',
command: `shx mkdir -p _actual && shx echo "0 && fail() || console.log('PASS');" | rollup - > _actual/out.js`
};
1 change: 1 addition & 0 deletions test/cli/samples/stdin-with-dash/_expected/out.js
@@ -0,0 +1 @@
console.log('PASS');
4 changes: 2 additions & 2 deletions test/function/index.js
Expand Up @@ -177,14 +177,14 @@ runTestSuiteWithSamples('function', path.resolve(__dirname, 'samples'), (dir, co
.join('\n')}`
);
}

if (config.show) console.groupEnd();

if (unintendedError) throw unintendedError;
if (config.after) config.after();
});
});
})
.catch(err => {
if (config.after) config.after();
if (config.error) {
compareError(err, config.error);
} else {
Expand Down
23 changes: 23 additions & 0 deletions test/function/samples/stdin-not-present/_config.js
@@ -0,0 +1,23 @@
const savedStdin = process.stdin;

module.exports = {
description: 'stdin reads should fail if process.stdin not present (as in a browser)',
options: {
input: '-'
},
before() {
delete process.stdin;
},
after() {
process.stdin = savedStdin;
},
error: {
// TODO we probably want a better error code here as this one is confusing
code: 'PLUGIN_ERROR',
hook: 'load',
// TODO the error message does not need to refer to browsers as there are other possible scenarios
message: 'Could not load -: stdin input is invalid in browser',
plugin: 'Rollup Core',
watchFiles: ['-']
}
};
1 change: 1 addition & 0 deletions test/function/samples/stdin-not-present/main.js
@@ -0,0 +1 @@
assert.equal( 1, 1 );
29 changes: 29 additions & 0 deletions test/function/samples/stdin-read-error/_config.js
@@ -0,0 +1,29 @@
const { Readable } = require('stream');
const savedStdin = process.stdin;

module.exports = {
description: 'stdin reads should fail if process.stdin not present (as in a browser)',
options: {
input: '-'
},
before() {
delete process.stdin;
process.stdin = new Readable({
encoding: 'utf8',
read() {
const error = new Error('Stream is broken.');
return this.destroy ? this.destroy(error) : this.emit('error', error);
}
});
},
after() {
process.stdin = savedStdin;
},
error: {
code: 'PLUGIN_ERROR',
hook: 'load',
message: 'Could not load -: Stream is broken.',
plugin: 'Rollup Core',
watchFiles: ['-']
}
};
1 change: 1 addition & 0 deletions test/function/samples/stdin-read-error/main.js
@@ -0,0 +1 @@
assert.equal( 1, 1 );
11 changes: 11 additions & 0 deletions test/function/samples/stdin-watch/_config.js
@@ -0,0 +1,11 @@
module.exports = {
description: 'throws when using the "watch" option with stdin "-"',
options: {
input: '-',
watch: true
},
error: {
// TODO add an error code
message: 'watch mode is incompatible with stdin input'
}
};
1 change: 1 addition & 0 deletions test/function/samples/stdin-watch/main.js
@@ -0,0 +1 @@
assert.equal( 1, 1 );
21 changes: 19 additions & 2 deletions test/misc/sanity-checks.js
Expand Up @@ -111,7 +111,7 @@ describe('sanity checks', () => {
});
});

it('throws on missing format option', () => {
it('throws on incorrect bundle.generate format option', () => {
const warnings = [];

return rollup
Expand All @@ -122,11 +122,28 @@ describe('sanity checks', () => {
})
.then(bundle => {
assert.throws(() => {
bundle.generate({ file: 'x' });
bundle.generate({ file: 'x', format: 'vanilla' });
}, /You must specify "output\.format", which can be one of "amd", "cjs", "system", "esm", "iife" or "umd"./);
});
});

it('defaults to output format `es` if not specified', () => {
const warnings = [];

return rollup
.rollup({
input: 'x',
plugins: [loader({ x: `export function foo(x){ console.log(x); }` })],
onwarn: warning => warnings.push(warning)
})
.then(bundle => {
return bundle.generate({});
})
.then(({ output: [{ code }] }) => {
assert.equal(code, `function foo(x){ console.log(x); }\n\nexport { foo };\n`);
});
});

it('reuses existing error object', () => {
let error;

Expand Down