Skip to content

Commit

Permalink
feat: add support for multiple entry files (#134)
Browse files Browse the repository at this point in the history
closes #55
  • Loading branch information
ToppleTheNun committed Feb 21, 2022
1 parent fdcdbaa commit 2651cd6
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 50 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Despite all the recent hype, setting up a new TypeScript (x React) library can b
- [`dts test`](#dts-test)
- [`dts lint`](#dts-lint)
- [`dts create`](#dts-create)
- [Multiple Entry Files](#multiple-entry-files)
- [Contributing](#contributing)
- [Author](#author)
- [License](#license)
Expand Down Expand Up @@ -581,6 +582,23 @@ Examples
$ dts create --husky false mypackage
```
### Multiple Entry Files
You can run `dts watch` or `dts build` with multiple entry files, for example:
```shell
dts build \
--entry src/index.ts \
--entry src/foo.ts \
--entry src/subdir/index.ts \
--entry src/globdir/**/*.ts
```
When given multiple entries, dts-cli 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/`.
dts-cli will also expand any globs.
## Contributing
Please see the [Contributing Guidelines](./CONTRIBUTING.md).
Expand Down
68 changes: 36 additions & 32 deletions src/createBuildConfigs.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { RollupOptions, OutputOptions } from 'rollup';
import { RollupOptions } from 'rollup';
import * as fs from 'fs-extra';
import { concatAllArray } from 'jpjs';

import { paths } from './constants';
import { DtsOptions, NormalizedOpts, PackageJson } from './types';
import {
DtsOptions,
DtsOptionsInput,
NormalizedOpts,
PackageJson,
} from './types';

import { createRollupConfig } from './createRollupConfig';
import logError from './logError';
Expand Down Expand Up @@ -35,18 +39,14 @@ if (fs.existsSync(paths.appConfigTs)) {
export async function createBuildConfigs(
opts: NormalizedOpts,
appPackageJson: PackageJson
): Promise<Array<RollupOptions & { output: OutputOptions }>> {
const allInputs = concatAllArray(
opts.input.map((input: string) =>
createAllFormats(opts, input).map(
(options: DtsOptions, 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<Array<RollupOptions>> {
const allInputs = createAllFormats(opts).map(
(options: DtsOptions, 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(
Expand All @@ -58,47 +58,51 @@ export async function createBuildConfigs(
);
}

function createAllFormats(
opts: NormalizedOpts,
input: string
): [DtsOptions, ...DtsOptions[]] {
function createAllFormats(opts: NormalizedOpts): [DtsOptions, ...DtsOptions[]] {
const sharedOpts: Omit<DtsOptions, '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: DtsOptionsInput, 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: Array.isArray(opts.name) ? opts.name[0] : opts.name,
};

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 [DtsOptions, ...DtsOptions[]];
}
21 changes: 12 additions & 9 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 Down Expand Up @@ -40,15 +40,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}`;
}

const tsCompilerOptions = typescriptCompilerOptions(opts.tsconfig);
const typesRollupEnabled =
Expand Down Expand Up @@ -92,8 +93,10 @@ export async function createRollupConfig(
},
// Establish Rollup output
output: {
// Set the output directory
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
62 changes: 55 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,39 @@ async function getInputs(
);
}

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 @@ -304,7 +337,11 @@ prog
await cleanDistFolder();
}
if (opts.format.includes('cjs')) {
await writeCjsEntryFile(opts.name);
await Promise.all(
opts.output.file.map((file) =>
writeCjsEntryFile(file, opts.input.length)
)
);
}

type Killer = execa.ExecaChildProcess | null;
Expand Down Expand Up @@ -413,7 +450,11 @@ prog
}
const logger = await createProgressEstimator();
if (opts.format.includes('cjs')) {
const promise = writeCjsEntryFile(opts.name).catch(logError);
const promise = Promise.all(
opts.output.file.map((file) =>
writeCjsEntryFile(file, opts.input.length).catch(logError)
)
);
logger(promise, 'Creating entry file');
}
try {
Expand Down Expand Up @@ -445,25 +486,31 @@ prog
});

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 @@ -473,7 +520,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: 7 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,21 @@ export interface WatchOpts extends BuildOpts {

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

export type DtsOptionsInput = { [entryAlias: string]: string };

export interface DtsOptions extends SharedOpts {
// Name of package
name: string;
// path to file
input: string;
input: string | DtsOptionsInput;
// Environment
env: 'development' | 'production';
// Module format
Expand Down
60 changes: 60 additions & 0 deletions test/e2e/dts-build-multipleEntries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as shell from 'shelljs';

import * as util from '../utils/fixture';
import { execWithCache } from '../utils/shell';

shell.config.silent = false;

const testDir = 'e2e';
const fixtureName = 'build-multipleEntries';
const stageName = `stage-${fixtureName}`;

describe('dts build :: multiple entries', () => {
beforeAll(() => {
util.teardownStage(stageName);
util.setupStageWithFixture(testDir, stageName, fixtureName);
});

it('should compile files into a dist directory', () => {
const output = execWithCache(
[
'node ../dist/index.js build',
'--entry src/index.ts',
'--entry src/returnsFalse.ts',
'--entry src/returnsTrue.ts',
].join(' ')
);

expect(shell.test('-f', 'dist/index.js')).toBeTruthy();
expect(shell.test('-f', 'dist/index.cjs.development.js')).toBeTruthy();
expect(shell.test('-f', 'dist/index.cjs.production.min.js')).toBeTruthy();
expect(shell.test('-f', 'dist/index.esm.js')).toBeTruthy();
expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy();

expect(shell.test('-f', 'dist/returnsFalse.js')).toBeTruthy();
expect(
shell.test('-f', 'dist/returnsFalse.cjs.development.js')
).toBeTruthy();
expect(
shell.test('-f', 'dist/returnsFalse.cjs.production.min.js')
).toBeTruthy();
expect(shell.test('-f', 'dist/returnsFalse.esm.js')).toBeTruthy();
expect(shell.test('-f', 'dist/returnsFalse.d.ts')).toBeTruthy();

expect(shell.test('-f', 'dist/returnsTrue.js')).toBeTruthy();
expect(
shell.test('-f', 'dist/returnsTrue.cjs.development.js')
).toBeTruthy();
expect(
shell.test('-f', 'dist/returnsTrue.cjs.production.min.js')
).toBeTruthy();
expect(shell.test('-f', 'dist/returnsTrue.esm.js')).toBeTruthy();
expect(shell.test('-f', 'dist/returnsTrue.d.ts')).toBeTruthy();

expect(output.code).toBe(0);
});

afterAll(() => {
util.teardownStage(stageName);
});
});
7 changes: 7 additions & 0 deletions test/e2e/fixtures/build-multipleEntries/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"scripts": {
"build": "dts build"
},
"name": "build-multipleentries",
"license": "MIT"
}
Loading

0 comments on commit 2651cd6

Please sign in to comment.