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

(fix): multiple entries should output multiple bundles #367

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Despite all the recent hype, setting up a new TypeScript (x React) library can b
- [`tsdx build`](#tsdx-build)
- [`tsdx test`](#tsdx-test)
- [`tsdx lint`](#tsdx-lint)
- [Multiple Entry Files](#multiple-entry-files)
- [Contributing](#contributing)
- [Author](#author)
- [License](#license)
Expand Down Expand Up @@ -322,8 +323,8 @@ The `options` object contains the following:

```tsx
export interface TsdxOptions {
// path to file
input: string;
// map: dist path -> entry path
input: { [entryAlias: string]: string };
// Name of package
name: string;
// JS target
Expand Down Expand Up @@ -492,6 +493,22 @@ Examples
$ tsdx lint src --report-file report.json
```

### Multiple Entry Files

You can run `tsdx watch` or `tsdx build` with multiple entry files, for example:

```shell
tsdx build \
--entry src/index.ts \
--entry src/foo.ts \
--entry src/subdir/index.ts \
--entry src/globdir/**/*.ts;
# outputs dist/index.js, dist/foo.js, dist/subdir/index.js, and dist/globdir/**/*.js
# as well as their respective formats and declarations
```

When given multiple entries, TSDX will output separate bundles for each file for each format, as well as their declarations. Each file will be output to `dist/` with the same name it has in the `src/` directory. Entries in subdirectories of `src/` will be mapped to equivalently named subdirectories in `dist/`. TSDX will also expand any globs.

## Contributing

Please see the [Contributing Guidelines](./CONTRIBUTING.md).
Expand Down
63 changes: 32 additions & 31 deletions src/createBuildConfigs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RollupOptions, OutputOptions } from 'rollup';
import { RollupOptions } from 'rollup';
import * as fs from 'fs-extra';
import { concatAllArray } from 'jpjs';

import { paths } from './constants';
import { TsdxOptions, NormalizedOpts } from './types';
Expand All @@ -20,70 +19,72 @@ if (fs.existsSync(paths.appConfig)) {

export async function createBuildConfigs(
opts: NormalizedOpts
): Promise<Array<RollupOptions & { output: OutputOptions }>> {
const allInputs = concatAllArray(
opts.input.map((input: string) =>
createAllFormats(opts, input).map(
(options: TsdxOptions, index: number) => ({
...options,
// We want to know if this is the first run for each entryfile
// for certain plugins (e.g. css)
writeMeta: index === 0,
})
)
)
): Promise<RollupOptions[]> {
const allInputs = createAllFormats(opts).map(
(options: TsdxOptions, index: number) => ({
...options,
// We want to know if this is the first run for each entryfile
// for certain plugins (e.g. css)
writeMeta: index === 0,
})
);

return await Promise.all(
allInputs.map(async (options: TsdxOptions) => {
allInputs.map(async (options: TsdxOptions, index: number) => {
// pass the full rollup config to tsdx.config.js override
const config = await createRollupConfig(options);
const config = await createRollupConfig(options, index);
return tsdxConfig.rollup(config, options);
})
);
}

function createAllFormats(
opts: NormalizedOpts,
input: string
opts: NormalizedOpts
): [TsdxOptions, ...TsdxOptions[]] {
const sharedOpts: Omit<TsdxOptions, 'format' | 'env'> = {
...opts,
// for multi-entry, we use an input object to specify where to put each
// file instead of output.file
input: opts.input.reduce((dict: TsdxOptions['input'], input, index) => {
dict[`${opts.output.file[index]}`] = input;
return dict;
}, {}),
// multiple UMD names aren't currently supported for multi-entry
// (can't code-split UMD anyway)
name: opts.name[0],
};

return [
opts.format.includes('cjs') && {
...opts,
...sharedOpts,
format: 'cjs',
env: 'development',
input,
},
opts.format.includes('cjs') && {
...opts,
...sharedOpts,
format: 'cjs',
env: 'production',
input,
},
opts.format.includes('esm') && { ...opts, format: 'esm', input },
opts.format.includes('esm') && { ...sharedOpts, format: 'esm' },
opts.format.includes('umd') && {
...opts,
...sharedOpts,
format: 'umd',
env: 'development',
input,
},
opts.format.includes('umd') && {
...opts,
...sharedOpts,
format: 'umd',
env: 'production',
input,
},
opts.format.includes('system') && {
...opts,
...sharedOpts,
format: 'system',
env: 'development',
input,
},
opts.format.includes('system') && {
...opts,
...sharedOpts,
format: 'system',
env: 'production',
input,
},
].filter(Boolean) as [TsdxOptions, ...TsdxOptions[]];
}
26 changes: 16 additions & 10 deletions src/createRollupConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { safeVariableName, safePackageName, external } from './utils';
import { safeVariableName, external } from './utils';
import { paths } from './constants';
import { RollupOptions } from 'rollup';
import { terser } from 'rollup-plugin-terser';
Expand All @@ -23,7 +23,8 @@ const errorCodeOpts = {
let shebang: any = {};

export async function createRollupConfig(
opts: TsdxOptions
opts: TsdxOptions,
outputNum: number
): Promise<RollupOptions> {
const findAndRecordErrorCodes = await extractErrors({
...errorCodeOpts,
Expand All @@ -33,15 +34,16 @@ export async function createRollupConfig(
const shouldMinify =
opts.minify !== undefined ? opts.minify : opts.env === 'production';

const outputName = [
`${paths.appDist}/${safePackageName(opts.name)}`,
opts.format,
opts.env,
shouldMinify ? 'min' : '',
'js',
]
const outputSuffix = [opts.format, opts.env, shouldMinify ? 'min' : '', 'js']
.filter(Boolean)
.join('.');
let entryFileNames = `[name].${outputSuffix}`;

// if there's only one input, uses the package name instead of the filename
const inputKeys = Object.keys(opts.input);
if (inputKeys.length === 1) {
entryFileNames = `${inputKeys[0]}.${outputSuffix}`;
}

let tsconfigJSON;
try {
Expand Down Expand Up @@ -81,8 +83,10 @@ export async function createRollupConfig(
},
// Establish Rollup output
output: {
// Set dir to output to
dir: paths.appDist,
// Set filenames of the consumer's package
file: outputName,
entryFileNames,
// Pass through the file format
format: opts.format,
// Do not let Rollup call Object.freeze() on namespace import objects
Expand Down Expand Up @@ -162,6 +166,8 @@ export async function createRollupConfig(
compilerOptions: {
// TS -> esnext, then leave the rest to babel-preset-env
target: 'esnext',
// only output declarations once
declaration: outputNum !== 0 ? false : undefined,
},
},
check: !opts.transpileOnly,
Expand Down
72 changes: 63 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,42 @@ async function getInputs(
return concatAllArray(inputs);
}

function getNamesAndFiles(
inputs: string[],
name?: string
): { names: string[]; files: string[] } {
if (inputs.length === 1) {
const singleName = name || appPackageJson.name;
return {
names: [singleName],
files: [safePackageName(singleName)],
};
}
// if multiple entries, each entry should retain its filename
const names: string[] = [];
const files: string[] = [];
inputs.forEach(input => {
// remove leading src/ directory
let filename = input;
const srcVars = ['src/', './src/'];
if (input.startsWith(srcVars[0]))
filename = input.substring(srcVars[0].length);
else if (input.startsWith(srcVars[1]))
filename = input.substring(srcVars[1].length);

// remove file extension
const noExt = filename
.split('.')
.slice(0, -1)
.join('.');

// UMD name shouldn't contain slashes, replace with __
names.push(noExt.replace('/', '__'));
files.push(noExt);
});
return { names, files };
}

prog
.version(pkg.version)
.command('create <pkg>')
Expand Down Expand Up @@ -294,7 +330,11 @@ prog
opts.name = opts.name || appPackageJson.name;
opts.input = await getInputs(opts.entry, appPackageJson.source);
if (opts.format.includes('cjs')) {
await writeCjsEntryFile(opts.name);
await Promise.all(
opts.output.file.map((file: string) =>
writeCjsEntryFile(file, opts.input.length)
)
);
}

type Killer = execa.ExecaChildProcess | null;
Expand Down Expand Up @@ -399,8 +439,15 @@ prog
await cleanDistFolder();
const logger = await createProgressEstimator();
if (opts.format.includes('cjs')) {
const promise = writeCjsEntryFile(opts.name).catch(logError);
logger(promise, 'Creating entry file');
const promise = Promise.all(
opts.output.file.map((file: string) =>
writeCjsEntryFile(file, opts.input.length).catch(logError)
)
);
logger(
promise,
`Creating CJS entry file${opts.input.length > 1 ? 's' : ''}`
);
}
try {
const promise = asyncro
Expand All @@ -409,40 +456,46 @@ prog
async (inputOptions: RollupOptions & { output: OutputOptions }) => {
let bundle = await rollup(inputOptions);
await bundle.write(inputOptions.output);
await deprecated.moveTypes();
}
)
.catch((e: any) => {
throw e;
});
logger(promise, 'Building modules');
await promise;
await deprecated.moveTypes();
} catch (error) {
logError(error);
process.exit(1);
}
});

async function normalizeOpts(opts: WatchOpts): Promise<NormalizedOpts> {
const inputs = await getInputs(opts.entry, appPackageJson.source);
const { names, files } = getNamesAndFiles(inputs, opts.name);

return {
...opts,
name: opts.name || appPackageJson.name,
input: await getInputs(opts.entry, appPackageJson.source),
name: names,
input: inputs,
format: opts.format.split(',').map((format: string) => {
if (format === 'es') {
return 'esm';
}
return format;
}) as [ModuleFormat, ...ModuleFormat[]],
output: {
file: files,
},
};
}

async function cleanDistFolder() {
await fs.remove(paths.appDist);
}

function writeCjsEntryFile(name: string) {
const baseLine = `module.exports = require('./${safePackageName(name)}`;
function writeCjsEntryFile(file: string, numEntries: number) {
const baseLine = `module.exports = require('./${file}`;
const contents = `
'use strict'

Expand All @@ -452,7 +505,8 @@ if (process.env.NODE_ENV === 'production') {
${baseLine}.cjs.development.js')
}
`;
return fs.outputFile(path.join(paths.appDist, 'index.js'), contents);
const filename = numEntries === 1 ? 'index.js' : `${file}.js`;
return fs.outputFile(path.join(paths.appDist, filename), contents);
}

function getAuthorName() {
Expand Down
9 changes: 6 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ export interface WatchOpts extends BuildOpts {

export interface NormalizedOpts
extends Omit<WatchOpts, 'name' | 'input' | 'format'> {
name: string;
name: string[];
input: string[];
format: [ModuleFormat, ...ModuleFormat[]];
output: {
file: string[];
};
}

export interface TsdxOptions extends SharedOpts {
// Name of package
name: string;
// path to file
input: string;
// map: dist path -> entry path
input: { [entryAlias: string]: string };
// Environment
env: 'development' | 'production';
// Module format
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/build-default/src/subdir1/glob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const glob = 'find me with a glob pattern!';
1 change: 1 addition & 0 deletions test/fixtures/build-default/src/subdir1/subdir1-2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const bar = () => 'foo';
Loading