Skip to content

Commit

Permalink
refactor(module-tools): use ast-grep to replace dts alias instead of …
Browse files Browse the repository at this point in the history
…babel
  • Loading branch information
10Derozan committed Dec 6, 2023
1 parent 4ee48fb commit f520530
Show file tree
Hide file tree
Showing 11 changed files with 123 additions and 202 deletions.
6 changes: 6 additions & 0 deletions .changeset/breezy-lemons-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modern-js/module-tools': patch
---

refactor(module-tools): use ast-grep to replace dts alias instead of babel
refactor(module-tools): 使用 ast-grep 替代 babel 处理 d.ts 文件里的别名
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,7 @@ During the bundleless build process, if an alias appears in the source code, e.g
import utils from '@common/utils';
```

Normally, the type files generated with `tsc` will also contain these aliases. However, Modern.js Module will convert the aliases in the type file generated by `tsc` to:

- Alias conversion is possible for code of the form `import '@common/utils'` or `import utils from '@common/utils'`.
- Aliasing is possible for code of the form `export { utils } from '@common/utils'`.

However, there are some cases that cannot be handled at this time.Output types of the form `Promise<import('@common/utils')>` cannot be converted at this time.
You can discuss it [here](https://github.com/web-infra-dev/modern.js/discussions/4511)
The type files generated with `tsc` will also contain these aliases. However, Modern.js Module will convert the aliases in the type file generated by `tsc`.

### Some examples of the use of `dts`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,7 @@ export default defineConfig({
import utils from '@common/utils';
```

正常来说,使用 `tsc` 生成的产物类型文件也会包含这些别名。不过 Modern.js Module 会对 `tsc` 生成的类型文件里的别名进行转换处理:

- 对于类似 `import '@common/utils'` 或者 `import utils from '@common/utils'` 这样形式的代码可以进行别名转换。
- 对于类似 `export { utils } from '@common/utils'` 这样形式的代码可以进行别名转换。

然而也存在一些情况,目前还无法处理,例如 `Promise<import('@common/utils')>` 这样形式的输出类型目前无法进行转换。
对于这种情况的解决办法,可以参与[讨论](https://github.com/web-infra-dev/modern.js/discussions/4511)
使用 `tsc` 生成的产物类型文件也会包含这些别名。不过 Modern.js Module 会对 `tsc` 生成的类型文件里的别名进行转换处理。

### 一些示例

Expand Down
6 changes: 0 additions & 6 deletions packages/solutions/module-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@
"dependencies": {
"@ampproject/remapping": "1.0.2",
"@ast-grep/napi": "0.12.0",
"@babel/generator": "^7.22.15",
"@babel/parser": "^7.22.15",
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.22.15",
"@modern-js/core": "workspace:*",
"@modern-js/new-action": "workspace:*",
"@modern-js/plugin": "workspace:*",
Expand Down Expand Up @@ -95,8 +91,6 @@
"@modern-js/self": "workspace:@modern-js/module-tools@*",
"@scripts/build": "workspace:*",
"@scripts/vitest-config": "workspace:*",
"@types/babel__generator": "7.6.4",
"@types/babel__traverse": "7.18.5",
"@types/convert-source-map": "1.5.2",
"@types/node": "^14",
"typescript": "^5"
Expand Down
12 changes: 3 additions & 9 deletions packages/solutions/module-tools/src/builder/dts/tsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import type { GeneratorDtsConfig, PluginAPI, ModuleTools } from '../../types';
import {
getTscBinPath,
printOrThrowDtsErrors,
resolveAlias,
addDtsFiles,
writeDtsFiles,
addBannerAndFooter,
withLogTitle,
processDtsFilesAfterTsc,
} from '../../utils';
import { watchDoneText } from '../../constants/dts';

Expand Down Expand Up @@ -100,9 +98,7 @@ const runTscBin = async (
resolveLog(childProgress, {
watch,
watchFn: async () => {
const result = await resolveAlias(config);
const dtsFiles = addBannerAndFooter(result, config.banner, config.footer);
await writeDtsFiles(config, dtsFiles);
await processDtsFilesAfterTsc(config);
runner.buildWatchDts({ buildType: 'bundleless' });
},
});
Expand All @@ -119,8 +115,6 @@ export const runTsc = async (
config: GeneratorDtsConfig,
) => {
await runTscBin(api, config);
const result = await resolveAlias(config);
const dtsFiles = addBannerAndFooter(result, config.banner, config.footer);
await writeDtsFiles(config, dtsFiles);
await processDtsFilesAfterTsc(config);
await addDtsFiles(config.distPath, config.appDirectory);
};
143 changes: 100 additions & 43 deletions packages/solutions/module-tools/src/utils/dts.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { join, dirname, isAbsolute } from 'path';
import { join, dirname, relative, resolve } from 'path';
import { chalk, fs, globby, json5, logger } from '@modern-js/utils';
import MagicString from 'magic-string';
import { createMatchPath, loadConfig } from '@modern-js/utils/tsconfig-paths';
import { ts } from '@ast-grep/napi';
import type {
ITsconfig,
GeneratorDtsConfig,
BuildType,
TsTarget,
} from '../types';
import { normalizeSlashes } from './builder';

type MatchModule = {
name?: string;
start: number;
end: number;
}[];

export const getProjectTsconfig = async (
tsconfigPath: string,
Expand Down Expand Up @@ -43,66 +52,114 @@ export const getTscBinPath = async (appDirectory: string) => {
return tscBinFile;
};

export const resolveAlias = async (config: GeneratorDtsConfig) => {
const { userTsconfig, distPath, tsconfigPath } = config;
const { transformDtsAlias } = await import('./tspath');
const dtsFilenames = await globby('**/*.d.ts', {
export const processDtsFilesAfterTsc = async (config: GeneratorDtsConfig) => {
const { distPath, tsconfigPath, userTsconfig, dtsExtension, banner, footer } =
config;
const dtsFilesPath = await globby('**/*.d.ts', {
absolute: true,
cwd: distPath,
});
const userBaseUrl = userTsconfig.compilerOptions?.baseUrl;
const baseUrl = isAbsolute(userBaseUrl || '.')
? userBaseUrl!
: join(dirname(tsconfigPath), userBaseUrl || '.');
const result = transformDtsAlias({
filenames: dtsFilenames,
baseUrl,
paths: userTsconfig.compilerOptions?.paths ?? {},
});
return result;
};

export const writeDtsFiles = async (
config: GeneratorDtsConfig,
result: { path: string; content: string }[],
) => {
const { dtsExtension } = config;
// write to dist
/**
* get matchPath func to support tsconfig paths
*/
const result = loadConfig(tsconfigPath);
if (result.resultType === 'failed') {
logger.error(result.message);
return;
}
const { absoluteBaseUrl, paths, mainFields, addMatchAll } = result;
const matchPath = createMatchPath(
absoluteBaseUrl,
paths,
mainFields,
addMatchAll,
);

/**
* `export $VAR from` is invalid, so we need `{$$$VAR}`, `*` and `* as $VAR`
* But `import $VAR from` is valid.
*/
const Pattern = [
`import $VAR from '$MATCH'`,
`import $VAR from "$MATCH"`,
`export {$$$VAR} from '$MATCH'`,
`export {$$$VAR} from "$MATCH"`,
`export * from '$MATCH'`,
`export * from "$MATCH"`,
`export * as $VAR from '$MATCH'`,
`export * as $VAR from "$MATCH"`,
`import('$MATCH')`,
`import("$MATCH")`,
];

await Promise.all(
result.map(({ path, content }) => {
const filepath =
dtsFilesPath.map(filePath => {
const code = fs.readFileSync(filePath, 'utf8');
let matchModule: MatchModule = [];
try {
const sgNode = ts.parse(code).root();
matchModule = Pattern.map(p => sgNode.findAll(p))
.flat()
.map(node => {
const matchNode = node.getMatch('MATCH')!;
return {
name: matchNode.text(),
start: matchNode.range().start.index,
end: matchNode.range().end.index,
};
});
} catch (e) {
logger.error('[parse error]', e);
}
const str: MagicString = new MagicString(code);

const originalFilePath = resolve(
absoluteBaseUrl,
userTsconfig?.compilerOptions?.rootDir || 'src',
relative(distPath, filePath),
);

matchModule.forEach(module => {
if (!module.name) {
return;
}
const { start, end, name } = module;
const absoluteImportPath = matchPath(name);
if (absoluteImportPath) {
const relativePath = relative(
dirname(originalFilePath),
absoluteImportPath,
);
const relativeImportPath = normalizeSlashes(
relativePath.startsWith('..') ? relativePath : `./${relativePath}`,
);
str.overwrite(start, end, relativeImportPath);
}
});

// add banner and footer
banner && str.prepend(`${banner}\n`);
footer && str.append(`\n${footer}\n`);

// rewrite dts file
const content = str.toString();
const finalPath =
// We confirm that users will not mix ts and c(m)ts files in their projects.
// If a mix is required, please configure separate buildConfig to handle different inputs.
// So we don't replace .d.(c|m)ts that generated by tsc directly, this can confirm that
// users can use c(m)ts directly rather than enable autoExtension, in this condition,
// users need to set esbuild out-extensions like { '.js': '.mjs' }
path.replace(/\.d\.ts/, dtsExtension);
fs.ensureFileSync(filepath);
filePath.replace(/\.d\.ts/, dtsExtension);
return fs.writeFile(
// only replace .d.ts, if tsc generate .d.m(c)ts, keep.
filepath,
finalPath,
content,
);
}),
);
};

export const addBannerAndFooter = (
result: { path: string; content: string }[],
banner?: string,
footer?: string,
) => {
return result.map(({ path, content }) => {
const ms = new MagicString(content);
banner && ms.prepend(`${banner}\n`);
footer && ms.append(`\n${footer}\n`);
return {
path,
content: ms.toString(),
};
});
};

export const printOrThrowDtsErrors = async (
error: unknown,
options: { abortOnError?: boolean; buildType: BuildType },
Expand Down
1 change: 0 additions & 1 deletion packages/solutions/module-tools/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ export * from './log';
export * from './map';
export * from './print';
export * from './style';
export * from './tspath';
export * from './outExtension';
110 changes: 0 additions & 110 deletions packages/solutions/module-tools/src/utils/tspath.ts

This file was deleted.

Loading

0 comments on commit f520530

Please sign in to comment.