storipress/karbon

View on GitHub
packages/karbon/src/cli/bundle/index.ts

Summary

Maintainability
C
1 day
Test Coverage
import process from 'node:process'
import { build as viteBuild } from 'vite'
import tailwind from 'tailwindcss'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
import { snakeCase } from 'scule'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import { basename, join } from 'pathe'
import Components from 'unplugin-vue-components/vite'
import virtual from '@rollup/plugin-virtual'
import fs from 'fs-extra'
import consola from 'consola'
import { loadNuxtConfig } from '@nuxt/kit'
import { once } from 'remeda'
import type { OutputChunk, OutputOptions } from 'rollup'
import type { BuildOptions } from 'vite'
import invariant from 'tiny-invariant'
import importerChecker from '../plugins/importer-checker'
import { ignoreSet, targetSet } from './setting'

export { bundleLayouts } from './layouts'
export { bundleEditorBlocks } from './editor-blocks'

const loadNuxtConfigOnce = once(() => loadNuxtConfig({}))

export function createConfig(
  name: string,
  dir: string,
  ssr?: boolean,
): BuildOptions['rollupOptions'] & { output: OutputOptions } {
  const config: BuildOptions['rollupOptions'] & { output: OutputOptions } = {
    ...(ssr ? {} : { input: 'entry' }),
    output: {
      dir: join(`.storipress/${dir}/`),
      format: 'commonjs',
      strict: false,
      plugins: [
        {
          name: 'convert-to-iife',
          generateBundle(_opts, bundle) {
            const items = Object.values(bundle)
            const chunks = items.filter((item): item is OutputChunk => item.type === 'chunk')
            invariant(chunks.length === 1, 'Expected exactly one chunk')
            chunks[0].code = `const customBlock = {
              name: '${name}',
              factory: (module, require) => {
              ${chunks[0].code}
              }
            };
            export default customBlock
            `
          },
        },
      ],
    },
    external: [
      'vue',
      '@vueuse/core',
      '@storipress/sdk/article/components',
      '@storipress/sdk/article/utils',
      '@storipress/custom-field',
      '@storipress/vue-advertising',
      '@storipress/sdk/resources',
    ],
  }

  return config
}

export async function bundle(path: string, vuefileName: string, layoutName: string, dir: string, ssr?: boolean) {
  process.env.NODE_ENV = 'production'
  const name = snakeCase(basename(vuefileName, '.vue'))
  const rollupOptions = createConfig(name, dir, ssr)
  const config = await loadNuxtConfigOnce()

  const _tailwindConfigPath = `${process.cwd()}/tailwind.config.js`
  const tailwindConfigPath = (await fs.pathExists(_tailwindConfigPath)) ? _tailwindConfigPath : undefined

  await viteBuild({
    clearScreen: false,
    resolve: {
      alias: config.alias,
    },
    build: {
      emptyOutDir: false,
      lib: {
        entry: join(path),
        name: 'block',
        fileName: layoutName,
        formats: ['cjs'],
      },
      rollupOptions,
      ssr,
    },
    css: {
      postcss: {
        plugins: [
          tailwind({
            presets: [
              {
                config: {
                  content: [
                    `${process.cwd()}/templates/article-layouts/*.{vue,ts,tsx}`,
                    `${process.cwd()}/templates/editor-blocks/*.{vue,ts,tsx}`,
                    `${process.cwd()}/templates/components/**/*.{vue,ts,tsx}`,
                  ],
                  corePlugins: {
                    preflight: false,
                  },
                  darkMode: ['class', '.force-use-dark-mode'],
                },
              },
            ],
            config: tailwindConfigPath ?? {
              content: [],
            },
          }),
        ],
      },
    },
    plugins: [
      ...(ssr
        ? []
        : [
            virtual({
              entry: `
                import component from './${join(path)}'
                import './main.css'
                
                export default component
              `,
              'main.css': `@tailwind base;
                @tailwind components;
                @tailwind utilities;
              `,
            }),
          ]),
      tsconfigPaths(),
      cssInjectedByJsPlugin({
        injectCodeFunction: function injectCodeCustomRunTimeFunction(cssCode) {
          try {
            if (typeof document != 'undefined') {
              const elementStyle = document.createElement('style')
              elementStyle.appendChild(document.createTextNode(`${cssCode}`))
              document.head.prepend(elementStyle)
            }
          } catch (e) {
            console.error('vite-plugin-css-injected-by-js', e)
          }
        },
      }),
      AutoImport({
        include: [
          /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
          /\.vue$/,
          /\.vue\?vue/, // .vue
          /\.md$/, // .md
        ],
        imports: [
          'vue',
          {
            '@storipress/custom-field': ['useField', 'FieldType'],
            '@storipress/sdk/article/utils': ['useArticle', 'useRecommendArticle', 'useSite', 'useColorMode'],
            '@storipress/sdk/resources': ['useResourceResolver'],
          },
        ],
        vueTemplate: true,
        dirs: [`${process.cwd()}/composables`],
        dts: false,
      }),
      vue({}),
      Components({
        extensions: ['vue'],
        include: [/\.vue$/, /\.vue\?vue/],
        dirs: ['components'],
        dts: false,
        resolvers: [
          (componentName) => {
            if (ignoreSet.has(componentName)) return
            if (targetSet.has(componentName)) {
              return { name: componentName, from: '@storipress/sdk/article/components' }
            }
            consola.warn(`Failed to resolve component: ${componentName}`)
          },
        ],
      }),
      importerChecker(),
    ],
    define: { __VUE_PROD_DEVTOOLS__: false },
    publicDir: false,
    logLevel: 'silent',
  })
}