Any package that appears in
apps/app/.next/node_modules/after a production build MUST be listed underdependencies, notdevDependencies.
Turbopack externalises packages by generating runtime symlinks in .next/node_modules/. pnpm deploy --prod excludes devDependencies, so any externalised package missing from dependencies causes ERR_MODULE_NOT_FOUND in production.
Step 1 — Build and check:
turbo run build --filter @growi/app
ls apps/app/.next/node_modules/ | grep <package-name>
dependenciesdevDependencies (if runtime code) or devDependencies (if build/test only)Step 2 — If unsure, check the import site:
| Import pattern | Classification |
|---|---|
import foo from 'pkg' at module level in SSR-executed code |
dependencies |
import type { Foo } from 'pkg' only |
devDependencies (type-erased at build) |
await import('pkg') inside useEffect / event handler |
Check .next/node_modules/ — may still be externalised (see fix-broken-next-symlinks skill) |
Used only in *.spec.ts, build scripts, or CI |
devDependencies |
dynamic({ ssr: false }) does NOT prevent Turbopack externalisation.
It skips HTML rendering for that component but Turbopack still externalises packages found via static import analysis inside the dynamically-loaded file.
useEffect-guarded import() does NOT guarantee devDependencies.
Bootstrap and i18next backends are loaded this way yet still appear in .next/node_modules/ due to transitive imports.
These were successfully removed from production artifact by eliminating their SSR import path:
| Package | Technique |
|---|---|
fslightbox-react |
Replaced static import with import() inside useEffect in LightBox.tsx |
socket.io-client |
Replaced static import with await import() inside useEffect in admin/states/socket-io.ts |
@emoji-mart/data |
Replaced runtime import with bundled static JSON (emoji-native-lookup.json) |
Just want to know if a package gets externalised by Turbopack?
turbo run build --filter @growi/app
ls apps/app/.next/node_modules/ | grep <package-name>
# Found → dependencies required
# Not found → devDependencies is safe
Turbopack build is incremental via cache, so subsequent runs after the first are fast.
reusable-app-prod.yml, authoritative)Trigger via workflow_dispatch before merging. Runs two jobs:
build-prod: turbo run build → assemble-prod.sh → check-next-symlinks.sh → archives production tarballlaunch-prod: extracts the tarball into a clean isolated directory (no workspace-root node_modules), runs pnpm run server:cicheck-next-symlinks.sh scans every symlink in .next/node_modules/ and fails the build if any are broken (except fslightbox-react which is intentionally broken but harmless). This catches classification errors regardless of which code paths are exercised at runtime.
server:ci = node dist/server/app.js --ci: the server starts fully (loading all modules), then immediately exits with code 0. If any module fails to load (ERR_MODULE_NOT_FOUND), the process exits with code 1, failing the CI job.
This exactly matches Docker production (no workspace fallback). A build-prod or launch-prod failure definitively means a missing dependencies entry.