Skip to content

Commit

Permalink
Merge pull request #224 from timocov/fix-module-resolution-with-non-r…
Browse files Browse the repository at this point in the history
…elative-imports

 Improve support of non-relative imports in projects with a baseUrl
  • Loading branch information
timocov committed Oct 2, 2022
2 parents a7a6c96 + 76c6ba7 commit 2a1a48f
Show file tree
Hide file tree
Showing 16 changed files with 385 additions and 168 deletions.
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

0 comments on commit 2a1a48f

Please sign in to comment.