Skip to content

Commit

Permalink
fix: Nuxt module throwing error due to parsing error (#267)
Browse files Browse the repository at this point in the history
* chore: add carousel

* chore: add oxc-parser

* feat: use oxc-parser to get ExportNamedDeclaration node

* chore: add todo
  • Loading branch information
zernonia authored Jan 12, 2024
1 parent dfbb738 commit 7a7cf9d
Show file tree
Hide file tree
Showing 12 changed files with 769 additions and 110 deletions.
3 changes: 1 addition & 2 deletions packages/module/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@
},
"dependencies": {
"@nuxt/kit": "^3.8.2",
"recast": "^0.23.4",
"ts-morph": "^19.0.0"
"oxc-parser": "^0.2.0"
},
"devDependencies": {
"@nuxt/devtools": "latest",
Expand Down
45 changes: 45 additions & 0 deletions packages/module/playground/components/ui/carousel/Carousel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
import emblaCarouselVue from 'embla-carousel-vue'
import { useProvideCarousel } from './useCarousel'
import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
orientation: 'horizontal',
})
const emits = defineEmits<CarouselEmits>()
const carouselArgs = useProvideCarousel(props, emits)
defineExpose(carouselArgs)
function onKeyDown(event: KeyboardEvent) {
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
if (event.key === prevKey) {
event.preventDefault()
carouselArgs.scrollPrev()
return
}
if (event.key === nextKey) {
event.preventDefault()
carouselArgs.scrollNext()
}
}
</script>

<template>
<div
:class="cn('relative', props.class)"
role="region"
aria-roledescription="carousel"
tabindex="0"
@keydown="onKeyDown"
>
<slot v-bind="carouselArgs" />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { WithClassAsProps } from './interface'
import { useCarousel } from './useCarousel'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<WithClassAsProps>()
const { carouselRef, orientation } = useCarousel()
</script>

<template>
<div ref="carouselRef" class="overflow-hidden">
<div
:class="
cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
props.class,
)"
v-bind="$attrs"
>
<slot />
</div>
</div>
</template>
23 changes: 23 additions & 0 deletions packages/module/playground/components/ui/carousel/CarouselItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { WithClassAsProps } from './interface'
import { useCarousel } from './useCarousel'
import { cn } from '@/lib/utils'
const props = defineProps<WithClassAsProps>()
const { orientation } = useCarousel()
</script>

<template>
<div
role="group"
aria-roledescription="slide"
:class="cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
props.class,
)"
>
<slot />
</div>
</template>
30 changes: 30 additions & 0 deletions packages/module/playground/components/ui/carousel/CarouselNext.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { ChevronRight } from 'lucide-vue-next'
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
const props = defineProps<WithClassAsProps>()
const { orientation, canScrollNext, scrollNext } = useCarousel()
</script>

<template>
<Button
:disabled="!canScrollNext"
:class="cn(
'absolute h-10 w-10 rounded-full p-0',
orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
props.class,
)"
variant="outline"
@click="scrollNext"
>
<slot>
<ChevronRight class="h-4 w-4 text-current" />
</slot>
</Button>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { ChevronLeft } from 'lucide-vue-next'
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
const props = defineProps<WithClassAsProps>()
const { orientation, canScrollPrev, scrollPrev } = useCarousel()
</script>

<template>
<Button
:disabled="!canScrollPrev"
:class="cn(
'absolute h-10 w-10 rounded-full p-0',
orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
props.class,
)"
variant="outline"
@click="scrollPrev"
>
<slot>
<ChevronLeft class="h-4 w-4 text-current" />
</slot>
</Button>
</template>
10 changes: 10 additions & 0 deletions packages/module/playground/components/ui/carousel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { default as Carousel } from './Carousel.vue'
export { default as CarouselContent } from './CarouselContent.vue'
export { default as CarouselItem } from './CarouselItem.vue'
export { default as CarouselPrevious } from './CarouselPrevious.vue'
export { default as CarouselNext } from './CarouselNext.vue'
export { useCarousel } from './useCarousel'

export type {
EmblaCarouselType as CarouselApi,
} from 'embla-carousel'
20 changes: 20 additions & 0 deletions packages/module/playground/components/ui/carousel/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {
EmblaCarouselType as CarouselApi,
EmblaOptionsType as CarouselOptions,
EmblaPluginType as CarouselPlugin,
} from 'embla-carousel'
import type { HTMLAttributes, Ref } from 'vue'

export interface CarouselProps {
opts?: CarouselOptions | Ref<CarouselOptions>
plugins?: CarouselPlugin[] | Ref<CarouselPlugin[]>
orientation?: 'horizontal' | 'vertical'
}

export interface CarouselEmits {
(e: 'init-api', payload: CarouselApi): void
}

export interface WithClassAsProps {
class?: HTMLAttributes['class']
}
57 changes: 57 additions & 0 deletions packages/module/playground/components/ui/carousel/useCarousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { createInjectionState } from '@vueuse/core'
import emblaCarouselVue from 'embla-carousel-vue'
import { onMounted, ref } from 'vue'
import type {
EmblaCarouselType as CarouselApi,
} from 'embla-carousel'
import type { CarouselEmits, CarouselProps } from './interface'

const [useProvideCarousel, useInjectCarousel] = createInjectionState(
({
opts, orientation, plugins,
}: CarouselProps, emits: CarouselEmits) => {
const [emblaNode, emblaApi] = emblaCarouselVue({
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y',
}, plugins)

function scrollPrev() {
emblaApi.value?.scrollPrev()
}
function scrollNext() {
emblaApi.value?.scrollNext()
}

const canScrollNext = ref(true)
const canScrollPrev = ref(true)

function onSelect(api: CarouselApi) {
canScrollNext.value = api.canScrollNext()
canScrollPrev.value = api.canScrollPrev()
}

onMounted(() => {
if (!emblaApi.value)
return

emblaApi.value?.on('init', onSelect)
emblaApi.value?.on('reInit', onSelect)
emblaApi.value?.on('select', onSelect)

emits('init-api', emblaApi.value)
})

return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }
},
)

function useCarousel() {
const carouselState = useInjectCarousel()

if (!carouselState)
throw new Error('useCarousel must be used within a <Carousel />')

return carouselState
}

export { useCarousel, useProvideCarousel }
2 changes: 2 additions & 0 deletions packages/module/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"@nuxtjs/tailwindcss": "^6.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"embla-carousel": "8.0.0-rc19",
"embla-carousel-vue": "8.0.0-rc19",
"lucide-vue-next": "^0.276.0",
"radix-vue": "^1.3.0",
"tailwind-merge": "^2.0.0",
Expand Down
43 changes: 27 additions & 16 deletions packages/module/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { readFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
import { addComponent, createResolver, defineNuxtModule } from '@nuxt/kit'
import { parse } from 'recast'
import oxc from 'oxc-parser'

// TODO: add test to make sure all registry is being parse correctly
// Module options TypeScript interface definition
export interface ModuleOptions {
/**
Expand Down Expand Up @@ -40,24 +41,34 @@ export default defineNuxtModule<ModuleOptions>({
try {
readdirSync(resolve(COMPONENT_DIR_PATH))
.forEach(async (dir) => {
const filePath = await resolvePath(join(COMPONENT_DIR_PATH, dir, 'index'), { extensions: ['.ts', '.js'] })
const content = readFileSync(filePath, { encoding: 'utf8' })
const ast = parse(content)
try {
const filePath = await resolvePath(join(COMPONENT_DIR_PATH, dir, 'index'), { extensions: ['.ts', '.js'] })
const content = readFileSync(filePath, { encoding: 'utf8' })
const ast = oxc.parseSync(content, {
sourceType: 'module',
sourceFilename: filePath,
})
const program = JSON.parse(ast.program)

const exportedKeys: string[] = ast.program.body
// @ts-expect-error parse return any
.filter(node => node.type === 'ExportNamedDeclaration')
// @ts-expect-error parse return any
.flatMap(node => node.specifiers.map(specifier => specifier.exported.name))
.filter((key: string) => /^[A-Z]/.test(key))
const exportedKeys: string[] = program.body
// @ts-expect-error parse return any
.filter(node => node.type === 'ExportNamedDeclaration')
// @ts-expect-error parse return any
.flatMap(node => node.specifiers.map(specifier => specifier.exported.name))
.filter((key: string) => /^[A-Z]/.test(key))

exportedKeys.forEach((key) => {
addComponent({
name: `${prefix}${key}`, // name of the component to be used in vue templates
export: key, // (optional) if the component is a named (rather than default) export
filePath: resolve(filePath),
exportedKeys.forEach((key) => {
addComponent({
name: `${prefix}${key}`, // name of the component to be used in vue templates
export: key, // (optional) if the component is a named (rather than default) export
filePath: resolve(filePath),
})
})
})
}
catch (err) {
if (err instanceof Error)
console.warn('Module error: ', err.message)
}
})
}
catch (err) {
Expand Down
Loading

0 comments on commit 7a7cf9d

Please sign in to comment.