Skip to content

Commit

Permalink
feat: add autoAddExts feature
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Sep 12, 2024
1 parent aa72de6 commit 0fff075
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 28 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,27 @@ export interface Options {

/** An extra directory layer for output files. */
extraOutdir?: string
/** Automatically add `.js` extension to resolve in `Node16` + ESM mode. */
autoAddExts?: boolean
}
```

### `autoAddExts`

Automatically add `.js` extension to resolve in Node 16+ ESM mode.

```ts
// index.d.ts
import {} from './foo'
```

With `autoAddExts`, it will be transformed to:

```ts
// index.d.ts
import {} from './foo.js'
```

## Sponsors

<p align="center">
Expand All @@ -107,3 +125,7 @@ export interface Options {
## License

[MIT](./LICENSE) License © 2024-PRESENT [三咲智子](https://github.com/sxzz)

```
```
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
},
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"magic-string": "^0.30.11",
"oxc-parser": "^0.27.0",
"unplugin": "^1.14.0"
},
Expand All @@ -103,6 +104,7 @@
"@sxzz/eslint-config": "^4.2.0",
"@sxzz/prettier-config": "^2.0.2",
"@types/node": "^22.5.4",
"@typescript-eslint/typescript-estree": "^8.5.0",
"bumpp": "^9.5.2",
"esbuild": "^0.23.1",
"eslint": "^9.10.0",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/core/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type Options = {
ignoreErrors?: boolean
/** An extra directory layer for output files. */
extraOutdir?: string
/** Automatically add `.js` extension to resolve in `Node16` + ESM mode. */
autoAddExts?: boolean
} & (
| {
/**
Expand Down Expand Up @@ -42,5 +44,6 @@ export function resolveOptions(options: Options): OptionsResolved {
transformer: options.transformer || 'typescript',
ignoreErrors: options.ignoreErrors || false,
extraOutdir: options.extraOutdir,
autoAddExts: options.autoAddExts || false,
}
}
112 changes: 85 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { createFilter } from '@rollup/pluginutils'
import MagicString from 'magic-string'
import { parseAsync } from 'oxc-parser'
import {
createUnplugin,
Expand All @@ -15,6 +16,7 @@ import {
tsTransform,
type TransformResult,
} from './core/transformer'
import type { TSESTree } from '@typescript-eslint/typescript-estree'
import type { PluginBuild } from 'esbuild'
import type { Plugin, PluginContext } from 'rollup'

Expand Down Expand Up @@ -94,6 +96,41 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
code: string,
id: string,
): Promise<undefined> {
let program: TSESTree.Program | undefined
try {
program = JSON.parse(
(await parseAsync(code, { sourceFilename: id })).program,
)
} catch {}

if (options.autoAddExts && program) {
const imports = program.body.filter(
(node) =>
node.type === 'ImportDeclaration' ||
node.type === 'ExportAllDeclaration' ||
node.type === 'ExportNamedDeclaration',
)
const s = new MagicString(code)
for (const i of imports) {
if (!i.source || path.basename(i.source.value).includes('.')) {
continue
}

const resolved = await resolve(context, i.source.value, id)
if (!resolved || resolved.external) continue
if (resolved.id.endsWith('.ts')) {
s.overwrite(
// @ts-expect-error
i.source.start,
// @ts-expect-error
i.source.end,
JSON.stringify(`${i.source.value}.js`),
)
}
}
code = s.toString()
}

let result: TransformResult
switch (options.transformer) {
case 'oxc':
Expand All @@ -120,25 +157,45 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
}
addOutput(id, sourceText)

let program: any
try {
program = JSON.parse(
(await parseAsync(code, { sourceFilename: id })).program,
)
} catch {
return
}
const typeImports = program.body.filter((node: any) => {
if (node.type !== 'ImportDeclaration') return false
if (node.importKind === 'type') return true
return (node.specifiers || []).every(
(spec: any) =>
spec.type === 'ImportSpecifier' && spec.importKind === 'type',
)
})
if (!program) return
const typeImports = program.body.filter(
(
node,
): node is
| TSESTree.ImportDeclaration
| TSESTree.ExportNamedDeclaration
| TSESTree.ExportAllDeclaration => {
if (node.type === 'ImportDeclaration') {
if (node.importKind === 'type') return true
return (
node.specifiers &&
node.specifiers.every(
(spec) =>
spec.type === 'ImportSpecifier' && spec.importKind === 'type',
)
)
}
if (
node.type === 'ExportNamedDeclaration' ||
node.type === 'ExportAllDeclaration'
) {
if (node.exportKind === 'type') return true
return (
node.type === 'ExportNamedDeclaration' &&
node.specifiers &&
node.specifiers.every(
(spec) =>
spec.type === 'ExportSpecifier' && spec.exportKind === 'type',
)
)
}
return false
},
)

for (const i of typeImports) {
const resolved = await resolve(context, i.source.value, id)
if (!i.source) continue
const resolved = (await resolve(context, i.source.value, id))?.id
if (resolved && filter(resolved) && !outputFiles[stripExt(resolved)]) {
let source: string
try {
Expand Down Expand Up @@ -212,22 +269,23 @@ export const IsolatedDecl: UnpluginInstance<Options | undefined, false> =
}
})

const resolve = async (
async function resolve(
context: UnpluginBuildContext,
id: string,
importer: string,
) => {
): Promise<{ id: string; external: boolean } | undefined> {
const nativeContext = context.getNativeBuildContext?.()
if (nativeContext?.framework === 'esbuild') {
return (
await nativeContext.build.resolve(id, {
importer,
resolveDir: path.dirname(importer),
kind: 'import-statement',
})
).path
const resolved = await nativeContext.build.resolve(id, {
importer,
resolveDir: path.dirname(importer),
kind: 'import-statement',
})
return { id: resolved.path, external: resolved.external }
}
return (await (context as PluginContext).resolve(id, importer))?.id
const resolved = await (context as PluginContext).resolve(id, importer)
if (!resolved) return
return { id: resolved.id, external: !!resolved.external }
}

function stripExt(filename: string) {
Expand Down
6 changes: 5 additions & 1 deletion tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@ export default defineConfig({
entry: ['./src/*.ts'],
format: ['cjs', 'esm'],
clean: true,
plugins: [IsolatedDecl()],
plugins: [
IsolatedDecl({
autoAddExts: true,
}),
],
})

0 comments on commit 0fff075

Please sign in to comment.