Skip to content

Commit

Permalink
feat: generate declaration files (#4)
Browse files Browse the repository at this point in the history
Co-authored-by: Pooya Parsa <pyapar@gmail.com>
  • Loading branch information
danielroe and pi0 authored Apr 8, 2021
1 parent b92ae02 commit 4b54426
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 20 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Lightweight file-to-file transpiler
## Usage

```bash
npx mkdist [rootDir] [--src=src] [--dist=dist] [--format=cjs|esm]
npx mkdist [rootDir] [--src=src] [--dist=dist] [--format=cjs|esm] [-d|--declaration]
```

## Compared to `tsc` / `babel`
Expand All @@ -29,7 +29,7 @@ npx mkdist [rootDir] [--src=src] [--dist=dist] [--format=cjs|esm]

✅ Faster, thanks to esbuild

🚧 (WIP) `.d.ts` generation
`.d.ts` generation

## License

Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,13 @@
"standard-version": "latest",
"ts-jest": "latest",
"typescript": "latest"
},
"peerDependencies": {
"typescript": ">=3.7"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}
5 changes: 3 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ async function main () {

if (args.help) {
// eslint-disable-next-line no-console
console.log('Usage: npx mkdist [rootDir] [--src=src] [--dist=dist] [--format=cjs|esm]')
console.log('Usage: npx mkdist [rootDir] [--src=src] [--dist=dist] [--format=cjs|esm] [-d|--declaration]')
process.exit(0)
}

const { writtenFiles } = await mkdist({
rootDir: args._[0],
srcDir: args.src,
distDir: args.dist,
format: args.format
format: args.format,
declaration: Boolean(args.declaration || args.d)
})

// eslint-disable-next-line no-console
Expand Down
4 changes: 3 additions & 1 deletion src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export type LoadFile = (input: InputFile) => LoaderResult | Promise<LoaderResult
export interface LoaderContext {
loadFile: LoadFile,
options: {
format?: 'cjs' | 'esm'
format?: 'cjs' | 'esm',
declaration?: boolean
}
}

Expand All @@ -35,6 +36,7 @@ export const defaultLoaders: Loader[] = [vueLoader, jsLoader]
export interface CreateLoaderOptions {
loaders?: Loader[]
format?: LoaderContext['options']['format']
declaration?: LoaderContext['options']['declaration']
}

export function createLoader (loaderOptions: CreateLoaderOptions = {}) {
Expand Down
20 changes: 18 additions & 2 deletions src/loaders/js.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { startService, Service, TransformOptions } from 'esbuild'
import jiti from 'jiti'
import type { Loader } from '../loader'

import type { Loader, LoaderResult } from '../loader'
import { getDeclaration } from '../utils/dts'

let esbuildService: Promise<Service>

Expand All @@ -21,6 +23,19 @@ export const jsLoader: Loader = async (input, { options }) => {

let contents = await input.getContents()

const declaration: LoaderResult = []

if (options.declaration && !input.srcPath?.endsWith('.d.ts')) {
const dtsContents = await getDeclaration(contents, input.srcPath)
if (dtsContents) {
declaration.push({
contents: dtsContents,
path: input.path,
extension: '.d.ts'
})
}
}

// typescript => js
if (input.extension === '.ts') {
contents = await transform(contents, { loader: 'ts' }).then(r => r.code)
Expand All @@ -36,6 +51,7 @@ export const jsLoader: Loader = async (input, { options }) => {
contents,
path: input.path,
extension: '.js'
}
},
...declaration
]
}
8 changes: 5 additions & 3 deletions src/loaders/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ export const vueLoader: Loader = async (input, { loadFile }) => {
const [, lang = 'js'] = attrs.match(/lang="([a-z]*)"/) || []
const extension = '.' + lang

const [scriptFile] = await loadFile({
const [scriptFile, ...declaration] = await loadFile({
getContents: () => script,
path: `_index${extension}`,
path: `${input.path}${extension}`,
srcPath: `${input.srcPath}${extension}`,
extension
}) || []

Expand All @@ -28,6 +29,7 @@ export const vueLoader: Loader = async (input, { loadFile }) => {
{
path: input.path,
contents: contents.replace(scriptBlock, `<script>\n${scriptFile.contents}</script>`)
}
},
...declaration
]
}
4 changes: 3 additions & 1 deletion src/make.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface mkdistOptions {
distDir?: string
cleanDist?: boolean
format?: CreateLoaderOptions['format']
declaration?: CreateLoaderOptions['declaration']
}

export async function mkdist (options: mkdistOptions /* istanbul ignore next */ = {}) {
Expand Down Expand Up @@ -39,7 +40,8 @@ export async function mkdist (options: mkdistOptions /* istanbul ignore next */
const writtenFiles: string[] = []

const { loadFile } = createLoader({
format: options.format
format: options.format,
declaration: options.declaration
})

for (const file of files) {
Expand Down
68 changes: 68 additions & 0 deletions src/utils/dts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { CompilerOptions, CompilerHost } from 'typescript'

const compilerOptions: CompilerOptions = {
allowJs: true,
declaration: true,
incremental: true,
skipLibCheck: true,
emitDeclarationOnly: true
}

let _ts: typeof import('typescript')
async function getTs () {
if (!_ts) {
try {
_ts = await import('typescript')
} catch (err) {
// eslint-disable-next-line no-console
console.warn('[mkdist] Could not load `typescript` for generating types. Do you have it installed?')
throw err
}
}
return _ts
}

const vfs = new Map<string, string>()
let _tsHost: CompilerHost
async function getTsHost () {
if (!_tsHost) {
const ts = await getTs()
_tsHost = ts.createCompilerHost!(compilerOptions)
}

// Use virtual filesystem
_tsHost.writeFile = (fileName: string, declaration: string) => {
vfs.set(fileName, declaration)
}
const _readFile = _tsHost.readFile
_tsHost.readFile = (filename) => {
if (vfs.has(filename)) { return vfs.get(filename) }
return _readFile(filename)
}

return _tsHost
}

export async function getDeclaration (contents: string, filename = '_input.ts') {
const dtsFilename = filename.replace(/\.(ts|js)$/, '.d.ts')
if (vfs.has(dtsFilename)) {
return vfs.get(dtsFilename)
}
try {
const ts = await getTs()
const host = await getTsHost()
if (vfs.has(filename)) {
throw new Error('Race condition for generating ' + filename)
}
vfs.set(filename, contents)
const program = ts.createProgram!([filename], compilerOptions, host)
await program.emit()
const result = vfs.get(dtsFilename)
vfs.delete(filename)
return result
} catch (err) {
// eslint-disable-next-line no-console
console.warn(`Could not generate declaration file for ${filename}:`, err)
return ''
}
}
31 changes: 31 additions & 0 deletions test/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`createLoader jsLoader will generate dts file (.js) 1`] = `
Object {
"contents": "declare const _default: 42;
export default _default;
",
"extension": ".d.ts",
"path": "test.js",
}
`;

exports[`createLoader jsLoader will generate dts file (.ts) 1`] = `
Object {
"contents": "declare const _default: 42;
export default _default;
",
"extension": ".d.ts",
"path": "test.ts",
}
`;

exports[`createLoader vueLoader will generate dts file 1`] = `
Object {
"contents": "declare const _default: 42;
export default _default;
",
"extension": ".d.ts",
"path": "test.vue.ts",
}
`;
11 changes: 7 additions & 4 deletions test/fixture/src/components/js.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
</template>

<script>
import { test } from '..'
import test from '..'
const str = 'test'
export default {
data () {
test: test()
}
data: () => ({
test: test(),
str
})
}
</script>
11 changes: 7 additions & 4 deletions test/fixture/src/components/ts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
</template>

<script lang="ts">
import { test } from '..'
import test from '..'
const str: 'test' = 'test'
export default {
data () {
test: test() as () => any
}
data: () => ({
test: test(),
str
})
}
</script>
60 changes: 59 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resolve } from 'upath'
import { mkdist } from '../src/make'
import { createLoader } from '../src/loader'
import { vueLoader } from '../src/loaders'
import { jsLoader, vueLoader } from '../src/loaders'

describe('mkdist', () => {
it('mkdist', async () => {
Expand All @@ -17,6 +17,24 @@ describe('mkdist', () => {
'dist/components/ts.vue'
].map(f => resolve(rootDir, f)))
})

it('mkdist (emit types)', async () => {
const rootDir = resolve(__dirname, 'fixture')
const { writtenFiles } = await mkdist({ rootDir, declaration: true })
expect(writtenFiles).toEqual([
'dist/README.md',
'dist/foo.js',
'dist/foo.d.ts',
'dist/index.js',
'dist/index.d.ts',
'dist/types.d.ts',
'dist/components/blank.vue',
'dist/components/js.vue',
'dist/components/js.vue.d.ts',
'dist/components/ts.vue',
'dist/components/ts.vue.d.ts'
].map(f => resolve(rootDir, f)))
}, 50000)
})

describe('createLoader', () => {
Expand All @@ -29,6 +47,7 @@ describe('createLoader', () => {
})
expect(results).toBeFalsy()
})

it('vueLoader handles no transpilation of script tag', async () => {
const { loadFile } = createLoader({
loaders: [vueLoader]
Expand All @@ -40,4 +59,43 @@ describe('createLoader', () => {
})
expect(results).toBeFalsy()
})

it('vueLoader will generate dts file', async () => {
const { loadFile } = createLoader({
loaders: [vueLoader, jsLoader],
declaration: true
})
const results = await loadFile({
extension: '.vue',
getContents: () => '<script lang="ts">export default bob = 42 as const</script>',
path: 'test.vue'
})
expect(results![1]).toMatchSnapshot()
})

it('jsLoader will generate dts file (.js)', async () => {
const { loadFile } = createLoader({
loaders: [jsLoader],
declaration: true
})
const results = await loadFile({
extension: '.js',
getContents: () => 'export default bob = 42',
path: 'test.js'
})
expect(results![1]).toMatchSnapshot()
})

it('jsLoader will generate dts file (.ts)', async () => {
const { loadFile } = createLoader({
loaders: [jsLoader],
declaration: true
})
const results = await loadFile({
extension: '.ts',
getContents: () => 'export default bob = 42 as const',
path: 'test.ts'
})
expect(results![1]).toMatchSnapshot()
})
})

0 comments on commit 4b54426

Please sign in to comment.