-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebpack.config.js
More file actions
329 lines (312 loc) · 14.1 KB
/
Copy pathwebpack.config.js
File metadata and controls
329 lines (312 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
// SPDX-License-Identifier: EUPL-1.2
// Copyright (C) 2026 Conduction B.V.
const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin')
const webpackConfig = require('@nextcloud/webpack-vue-config')
const { VueLoaderPlugin } = require('vue-loader')
// ⚠️ `node-polyfill-webpack-plugin` and `terser-webpack-plugin` are BUILD-TIME
// requirements of `@nextcloud/webpack-vue-config@7` that this app must declare
// itself. The first is only a `peerDependency` of that package (and `.npmrc`
// sets `legacy-peer-deps=true`, so peers are not auto-installed); the second is
// `require()`d without being declared at all. Neither is referenced anywhere in
// `src/`, so both look like dead dependencies — dropping either fails the build
// immediately with `Cannot find module …` from inside node_modules.
const buildMode = process.env.NODE_ENV
const isDev = buildMode === 'development'
// Production builds disable source maps entirely. The full `source-map` devtool
// (and Terser's own source-map generation) added significant memory and time on
// top of compilation, and emitted ~77 MB of .map files into js/. Dropping them
// keeps the output minified while lowering peak memory. Dev keeps cheap, fast
// line-level maps.
webpackConfig.devtool = isDev ? 'cheap-source-map' : false
// The base @nextcloud/webpack-vue-config hardcodes
// output.publicPath = '/apps/<appName>/js/'
// which is wrong when the app lives in `apps-extra/`. The entry scripts are
// injected by PHP (Util::addScript) with the correct `/custom_apps/pipelinq/js/`
// webroot, but webpack's runtime loader uses output.publicPath for dynamically
// imported chunks, so those resolve against `/apps/pipelinq/js/`.
//
// ⚠️ That wrong path does NOT 404 — Nextcloud's PHP router answers 200 with
// `text/html` (the SPA shell), so the browser reports a MIME refusal and a
// ChunkLoadError rather than a missing file. Vue 2 never surfaced this because
// it emitted no async chunks; the Vue 3 dependency set splits @nextcloud/dialogs,
// @nextcloud/files, @nextcloud/paths and @mdi/js into dozens.
//
// 'auto' makes webpack derive the public path from the URL of the executing
// entry script at runtime, so lazy chunks load from wherever the app is
// actually mounted.
webpackConfig.output = {
...webpackConfig.output,
publicPath: 'auto',
}
webpackConfig.stats = {
colors: true,
modules: false,
}
const appId = 'pipelinq'
webpackConfig.entry = {
main: {
import: path.join(__dirname, 'src', 'main.js'),
filename: appId + '-main.js',
},
adminSettings: {
import: path.join(__dirname, 'src', 'settings.js'),
filename: appId + '-settings.js',
},
dealsOverviewWidget: {
import: path.join(__dirname, 'src', 'dealsOverviewWidget.js'),
filename: appId + '-dealsOverviewWidget.js',
},
myLeadsWidget: {
import: path.join(__dirname, 'src', 'myLeadsWidget.js'),
filename: appId + '-myLeadsWidget.js',
},
recentActivitiesWidget: {
import: path.join(__dirname, 'src', 'recentActivitiesWidget.js'),
filename: appId + '-recentActivitiesWidget.js',
},
findClientWidget: {
import: path.join(__dirname, 'src', 'findClientWidget.js'),
filename: appId + '-findClientWidget.js',
},
startRequestWidget: {
import: path.join(__dirname, 'src', 'startRequestWidget.js'),
filename: appId + '-startRequestWidget.js',
},
createLeadWidget: {
import: path.join(__dirname, 'src', 'createLeadWidget.js'),
filename: appId + '-createLeadWidget.js',
},
portal: {
import: path.join(__dirname, 'src', 'portal.js'),
filename: appId + '-portal.js',
},
}
// Use local source when available (monorepo dev), otherwise fall back to the
// published npm package.
//
// ⚠️ `USE_LOCAL_LIB` is opt-OUT across the fleet and the shared
// `apps-extra/nextcloud-vue` checkout sits on the Vue 2 (beta.*) line, so a
// default-on local build would silently compile Vue 2 library sources into this
// Vue 3 app. Opt IN explicitly (USE_LOCAL_LIB=true) and hard-fail if the local
// tree is not on the Vue 3 major.
const localLib = path.resolve(__dirname, '../nextcloud-vue/src')
let useLocalLib = process.env.USE_LOCAL_LIB === 'true' && fs.existsSync(localLib)
if (useLocalLib) {
// The peer-vue test this replaces asked the wrong question. The sibling's
// `peerDependencies.vue` is ^3.5.0 — it IS a Vue 3 library — so the check
// passed, while the sibling was still 2.0.5 against a declared ^2.3.0. Being
// on the Vue 3 line and being the version this app asked for are different
// things, and only the second one is safe to alias in.
//
// Fail CLOSED: if the check cannot run, the sibling is refused.
let localVersion = 'unreadable'
let satisfied = false
try {
// eslint-disable-next-line n/no-extraneous-require
const semver = require('semver')
const required =
require('./package.json').dependencies['@conduction/nextcloud-vue']
localVersion = String(
JSON.parse(
fs.readFileSync(
path.resolve(__dirname, '../nextcloud-vue/package.json'),
'utf8',
),
).version || '',
)
satisfied = semver.satisfies(localVersion, required, {
includePrerelease: true,
})
} catch (e) {
satisfied = false
}
if (!satisfied) {
// Warn rather than throw: refusing the sibling still produces a complete
// build against the pinned npm package, so there is nothing to repair
// before the build can proceed.
// eslint-disable-next-line no-console
console.warn(
`[pipelinq] IGNORING sibling @conduction/nextcloud-vue@${localVersion} — `
+ "it does not satisfy this app's declared range. Building against the npm dist.",
)
useLocalLib = false
}
}
webpackConfig.resolve = {
extensions: ['.vue', '.js', '.mjs'],
alias: {
'@': path.resolve(__dirname, 'src'),
...(useLocalLib
? { '@conduction/nextcloud-vue': localLib }
: // Published mode: the package's main entry is dist/, but src/main.js
// imports the integration-registry helpers from their 0-hop definition
// modules (`@conduction/nextcloud-vue/integrations/...`) rather than the
// barrel — the barrel's multi-hop re-exports of these functions resolve
// to `undefined` across pipelinq's split shared-nc-vue chunk. Those
// subpaths exist only under the package's published `src/` tree, so map
// the `integrations` subpath there. The registry installs onto the
// `window.OCA.OpenRegister.integrations` global, so resolving these from
// src/ shares the same singleton as the dist components (no dual instance).
{
'@conduction/nextcloud-vue/integrations': path.resolve(
__dirname,
'node_modules/@conduction/nextcloud-vue/src/integrations',
),
}),
// Deduplicate shared packages so the aliased library source uses the same
// instances as the app (prevents dual-Pinia / dual-Vue / dual-router bugs).
//
// ⚠️ An alias that resolves to a package DIRECTORY makes webpack fall back
// to `main`/`mainFields` and skip the package's `exports` map entirely.
// `@nextcloud/vue@9`, `@nextcloud/dialogs@7` and `vue-router@5` ship NO
// `main` and NO `module` — they are reachable only through their exports
// map — so a directory alias for those resolves to NOTHING and produces one
// `Can't resolve '<pkg>'` error per importing module (234 on petstore).
// Point singleton aliases at a concrete entry FILE. (`vue` and `pinia`
// still declare `main`, so a directory alias resolves for them.)
vue$: path.resolve(__dirname, 'node_modules/vue'),
pinia$: path.resolve(__dirname, 'node_modules/pinia'),
'@nextcloud/vue$': path.resolve(
__dirname,
'node_modules/@nextcloud/vue/dist/index.mjs',
),
// @nextcloud/vue@9 hard-depends on vue-router ^5.1.0 while the app is on
// vue-router 4, so a DUAL COPY is inevitable and the router injection keys
// (module-local Symbols) would not match across it — `useRoute()` inside
// library components would return undefined. Force one copy.
'vue-router$': path.resolve(
__dirname,
'node_modules/vue-router/dist/vue-router.mjs',
),
},
}
// Keep the base module rules from @nextcloud/webpack-vue-config (VUE, CSS, SCSS, JS, ASSETS).
// Only replace plugins to avoid duplicate VueLoaderPlugin (base config also registers one).
webpackConfig.plugins = [
new VueLoaderPlugin(),
new webpack.DefinePlugin({ appName: JSON.stringify(appId) }),
new webpack.DefinePlugin({
appVersion: JSON.stringify(process.env.npm_package_version),
}),
]
// The former `@nextcloud/dialogs` + `@nextcloud/dialogs/style.css` directory
// aliases are GONE.
//
// They existed to stop "the nextcloud-vue submodule's nested deps (Vue 3)"
// leaking into a Vue 2 app — a rationale this migration inverts. Worse, they
// were the same landmine as the `@nextcloud/vue` alias above: dialogs v7 ships
// NO `main`, only `exports`, so aliasing the bare specifier to the package
// DIRECTORY makes every `import … from '@nextcloud/dialogs'` unresolvable. The
// companion `style.css$` alias is likewise unnecessary — v7's exports map
// publishes `./style.css` directly.
// The former `@nextcloud/axios$` shim (build/nextcloud-axios-default.cjs) is
// GONE. It existed because @nextcloud/vue@8's CJS bundle did
// `require('@nextcloud/axios')`, which failed webpack 5's exports check, and
// then double-wrapped the ESM namespace so `.interceptors` came back undefined.
// @nextcloud/vue@9 is ESM-only, so the bare `import` condition resolves
// normally and no shim is needed.
// The former `vue-demi$` pin to `lib/v2.7/index.mjs` is GONE — that variant is
// the Vue 2 shim by construction. vue-demi picks its shim in a `postinstall`,
// which npm does NOT re-run for an already-present version, so the switch is
// verified explicitly in CI/locally rather than pinned here (see the check in
// the migration notes: node_modules/vue-demi/lib/index.mjs must contain
// `import * as Vue` and `isVue2 = false`).
// nc-vue pulls ESM-only @nextcloud packages that declare only the `import`
// export condition; alias to the ESM entry so a CJS `require()` inside a
// transitive dependency still resolves.
webpackConfig.resolve.alias['@nextcloud/paths$'] = path.resolve(
__dirname,
'node_modules/@nextcloud/paths/dist/index.mjs',
)
webpackConfig.resolve.alias['@nextcloud/notify_push$'] = path.resolve(
__dirname,
'node_modules/@nextcloud/notify_push/dist/index.js',
)
// @nextcloud/files (pulled transitively via @nextcloud/axios → @nextcloud/auth)
// references the Node core `stream` module, which webpack 5 does not polyfill for
// the browser. It is on a code path the app never hits, so provide an empty module.
// Same story for `path`: @nextcloud/dialogs v7 drags in a FilePicker chunk that
// imports it, and the app only uses the toast APIs (showError/showSuccess/
// showWarning), so the FilePicker code path never executes.
webpackConfig.resolve.fallback = {
...(webpackConfig.resolve.fallback || {}),
stream: false,
path: false,
}
// Share Vue + @nextcloud/vue + pinia + icons + @conduction/nextcloud-vue
// across every entry-point so each widget bundle no longer inlines its own
// ~5 MB framework copy. Stable filenames (no contenthash in the JS name)
// mean each widget's `Util::addScript` PHP call can reference the chunk
// directly without a manifest. The vendor chunk is loaded once and cached
// across every widget/page in the app.
webpackConfig.optimization = {
...(webpackConfig.optimization || {}),
splitChunks: {
...(webpackConfig.optimization?.splitChunks || {}),
chunks: 'all',
cacheGroups: {
default: false,
defaultVendors: false,
ncVue: {
name: appId + '-shared-nc-vue',
// Initial chunks ONLY. The outer `chunks: 'all'` would also match
// modules the library imports dynamically, hoisting them into this
// eager chunk and destroying the code-splitting they exist for —
// nc-vue's RVO icon set (~1.9 MB) landed here in full, loaded on
// every page, instead of being fetched when its picker tab is
// opened. 'initial' leaves async imports in their own chunks.
chunks: 'initial',
// Matches both node_modules entries AND the monorepo-dev alias
// `../nextcloud-vue/src/...` which webpack resolves outside
// node_modules when @conduction/nextcloud-vue is aliased to it.
test: /[\\/]node_modules[\\/](@nextcloud[\\/]vue|@conduction[\\/]nextcloud-vue)[\\/]|[\\/]nextcloud-vue[\\/]src[\\/]/,
priority: 30,
reuseExistingChunk: true,
enforce: true,
filename: appId + '-shared-nc-vue.js',
},
vendor: {
name: appId + '-shared-vendor',
test: /[\\/]node_modules[\\/](vue|vue-router|vue-i18n|pinia|vue-material-design-icons|@vueuse|core-js|axios)[\\/]/,
priority: 20,
reuseExistingChunk: true,
enforce: true,
filename: appId + '-shared-vendor.js',
},
},
},
}
if (!isDev) {
// Minify with esbuild instead of Terser in production. Terser parses every
// chunk into a full JS AST held in the Node heap and runs `CPU cores - 1`
// parallel workers; across these large entrypoints the build peaked at ~12 GB
// in ~33s. esbuild minifies in native (Go) code with a tiny heap footprint and
// is ~10-100x faster, at the cost of only ~1-2% larger output. We reuse
// terser-webpack-plugin purely as the wiring and swap its engine to the
// built-in esbuild minifier.
webpackConfig.optimization.minimizer = [
new TerserPlugin({
minify: TerserPlugin.esbuildMinify,
// esbuild parallelizes internally (in Go), so terser-webpack-plugin's
// default `cpus-1` Node worker processes only add memory overhead.
// Disabling them lowers peak build RAM with no real speed cost.
parallel: false,
// esbuild minify options (NOT terserOptions). Keep legal/license
// comments at end-of-file so MIT/AGPL attribution required by our
// deps survives minification. (esbuild's sidecar-emitting 'linked'
// mode is unavailable here — terser-webpack-plugin drives esbuild via
// its transform API, which rejects 'linked'/'external'.)
terserOptions: {
legalComments: 'eof',
},
}),
]
// The base config keeps an in-memory cache (`cache: true`) in production.
// A one-shot build never reuses it, so it only inflates the webpack main
// process — the dominant memory consumer here. Disable it for the build.
webpackConfig.cache = false
}
module.exports = webpackConfig