Skip to content

Commit

Permalink
feat(vue): support transpilation of <style> blocks with sass (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe authored Oct 27, 2022
1 parent 8f3a513 commit 36e5b4f
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 28 deletions.
4 changes: 4 additions & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
externals: ['sass']
})
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,20 @@
"@vitest/coverage-c8": "^0.24.3",
"c8": "latest",
"eslint": "^8.26.0",
"sass": "^1.55.0",
"standard-version": "^9.5.0",
"typescript": "^4.8.4",
"unbuild": "^0.9.4",
"vitest": "^0.24.3"
},
"peerDependencies": {
"sass": "^1.49.7",
"typescript": ">=4.8.4"
},
"peerDependenciesMeta": {
"sass": {
"optional": true
},
"typescript": {
"optional": true
}
Expand Down
78 changes: 71 additions & 7 deletions pnpm-lock.yaml

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

10 changes: 5 additions & 5 deletions src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { vueLoader, jsLoader } from './loaders'
import { vueLoader, jsLoader, sassLoader } from './loaders'

export interface InputFile {
path: string
Expand All @@ -25,20 +25,20 @@ export type LoaderResult = OutputFile[] | undefined
export type LoadFile = (input: InputFile) => LoaderResult | Promise<LoaderResult>

export interface LoaderOptions {
ext?: 'mjs' | 'js' | 'ts',
format?: 'cjs' | 'esm',
ext?: 'mjs' | 'js' | 'ts'
format?: 'cjs' | 'esm'
declaration?: boolean
}

export interface LoaderContext {
loadFile: LoadFile,
loadFile: LoadFile
options: LoaderOptions
}

export type Loader = (input: InputFile, context: LoaderContext)
=> LoaderResult | Promise<LoaderResult>

export const defaultLoaders: Loader[] = [vueLoader, jsLoader]
export const defaultLoaders: Loader[] = [vueLoader, jsLoader, sassLoader]

export interface CreateLoaderOptions extends LoaderOptions {
loaders?: Loader[]
Expand Down
1 change: 1 addition & 0 deletions src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './js'
export * from './sass'
export * from './vue'
21 changes: 21 additions & 0 deletions src/loaders/sass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Loader, LoaderResult } from '../loader'

export const sassLoader: Loader = async (input, { options }) => {
if (!['.sass', '.scss'].includes(input.extension)) {
return
}

const compileString = await import('sass').then(r => r.compileString || r.default.compileString)

const output: LoaderResult = []

const contents = await input.getContents()

output.push({
contents: compileString(contents).css,
path: input.path,
extension: `.${options.ext || 'css'}`
})

return output
}
72 changes: 57 additions & 15 deletions src/loaders/vue.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,86 @@
import type { Loader } from '../loader'
import type { Loader, LoaderResult } from '../loader'

export const vueLoader: Loader = async (input, { loadFile }) => {
export const vueLoader: Loader = async (input, ctx) => {
if (input.extension !== '.vue') {
return
}

const output: LoaderResult = [{
path: input.path,
contents: await input.getContents()
}]

let earlyReturn = true

for (const blockLoader of [sassLoader, scriptLoader]) {
const result = await blockLoader({ ...input, getContents: () => output[0].contents! }, ctx)
if (!result) { continue }

earlyReturn = false
const [vueFile, ...files] = result
output[0] = vueFile
output.push(...files)
}

if (earlyReturn) { return }

return output
}

interface BlockLoaderOptions {
type: 'script' | 'style' | 'template'
outputLang: string
defaultLang?: string
validExtensions?: string[]
exclude?: RegExp[]
}

const vueBlockLoader = (opts: BlockLoaderOptions): Loader => async (input, { loadFile }) => {
const contents = await input.getContents()

const BLOCK_RE = new RegExp(`<${opts.type}((\\s[^>\\s]*)*)>([\\S\\s.]*?)<\\/${opts.type}>`)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [scriptBlock, attrs = '', _lastAttr, script] = contents.match(/<script((\s[^>\s]*)*)>([\S\s.]*?)<\/script>/) || []
if (!scriptBlock || !script) {
const [block, attrs = '', _, blockContents] = contents.match(BLOCK_RE) || []

if (!block || !blockContents) {
return
}

// do not transpile `<script setup>`, #14
if (attrs.split(/\s+/g).includes('setup')) {
if (opts.exclude?.some(re => re.test(attrs))) {
return
}

const [, lang = 'js'] = attrs.match(/lang="([a-z]*)"/) || []
const [, lang = opts.outputLang] = attrs.match(/lang="([a-z]*)"/) || []
const extension = '.' + lang

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

const scriptFile = files.find(f => ['.js', '.mjs'].includes(f.extension!))
if (!scriptFile) {
return
}
const blockOutputFile = files.find(f => f.extension === `.${opts.outputLang}` || opts.validExtensions?.includes(f.extension!))
if (!blockOutputFile) { return }

const newAttrs = attrs.replace(new RegExp(`\\s?lang="${lang}"`), '')

return [
{
path: input.path,
contents: contents.replace(scriptBlock, `<script${newAttrs}>\n${scriptFile.contents}</script>`)
contents: contents.replace(block, `<${opts.type}${newAttrs}>\n${blockOutputFile.contents?.trim()}\n</${opts.type}>`)
},
...files.filter(f => f !== scriptFile)
...files.filter(f => f !== blockOutputFile)
]
}

const sassLoader = vueBlockLoader({
outputLang: 'css',
type: 'style'
})

const scriptLoader = vueBlockLoader({
outputLang: 'js',
type: 'script',
exclude: [/\bsetup\b/],
validExtensions: ['.js', '.mjs']
})
8 changes: 8 additions & 0 deletions test/fixture/src/components/js.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ export default {
})
}
</script>

<style lang="scss" scoped>
$color: red;
.test {
color: $color;
}
</style>
Loading

0 comments on commit 36e5b4f

Please sign in to comment.