@digigov/cli-app 2.4.3-govgr-4752.27-07-26-12-24 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,19 +1,25 @@
1
1
  # Change Log - @digigov/cli-app
2
2
 
3
- <!-- This log was last generated on Mon, 27 Jul 2026 12:24:19 GMT and should not be manually modified. -->
3
+ <!-- This log was last generated on Mon, 27 Jul 2026 17:23:56 GMT and should not be manually modified. -->
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
- ## 2.4.3-govgr-4752.27-07-26-12-24
7
+ ## 2.5.0
8
8
 
9
- Mon, 27 Jul 2026 12:24:19 GMT
9
+ Mon, 27 Jul 2026 17:23:56 GMT
10
10
 
11
11
  ### Minor changes
12
12
 
13
- - Bump @digigov/cli-build to v2.4.3-govgr-4752.27-07-26-12-24
14
- - Bump @digigov/cli to v2.4.3-govgr-4752.27-07-26-12-24
15
- - Bump @digigov/cli-lint to v2.4.3-govgr-4752.27-07-26-12-24
16
- - Bump @digigov/css to v2.4.3-govgr-4752.27-07-26-12-24
13
+ - Next.js 13.1.1 -> 16.2.12: builds now use static export (output: 'export', next export was removed in Next 14) while next dev keeps the regular dev server, and digigov app start serves out/ statically (-p/--port, -H/--hostname). Drop next-images/url-loader/babel-loader in favor of built-in static image imports, remove the rimraf peer dependency, and modernize the dev proxy to http-proxy-middleware v3 running in-process, arming websocket upgrades for ws: true proxy entries at startup so sockets reconnecting right after a restart are not dropped. (sangelopoulos@admin.grnet.gr)
14
+ - Bump @digigov/cli-build to v2.5.0
15
+ - Bump @digigov/cli to v2.5.0
16
+ - Bump @digigov/cli-lint to v2.5.0
17
+ - Bump @digigov/css to v2.5.0
18
+
19
+ ### Patches
20
+
21
+ - default images.unoptimized in dev as well as build so dev image rendering matches the exported artifact and @digigov/nextjs Image's basePath prepend behaves the same everywhere (sangelopoulos@admin.grnet.gr)
22
+ - route dev proxy and Next config output through the @digigov/cli logger instead of bare console calls (sangelopoulos@admin.grnet.gr)
17
23
 
18
24
  ## 2.4.2
19
25
 
package/next.common.cjs CHANGED
@@ -1,8 +1,40 @@
1
1
  const lib = require('@digigov/cli/lib');
2
+ const { logger } = require('@digigov/cli/lib/logger');
3
+ const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
2
4
  let aliases = lib.aliases(true);
3
- const withImages = require('next-images');
4
5
 
5
- function makeNextConfig(dir) {
6
+ // `next export` was removed in Next 14: outside the dev server the app must be
7
+ // built with `output: 'export'` for `next build` to emit the static site in
8
+ // `out/`. Export mode breaks `next dev` (and the dev proxy), so it can only be
9
+ // applied per phase. The built-in i18n routing is unsupported under static
10
+ // export.
11
+ // Images are unoptimized in every phase (not just export, where the default
12
+ // loader is unsupported): next/image never applies basePath to unoptimized
13
+ // srcs, so @digigov/nextjs's Image prepends it manually — with the optimizer
14
+ // active in dev that prepend would break, and dev would render differently
15
+ // from the deployed artifact.
16
+ function withStaticExport(config, phase) {
17
+ const common = {
18
+ ...config,
19
+ images: { unoptimized: true, ...config.images },
20
+ };
21
+ if (phase === PHASE_DEVELOPMENT_SERVER) return common;
22
+ const exported = {
23
+ output: 'export',
24
+ ...common,
25
+ };
26
+ if (exported.output === 'export' && exported.i18n) {
27
+ logger.warn(
28
+ 'Built-in i18n routing is not supported with static export; ' +
29
+ 'dropping the `i18n` config for this build. Locale routing must be ' +
30
+ 'handled in userland (e.g. i18next without the Next.js `i18n` key).'
31
+ );
32
+ delete exported.i18n;
33
+ }
34
+ return exported;
35
+ }
36
+
37
+ function makeBaseConfig(dir) {
6
38
  const project = lib.resolveProject(dir);
7
39
  const digigov = project.digigov || {};
8
40
  if (digigov.aliases) {
@@ -11,15 +43,52 @@ function makeNextConfig(dir) {
11
43
  ...digigov.aliases,
12
44
  };
13
45
  }
14
- console.log(`Creating aliases for ${Object.keys(aliases).join(', ')}`);
15
- const nextConfig = withImages({
46
+ logger.info(`Creating aliases for ${Object.keys(aliases).join(', ')}`);
47
+ return {
16
48
  poweredByHeader: false,
17
49
  trailingSlash: true,
18
- transpilePackages: Object.keys(aliases),
19
- });
20
- return nextConfig;
50
+ // Published-package consumers need more than the workspace aliases here.
51
+ // Turbopack bundles app imports of node_modules on the server, but
52
+ // externalizes the same imports made from inside node_modules (e.g. from
53
+ // @digigov/nextjs) — so any package holding shared state (React contexts,
54
+ // singletons) must be listed to unify every import onto one bundled
55
+ // module instance:
56
+ // - @digigov/nextjs also must never load as a Node external: its ESM dist
57
+ // does `import Document from 'next/document'` (CJS), which only
58
+ // resolves to the class under bundler interop — externally the default
59
+ // import yields the whole exports object and SSR crashes.
60
+ // - @digigov/ui + react-core carry LinkProvider/I18NProvider contexts: a
61
+ // split copy makes SSR fall back to plain anchors (no next/link, no
62
+ // trailing-slash normalization → hydration mismatch).
63
+ // - i18next/react-i18next are singletons initialized once by initI18n():
64
+ // a split copy renders raw translation keys during SSR.
65
+ // Inside the SDK workspace the aliases already cover the workspace libs;
66
+ // the Set just dedupes.
67
+ transpilePackages: [
68
+ ...new Set([
69
+ ...Object.keys(aliases),
70
+ '@digigov/nextjs',
71
+ '@digigov/ui',
72
+ '@digigov/react-core',
73
+ '@digigov/react-icons',
74
+ '@digigov/form',
75
+ 'i18next',
76
+ 'react-i18next',
77
+ ]),
78
+ ],
79
+ };
21
80
  }
81
+
82
+ const baseConfig = makeBaseConfig();
83
+
84
+ function makeNextConfig(dir) {
85
+ const base = dir ? makeBaseConfig(dir) : baseConfig;
86
+ return (phase) => withStaticExport(base, phase);
87
+ }
88
+
22
89
  module.exports = {
90
+ baseConfig,
23
91
  makeNextConfig,
92
+ withStaticExport,
24
93
  config: makeNextConfig(),
25
94
  };
package/nextrc.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  const { makeConfig } = require('@digigov/cli/lib');
2
+ const { baseConfig, withStaticExport } = require('./next.common.cjs');
2
3
 
3
4
  module.exports = (overrides) => {
4
- return makeConfig(overrides, require('./next.common.cjs').config);
5
+ return (phase) => withStaticExport(makeConfig(overrides, baseConfig), phase);
5
6
  };
package/package.json CHANGED
@@ -1,33 +1,29 @@
1
1
  {
2
2
  "name": "@digigov/cli-app",
3
- "version": "2.4.3-govgr-4752.27-07-26-12-24",
3
+ "version": "2.5.0",
4
4
  "description": "@digigov developer cli tool",
5
5
  "main": "./index.js",
6
6
  "type": "module",
7
7
  "author": "GRNET Developers <devs@lists.grnet.gr>",
8
8
  "license": "BSD-2-Clause",
9
9
  "dependencies": {
10
- "babel-loader": "8.2.1",
11
- "express": "4.17.1",
12
- "http-proxy-middleware": "1.0.3",
13
- "next-images": "1.3.0",
14
- "url-loader": "4.1.1",
15
- "next": "13.1.1",
10
+ "express": "4.22.2",
11
+ "http-proxy-middleware": "3.0.5",
12
+ "next": "16.2.12",
16
13
  "commander": "12.1.0",
17
14
  "globby": "11.0.0",
18
15
  "publint": "0.1.8",
19
- "@digigov/cli-build": "2.4.3-govgr-4752.27-07-26-12-24"
16
+ "@digigov/cli-build": "2.5.0"
20
17
  },
21
18
  "peerDependencies": {
22
- "rimraf": "3.0.2",
23
19
  "typescript": "5.6.2",
24
20
  "@types/node": "20.17.24",
25
21
  "@types/react": "19.1.3",
26
22
  "autoprefixer": "10.4.16",
27
23
  "postcss": "8.4.4",
28
24
  "tailwindcss": "3.4.13",
29
- "@digigov/cli": "2.4.3-govgr-4752.27-07-26-12-24",
30
- "@digigov/css": "2.4.3-govgr-4752.27-07-26-12-24"
25
+ "@digigov/cli": "2.5.0",
26
+ "@digigov/css": "2.5.0"
31
27
  },
32
28
  "exports": {
33
29
  ".": "./index.js",
@@ -45,8 +41,8 @@
45
41
  "typescript": "5.6.2",
46
42
  "eslint": "9.16.0",
47
43
  "prettier": "3.4.2",
48
- "@digigov/cli": "2.4.3-govgr-4752.27-07-26-12-24",
49
- "@digigov/cli-lint": "2.4.3-govgr-4752.27-07-26-12-24"
44
+ "@digigov/cli": "2.5.0",
45
+ "@digigov/cli-lint": "2.5.0"
50
46
  },
51
47
  "scripts": {
52
48
  "publint": "publint",
@@ -1,14 +1,22 @@
1
- import { DigigovCommand } from '@digigov/cli/lib';
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ import { DigigovCommand, resolveProject } from '@digigov/cli/lib';
2
5
  import { EXEC_CONFIG } from '../common.js';
3
6
 
4
7
  const buildCommand = new DigigovCommand('build');
5
8
 
9
+ const project = resolveProject();
10
+
6
11
  buildCommand
7
12
  .argument('[...args]', 'Arguments to pass to the `next build` command')
8
13
  .helpOption(false)
9
14
  .allowUnknownOption()
10
15
  .action(async (args = [], _, ctx) => {
11
- await ctx.exec('rimraf', ['out']);
16
+ await fs.rm(path.join(project.root, 'out'), {
17
+ recursive: true,
18
+ force: true,
19
+ });
12
20
  await ctx.exec('next', ['build', ...args], EXEC_CONFIG);
13
21
  });
14
22
 
@@ -1,11 +1,7 @@
1
1
  import { DigigovCommand, resolveProject, logger } from '@digigov/cli/lib';
2
2
  import { EXEC_CONFIG } from '../common.js';
3
3
 
4
- import path from 'path';
5
- import { fileURLToPath } from 'url';
6
-
7
4
  const { digigov } = resolveProject();
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
5
 
10
6
  const devCommand = new DigigovCommand('dev');
11
7
 
@@ -16,11 +12,11 @@ devCommand
16
12
  .action(async (args = [], _, ctx) => {
17
13
  logger.info('Starting development server...');
18
14
  if (digigov && digigov.proxy) {
19
- await ctx.exec(
20
- 'node',
21
- [path.resolve(__dirname, '../utils/proxy-server.js')],
22
- EXEC_CONFIG
23
- );
15
+ // The proxy server wraps its own Next dev instance and replaces
16
+ // `next dev` entirely; running both would bind two servers. It starts
17
+ // listening on import.
18
+ await import('../utils/proxy-server.js');
19
+ return;
24
20
  }
25
21
  await ctx.exec('next', ['dev', ...(args ?? [])], EXEC_CONFIG);
26
22
  });
@@ -1,16 +1,26 @@
1
- import { DigigovCommand } from '@digigov/cli/lib';
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ import { DigigovCommand, resolveProject } from '@digigov/cli/lib';
2
5
  import { EXEC_CONFIG } from '../common.js';
3
6
 
4
7
  const exportCommand = new DigigovCommand('export');
5
8
 
9
+ const project = resolveProject();
10
+
11
+ // `next export` was removed in Next 14; `next build` itself emits `out/`
12
+ // because the shared config sets `output: 'export'` outside the dev phase.
13
+ // Kept as an alias of `build` so existing `digigov app export` setups work.
6
14
  exportCommand
7
- .argument('[...args]', 'Arguments to pass to the `next export` command')
15
+ .argument('[...args]', 'Arguments to pass to the `next build` command')
8
16
  .helpOption(false)
9
17
  .allowUnknownOption()
10
18
  .action(async (args = [], _, ctx) => {
11
- await ctx.exec('rimraf', ['out']);
12
- await ctx.exec('next', ['build']);
13
- await ctx.exec('next', ['export', ...args], EXEC_CONFIG);
19
+ await fs.rm(path.join(project.root, 'out'), {
20
+ recursive: true,
21
+ force: true,
22
+ });
23
+ await ctx.exec('next', ['build', ...args], EXEC_CONFIG);
14
24
  });
15
25
 
16
26
  export default exportCommand;
@@ -1,6 +1,9 @@
1
1
  import fs from 'fs';
2
+ import { createRequire } from 'module';
2
3
 
3
- import { DigigovCommand, resolveProject } from '@digigov/cli/lib';
4
+ import express from 'express';
5
+ import { PHASE_PRODUCTION_SERVER } from 'next/constants.js';
6
+ import { DigigovCommand, resolveProject, logger } from '@digigov/cli/lib';
4
7
  import { EXEC_CONFIG } from '../common.js';
5
8
  import path from 'path';
6
9
 
@@ -8,15 +11,60 @@ const startCommand = new DigigovCommand('start');
8
11
 
9
12
  const project = resolveProject();
10
13
 
14
+ // `next start` does not support `output: 'export'`, so serve the exported
15
+ // site the same way production does (static files behind a web server).
11
16
  startCommand
12
- .argument('[...args]', 'Arguments to pass to the `next start` command')
13
- .helpOption(false)
14
- .allowUnknownOption()
15
- .action(async (args = [], _, ctx) => {
16
- if (!fs.existsSync(path.join(project.root, '.next'))) {
17
- await ctx.exec('next', ['build']);
17
+ .option(
18
+ '-p, --port <port>',
19
+ 'Port to listen on',
20
+ process.env['PORT'] ?? '3000'
21
+ )
22
+ .option('-H, --hostname <hostname>', 'Hostname to bind to', '0.0.0.0')
23
+ .action(async (options, ctx) => {
24
+ const port = parseInt(options.port, 10);
25
+ if (Number.isNaN(port)) {
26
+ logger.error(`Invalid port: ${options.port}`);
27
+ process.exit(1);
18
28
  }
19
- await ctx.exec('next', ['start', ...args], EXEC_CONFIG);
29
+ const outDir = path.join(project.root, 'out');
30
+ if (!fs.existsSync(outDir)) {
31
+ await ctx.exec('next', ['build'], EXEC_CONFIG);
32
+ }
33
+ // The exported HTML references pages and assets under `basePath`, so the
34
+ // static site must be mounted there — as `next start` used to do.
35
+ const configPath = path.join(project.root, 'next.config.js');
36
+ let conf = {};
37
+ if (fs.existsSync(configPath)) {
38
+ const confModule = createRequire(configPath)(configPath);
39
+ conf =
40
+ typeof confModule === 'function'
41
+ ? await confModule(PHASE_PRODUCTION_SERVER, { defaultConfig: {} })
42
+ : confModule;
43
+ }
44
+ const basePath = conf.basePath ?? '';
45
+ const server = express();
46
+ server.use(
47
+ basePath || '/',
48
+ express.static(outDir, { extensions: ['html'] })
49
+ );
50
+ server.use((_req, res) => {
51
+ const notFoundPage = path.join(outDir, '404.html');
52
+ if (fs.existsSync(notFoundPage)) {
53
+ res.status(404).sendFile(notFoundPage);
54
+ } else {
55
+ res.status(404).send('Not Found');
56
+ }
57
+ });
58
+ server
59
+ .listen(port, options.hostname, () => {
60
+ logger.info(
61
+ `Serving ${outDir} on http://${options.hostname}:${port}${basePath}`
62
+ );
63
+ })
64
+ .on('error', (err) => {
65
+ logger.error(`Unable to start server: ${err.message}`);
66
+ process.exit(1);
67
+ });
20
68
  });
21
69
 
22
70
  export default startCommand;
@@ -2,8 +2,15 @@ import express from 'express';
2
2
  import next from 'next';
3
3
  import path from 'path';
4
4
  import { createRequire } from 'module';
5
+ import { createProxyMiddleware } from 'http-proxy-middleware';
6
+ // `next` publishes no `exports` map, so the subpath needs the explicit
7
+ // extension to resolve under ESM.
8
+ import {
9
+ PHASE_DEVELOPMENT_SERVER,
10
+ PHASE_PRODUCTION_SERVER,
11
+ } from 'next/constants.js';
5
12
 
6
- import { resolveProject } from '@digigov/cli/lib';
13
+ import { resolveProject, logger } from '@digigov/cli/lib';
7
14
 
8
15
  const port = parseInt(process.env['PORT'] ?? '3000', 10);
9
16
  const isDev = process.env.NODE_ENV !== 'production';
@@ -12,37 +19,73 @@ const project = resolveProject();
12
19
  const devProxy = project.digigov.proxy;
13
20
 
14
21
  const require = createRequire(import.meta.url);
15
- const conf = require(path.join(project.root, 'next.config.js'));
22
+ const confModule = require(path.join(project.root, 'next.config.js'));
16
23
 
17
- const app = next.default({
24
+ // nextrc exports a phase → config function; `next({ conf })` spreads the
25
+ // value without invoking it, so the phase must be resolved here.
26
+ const conf =
27
+ typeof confModule === 'function'
28
+ ? await confModule(
29
+ isDev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER,
30
+ { defaultConfig: {} }
31
+ )
32
+ : confModule;
33
+
34
+ // Next 16 removed the CJS `.default` interop export: `module.exports` itself is
35
+ // the server factory (on 13 too), but the d.ts still types the module as a
36
+ // non-callable namespace — hence the cast.
37
+ const createNextServer = /** @type {import('next')['default']} */ (
38
+ /** @type {unknown} */ (next)
39
+ );
40
+
41
+ const app = createNextServer({
18
42
  dir: project.root, // base directory where everything is, could move to src later
19
43
  dev: isDev,
20
44
  conf,
21
45
  });
22
46
  const handle = app.getRequestHandler();
23
47
 
24
- app
25
- .prepare()
26
- .then(() => {
27
- const server = express();
28
-
29
- // Set up the proxy.
30
- if (isDev && devProxy) {
31
- const { createProxyMiddleware } = require('http-proxy-middleware');
32
- Object.keys(devProxy).forEach(function (context) {
33
- server.use(createProxyMiddleware(context, devProxy[context]));
34
- });
48
+ try {
49
+ await app.prepare();
50
+ const server = express();
51
+
52
+ // Set up the proxy.
53
+ const wsProxies = [];
54
+ if (isDev && devProxy) {
55
+ for (const [pathFilter, options] of Object.entries(devProxy)) {
56
+ const proxy = createProxyMiddleware({ pathFilter, ...options });
57
+ server.use(proxy);
58
+ if (options.ws) {
59
+ wsProxies.push(proxy);
60
+ }
35
61
  }
62
+ }
36
63
 
37
- // Default catch-all handler to allow Next.js to handle all other routes
38
- server.all('*', (req, res) => handle(req, res));
64
+ // Default catch-all handler to allow Next.js to handle all other routes
65
+ server.use((req, res) => handle(req, res));
39
66
 
40
- server.listen(port, () => {
41
- console.log(`> Ready on port ${port}`);
42
- });
43
- return;
44
- })
45
- .catch((err) => {
46
- console.log('An error occurred, unable to start the server');
47
- console.log(err);
67
+ const httpServer = server.listen(port, () => {
68
+ logger.info(`Ready on port ${port}`);
48
69
  });
70
+
71
+ // Next and http-proxy-middleware each register their own `upgrade` listener,
72
+ // but only lazily — on the first HTTP request they see. A websocket that
73
+ // connects right after a restart, before any HTTP request (e.g. an open tab
74
+ // reconnecting through a `ws: true` proxy), finds no listener and hangs.
75
+ // Arm the ws proxies up front: `upgrade` checks its own pathFilter and
76
+ // no-ops once the proxy's internal listener exists, so nothing is handled
77
+ // twice. Next needs no arming — its HMR socket always follows a page load.
78
+ if (wsProxies.length > 0) {
79
+ httpServer.on('upgrade', (req, socket, head) => {
80
+ for (const proxy of wsProxies) {
81
+ proxy.upgrade(req, socket, head);
82
+ }
83
+ });
84
+ }
85
+ } catch (err) {
86
+ logger.error(
87
+ 'Unable to start the server:',
88
+ err instanceof Error ? err.stack : err
89
+ );
90
+ process.exit(1);
91
+ }