Nathan Lamont

Notes to Self

Making a Vue/Nuxt Module

You wanted your work on "shared element transitions" to be re-usable by others, so you began work on creating a Nuxt module using their "starter template."

npx nuxi init -t module vue-shared-element-transition

This includes a "playground" where an example app lives to test your module.

The pieces for shared element transitions were:

  • a composable that does most of the work
  • a plugin to handle scroll behavior
  • a component for cases where your which to apply the transition to something other than a page

This seemed to work well. Notably, the weird callback stuff (diagrammed here: [[Shared Element Transitions + Scroll Behavior.canvas]]) was not necessary as, in the playground, the scrollBehavior hook was being called at the "right" time.

But you realized that there might be utility for a plain Vue module, not just Nuxt. Your Nuxt module would then use this Vue module.

There doesn't seem to be an official starter template for Vue modules. You basically took this "boilerplate" and removed the storybook parts (storybook is a big tool for documenting/testing components).

This template looks lighter but you have not tried it yet.

You got the Vue module working. The problem came when you wanted to test it in the Nuxt module.

The correct way to do this is with npm/yarn link. You go to the directory of the project you want to link to, in this case the Vue module. You type

~/vue-shared-element-transition/ > yarn link

Then you to the directory you want to link from, as if the module were installed:

~/nuxt-shared-element-transition/ > yarn link vue-shared-element-transition

However, when attempting to import in the code, e.g.

import { useSharedElementTransition } from "vue-shared-element-transition";

You could get an error message stating that the file couldn't be found, even though it correctly listed the actual path to the file it was looking for.

There is some symLink setting for some part of vite, but it doesn't appear to be exposed in Nuxt? You are about to attempt to simplify the problem by creating a new Vue component and a new Nuxt component to more destructively experiment with.

Update

When you attempted to recreate with bare projects, you saw additional info pointing you to the following solution.

For the link to work, you need to add the following to nuxt config:

import { searchForWorkspaceRoot } from 'vite'

export default defineNuxtConfig({
  // ...
  vite: {
    server: {
      fs: {
        strict: false
      }
    }
  }
})

You did this in playground's nuxt.config.ts -- doesn't seem necessary to build though? Not sure.

!warning May be obvious, but you will need to release the vue component in order to release the nuxt component. Don't forget.

** CONFIRMED ** The above worked as a fix for the real modules I was working on. Next steps:

  1. Review packages and methods from both "boilerplate" vue projects you used. VitePress used on the second project seems like a useful addition, as it can be used to both test, demonstrate, and document the component.
  2. Once settled:
  3. Init git; ensure that .gitinit is properly configured
  4. Ensure that linting/prettying is in place
  5. Create demonstration in Vue project (like demo in Nuxt project)
  6. Create documentation in Vue Project
  7. Explore vue router - that's where scrollPosition comes in, so maybe should not be limited to nuxt
  8. Publish
  9. Repeat for nuxt project

Unanticipated additional work:

  • The way to have a module that automatically can be used universally in vue is to have its main library export a default with an import function, e.g. return default { import(app:Vue): {} }. There are two kinds of modules you may need to support, umd and esm (which I guess just uses an es file extension) but umd is weird about exporting both default (for magic) and named exports. You are going to ignore umd for now. EDIT: no you figured it out, kind of. vue3-boilerplate had es and umd flipped (umd is for require, es cannot be required). Can't export default, so have to export named import function, then import that import function and use it from plugin.
  • There is some issue with a lynchpin in your shared-element-transition: you are using "globals" in the sharedElementTransition composable to store data about the elements that are transitioning. Not good for es module as it seems one of the globals is reduced to h, and a different h is prepended. The composable is called from different components -- how else to share that info (both from and to elements must be considered). Note: you refactored consts into single object g and the different h import that was prepended magically went away. WTF.

Unexpected complexity:

You need to have

  • Vue Project with
    • VitePress site built in to test and doc
  • External example vue project to test import
  • Nuxt Project with
    • Nuxt playground site built in
  • External example nuxt project to test import
PropertyTypeDefaultDescription
speednumber222Speed of the transition, in milliseconds
groupstring"page"An arbitrary group name to associate transitions with. Transitions belonging to different groups will not affect each other.
comparatorComparatorType* see noteA comparator function to use when considering Relative Slide transitions.
xstring"25%"A valid CSS unit value for horizontal animation on Relative Slide transitions.
ystring"0%"A valid CSS unit value for vertical animation on Relative Slide transitions.

Getting Info from Nuxt Config into Front End/Client

Nuxt module doesn’t run client side. It’s config info only exists at build time/server side.

To pass info, you have to modify the runtimeConfig object available on the nuxt instance the module receives.

// your module.ts

import { defineNuxtModule, addPlugin, addImports, createResolver } from '@nuxt/kit'

// Module options TypeScript interface definition
// this configurable in nuxt.config.ts
export interface ModuleOptions {
  foo: string;
}

export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: 'your-module',
    configKey: 'yourModule',
    compatibility: {
      nuxt: '^3.0.0'
    }
  },
  
  // Default configuration options of the Nuxt module
  defaults: { foo: 'bar' },
  
  setup (options, nuxt) {
    nuxt.options.runtimeConfig.public.yourModule = {
      foo: options.foo
    };
    // etc. 
  }
})
// nuxt.config.ts
export default defineNuxtConfig({
  yourModule: {
    foo: 'baz'
  }
})
// then, elsewhere
import { useRuntimeConfig } from '#app'
console.log(runtimeConfig.public.yourModule.foo)
// -> "baz" b/c that's what's in defineNuxtConfig

this was helpful but outdated

PROBLEM

Can’t build because the playground gets in inferred type for the runtime config.

In generated playground/.nuxt/types/schema.d.ts we get

import { NuxtModule, RuntimeConfig } from 'nuxt/schema'
declare module 'nuxt/schema' {
  // ...
  interface PublicRuntimeConfig {
   contextualTransition: {
      hashScroll: boolean, // or string depending on how its set
   },
  }
}

where we declare the module options in module.ts as

export type HashScrollType = false | 'smooth' | 'instant' | 'auto';

export interface ModuleOptions {
  hashScroll: HashScrollType;
}

The playground works, but yarn prepack fails because:

src/module.ts(34,5): error TS2322: Type '{ hashScroll: HashScrollType; }' is not assignable to type '{ hashScroll: boolean; }'.
  Types of property 'hashScroll' are incompatible.
    Type 'string | boolean' is not assignable to type 'boolean'.
      Type 'string' is not assignable to type 'boolean'.

This states that you can manually set the runtime config. But where does index.d.ts get used?

There is a weird reliance on the generated playground/.nuxt/tsconfig.json file from the main module directory’s tsconfig.json file. I discovered by manually including a directory with my index.d.ts in it (types/index.d.ts), along with the paths originally automatically set in the playground’s version, I could get both the playground and the prepack command to work:

{
  "extends": "./playground/.nuxt/tsconfig.json",
  "include": [
      "./types",
      "./playground/.nuxt/nuxt.d.ts",
      "./playground/**/*",
      "./**/*"
  ]
}

But without that, I couldn’t figure out where to put the index.d.ts so that it would get picked up, or, at least, so that yarn prepack would work.