Skip to content

Publish QForm Builder as a Nuxt Layer npm package

This phase prepares QForm Builder as a Nuxt Layer publishable on npm.

The goal is to install the package in a new or existing Nuxt 4 application and activate it with extends.

Why publish as a layer first?

The current source tree already follows the Nuxt layer model:

txt
app/components
app/composables
app/stores
app/types
app/utils
nuxt.config.ts
formkit.config.ts

Publishing as a layer is therefore the fastest and least risky route for the Community edition.

Structure selected for phase 1

txt
qform-builder-layer/
├── app/
├── .playground/
├── docs/
├── formkit.config.ts
├── nuxt.config.ts
└── package.json

Demo pages are kept in .playground so they are not injected into applications consuming the layer.

Layer nuxt.config.ts

The layer must not force application-level decisions such as ssr: false. The host application remains responsible for SSR, modules and runtime configuration.

Publication package.json

The package exposes the root nuxt.config.ts as its main entry:

json
{
  "name": "@vevedh/qform-builder-layer",
  "main": "./nuxt.config.ts",
  "publishConfig": {
    "access": "public"
  }
}

Peer dependencies

Nuxt, Vue, Quasar, Pinia and FormKit are declared as peer dependencies. This keeps the host application in control of framework versions.

Test before publishing

bash
bun run docs:build
bun run typecheck
bun run pack:dry-run

The typecheck script automatically runs nuxi prepare .playground before vue-tsc. A complete local validation/startup cycle is:

powershell
bun run clean && bun i && bun run typecheck && bun dev

PowerShell 7 uses && so bun dev is not started when type checking fails. Using ; would continue even after a non-zero exit code.

Publish on npm

bash
npm login
bun run npm:publish

Install in a Nuxt 4 application

bash
bun add @vevedh/qform-builder-layer
ts
export default defineNuxtConfig({
  extends: ['@vevedh/qform-builder-layer'],
})

Minimal example

vue
<script setup lang="ts">
import type { FormBuilderSchema, FormBuilderValues } from '@vevedh/qform-builder-layer/types'

const schema = ref<FormBuilderSchema>([])
const values = ref<FormBuilderValues>({})
</script>

<template>
  <ClientOnly>
    <FormBuilder
      builder-id="consumer-builder"
      v-model:schema="schema"
      v-model:values="values"
    />
  </ClientOnly>
</template>

Layer-owned styles

A published layer must resolve its stylesheet from import.meta.url instead of using ~/assets/...:

ts
const layerStylesheetUrl = new URL(
  './app/assets/scss/index.scss',
  import.meta.url,
)

const layerStylesheet = decodeURIComponent(layerStylesheetUrl.pathname)
  .replace(/^\/([A-Za-z]:\/)/, '$1')

export default defineNuxtConfig({
  css: [layerStylesheet],

  vite: {
    optimizeDeps: {
      include: [
        '@formkit/core',
        '@formkit/i18n',
        '@formkit/utils',
        'shiki',
      ],
    },
  },
})

In a playground or consuming application, ~ points to the host application, not to the installed layer.

This variant does not import node:url, so the generated playground tsconfig does not require a direct @types/node dependency. The normalization only removes the leading slash from Windows paths such as /C:/....

Best practice

Run npm pack --dry-run before every publication and verify that generated files, playground files and internal patch logs are not included.

Final Community contract

Community must remain stable, lightweight and documented. Advanced commercial features should be added through a separate Pro package.

bash
bun run release:doctor
bun run npm:publish:dry-run
bun run npm:publish

FormKit configuration owned by the layer

The layer resolves and binds its own formkit.config.ts through formkit.configFile. This prevents a playground or host auto-detected file from shadowing the Quasar adapter registry. A host application that intentionally overrides this option must compose qformBuilderQuasarPlugin again.

Version 0.1.3 — QBtn verification

Before publishing 0.1.3, also verify the button panel and its tooltip:

bash
bun run quasar-adapters:check
bun run e2e:static:check
bun run e2e:visual:update
bun run e2e:visual

The scenario must confirm that only one Button label field is exposed and that info appears as a nested QTooltip when hovering the QBtn.

Version 0.1.2 and visual tests

Before publishing 0.1.2, also run:

bash
bun run quasar-advanced:check
bun run quasar-advanced:runtime:check
bun run drop-indicator:check
bun run e2e:static:check
bun run e2e:visual

Playwright screenshots belong to the development repository and are not published in the runtime package. The @vevedh/qform-builder-layer/quasar-options subpath is included in the npm tarball.

Version 0.1.5 — shared QTooltip verification

Before publishing, verify the shared contract and the absence of QBtn duplicates:

bash
bun run tooltip:check
bun run tooltip:runtime:check
bun run quasar-adapters:check
bun run e2e:static:check
bun run e2e:visual

In /quasar-advanced, switch to preview mode and hover QEditor, QSelect, QRating, QInput and the static paragraph. Each info property must display exactly one QTooltip. All four Community button variants must keep buttonLabel and their QTooltip as a direct child of QBtn.

Version 0.1.6 — reliable visual scenarios

Static checks do not replace browser execution. Playwright scenarios now use the following stable markers:

html
<div data-qform-preview-mode="previewing" aria-label="Preview"></div>
<main data-qform-e2e-page="qtooltip-audit"></main>

QTab exposes the ARIA tab role and must not be located as a button. Tooltip assertions must target the teleported Quasar overlay:

ts
const tooltip = page.locator('.q-tooltip').filter({ hasText: expectedText }).last()
await expect(tooltip).toBeVisible()

Generate reference screenshots and then verify them with:

bash
bun run e2e:visual:update
bun run e2e:visual

The E2E helper also waits for the cold compilation of /quasar-advanced and /qtooltip-audit, with one controlled retry when Vite performs a transient optimization reload.

Version 0.1.7 — promoted FormKit properties

FormKit exposes properties registered through node.define({ props: [...] }) directly on the component context. A Quasar adapter must therefore not read context.attrs exclusively.

ts
const attrs = resolveQFormBuilderFormKitAttrs(context, [
  'buttonLabel',
  'info',
  'description',
])

Before publishing, verify that all four buttons on /quasar-advanced have a visible label, a data-qform-field-name marker and exactly one QTooltip. Also verify every dedicated structure tooltip on /qtooltip-audit.

bash
bun run quasar-adapters:check
bun run tooltip:check
bun run e2e:static:check
bun run e2e:visual:update
bun run e2e:visual

Version 0.1.8 — light/dark contract

Release 0.1.8 adds two mandatory gates:

bash
bun run dark-mode:check
bun run dark-mode:runtime:check

The release build must also run the /dark-mode-audit Playwright scenarios. Switching Quasar appearance must not mutate the serialized theme; the dark palette is derived for rendering only.

Version 0.1.9 — template registry

Release 0.1.9 adds the public ./templates subpath and two mandatory gates:

bash
bun run templates:check
bun run templates:runtime:check

The runtime check must cover key-based merging, data sanitization, prototype-pollution key rejection, name collisions, condition and cross-field validation rewriting, initial values, stepper conflicts and active-theme preservation.

Before publishing, open /templates, verify search, append, confirmed replacement and the Quasar tooltip on the unavailable append action, then run:

bash
bun run typecheck
bun run runtime:build:check
bun run docs:build
bun run e2e:visual
npm pack --dry-run

The tarball must include app/templates/** and exclude .playground, tests, patch-memory, AGENTS.md and PROMPT_CONTEXT.md.

Release 0.1.16 — accessibility

The tarball must include app/accessibility/** and expose @vevedh/qform-builder-layer/accessibility. Before publishing:

bash
bun install
bun run accessibility:check
bun run release:browser:check
npm pack --dry-run

The browser audit never updates visual baselines. Any new baseline must be generated separately, reviewed, and then committed.

0.1.17 release — CI matrix and verified artifact

Before a release candidate, the repository must now produce the same contract on Windows and Linux:

bash
bun run ci:quality
bun run e2e:ci:functional
bun run release:browser:check
bun run pack:artifact

The package job retains the .tgz file and package-manifest.json as GitHub Actions artifacts. The manifest contains package identity, size, npm integrity and the tarball SHA-256 digest. pack:verify rejects fixtures, workflows, tests, lockfiles and internal journals from the published package.

The tests/host-app build complements the playground by validating real SSR consumption of the layer and its public subpaths before publishing.

QForm Builder — Nuxt 4, Quasar and FormKit layer for dynamic forms.