Skip to content

Commit

Permalink
Merge pull request #13 from nextcloud-libraries/fix/support-full-css-…
Browse files Browse the repository at this point in the history
…inlinining-for-libraries

Allow to properly include CSS in library builds by importing the CSS files
  • Loading branch information
susnux authored Aug 1, 2023
2 parents 4e7e5f5 + aac674d commit 7554162
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 28 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,14 @@ export default createLibConfig({
},
})
```

## Notes

### Inlining / injecting CSS
You can enable inlining CSS code, but please note that this is handled differently for apps and libraries.

For apps any styles can be injected in the JS by dynamically inject the styles in the document (creating `<style>` tags).
But this only works in DOM environments, so for libraries this might not work (e.g. while testing in the Node environment).

So for apps the CSS will still be extracted by Vite, but the extracted CSS assets will be imported.
This way the library user can decide how to handle the imported CSS without relying on a DOM environment.
7 changes: 7 additions & 0 deletions lib/appConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createBaseConfig } from './baseConfig.js'

import EmptyJSDirPlugin from './plugins/EmptyJSDir.js'
import replace from '@rollup/plugin-replace'
import injectCSSPlugin from 'vite-plugin-css-injected-by-js'

export const appName = process.env.npm_package_name
export const appVersion = process.env.npm_package_version
Expand Down Expand Up @@ -59,11 +60,17 @@ export const createAppConfig = (entries: { [entryAlias: string]: string }, optio
const userConfig = await Promise.resolve(typeof options.config === 'function' ? options.config(env) : options.config)

const plugins = []
// Inject all imported styles into the javascript bundle by creating dynamic styles on the document
if (options?.inlineCSS !== false) {
plugins.push(injectCSSPlugin())
}

// defaults to true so only not adding if explicitly set to false
if (options?.emptyOutputDirectory !== false) {
// Ensure `js/` is empty as we can not use the build in option (see below)
plugins.push(EmptyJSDirPlugin())
}

// When building in serve mode (e.g. unit tests with vite) the intro option below will be ignored, so we must replace that values
if (env.command === 'serve') {
plugins.push(replace({
Expand Down
5 changes: 0 additions & 5 deletions lib/baseConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import replace from '@rollup/plugin-replace'
import vue2 from '@vitejs/plugin-vue2'
import browserslistToEsbuild from 'browserslist-to-esbuild'
import license from 'rollup-plugin-license'
import injectCSSPlugin from 'vite-plugin-css-injected-by-js'

export type NodePolyfillsOptions = Parameters<typeof nodePolyfills>[0]

Expand Down Expand Up @@ -66,10 +65,6 @@ export function createBaseConfig(options: BaseOptions = {}): UserConfigFn {
const userConfig = await Promise.resolve(typeof options.config === 'function' ? options.config(env) : options.config)

const plugins = []
if (options?.inlineCSS !== false) {
// Inject all imported styles into the javascript bundle
plugins.push(injectCSSPlugin())
}
if (options?.nodePolyfills) {
// Add polyfills for node packages
plugins.push(nodePolyfills(typeof options.nodePolyfills === 'object' ? options.nodePolyfills : {}))
Expand Down
21 changes: 13 additions & 8 deletions lib/libConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createBaseConfig } from './baseConfig.js'

import DTSPlugin, { type PluginOptions as DTSOptions } from 'vite-plugin-dts'
import { nodeExternals, type ExternalsOptions } from 'rollup-plugin-node-externals'
import { ImportCSSPlugin } from './plugins/ImportCSS.js'

type OutputOptions = BuildOptions['rollupOptions']['output']

Expand Down Expand Up @@ -69,6 +70,11 @@ export const createLibConfig = (entries: { [entryAlias: string]: string }, optio
node,
]

// Handle inline CSS, this is different on library than on app mode, because app will have a DOM env so styles can injected in the document while libraries might run in node
if (options.inlineCSS) {
plugins.push(ImportCSSPlugin())
}

// Handle the DTS plugin
if (options?.DTSPluginOptions !== false) {
plugins.push(DTSPlugin(options.DTSPluginOptions))
Expand All @@ -83,24 +89,22 @@ export const createLibConfig = (entries: { [entryAlias: string]: string }, optio

const assetFileNames = (assetInfo) => {
const extType = assetInfo.name.split('.').pop()
if (/css/i.test(extType)) {
if (!options.inlineCSS && /css/i.test(extType)) {
return '[name].css'
}
return '[name]-[hash][extname]'
return 'assets/[name][extname]'
}

// Manually define output options for file extensions
const outputOptions: OutputOptions = options.libraryFormats.map(format => {
const extension = format === 'es' ? 'mjs' : (format === 'cjs' ? 'cjs' : `${format}.js`)
return {
format,
hoistTransitiveImports: false, // For libraries this might otherwise introduce side effects
preserveModules: false,
assetFileNames,
entryFileNames: () => {
return `[name].${extension}`
},
chunkFileNames: () => {
return `chunks/[name]-[hash].${extension}`
},
entryFileNames: `[name].${extension}`,
chunkFileNames: `chunks/[name]-[hash].${extension}`,
}
})

Expand All @@ -112,6 +116,7 @@ export const createLibConfig = (entries: { [entryAlias: string]: string }, optio
...entries,
},
},
cssCodeSplit: true,
outDir: 'dist',
rollupOptions: {
external: [/^core-js\//],
Expand Down
41 changes: 41 additions & 0 deletions lib/plugins/ImportCSS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* SPDX-FileCopyrightText: 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Plugin } from 'vite'
import MagicString from 'magic-string'
import { dirname, relative } from 'node:path'

/**
* Plugin that imports splitted CSS assets used by current entry point
*
* Basically this automatically imports all splitted CSS so that the users build system can decide what to do with the styles.
* Because injecting is only possible with a DOM environement (not on SSR).
*/
export const ImportCSSPlugin: () => Plugin = () => {
return {
name: 'vite-import-css-libmode',
enforce: 'post',
renderChunk(code, chunk) {
if (!chunk.viteMetadata) return
const { importedCss } = chunk.viteMetadata
if (!importedCss.size) return

/**
* Inject the referenced style files at the top of the chunk.
*/
const ms = new MagicString(code)
for (const cssFileName of importedCss) {
let cssFilePath = relative(dirname(chunk.fileName), cssFileName)
cssFilePath = cssFilePath.startsWith('.') ? cssFilePath : `./${cssFilePath}`
ms.prepend(`import '${cssFilePath}';\n`)
}
return {
code: ms.toString(),
map: ms.generateMap(),
}
},
}
}
42 changes: 27 additions & 15 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@rollup/plugin-replace": "^5.0.2",
"@vitejs/plugin-vue2": "^2.2.0",
"browserslist-to-esbuild": "^1.2.0",
"magic-string": "^0.30.2",
"rollup-plugin-corejs": "^1.0.0-beta.0",
"rollup-plugin-esbuild-minify": "^1.1.0",
"rollup-plugin-license": "^3.0.1",
Expand Down

0 comments on commit 7554162

Please sign in to comment.