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

Improve support of non-relative imports in projects with a baseUrl #224

Merged
merged 7 commits into from Oct 2, 2022
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
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- run: npm install
- run: npm run lint

Expand All @@ -27,9 +27,9 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- run: npm install
- run: npm install typescript@3.0.1
- run: npm install typescript@4.5.2
- run: npm run tsc

ts-current:
Expand All @@ -40,7 +40,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- run: npm install
- run: npm run tsc
- run: npm run test
Expand All @@ -54,7 +54,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- run: npm install
- run: npm install typescript@next
- run: npm run tsc
Expand Down
2 changes: 1 addition & 1 deletion .mocharc.js
Expand Up @@ -7,7 +7,7 @@ const config = {
recursive: true,
diff: true,
timeout: 10000,
slow: 2500,
slow: 5000,
};

if (process.env.TESTS_REPORT_FILE) {
Expand Down
24 changes: 12 additions & 12 deletions package.json
Expand Up @@ -17,24 +17,24 @@
},
"homepage": "https://github.com/timocov/dts-bundle-generator",
"dependencies": {
"typescript": ">=3.0.1",
"yargs": "^17.2.1"
"typescript": ">=4.5.2",
"yargs": "^17.6.0"
},
"devDependencies": {
"@types/mocha": "~9.1.1",
"@types/node": "~12.12.47",
"@types/yargs": "~17.0.5",
"@typescript-eslint/eslint-plugin": "~5.28.0",
"@typescript-eslint/parser": "~5.28.0",
"eslint": "~8.18.0",
"@types/mocha": "~10.0.0",
"@types/node": "~14.18.26",
"@types/yargs": "~17.0.13",
"@typescript-eslint/eslint-plugin": "~5.38.1",
"@typescript-eslint/parser": "~5.38.1",
"eslint": "~8.24.0",
"eslint-plugin-import": "~2.26.0",
"eslint-plugin-prefer-arrow": "~1.2.1",
"eslint-plugin-unicorn": "~42.0.0",
"eslint-plugin-unicorn": "~44.0.0",
"mocha": "~10.0.0",
"npm-run-all": "~4.1.5",
"rimraf": "~3.0.2",
"ts-compiler": "npm:typescript@4.8.2",
"ts-node": "~10.8.1"
"ts-compiler": "npm:typescript@4.8.4",
"ts-node": "~10.9.1"
},
"license": "MIT",
"readme": "README.md",
Expand All @@ -43,7 +43,7 @@
"url": "git+https://github.com/timocov/dts-bundle-generator.git"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
},
"scripts": {
"clean": "rimraf dist/ dts-out/",
Expand Down
24 changes: 18 additions & 6 deletions src/bundle-generator.ts
Expand Up @@ -229,8 +229,17 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:

return leftSymbols.some((leftSymbol: ts.Symbol) => rightSymbols.includes(leftSymbol));
},
resolveReferencedModule: (node: ts.ExportDeclaration | ts.ModuleDeclaration) => {
const moduleName = ts.isExportDeclaration(node) ? node.moduleSpecifier : node.name;
resolveReferencedModule: (node: ts.ExportDeclaration | ts.ModuleDeclaration | ts.ImportTypeNode) => {
let moduleName: ts.Expression | ts.LiteralTypeNode | undefined;

if (ts.isExportDeclaration(node)) {
moduleName = node.moduleSpecifier;
} else if (ts.isModuleDeclaration(node)) {
moduleName = node.name;
} else if (ts.isLiteralTypeNode(node.argument) && ts.isStringLiteral(node.argument.literal)) {
moduleName = node.argument.literal;
}

if (moduleName === undefined) {
return null;
}
Expand Down Expand Up @@ -356,9 +365,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
return false;
}

// we don't need to specify exact file here since we need to figure out whether a file is external or internal one
const moduleFileName = resolveModuleFileName(rootSourceFile.fileName, node.argument.literal.text);
return !getModuleInfo(moduleFileName, criteria).isExternal;
const resolvedModule = updateResultCommonParams.resolveReferencedModule(node);
if (resolvedModule === null) {
return false;
}

return !updateResultCommonParams.getModuleInfo(resolvedModule).isExternal;
},
},
{
Expand Down Expand Up @@ -393,7 +405,7 @@ interface UpdateParams {
getDeclarationsForExportedAssignment(exportAssignment: ts.ExportAssignment): ts.Declaration[];
getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set<ts.SourceFile | ts.ModuleDeclaration>;
areDeclarationSame(a: ts.NamedDeclaration, b: ts.NamedDeclaration): boolean;
resolveReferencedModule(node: ts.ExportDeclaration | ts.ModuleDeclaration): ts.SourceFile | ts.ModuleDeclaration | null;
resolveReferencedModule(node: ts.ExportDeclaration | ts.ModuleDeclaration | ts.ImportTypeNode): ts.SourceFile | ts.ModuleDeclaration | null;
}

const skippedNodes = [
Expand Down
185 changes: 87 additions & 98 deletions src/generate-output.ts
@@ -1,7 +1,7 @@
import * as ts from 'typescript';

import { hasNodeModifier } from './helpers/typescript';
import { packageVersion } from './helpers/package-version';
import { getModifiers, modifiersToMap, recreateRootLevelNodeWithModifiers } from './helpers/typescript';

export interface ModuleImportsSet {
defaultImports: Set<string>;
Expand Down Expand Up @@ -58,13 +58,17 @@ export function generateOutput(params: OutputParams, options: OutputOptions = {}
}
}

const statements = params.statements.map((statement: ts.Statement) => getStatementText(statement, params));
const statements = params.statements.map((statement: ts.Statement) => getStatementText(
statement,
Boolean(options.sortStatements),
params
));

if (options.sortStatements) {
statements.sort(compareStatementText);
}

resultOutput += statementsTextToString(statements, params);
resultOutput += statementsTextToString(statements);

if (params.renamedExports.length !== 0) {
resultOutput += `\n\nexport {\n\t${params.renamedExports.sort().join(',\n\t')},\n};`;
Expand All @@ -82,127 +86,122 @@ export function generateOutput(params: OutputParams, options: OutputOptions = {}
}

interface StatementText {
leadingComment?: string;
text: string;
sortingValue: string;
}

function statementTextToString(s: StatementText): string {
if (s.leadingComment === undefined) {
return s.text;
}

return `${s.leadingComment}\n${s.text}`;
}

function statementsTextToString(statements: StatementText[], helpers: OutputHelpers): string {
const statementsText = statements.map(statementTextToString).join('\n');
return spacesToTabs(prettifyStatementsText(statementsText, helpers));
function statementsTextToString(statements: StatementText[]): string {
const statementsText = statements.map(statement => statement.text).join('\n');
return spacesToTabs(prettifyStatementsText(statementsText));
}

function prettifyStatementsText(statementsText: string, helpers: OutputHelpers): string {
function prettifyStatementsText(statementsText: string): string {
const sourceFile = ts.createSourceFile('output.d.ts', statementsText, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
const printer = ts.createPrinter(
{
newLine: ts.NewLineKind.LineFeed,
removeComments: false,
},
{
substituteNode: (hint: ts.EmitHint, node: ts.Node) => {
// `import('module').Qualifier` or `typeof import('module').Qualifier`
if (ts.isImportTypeNode(node) && node.qualifier !== undefined && helpers.needStripImportFromImportTypeNode(node)) {
if (node.isTypeOf) {
// I personally don't like this solution because it spreads the logic of modifying nodes in the code
// I'd prefer to have it somewhere near getStatementText or so
// but at the moment it seems that it's the fastest and most easiest way to remove `import('./module').` form the code
// if you read this and know how to make it better - feel free to share your ideas/PR with fixes
// tslint:disable-next-line:deprecation
return ts.createTypeQueryNode(node.qualifier);
}

return ts.createTypeReferenceNode(node.qualifier, node.typeArguments);
}

return node;
},
}
);

return printer.printFile(sourceFile).trim();
}

function compareStatementText(a: StatementText, b: StatementText): number {
if (a.text > b.text) {
if (a.sortingValue > b.sortingValue) {
return 1;
} else if (a.text < b.text) {
} else if (a.sortingValue < b.sortingValue) {
return -1;
}

return 0;
}

function needAddDeclareKeyword(statement: ts.Statement, nodeText: string): boolean {
// for some reason TypeScript allows to not write `declare` keyword for ClassDeclaration, FunctionDeclaration and VariableDeclaration
// if it already has `export` keyword - so we need to add it
// to avoid TS1046: Top-level declarations in .d.ts files must start with either a 'declare' or 'export' modifier.
if (ts.isClassDeclaration(statement) && (/^class\b/.test(nodeText) || /^abstract\b/.test(nodeText))) {
return true;
}

if (ts.isFunctionDeclaration(statement) && /^function\b/.test(nodeText)) {
return true;
}
function getStatementText(statement: ts.Statement, includeSortingValue: boolean, helpers: OutputHelpers): StatementText {
const shouldStatementHasExportKeyword = helpers.shouldStatementHasExportKeyword(statement);
const needStripDefaultKeyword = helpers.needStripDefaultKeywordForStatement(statement);

if (ts.isVariableStatement(statement) && /^(const|let|var)\b/.test(nodeText)) {
return true;
}
const printer = ts.createPrinter(
{
newLine: ts.NewLineKind.LineFeed,
removeComments: false,
},
{
// eslint-disable-next-line complexity
substituteNode: (hint: ts.EmitHint, node: ts.Node) => {
// `import('module').Qualifier` or `typeof import('module').Qualifier`
if (ts.isImportTypeNode(node) && node.qualifier !== undefined && helpers.needStripImportFromImportTypeNode(node)) {
if (node.isTypeOf) {
return ts.factory.createTypeQueryNode(node.qualifier);
}

if (ts.isEnumDeclaration(statement) && (/^(const)\b/.test(nodeText) || /^(enum)\b/.test(nodeText))) {
return true;
}
return ts.factory.createTypeReferenceNode(node.qualifier, node.typeArguments);
}

return false;
}
if (node !== statement) {
return node;
}

function getStatementText(statement: ts.Statement, helpers: OutputHelpers): StatementText {
const shouldStatementHasExportKeyword = helpers.shouldStatementHasExportKeyword(statement);
const needStripDefaultKeyword = helpers.needStripDefaultKeywordForStatement(statement);
const hasStatementExportKeyword = ts.isExportAssignment(statement) || hasNodeModifier(statement, ts.SyntaxKind.ExportKeyword);
const modifiersMap = modifiersToMap(getModifiers(node));

let nodeText = getTextAccordingExport(statement.getText(), hasStatementExportKeyword, shouldStatementHasExportKeyword);
if (
ts.isEnumDeclaration(node)
&& modifiersMap[ts.SyntaxKind.ConstKeyword]
&& helpers.needStripConstFromConstEnum(node)
) {
modifiersMap[ts.SyntaxKind.ConstKeyword] = false;
}

if (
ts.isEnumDeclaration(statement)
&& hasNodeModifier(statement, ts.SyntaxKind.ConstKeyword)
&& helpers.needStripConstFromConstEnum(statement)) {
nodeText = nodeText.replace(/\bconst\s/, '');
}
// strip the `default` keyword from node
if (modifiersMap[ts.SyntaxKind.DefaultKeyword] && needStripDefaultKeyword) {
// we need just to remove `default` from any node except class node
// for classes we need to replace `default` with `declare` instead
modifiersMap[ts.SyntaxKind.DefaultKeyword] = false;
if (ts.isClassDeclaration(node)) {
modifiersMap[ts.SyntaxKind.DeclareKeyword] = true;
}
}

// strip the `default` keyword from node
if (hasNodeModifier(statement, ts.SyntaxKind.DefaultKeyword) && needStripDefaultKeyword) {
// we need just to remove `default` from any node except class node
// for classes we need to replace `default` with `declare` instead
nodeText = nodeText.replace(/\bdefault\s/, ts.isClassDeclaration(statement) ? 'declare ' : '');
}
if (!shouldStatementHasExportKeyword) {
modifiersMap[ts.SyntaxKind.ExportKeyword] = false;
} else {
modifiersMap[ts.SyntaxKind.ExportKeyword] = true;
}

if (needAddDeclareKeyword(statement, nodeText)) {
nodeText = `declare ${nodeText}`;
}
// for some reason TypeScript allows to not write `declare` keyword for ClassDeclaration, FunctionDeclaration and VariableDeclaration
// if it already has `export` keyword - so we need to add it
// to avoid TS1046: Top-level declarations in .d.ts files must start with either a 'declare' or 'export' modifier.
if (!modifiersMap[ts.SyntaxKind.ExportKeyword] &&
(ts.isClassDeclaration(node)
|| ts.isFunctionDeclaration(node)
|| ts.isVariableStatement(node)
|| ts.isEnumDeclaration(node)
)
) {
modifiersMap[ts.SyntaxKind.DeclareKeyword] = true;
}

const result: StatementText = {
text: nodeText,
};

// add jsdoc for exported nodes only
if (shouldStatementHasExportKeyword) {
const start = statement.getStart();
const jsDocStart = statement.getStart(undefined, true);
const nodeJSDoc = statement.getSourceFile().getFullText().substring(jsDocStart, start).trim();
if (nodeJSDoc.length !== 0) {
result.leadingComment = nodeJSDoc;
return recreateRootLevelNodeWithModifiers(node, modifiersMap, shouldStatementHasExportKeyword);
},
}
);

const statementText = printer.printNode(ts.EmitHint.Unspecified, statement, statement.getSourceFile()).trim();

let sortingValue = '';

if (includeSortingValue) {
// it looks like there is no way to get node's text without a comment at the same time as printing it
// so to get the actual node text we have to parse it again
// hopefully it shouldn't take too long (we don't need to do type check, just parse the AST)
// also let's do it opt-in so if someone isn't using node sorting it won't affect them
const tempSourceFile = ts.createSourceFile('temp.d.ts', statementText, ts.ScriptTarget.ESNext);

// we certainly know that there should be 1 statement at the root level (the printed statement)
sortingValue = tempSourceFile.getChildren()[0].getText();
}

return result;
return { text: statementText, sortingValue };
}

function generateImports(libraryName: string, imports: ModuleImportsSet): string[] {
Expand All @@ -228,16 +227,6 @@ function generateReferenceTypesDirective(libraries: string[]): string {
}).join('\n');
}

function getTextAccordingExport(nodeText: string, isNodeExported: boolean, shouldNodeBeExported: boolean): string {
if (shouldNodeBeExported && !isNodeExported) {
return 'export ' + nodeText;
} else if (isNodeExported && !shouldNodeBeExported) {
return nodeText.slice('export '.length);
}

return nodeText;
}

function spacesToTabs(text: string): string {
// eslint-disable-next-line no-regex-spaces
return text.replace(/^( )+/gm, (substring: string) => {
Expand Down