Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@
],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "node",
"request": "launch",
"name": "Debug current test",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"cwd": "${workspaceFolder}",
"args": [
"-w",
"--extension",
"ts,js",
"${file}"
],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "node",
"request": "attach",
Expand Down
19 changes: 10 additions & 9 deletions src/bundle-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export interface OutputOptions {
/**
* By default all interfaces, types and const enums are marked as exported even if they aren't exported directly.
* This option allows you to disable this behavior so a node will be exported if it is exported from root source file only.
* @default true
*/
exportReferencedTypes?: boolean;
}
Expand All @@ -107,20 +108,20 @@ export interface LibrariesOptions {
* Array of package names from node_modules to inline typings from.
* Used types will be inlined into the output file.
*/
inlinedLibraries?: string[];
inlinedLibraries?: (string | RegExp)[];

/**
* Array of package names from node_modules to import typings from.
* Used types will be imported using `import { First, Second } from 'library-name';`.
* By default all libraries will be imported (except inlined libraries and libraries from @types).
* By default all libraries will be imported (except inlined libraries and `@types`).
*/
importedLibraries?: string[];
importedLibraries?: (string | RegExp)[];

/**
* Array of package names from @types to import typings from via the triple-slash reference directive.
* Array of package names from `@types` to import typings from via the triple-slash reference directive.
* By default all packages are allowed and will be used according to their usages.
*/
allowedTypesLibraries?: string[];
allowedTypesLibraries?: (string | RegExp)[];
}

export interface EntryPointConfig {
Expand Down Expand Up @@ -660,7 +661,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
importItem.requireImports.add(collisionsResolver.addTopLevelIdentifier(preferredLocalName));
}

function addNamedImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier, importedIdentifier: ts.Identifier): void {
function addNamedImport(importItem: ModuleImportsSet, preferredLocalName: ts.ModuleExportName, importedIdentifier: ts.ModuleExportName): void {
const newLocalName = collisionsResolver.addTopLevelIdentifier(preferredLocalName);
const importedName = importedIdentifier.text;
importItem.namedImports.set(newLocalName, importedName);
Expand All @@ -671,7 +672,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
importItem.reExports.set(reExportedName, moduleExportedName);
}

function addNsImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier): void {
function addNsImport(importItem: ModuleImportsSet, preferredLocalName: ts.ModuleExportName): void {
if (importItem.nsImport === null) {
importItem.nsImport = collisionsResolver.addTopLevelIdentifier(preferredLocalName);
}
Expand Down Expand Up @@ -1092,7 +1093,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
}

// eslint-disable-next-line complexity
function getIdentifierOfNamespaceImportFromInlinedModule(nsSymbol: ts.Symbol): ts.Identifier | null {
function getIdentifierOfNamespaceImportFromInlinedModule(nsSymbol: ts.Symbol): ts.ModuleExportName | null {
// handling namespaced re-exports/imports
// e.g. `export * as NS from './local-module';` or `import * as NS from './local-module'; export { NS }`
for (const decl of getDeclarationsForSymbol(nsSymbol)) {
Expand Down Expand Up @@ -1238,7 +1239,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
throw new Error(`Cannot find symbol or exports for source file ${sourceFile.fileName}`);
}

let namespaceIdentifier: ts.Identifier | null = null;
let namespaceIdentifier: ts.ModuleExportName | null = null;

forEachImportOfStatement(sourceFile, (imp: ImportOfStatement) => {
// here we want to handle creation of artificial namespace for a inlined module
Expand Down
2 changes: 1 addition & 1 deletion src/collisions-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class CollisionsResolver {
/**
* Adds (or "registers") a top-level {@link identifier} (which takes a top-level scope name to use).
*/
public addTopLevelIdentifier(identifier: ts.Identifier | ts.DefaultKeyword): string {
public addTopLevelIdentifier(identifier: ts.Identifier | ts.DefaultKeyword | ts.ModuleExportName): string {
const symbol = getDeclarationNameSymbol(identifier, this.typeChecker);
if (symbol === null) {
throw new Error(`Something went wrong - cannot find a symbol for top-level identifier ${identifier.getText()} (from ${identifier.parent.parent.getText()})`);
Expand Down
2 changes: 1 addition & 1 deletion src/compile-dts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function changeExtensionToDts(fileName: string): string {
let ext: ts.Extension | undefined;

// `path.extname` doesn't handle `.d.ts` cases (it returns `.ts` instead of `.d.ts`)
if (fileName.endsWith(ts.Extension.Dts)) {
if (fileName.endsWith(ts.Extension.Dts) || fileName.endsWith(ts.Extension.Dmts) || fileName.endsWith(ts.Extension.Dcts)) {
return fileName;
}

Expand Down
79 changes: 57 additions & 22 deletions src/config-file/check-schema-match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,37 @@ export interface PrimitiveValues {
requiredBoolean: true;
string: '';
requiredString: 'REQUIRED';
stringOrRegExp: 'StringOrRegExp';
}

export type SchemeDescriptor<T> = {
[P in keyof T]-?: T[P] extends unknown[] ? [SchemeDescriptor<T[P][number]>] : SchemeDescriptor<T[P]>;
export type SchemeDescriptorObject<T extends object> = {
[P in keyof T]-?: SchemeDescriptor<NonNullable<T[P]>>;
};

export type SchemeDescriptor<T> =
// Check for string-only first. [] prevents distributive conditional types from being applied to `string | RegExp`.
[T] extends [string]
? PrimitiveValues['string'] | PrimitiveValues['requiredString']
: [T] extends [string | RegExp]
? PrimitiveValues['stringOrRegExp']
: T extends boolean
? PrimitiveValues['boolean'] | PrimitiveValues['requiredBoolean']
: T extends unknown[]
? [SchemeDescriptor<T[number]>]
: T extends object
? SchemeDescriptorObject<T>
// Value type is not currently supported
: never;

export const schemaPrimitiveValues: Readonly<PrimitiveValues> = {
boolean: false,
requiredBoolean: true,
string: '',
requiredString: 'REQUIRED',
stringOrRegExp: 'StringOrRegExp',
};

const schemaRequiredValues = new Set([
const schemaRequiredValues = new Set<unknown>([
schemaPrimitiveValues.requiredBoolean,
schemaPrimitiveValues.requiredString,
]);
Expand All @@ -31,57 +48,75 @@ export function checkSchemaMatch<T>(value: unknown, schema: SchemeDescriptor<T>,
}

// eslint-disable-next-line complexity
function checkSchemaMatchRecursively<T>(value: unknown, schema: SchemeDescriptor<T> | [SchemeDescriptor<T>], prefix: string, errors: string[]): value is T {
if (typeof schema === 'boolean' || typeof schema === 'string') {
const schemeType = typeof schema;
if (value === undefined && schemaRequiredValues.has(schema)) {
errors.push(`Value for "${prefix}" is required and must have type "${schemeType}"`);
function checkSchemaMatchRecursively<T>(value: unknown, schema: SchemeDescriptor<T>, prefix: string, errors: string[]): value is T {
if (value === undefined && schemaRequiredValues.has(schema)) {
errors.push(`Value for "${prefix}" is required and must have type "${typeof schema}"`);
return false;
}

if (value === undefined || value === null) {
return true;
}

if (schema === schemaPrimitiveValues.stringOrRegExp) {
if (!(value instanceof RegExp) && typeof value !== 'string') {
errors.push(`Value for "${prefix}" must be a string or RegExp`);
return false;
}
return true;
}

if (typeof schema === 'boolean' || typeof schema === 'string') {
const schemeType = typeof schema;
const valueType = typeof value;
if (value !== undefined && typeof schema !== valueType) {
errors.push(`Type of values for "${prefix}" is not the same, expected=${schemeType}, actual=${valueType}`);
if (schemeType !== valueType) {
errors.push(`Incorrect value type for "${prefix}": expected=${schemeType}, actual=${valueType}`);
return false;
}

return true;
}

if (value === undefined) {
return true;
}

if (Array.isArray(schema)) {
if (!Array.isArray(value)) {
errors.push(`Value for "${prefix}" must be an array`);
return false;
}

let result = true;
for (let i = 0; i < value.length; ++i) {
if (!checkSchemaMatchRecursively(value[i], schema[0], `${prefix}[${i}]`, errors)) {
if (value[i] === undefined || value[i] === null) {
// undefined is not valid within arrays
errors.push(`Value for "${prefix}[${i}]" is ${value[i]}`);
result = false;
} else if (!checkSchemaMatchRecursively(value[i], schema[0], `${prefix}[${i}]`, errors)) {
result = false;
}
}

return result;
}

type SchemeKey = keyof SchemeDescriptor<T>;
type SchemeSubValue = SchemeDescriptor<T[keyof T]>;
if (typeof value !== 'object') {
errors.push(`Value for "${prefix}" must be an object`);
return false;
}

// At this point the schema and T are objects, but the compiler can't infer it
const schemaObject = schema as SchemeDescriptorObject<Record<string, unknown>>;

let result = true;
for (const valueKey of Object.keys(value as object)) {
if (schema[valueKey as keyof T] === undefined) {
errors.push(`Exceeded property "${valueKey}" found in ${prefix.length === 0 ? 'the root' : prefix}`);
for (const valueKey of Object.keys(value)) {
if (schemaObject[valueKey] === undefined) {
errors.push(`Excess property "${valueKey}" found in ${prefix.length === 0 ? 'the root' : prefix}`);
result = false;
}
}

for (const schemaKey of Object.keys(schema)) {
for (const schemaKey of Object.keys(schemaObject)) {
const isSubValueSchemeMatched = checkSchemaMatchRecursively(
(value as Record<string, unknown>)[schemaKey],
schema[schemaKey as SchemeKey] as SchemeSubValue,
schemaObject[schemaKey],
prefix.length === 0 ? schemaKey : `${prefix}.${schemaKey}`,
errors
);
Expand Down
8 changes: 4 additions & 4 deletions src/config-file/load-config-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function loadConfigFile(configPath: string): BundlerConfig {
const possibleConfig = require(getAbsolutePath(configPath));

const errors: string[] = [];
if (!checkSchemaMatch(possibleConfig, configScheme, errors)) {
if (!checkSchemaMatch<BundlerConfig>(possibleConfig, configScheme, errors)) {
errorLog(errors.join('\n'));
throw new Error('Cannot parse config file');
}
Expand Down Expand Up @@ -67,9 +67,9 @@ const configScheme: SchemeDescriptor<BundlerConfig> = {
failOnClass: schemaPrimitiveValues.boolean,
noCheck: schemaPrimitiveValues.boolean,
libraries: {
allowedTypesLibraries: [schemaPrimitiveValues.string],
importedLibraries: [schemaPrimitiveValues.string],
inlinedLibraries: [schemaPrimitiveValues.string],
allowedTypesLibraries: [schemaPrimitiveValues.stringOrRegExp],
importedLibraries: [schemaPrimitiveValues.stringOrRegExp],
inlinedLibraries: [schemaPrimitiveValues.stringOrRegExp],
},
output: {
inlineDeclareGlobals: schemaPrimitiveValues.boolean,
Expand Down
18 changes: 10 additions & 8 deletions src/module-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export interface UsedForModulesModuleInfo extends UsedModuleInfoCommon {
export type ModuleInfo = InlinedModuleInfo | ImportedModuleInfo | ReferencedModuleInfo | UsedForModulesModuleInfo;

export interface ModuleCriteria {
inlinedLibraries: string[];
importedLibraries: string[] | undefined;
allowedTypesLibraries: string[] | undefined;
inlinedLibraries: (string | RegExp)[];
importedLibraries: (string | RegExp)[] | undefined;
allowedTypesLibraries: (string | RegExp)[] | undefined;
typeRoots?: string[];
}

Expand Down Expand Up @@ -120,15 +120,15 @@ function getModuleInfoImpl(currentFilePath: string, originalFileName: string, cr
return { type: ModuleType.ShouldBeUsedForModulesOnly, fileName: originalFileName, isExternal: true };
}

function shouldLibraryBeInlined(npmLibraryName: string, typesLibraryName: string | null, inlinedLibraries: string[]): boolean {
function shouldLibraryBeInlined(npmLibraryName: string, typesLibraryName: string | null, inlinedLibraries: (string | RegExp)[]): boolean {
return isLibraryAllowed(npmLibraryName, inlinedLibraries) || typesLibraryName !== null && isLibraryAllowed(typesLibraryName, inlinedLibraries);
}

function shouldLibraryBeImported(
npmLibraryName: string,
typesLibraryName: string | null,
importedLibraries: string[] | undefined,
allowedTypesLibraries: string[] | undefined
importedLibraries: (string | RegExp)[] | undefined,
allowedTypesLibraries: (string | RegExp)[] | undefined
): boolean {
if (typesLibraryName === null) {
return isLibraryAllowed(npmLibraryName, importedLibraries);
Expand All @@ -144,8 +144,10 @@ function shouldLibraryBeImported(
return false;
}

function isLibraryAllowed(libraryName: string, allowedArray?: string[]): boolean {
return allowedArray === undefined || allowedArray.indexOf(libraryName) !== -1;
function isLibraryAllowed(libraryName: string, allowed: (string | RegExp)[] | undefined): boolean {
return Array.isArray(allowed)
? allowed.some(item => typeof item === 'string' ? item === libraryName : item.test(libraryName))
: true;
}

function remapToTypesFromNodeModules(pathRelativeToTypesRoot: string): string {
Expand Down
13 changes: 13 additions & 0 deletions tests/e2e/test-cases/inline-from-deps-regexp/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TestCaseConfig } from '../test-case-config';

const config: TestCaseConfig = {
libraries: {
inlinedLibraries: [
/^fake-package$/,
'fake-types-lib-1',
'fake-types-lib-', // not matched
]
},
};

export = config;
1 change: 1 addition & 0 deletions tests/e2e/test-cases/inline-from-deps-regexp/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('../run-test-case').runTestCase(__dirname);
10 changes: 10 additions & 0 deletions tests/e2e/test-cases/inline-from-deps-regexp/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Interface, Type, ModuleWithoutQuotes } from 'fake-package';
import { Derived } from 'fake-types-lib-2';
import { FooBar } from 'fake-types-lib-3'
import { SomeClass } from 'fake-package/some-class';

export type TestType = Interface | Type;
export class MyClass extends SomeClass {}
export type ReExportedTypes = Derived;
export type T = ModuleWithoutQuotes.A;
export type Foo = FooBar<ArrayConstructor>;
21 changes: 21 additions & 0 deletions tests/e2e/test-cases/inline-from-deps-regexp/output.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Derived } from 'fake-types-lib-2';
import { FooBar } from 'fake-types-lib-3';

export interface Interface {
}
export type Type = number | string;
declare module ModuleWithoutQuotes {
export type A = string;
}
declare class SomeClass {
private x;
public constructor();
}
export type TestType = Interface | Type;
export declare class MyClass extends SomeClass {
}
export type ReExportedTypes = Derived;
export type T = ModuleWithoutQuotes.A;
export type Foo = FooBar<ArrayConstructor>;

export {};
Loading