Skip to content

Commit

Permalink
feat: add exportExpose (#376)
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed May 23, 2023
1 parent 66f18d7 commit 1e4a1f4
Show file tree
Hide file tree
Showing 35 changed files with 625 additions and 3 deletions.
9 changes: 9 additions & 0 deletions .changeset/cool-badgers-hang.md
@@ -0,0 +1,9 @@
---
'unplugin-vue-macros': minor
'@vue-macros/export-expose': patch
'@vue-macros/volar': minor
---

- Disable `exportExpose` and `exportProps` by default.
- Introduce `exportExpose`.
- Add `include` option for Volar plugins (`exportExpose` and `exportProps`).
2 changes: 2 additions & 0 deletions docs/guide/configurations.md
Expand Up @@ -8,6 +8,8 @@ All features are enabled by default except the following.
- `defineSlots` (Vue >= 3.3)
- `hoistStatic` (Vue >= 3.3)
- `shortEmits` (Vue >= 3.3)
- `exportExpose`
- `exportProps`

You can disable them by setting the option to `false`.

Expand Down
3 changes: 3 additions & 0 deletions packages/export-expose/README.md
@@ -0,0 +1,3 @@
# @vue-macros/export-expose [![npm](https://img.shields.io/npm/v/@vue-macros/export-expose.svg)](https://npmjs.com/package/@vue-macros/export-expose)

Please refer to [README.md](https://github.com/sxzz/vue-macros#readme)
102 changes: 102 additions & 0 deletions packages/export-expose/package.json
@@ -0,0 +1,102 @@
{
"name": "@vue-macros/export-expose",
"version": "0.0.0",
"packageManager": "[email protected]",
"description": "export-expose feature from Vue Macros.",
"keywords": [
"vue-macros",
"macros",
"vue",
"sfc",
"setup",
"script-setup",
"export-expose",
"unplugin"
],
"license": "MIT",
"homepage": "https://github.com/sxzz/vue-macros#readme",
"bugs": {
"url": "https://github.com/sxzz/vue-macros/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sxzz/vue-macros.git",
"directory": "packages/export-expose"
},
"author": "三咲智子 <[email protected]>",
"files": [
"dist",
"*.d.ts"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"dev": "./src/index.ts",
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./api": {
"dev": "./src/api.ts",
"types": "./dist/api.d.ts",
"require": "./dist/api.js",
"import": "./dist/api.mjs"
},
"./esbuild": {
"dev": "./src/esbuild.ts",
"types": "./dist/esbuild.d.ts",
"require": "./dist/esbuild.js",
"import": "./dist/esbuild.mjs"
},
"./rollup": {
"dev": "./src/rollup.ts",
"types": "./dist/rollup.d.ts",
"require": "./dist/rollup.js",
"import": "./dist/rollup.mjs"
},
"./vite": {
"dev": "./src/vite.ts",
"types": "./dist/vite.d.ts",
"require": "./dist/vite.js",
"import": "./dist/vite.mjs"
},
"./webpack": {
"dev": "./src/webpack.ts",
"types": "./dist/webpack.d.ts",
"require": "./dist/webpack.js",
"import": "./dist/webpack.mjs"
},
"./*": [
"./*",
"./*.d.ts"
]
},
"typesVersions": {
"<=4.9": {
"*": [
"./dist/*",
"./*"
]
}
},
"scripts": {
"build": "tsup && tsx ../../scripts/postbuild.mts",
"dev": "DEV=true tsup"
},
"peerDependencies": {
"vue": "^2.7.0 || ^3.2.25"
},
"dependencies": {
"@vue-macros/common": "workspace:~",
"@vue/compiler-sfc": "^3.3.4",
"unplugin": "^1.3.1"
},
"devDependencies": {
"vue": "^3.3.4"
},
"engines": {
"node": ">=16.14.0"
}
}
1 change: 1 addition & 0 deletions packages/export-expose/src/api.ts
@@ -0,0 +1 @@
export * from './core'
119 changes: 119 additions & 0 deletions packages/export-expose/src/core/index.ts
@@ -0,0 +1,119 @@
import {
HELPER_PREFIX,
MagicString,
getTransformResult,
parseSFC,
} from '@vue-macros/common'
import { extractIdentifiers } from '@vue/compiler-sfc'

const MACROS_VAR_PREFIX = `${HELPER_PREFIX}expose_`

export function transformExportExpose(code: string, id: string) {
const { scriptSetup, getSetupAst } = parseSFC(code, id)
if (!scriptSetup) return

const s = new MagicString(code)
const nodes = getSetupAst()!.body
const offset = scriptSetup.loc.start.offset

const exposed: Record<string, string> = {}

let i = 0
for (const stmt of nodes) {
const start = offset + stmt.start!
const end = start + 6 /* 'export'.length */

if (stmt.type === 'ExportNamedDeclaration' && stmt.exportKind === 'value') {
if (stmt.declaration) {
if (
stmt.declaration.type === 'VariableDeclaration' &&
!stmt.declaration.declare
) {
for (const decl of stmt.declaration.declarations) {
for (const id of extractIdentifiers(decl.id)) {
exposed[id.name] = id.name
}
}
} else if (
(stmt.declaration.type === 'FunctionDeclaration' ||
stmt.declaration.type === 'ClassDeclaration' ||
stmt.declaration.type === 'TSEnumDeclaration') &&
!stmt.declaration.declare
) {
exposed[stmt.declaration.id!.name] = stmt.declaration.id!.name
}

s.remove(start, end)
} else {
for (const specifier of stmt.specifiers) {
let exported: string, local: string
if (specifier.type === 'ExportSpecifier') {
if (specifier.exportKind === 'type') continue

exported =
specifier.exported.type === 'Identifier'
? specifier.exported.name
: specifier.exported.value

if (stmt.source) {
// rename variable
local = MACROS_VAR_PREFIX + String(i++)
if (specifier.local.name === exported) {
s.overwriteNode(
specifier.local,
`${specifier.local.name} as ${local}`,
{ offset }
)
} else {
s.overwriteNode(specifier.exported, local, { offset })
}
} else {
local = specifier.local.name
}
} else if (specifier.type === 'ExportNamespaceSpecifier') {
local = MACROS_VAR_PREFIX + String(i++)
exported = specifier.exported.name

s.overwriteNode(specifier.exported, local, { offset })
} else continue

exposed[exported] = local
}

if (stmt.source) {
s.overwrite(start, end, 'import')
} else {
s.removeNode(stmt, { offset })
}
}
} else if (
stmt.type === 'ExportAllDeclaration' &&
stmt.exportKind === 'value'
) {
throw new Error(
'export from another module is not supported. Please import and export separately.'
)
} else if (stmt.type === 'ExportDefaultDeclaration') {
throw new Error(
'export default is not supported. Please use named export.'
)
}
}

if (Object.keys(exposed).length === 0) return

let codegen = ''
for (const [exported, local] of Object.entries(exposed)) {
codegen += `\n `
if (exported === local) {
codegen += `${exported},`
} else {
codegen += `${exported}: ${local},`
}
}
codegen = `defineExpose({${codegen}\n})`

s.prependLeft(scriptSetup.loc.end.offset, `${codegen}\n`)

return getTransformResult(s, id)
}
3 changes: 3 additions & 0 deletions packages/export-expose/src/esbuild.ts
@@ -0,0 +1,3 @@
import unplugin from '.'

export default unplugin.esbuild
47 changes: 47 additions & 0 deletions packages/export-expose/src/index.ts
@@ -0,0 +1,47 @@
import { createUnplugin } from 'unplugin'
import {
type BaseOptions,
type MarkRequired,
REGEX_SETUP_SFC,
REGEX_VUE_SFC,
REGEX_VUE_SUB,
createFilter,
detectVueVersion,
} from '@vue-macros/common'
import { transformExportExpose } from './core'

export { transformExportExpose as transformDefineProps } from './core'

export type Options = BaseOptions
export type OptionsResolved = MarkRequired<Options, 'include' | 'version'>

function resolveOption(options: Options): OptionsResolved {
const version = options.version || detectVueVersion()
return {
include: [REGEX_VUE_SFC, REGEX_SETUP_SFC, REGEX_VUE_SUB],
...options,
version,
}
}

const name = 'unplugin-vue-export-expose'

export default createUnplugin<Options | undefined, false>(
(userOptions = {}) => {
const options = resolveOption(userOptions)
const filter = createFilter(options)

return {
name,
enforce: 'pre',

transformInclude(id) {
return filter(id)
},

transform(code, id) {
return transformExportExpose(code, id)
},
}
}
)
3 changes: 3 additions & 0 deletions packages/export-expose/src/rollup.ts
@@ -0,0 +1,3 @@
import unplugin from '.'

export default unplugin.rollup
3 changes: 3 additions & 0 deletions packages/export-expose/src/vite.ts
@@ -0,0 +1,3 @@
import unplugin from '.'

export default unplugin.vite
3 changes: 3 additions & 0 deletions packages/export-expose/src/webpack.ts
@@ -0,0 +1,3 @@
import unplugin from '.'

export default unplugin.webpack
69 changes: 69 additions & 0 deletions packages/export-expose/tests/__snapshots__/fixtures.test.ts.snap
@@ -0,0 +1,69 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`fixtures > ./fixtures/basic.vue 1`] = `
"<script setup lang=\\"ts\\">
const foo: string = 'foo',
bar = 10
let baz: string | undefined
var qux = fn()
const { a, b, c } = { a: 1, b: 2, c: 3 }
function fn() {}
class A {}
defineExpose({
foo,
bar,
baz,
qux,
a,
b,
c,
fn,
A,
foo1: foo,
})
</script>
"
`;
exports[`fixtures > ./fixtures/error/export-all.vue 1`] = `"export from another module is not supported. Please import and export separately."`;
exports[`fixtures > ./fixtures/error/export-default.vue 1`] = `"export default is not supported. Please use named export."`;
exports[`fixtures > ./fixtures/export-from-other.vue 1`] = `
"<script setup lang=\\"ts\\">
import { foo as __MACROS_expose_0, type Foo, foo as __MACROS_expose_1 } from './types'
defineExpose({
foo: __MACROS_expose_0,
bar: __MACROS_expose_1,
})
</script>
"
`;
exports[`fixtures > ./fixtures/namespace-export.vue 1`] = `
"<script setup lang=\\"ts\\">
import * as __MACROS_expose_0 from './types'
defineExpose({
foo: __MACROS_expose_0,
})
</script>
"
`;
exports[`fixtures > ./fixtures/rename.vue 1`] = `
"<script setup lang=\\"ts\\">
const foo = 1,
bar = 1
import { foo as __MACROS_expose_0 } from './types'
import * as __MACROS_expose_1 from './types'
defineExpose({
foo: __MACROS_expose_0,
bar: __MACROS_expose_1,
})
</script>
"
`;
13 changes: 13 additions & 0 deletions packages/export-expose/tests/fixtures.test.ts
@@ -0,0 +1,13 @@
import { testFixtures } from '@vue-macros/test-utils'
import { describe } from 'vitest'
import { transformExportExpose } from '../src/core'

describe('fixtures', async () => {
await testFixtures(
import.meta.glob('./fixtures/**/*.vue', {
eager: true,
as: 'raw',
}),
(args, id, code) => transformExportExpose(code, id)?.code
)
})

0 comments on commit 1e4a1f4

Please sign in to comment.