跪拜 Guibai
← Back to the summary

vite-plugin-uni-manifest 0.7: Generate manifest.json Before uni-app Reads It

vite-plugin-uni-manifest 0.7: Generate Before uni-app Reads

image.png

0.5 handled module format and options, 0.6 completed uni-app x types. After that, feedback concentrated on two things: manifest.config.ts couldn't read values from .env, and default config arrays always concatenated with what you wrote. Further down, there were a batch of edge cases that "could break once installed": when no config file existed, the hand-written manifest.json in the project would be overwritten with defaults; outDir pointing to a nonexistent directory made the build fail directly; enabling debug logs would overwrite your DEBUG variable.

0.7 mainly solves these two kinds of problems. The old timing issue finally has a built-in solution.

Read Before Upgrading: Four Breaking Changes

writeManifestJson Uses Output Spec

The second parameter in 0.6 was user options; minify, indent, eol were recalculated by the writer with default values, duplicating the plugin's internal parsing. 0.7 changes it to accept an already-parsed OutputSpec, with minify folded into indent: 0 at this step.

// 0.6
import { writeManifestJson } from '@uni-helper/vite-plugin-uni-manifest'

writeManifestJson(config, { minify: true, indent: 2, eol: '\n' })
// 0.7
import { resolveOutputSpec, writeManifestJson } from '@uni-helper/vite-plugin-uni-manifest'

const spec = resolveOutputSpec({
  cwd: process.cwd(),
  minify: true,
  indent: 2,
  eol: '\n',
  insertFinalNewline: false,
})
writeManifestJson(config, spec)

Only needed when calling this function directly; the plugin internally has already switched.

Relative outDir Now Based on cwd

In 0.6, relative outDir was resolved against process.cwd(), while the cwd option pointed to the project root. In a monorepo, when running commands from the repo root, these are not the same:

UniManifest({
  cwd: 'packages/app',
  outDir: 'src', // 0.7 writes to packages/app/src, 0.6 writes to <repo-root>/src
})

In a single-package project, both bases are the same, so no impact.

No File Written When No Config File Exists

In 0.6, when manifest.config.* was not found, a manifest.json would be written using built-in default config. In the phase where the plugin is installed but no config file has been created, a hand-written manifest.json in the project would be replaced with an empty shell after one build, losing appid, permissions, and other configurations.

0.7 skips writing when c12 reports no config file; this skips both the first write and cases where the config file is deleted during runtime.

Three Exports Removed

Removed Replacement
ensureManifestJsonExists() None. It had no callers, and its behavior contradicted the convention of "no placeholder file when config load fails".
ManifestContext createManifestWatcher()
debug (entry export) None. The logger object is now internal; DebugType is still exported.

By the way, debug in logger.ts was previously exposed as a public interface via export *, so users could directly modify the plugin's internal logger. 0.7 removes that line; other exports remain unchanged.

New Capabilities

prepare(): Generate Before uni-app Reads

@dcloudio/vite-plugin-uni reads manifest.json via parseManifestJsonOnce in its own config hook and caches it. The plugin could previously only write in configResolved. This timing issue has been documented in the FAQ since 0.5, and could only be worked around with @uni-helper/unh or custom scripts.

0.7 adds prepare() to the plugin, which runs a full generation before Vite loads the config:

// vite.config.mts
import Uni from '@uni-helper/plugin-uni'
import UniManifest from '@uni-helper/vite-plugin-uni-manifest'
import { defineConfig } from 'vite'

export default defineConfig(async () => ({
  plugins: [
    await UniManifest().prepare(),
    Uni(),
  ],
}))

Array elements are evaluated in order, so Uni() is created only after preparation completes. When Vite takes over, it writes again; the writer detects identical content and skips, avoiding unnecessary writes.

prepare() returns the same plugin instance and can be called step by step. cwd and mode can be overridden for this run; mode determines which .env.{mode} is read. By default it uses process.env.NODE_ENV. If you use --mode staging in the command, you must explicitly pass { mode: 'staging' }, otherwise the preparation phase reads .env.production.

image.png

For scenarios requiring subprocess isolation or generating multiple files at once, unh is still more appropriate.

dotenv: Let manifest.config.ts Read .env

c12's dotenv is opt-in, and the plugin didn't pass it before, so values from .env and .env.{mode} didn't get through. 0.7 adds a dotenv option:

UniManifest({ dotenv: true })

Reading is delegated to Vite's own loadEnv, with file and parsing rules identical to Vite, no prefix, and variable names unrestricted by VITE_:

File Description
.env Shared by all modes
.env.local Local overrides, usually not committed
.env.{mode} Specific to current mode
.env.{mode}.local Local overrides for current mode

Later files override earlier ones; existing values in process.env take priority over files. Files are read only once at startup; changes to .env require a restart.

.env, .env.local, .env.{mode}, .env.{mode}.local merged sequentially into process.env, then passed to manifest.config.ts

replaceArrays: Full Array Override

c12's default merge strategy is defu, which concatenates arrays: default config arrays come first, yours appended after. To fully replace arrays, enable replaceArrays:

UniManifest({ replaceArrays: true })
export default defineManifestConfig({
  'app-plus': {
    distribute: {
      android: {
        // When off, concatenates with default permissions; when on, keeps only this one
        permissions: ['<uses-permission android:name="android.permission.INTERNET"/>'],
      },
    },
  },
})

Objects still deep-merge normally, scalars follow your values, and only array handling changes.

image.png

Finishing Watcher and Writing

One-off builds previously also created a chokidar watcher that wasn't closed until buildEnd. That watcher serves no purpose in a one-off build and, in macOS environments with tight file descriptors, could push toward EMFILE (#88). 0.7 creates a watcher only when the build is persistent; one-off builds load the config once.

image.png

Meanwhile, watch builds got a subtler fix: the plugin originally closed the watcher in buildEnd, and buildEnd triggers after every rebuild, so when changing manifest.config.ts in uni-app mini-program development, the first build would stop reacting. 0.7 no longer closes the watcher proactively, leaving it to process exit.

Remaining are a few edge cases that could break or confuse builds:

Documentation and Engineering

The repository gained a VitePress documentation site, split into guide, API, and misc, with option tables, prepare() parameters and limitations, programmatic interface, FAQ, and a migration guide from 0.6 to 0.7, plus deployment config for Netlify and Cloudflare Pages. The previous 0.5 and 0.6 release notes also moved into the site's blog.

CI and release flow got three new gates: after pnpm build, compare packages/schema/schema.json with the commit; the release flow runs build and tests first; changelogithub locks the major version.

The internal debug was replaced with obug, an ESM-native, typed debug fork; @types/debug was removed. Output log format stays the same.

Test files increased from 6 to 7, test cases from 46 to 64. New load-config.test.ts and prepare.test.ts run the full load flow with real c12, real .env, and temporary directories, instead of only asserting mock call parameters.

Upgrade Checklist

  1. Upgrade dependencies, confirm runtime is at least Node 22.22.2 or 24.15.0, then run a build to see the manifest.json diff.
  2. Replace direct calls to writeManifestJson() with resolveOutputSpec().
  3. In monorepos, check relative outDir and rewrite it relative to cwd.
  4. For anything using ensureManifestJsonExists(), ManifestContext, or entry-exported debug, replace per the table above.
  5. If you want .env or array overrides, enable dotenv and replaceArrays; for --mode scenarios, remember to pass mode to prepare().

Complete rules and comparison of old syntax are in the migration guide.

Acknowledgments

Thanks to @ByteRax for reporting EMFILE caused by the surplus watcher during builds (#88) and submitting #89 to split one-off and watch builds. That PR's check of config.command was later supplemented with a build.watch check, because uni-app mini-program development also uses Vite's build API with watch.

.env loading and array merge needs came from #91, and the implementation referenced oiyo. Thanks to @skiyee for open-sourcing that project. Also thanks to friends who submitted reproductions and feedback in issues.

Related Links