Search code examples
vuejs3pnpmpnpm-workspace

pnpm monorepo: how to set up a simple reusable (vue) component for reuse?


New to pnpm, and trying to get my head around some of the basics. But can't find a lot of documentation around it (which often means that it's either very simple, or I'm doing it wrong...).

I have set up a basic pnpm monorepo with an apps and packages folder by basically creating the monorepo folder, running pnpm init and tweaking the result a bit. I got:


package.json

{
  "name": "@myorg/root",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

pnpm-workspace.yaml

packages:
  - "packages/**"
  - "apps/**"

.npmrc

shamefully-hoist=true

Notes:

  • I did not create an index.js, as I have no idea what to put in there..
  • I know that I can run/build the various apps from the root folder by adding pnpm run dev with a filter to the scripts section, I've not yet gotten around to setting that up, but I believe it's not critical to running the monorepo. (?)

In the apps folder I've already created some Vue3 apps (this works fine). And now I'd like to move some of the Vue components used there into the packages folder of the monorepo, so I can reuse them in the various apps. This is where i'm getting stuck in the sand...

I'm not entirely sure how much scaffolding you're supposed to add to these shared components. Is each one an entire Vue-project by themselves? (I'm guessing yes), and then, how to specify in that project what parts to export?

I have created the folder "y-theme-select" in the "packages" folder, and ran pnpm init and pnpm add vue on it. Now lets say I want to add the following component (let's keep it very simple):

y-theme-select.vue

<template>
    <div>
        Hello world!
    </div>
</template>
  • Where do I store it? (eg. packages\y-theme-select\src\y-theme-select.vue?)
  • How do I export it? (clueless)
  • How do I import it in a shared project (I recon something like "@myorg/y-theme-select": "1.0.0" in the dependencies section of the package.json?)
  • How to use Vuetify components in here?

Nb. for completeness sake, found two related questions:


Solution

  • First off, the reason it's not documented at pnpm is that it's, except for a few properties, not a PNPM concern.

    Secondly, what I found is that reusable components all share a few basic principles, but other than that can vary fairly wildly in setup.

    Thirdly, this answer works. But has a few issues, as described at the end of the answer.

    I also want to mention the excellent video by "How To Create A Vue.js Plugin Component" by Erik Hanchett, which laid a foundation to this answer.

    UPDATE: I stopped building components. As you add functionality to them there's always some new weird issue. Now that scoped CSS turned out to not work, I've changed direction. Here is a super-simple low-tech solution to creating a library of components in this pnpm monorepo:

    import YSwitchLang from "/../../packages/common-vtfy/src/components/YSwitchLang.vue"

    Just reference the packages folder from within your apps project. (Fingers crossed I won't run into anything new, but so-far so-good.) The instructions below are still valid, but in this scenario you only need step I.

    I. Initialization of the project

    I'm creating a package that will hold a few generic and similar Vuetify components, so I will call it "common-vtfy". This project will use Vite+Rollup as bundlers. I also use the rollup-plugin-typescript2 package to create the typescript definitions. You can simply leave out vuetify package if your component doesn't depend on it.

    cd packages
    pnpm create vue@latest
    -> common-vtfy
    -> Typescript
    -> ESLint
    cd common-vtfy
    echo auto-install-peers = true > .npmrc
    pnpm add -D vuetify rollup-plugin-typescript2
    pnpm install
    

    In package.json:

    • prefix the package name with your monorepo name, like so: "name": "@myorg/common-vtfy".

    • Move "vue": "^3.2.45" entry to devDependencies (not a biggie to leave it here, because we externalize it as well in the build section of vite.config.ts)

    • Add peerDependencies (not sure if this is needed, but probably won't hurt):

      "peerDependencies": {
        "vue": "~3.x",
        "vuetify": "~3.x"
      }
      

    At this point you could run pnpm dev, and see an otherwise empty Vue project, which has way more stuff than we'll be requiring, so go ahead and delete:

    • public\favicon.ico
    • src\assets\logo.*
    • src\components*

    Not sure if we could/should delete the css files from src\assets as well. #TBD.

    II. Build component

    Now we create the components, and setup App.vue to see the results:

    YSwitchTheme.vue

    <script setup lang="ts"></script>
    
    <template>
        <div>
            Hello, I'm YSwitchTheme <v-chip>Vuetify Test</v-chip>
        </div>
    </template>
    

    And similarly for YSwitchLang.vue


    App.vue

    <script setup lang="ts">
        import YSwitchTheme from "./components/YSwitchTheme.vue" // not required if using the vue plugin system
    </script>
    
    <template>
        <div>
            <YSwitchTheme/>
        </div>
    </template>
    

    III. Create the plugin

    Create two files:

    src\components\index.ts

    export {default as YSwitchLang} from "./YSwitchLang.vue"
    export {default as YSwitchTheme} from "./YSwitchTheme.vue"
    

    I believe that this "registers" the components, but the details are not exactly clear to me.


    src\CommonVtfyPlugin.ts

    The plugin entry file. More information: https://vuejs.org/guide/reusability/plugins.html#writing-a-plugin

    I have tried to export the components both as a plugin, and as a individually importable components, which does not require the user to load it as a plugin. However, this did not end up working, so I've commented out that last bit. The plugin must be imported using the Vue plugin system (more on that later)

    import type { App } from "vue"
    import { YSwitchLang, YSwitchTheme } from "./components"
    
    // Export as plugin
    export default {
        install: (app: App) => {
            app.component("YSwitchLang", YSwitchLang)
            app.component("YSwitchTheme", YSwitchTheme)
        }
    }
    
    // Export as individually importable components
    // export { YSwitchLang, YSwitchTheme }
    

    IV. "Local" testing/usage demonstration

    To use the plugin we add it to our main.ts, and this is something we can do in this same project. The resulting code is the same as you would use when you are importing it later in your other projects.

    main.ts Add import:

    import CommonVtfyPlugin from './CommonVtfyPlugin'
    

    If you're using Vuetify, then also add:

    // Vuetify
    import 'vuetify/styles'
    import { createVuetify } from 'vuetify'
    import * as components from 'vuetify/components'
    import * as directives from 'vuetify/directives'
    
    const vuetify = createVuetify({
      components,
      directives,
    })
    

    And add the .use clause, in the following manner:

    const app = createApp(App)
    app.use(vuetify).use(CommonVtfyPlugin)
    app.mount('#app')
    

    Now in App.vue, comment out the import statements.

    V. Build it

    Here we're going to use rollup-plugin-typescript2 to generate the typescript files.

    vite.config.ts

    Add to the imports:

    import vuetify from "vite-plugin-vuetify"
    import typeScript2 from "rollup-plugin-typescript2"
    

    Add to the plugins:

    vuetify({
        autoImport: true,
    }),
    typeScript2({
        check: false,
        include: ["src/components/*.vue"],
        tsconfigOverride: {
          compilerOptions: {
            sourceMap: true,
            declaration: true,
            declarationMap: true,
          }
        },
        exclude: [
          "vite.config.ts"
        ]
      })
    

    Add a new section build to the defineConfig:

      build: {
        cssCodeSplit: false,
        lib: {
          entry: "./src/CommonVtfyPlugin.ts",
          formats: ["es", "cjs"],
          name: "CommonVtfyPlugin",
          fileName: format => (format == "es" ? "index.js" : "index.cjs"),
        },
        rollupOptions: {
          external: ["vue"],
          output: {
            globals: {
              vue: "Vue"
            }
          }
        }
      },
    

    Now you're ready to build it by running pnpm build.


    Wrap it up by updating package.json, adding four properties:

      "type": "module",
      "exports": {
        ".": "./dist/index.js"
      },
      "types": "./dist/index.d.ts",
      "files": [
        "dist"
      ],
    

    One issue the I've not yet figured out is how to generate a single index.d.ts declarations file. For now I just create the following file by hand in the dist folder. Inspired by the Vuetify project, I have not yet figured out why/how this works.

    index.d.ts

    declare module '@myorg/common-vtfy' {
        import { VueConstructor } from 'vue'
      
        const YSWitchLang: VueConstructor
        const YSWitchTheme: VueConstructor
    
        export {
            YSWitchLang,
            YSWitchTheme
        }
    }
    

    VI. Use it!

    Go back to a project that wants to use these components and add them to the project using pnpm add @myorg/common-vtfy (replace myorg with the name of your monorepo). You should see a new dependency in the package.json file that reads something like "@myorg/common-vtfy": "workspace:^1.0.0".

    main.ts or plugins\index.ts (wherever you load your plugins)

    Import the components:

    import YSwitchTheme from '@myorg/common-vtfy'
    import YSwitchLang from '@myorg/common-vtfy'
    

    I was expecting to be able to import the modules using a {}-style import, but this doesn't work. I think this means that we're not correctly exporting the components from the plugin. See issues section.

    import { YSwitchTheme, YSwitchLang} from '@myorg/common-vtfy'
    

    And finally, to use the plugin, do:

    app.use(YSwitchTheme)
    app.use(YSwitchLang)
    

    VII. Updating

    At some point you're going to make changes to the component.

    • Component: Make the changes
    • Component: pnpm build
    • Component: Recreate index.d.ts
    • Application: pnpm update @myorg/common-vtfy

    Open issues

    • Scoped CSS is ignored. I've tried all kinds of different rollup settings.

    • Confirm that peerDepencies is either a good thing or useless.

    • I've not been able to figure out how to generate a single index.d.ts typescript declaration file.

    • Pnpm monorepo update I think you should run the pnpm update on the whole repo. But haven't dived into that yet.


    I will attempt to update and further refine this answer as I gain understanding, and/or when usefull comments are posted here.