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
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.
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.
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.
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.
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:
- If
outDirpoints to a nonexistent directory, it is created first, no moreENOENT - If the config exports something that isn't an object, throw
TypeErrorand keep the original file. Arrays previously would be written verbatim intomanifest.json - Enabling
debugno longer overwrites yourprocess.env.DEBUG; other obug logs in the same process work as before - Generation path priority (
outDir→UNI_INPUT_DIR→cwd/src) and formatting rules centralized into the writer;paths.tsmerged away, one less module
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
- Upgrade dependencies, confirm runtime is at least Node 22.22.2 or 24.15.0, then run a build to see the
manifest.jsondiff. - Replace direct calls to
writeManifestJson()withresolveOutputSpec(). - In monorepos, check relative
outDirand rewrite it relative tocwd. - For anything using
ensureManifestJsonExists(),ManifestContext, or entry-exporteddebug, replace per the table above. - If you want
.envor array overrides, enabledotenvandreplaceArrays; for--modescenarios, remember to passmodetoprepare().
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.