@nutin/cli 2.0.0 → 2.1.1

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +5 -8
  3. package/lib/src/create-app/success-message.mjs +2 -3
  4. package/lib/src/update-app/project-updater.mjs +6 -0
  5. package/package.json +4 -4
  6. package/templates/base/.gitignore.hbs +1 -0
  7. package/templates/base/AGENTS.md.hbs +3 -3
  8. package/templates/base/GETTING_STARTED.md.hbs +40 -65
  9. package/templates/base/nutin.config.js.hbs +1 -8
  10. package/templates/base/src/app/components/.gitkeep +0 -0
  11. package/templates/base/src/app/main.ts.hbs +0 -1
  12. package/templates/base/src/app/routes.ts.hbs +2 -1
  13. package/templates/base/src/app/services/.gitkeep +0 -0
  14. package/templates/base/src/app/views/home/home.view.ts.hbs +1 -1
  15. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +1 -1
  16. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +46 -0
  17. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +5 -2
  18. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +45 -0
  19. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +15 -1
  20. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +14 -3
  21. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +10 -3
  22. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +4 -1
  23. package/templates/base/src/core/base-classes/view/view.test.js.hbs +11 -14
  24. package/templates/base/src/core/base-classes/view/view.ts.hbs +8 -9
  25. package/templates/base/src/core/config.ts.hbs +3 -0
  26. package/templates/base/src/core/services/globals/globals.test.js.hbs +38 -0
  27. package/templates/base/src/core/services/globals/globals.ts.hbs +19 -10
  28. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +30 -2
  29. package/templates/base/src/core/services/http-client/http-client.ts.hbs +4 -1
  30. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +142 -0
  31. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +42 -4
  32. package/templates/base/src/core/services/router/router.test.js.hbs +93 -0
  33. package/templates/base/src/core/services/router/router.ts.hbs +26 -15
  34. package/templates/base/src/index.html.hbs +1 -0
  35. package/templates/base/src/styles/_styles.scss.hbs +1 -113
  36. package/templates/base/src/styles/main.scss.hbs +1 -1
  37. package/templates/base/tools/builder/builder.js.hbs +8 -2
  38. package/templates/base/tools/builder/core/html-index/add-tags.js.hbs +1 -4
  39. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +3 -3
  40. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +11 -2
  41. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +19 -3
  42. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +24 -1
  43. package/templates/base/tools/builder/core/seo/route-output.js.hbs +2 -2
  44. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +5 -1
  45. package/templates/base/tools/builder/core/styles/sass.js.hbs +5 -4
  46. package/templates/base/tools/dev/dev-serve.js.hbs +43 -4
  47. package/templates/base/tools/dev/watcher.js.hbs +42 -26
  48. package/templates/base/tools/generator/generator.js.hbs +32 -47
  49. package/templates/base/tools/generator/handle-file.js.hbs +2 -24
  50. package/templates/base/tools/generator/handle-file.test.js.hbs +1 -20
  51. package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +5 -1
  52. package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +11 -1
  53. package/templates/base/tools/generator/templates/view/view.template.js.hbs +1 -1
  54. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
  55. package/templates/base/tools/utils/build-lock.js.hbs +69 -0
  56. package/templates/base/tools/utils/index.js.hbs +1 -0
  57. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +2 -2
  58. package/templates/features/docker/tools/docker/nginx.conf.template +15 -0
  59. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +12 -0
  60. package/templates/base/src/app/components/index.ts.hbs +0 -1
  61. package/templates/base/src/app/services/index.ts.hbs +0 -1
  62. package/templates/base/src/app/views/index.ts.hbs +0 -3
@@ -2,23 +2,11 @@
2
2
 
3
3
  import path from "path";
4
4
  import { allFormats, getLastWord, print, promptBoolean, errorExit } from "../utils/index.js";
5
- import { generateFile, appendToIndex, generateLocalesJson } from "./handle-file.js";
5
+ import fs from "fs";
6
+ import { generateFile, generateLocalesJson } from "./handle-file.js";
6
7
  import { serviceTemplate, componentTemplate, viewTemplate, htmlTemplate, scssTemplate, testTemplate } from "./templates/index.js";
7
8
  import nutinConfig from "../../nutin.config.js";
8
9
 
9
- // Constants and Setup
10
- const [, , rawType, rawFullPath] = process.argv;
11
-
12
- if (!rawType || !rawFullPath) {
13
- showUsageAndExit("Missing arguments.");
14
- process.exit(1);
15
- }
16
-
17
- const type = allFormats(rawType);
18
- const fullPath = allFormats(rawFullPath);
19
- const name = allFormats(getLastWord(fullPath.kebab));
20
- const targetPath = path.join('src', 'app', `${type.kebab}s/${fullPath.kebab}`);
21
-
22
10
  // Creator Mapping
23
11
  const creators = {
24
12
  service: async (name, targetPath) => {
@@ -26,8 +14,7 @@ const creators = {
26
14
  print.section(`Generating service: ${name.capitalized}`);
27
15
  try {
28
16
  generateFile({ name, targetPath, templateFn: serviceTemplate, suffix: suffix });
29
- appendToIndex({ name, targetPath, suffix: suffix });
30
- if (nutinConfig.generator.generateTest) await generateTest({ name, targetPath, suffix: suffix });
17
+ await generateTest({ name, targetPath, suffix: suffix });
31
18
  } catch (err) {
32
19
  handleError("Failed to generate service", err);
33
20
  }
@@ -39,10 +26,9 @@ const creators = {
39
26
  try {
40
27
  generateFile({ name, targetPath, templateFn: componentTemplate, suffix: suffix });
41
28
  generateFile({ name, targetPath, templateFn: htmlTemplate, suffix: suffix, extension: 'html' });
42
- if (nutinConfig.generator.generateStylesheet) generateFile({ name, targetPath, templateFn: scssTemplate, suffix: suffix, extension: 'scss' });
43
- if (nutinConfig.generator.generateLocales) await generateLocales({ targetPath, name });
44
- appendToIndex({ name, targetPath, suffix: suffix });
45
- if (nutinConfig.generator.generateTest) await generateTest({ name, targetPath, suffix });
29
+ generateFile({ name, targetPath, templateFn: scssTemplate, suffix: suffix, extension: 'scss' });
30
+ await generateLocales({ targetPath, name, isView: false });
31
+ await generateTest({ name, targetPath, suffix });
46
32
  } catch (err) {
47
33
  handleError("Failed to generate component", err);
48
34
  }
@@ -54,20 +40,39 @@ const creators = {
54
40
  try {
55
41
  generateFile({ name, targetPath, templateFn: viewTemplate, suffix: suffix });
56
42
  generateFile({ name, targetPath, templateFn: htmlTemplate, suffix: suffix, extension: 'html' });
57
- if (nutinConfig.generator.generateStylesheet) generateFile({ name, targetPath, templateFn: scssTemplate, suffix: suffix, extension: 'scss' });
58
- if (nutinConfig.generator.generateLocales) await generateLocales({ targetPath, name });
59
- appendToIndex({ name, targetPath, suffix: suffix });
60
- if (nutinConfig.generator.generateTest) await generateTest({ name, targetPath, suffix });
43
+ generateFile({ name, targetPath, templateFn: scssTemplate, suffix: suffix, extension: 'scss' });
44
+ await generateLocales({ targetPath, name, isView: true });
45
+ await generateTest({ name, targetPath, suffix });
61
46
  } catch (err) {
62
47
  handleError("Failed to generate view", err);
63
48
  }
64
49
  },
65
50
  };
66
51
 
52
+ // Constants and Setup
53
+ const [, , rawType, rawFullPath] = process.argv;
54
+
55
+ if (!rawType || !rawFullPath) {
56
+ showUsageAndExit("Missing arguments.");
57
+ process.exit(1);
58
+ }
59
+
60
+ const type = allFormats(rawType);
61
+ const fullPath = allFormats(rawFullPath);
62
+ const name = allFormats(getLastWord(fullPath.kebab));
63
+ const targetPath = path.join('src', 'app', fullPath.kebab);
64
+
67
65
  // Main Execution
68
66
  const create = creators[type.kebab];
69
67
 
70
68
  if (create) {
69
+ // Folder names carry no type suffix, so a view and a component with the same path would
70
+ // share one folder (and clobber each other's locales/) — refuse before writing anything.
71
+ if (fs.existsSync(targetPath) && fs.readdirSync(targetPath).length > 0) {
72
+ print.boldError(`\nA folder with this name already exists: ${targetPath}`);
73
+ print.warn("Pick another path, e.g. user/user-view and user/user-card.");
74
+ process.exit(1);
75
+ }
71
76
  await create(name, targetPath);
72
77
  print.boldSuccess(`\n${type.capitalized} ${name.capitalized} generated in ${targetPath}.\n`)
73
78
  } else {
@@ -77,31 +82,11 @@ if (create) {
77
82
 
78
83
  // Helper Functions
79
84
  async function generateTest({ targetPath, name, suffix }) {
80
- let isGenerate = true;
81
- if (!nutinConfig.testinNutin.includeApp) {
82
- print.warn('⚠️ Enable includeApp in nutin.config.js testinNutin object to use test files.');
83
- if (!process.stdin.isTTY) {
84
- print.warn('Non-interactive shell detected — skipping test file generation.');
85
- isGenerate = false;
86
- } else {
87
- isGenerate = await promptBoolean('Do you want to generate the test file anyway ?');
88
- }
89
- }
90
- if (isGenerate) generateFile({ targetPath, name, templateFn: testTemplate, extension: 'test.js', suffix: suffix });
85
+ if (nutinConfig.testinNutin.includeApp) generateFile({ targetPath, name, templateFn: testTemplate, extension: 'test.js', suffix: suffix });
91
86
  }
92
87
 
93
- async function generateLocales({ targetPath, name }) {
94
- let isGenerate = true;
95
- if (!nutinConfig.i18n) {
96
- print.warn('⚠️ Enable i18n in nutin.config.js to use json-based content per language.');
97
- if (!process.stdin.isTTY) {
98
- print.warn('Non-interactive shell detected — skipping locales file generation.');
99
- isGenerate = false;
100
- } else {
101
- isGenerate = await promptBoolean('Do you want to generate the locales file(s) anyway ?');
102
- }
103
- }
104
- if (isGenerate) generateLocalesJson({ targetPath, name });
88
+ async function generateLocales({ targetPath, name, isView }) {
89
+ if (nutinConfig.i18n) generateLocalesJson({ targetPath, name, isView });
105
90
  }
106
91
 
107
92
  function showUsageAndExit(message) {
@@ -1,5 +1,4 @@
1
1
  import fs from 'fs';
2
- import path from 'path';
3
2
  import { print, LANGUAGES } from '../utils/index.js';
4
3
  import { localeTemplate } from './templates/index.js';
5
4
 
@@ -22,32 +21,11 @@ export function generateFile({
22
21
  fs.writeFileSync(filePath, template);
23
22
  }
24
23
 
25
- export function appendToIndex({ name, targetPath, suffix }) {
26
- const absTargetPath = path.resolve(process.cwd(), targetPath);
27
-
28
- const parts = targetPath.split(path.sep);
29
- const basePath = parts.slice(0, 3).join(path.sep);
30
-
31
- const indexFilePath = path.join(basePath, 'index.ts');
32
-
33
- const absFilePath = path.join(absTargetPath, `${name.kebab}.${suffix}.js`);
34
- const relPath = path.relative(basePath, absFilePath).replace(/\\/g, '/');
35
-
36
- const lineToAppend = `export * from './${relPath}';\n`;
37
-
38
- try {
39
- fs.appendFileSync(indexFilePath, lineToAppend, 'utf8');
40
- print.gray(`${suffix}s/index.ts updated.`);
41
- } catch (err) {
42
- throw new Error(`Failed to update ${indexFilePath}: ${err.message}`, { cause: err });
43
- }
44
- }
45
-
46
- export function generateLocalesJson({ targetPath, name }) {
24
+ export function generateLocalesJson({ targetPath, name, isView }) {
47
25
  const localesDir = `${targetPath}/locales`;
48
26
  fs.mkdirSync(localesDir, { recursive: true });
49
27
 
50
- const template = localeTemplate(name);
28
+ const template = localeTemplate(name, isView);
51
29
 
52
30
  const failedLangs = [];
53
31
  for (const lang of LANGUAGES) {
@@ -1,22 +1,19 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { generateFile, appendToIndex } from './handle-file.js';
4
+ import { generateFile } from './handle-file.js';
5
5
 
6
6
  describe('handle-file', () => {
7
7
  let tmpDir;
8
- let originalCwd;
9
8
  let exitSpy;
10
9
 
11
10
  beforeEach(() => {
12
11
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nutin-handle-file-'));
13
- originalCwd = process.cwd();
14
12
  exitSpy = spyOn(process, 'exit').andCallFake(() => {});
15
13
  });
16
14
 
17
15
  afterEach(() => {
18
16
  exitSpy.restore();
19
- process.chdir(originalCwd);
20
17
  fs.rmSync(tmpDir, { recursive: true, force: true });
21
18
  });
22
19
 
@@ -53,20 +50,4 @@ describe('handle-file', () => {
53
50
  expect(exitSpy.callCount).toBe(1);
54
51
  expect(exitSpy.lastCall).toEqual([1]);
55
52
  });
56
-
57
- it('appendToIndex appends an export line to the barrel three segments up from targetPath', () => {
58
- process.chdir(tmpDir);
59
- const basePath = path.join('src', 'app', 'components');
60
- fs.mkdirSync(basePath, { recursive: true });
61
- fs.writeFileSync(path.join(basePath, 'index.ts'), "export * from './existing.js';\n");
62
-
63
- const targetPath = path.join('src', 'app', 'components', 'widget');
64
- const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
65
-
66
- // appendToIndex prints a "index.ts updated" log via console.info
67
- silenceConsole('info', () => appendToIndex({ name, targetPath, suffix: 'component' }));
68
-
69
- const indexContent = fs.readFileSync(path.join(basePath, 'index.ts'), 'utf8');
70
- expect(indexContent).toBe("export * from './existing.js';\nexport * from './widget/widget.component.js';\n");
71
- });
72
53
  });
@@ -1,4 +1,8 @@
1
- export const localeTemplate = (name) => `{
1
+ export const localeTemplate = (name, isView) => isView ? `{
2
+ "title": "${name.capitalized}",
3
+ "default": "${name.pascal} works !"
4
+ }
5
+ ` : `{
2
6
  "default": "${name.pascal} works !"
3
7
  }
4
8
  `;
@@ -2,6 +2,16 @@ import { localeTemplate } from './locale.template.js';
2
2
 
3
3
  describe('localeTemplate', () => {
4
4
  it('renders a locale JSON stub with a "default" key', () => {
5
- expect(localeTemplate({ pascal: 'Widget' })).toBe('{\n "default": "Widget works !"\n}\n');
5
+ expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' })).toBe('{\n "default": "Widget works !"\n}\n');
6
+ });
7
+
8
+ it('does not include a "title" key when isView is false', () => {
9
+ expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' }, false)).toBe('{\n "default": "Widget works !"\n}\n');
10
+ });
11
+
12
+ it('includes a top-level "title" key seeded from the capitalized name when isView is true', () => {
13
+ expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' }, true)).toBe(
14
+ '{\n "title": "Widget",\n "default": "Widget works !"\n}\n'
15
+ );
6
16
  });
7
17
  });
@@ -10,7 +10,7 @@ const template = \`${htmlTemplate}\`;
10
10
 
11
11
  export class ${name.pascal}View extends View {
12
12
  constructor() {
13
- super({template});
13
+ super({ template, viewName: '${name.kebab}' });
14
14
  }
15
15
 
16
16
  }
@@ -2,7 +2,7 @@ import { viewTemplate } from './view.template.js';
2
2
 
3
3
  describe('viewTemplate', () => {
4
4
  it('renders a view class importing View via the relative core path', () => {
5
- const name = { pascal: 'Widget' };
5
+ const name = { kebab: 'widget', pascal: 'Widget' };
6
6
  const targetPath = 'src/app/components/widget';
7
7
 
8
8
  // The generator always leaves the html body as a placeholder — the builder
@@ -14,7 +14,7 @@ const template = \`__TEMPLATE_PLACEHOLDER__\`;
14
14
 
15
15
  export class WidgetView extends View {
16
16
  constructor() {
17
- super({template});
17
+ super({ template, viewName: 'widget' });
18
18
  }
19
19
 
20
20
  }
@@ -0,0 +1,69 @@
1
+ import * as fs from 'fs';
2
+ import path from 'path';
3
+ import { print } from './print.js';
4
+
5
+ // Guards the whole build pipeline against concurrent invocations — e.g. a manual
6
+ // `{{packageManager}} run build` running alongside the dev watcher's auto-rebuild, or (as actually
7
+ // happened) multiple orphaned watcher.js processes each kicking off their own build.
8
+ // copy-static.js starts every run by wiping and recreating dist-build; two builds
9
+ // doing that at once corrupt each other's output (missing/non-empty dist-build,
10
+ // merge-templates.js "has both an inline and external template" conflicts, etc).
11
+ const LOCK_DIR = path.resolve('.build-lock');
12
+ const STALE_MS = 5 * 60 * 1000; // a full build takes seconds — anything older is a dead leftover
13
+ const POLL_MS = 200;
14
+ const MAX_WAIT_MS = 2 * 60 * 1000;
15
+
16
+ function tryAcquire() {
17
+ try {
18
+ fs.mkdirSync(LOCK_DIR);
19
+ fs.writeFileSync(path.join(LOCK_DIR, 'pid'), String(process.pid));
20
+ return true;
21
+ } catch (err) {
22
+ if (err.code === 'EEXIST') return false;
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ function isStale() {
28
+ try {
29
+ const { mtimeMs } = fs.statSync(LOCK_DIR);
30
+ return Date.now() - mtimeMs > STALE_MS;
31
+ } catch {
32
+ return true; // lock vanished mid-check (released concurrently) — treat as free
33
+ }
34
+ }
35
+
36
+ function release() {
37
+ try {
38
+ fs.rmSync(LOCK_DIR, { recursive: true, force: true });
39
+ } catch {
40
+ // already released
41
+ }
42
+ }
43
+
44
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
45
+
46
+ export async function acquireBuildLock() {
47
+ const start = Date.now();
48
+ let warned = false;
49
+
50
+ while (!tryAcquire()) {
51
+ if (isStale()) {
52
+ release();
53
+ continue;
54
+ }
55
+
56
+ if (!warned) {
57
+ print.boldInfo('\nAnother build is already in progress — waiting for it to finish...');
58
+ warned = true;
59
+ }
60
+
61
+ if (Date.now() - start > MAX_WAIT_MS) {
62
+ throw new Error(`Timed out waiting for the build lock (${LOCK_DIR}) held by another process.`);
63
+ }
64
+
65
+ await sleep(POLL_MS);
66
+ }
67
+
68
+ process.on('exit', release);
69
+ }
@@ -6,3 +6,4 @@ export * from './run-script.js';
6
6
  export * from './prompt-boolean.js';
7
7
  export * from './languages.js';
8
8
  export * from './error-exit.js';
9
+ export * from './build-lock.js';
@@ -1,5 +1,5 @@
1
- ARG NODE_VERSION=22.23.2
2
- ARG ALPINE_VERSION=3.20
1
+ ARG NODE_VERSION=24.21.0
2
+ ARG ALPINE_VERSION=3.24
3
3
  ARG PORT="__PORTS_PLACEHOLDER__"
4
4
 
5
5
  FROM node:${NODE_VERSION}-alpine AS builder
@@ -42,6 +42,11 @@ server {
42
42
  __PORTS_PLACEHOLDER__
43
43
  server_name _;
44
44
 
45
+ # Keep nginx's own automatic redirects (e.g. the missing-trailing-slash 301 on a
46
+ # prerendered route directory) relative, so they never leak this container's
47
+ # internal port/scheme when served behind a reverse proxy or a remapped port.
48
+ absolute_redirect off;
49
+
45
50
  # ---------------------------
46
51
  # ROOT + ROUTING
47
52
  # ---------------------------
@@ -51,6 +56,10 @@ server {
51
56
  # Hide nginx version
52
57
  server_tokens off;
53
58
 
59
+ # With i18n enabled, redirects bare "/" to the default language so the served URL
60
+ # always matches each page's canonical/og:url (filled in by validate-docker).
61
+ __ROOT_REDIRECT_PLACEHOLDER__
62
+
54
63
  # SPA routing: serve index.html for unknown paths
55
64
  location / {
56
65
  try_files $uri $uri/ /index.html;
@@ -112,6 +121,12 @@ server {
112
121
  # --------------------------
113
122
  error_page 404 /404.html;
114
123
 
124
+ # A route directory that exists on disk but has no index.html of its own (e.g. a
125
+ # parent route whose only prerendered pages are its children) makes try_files'
126
+ # $uri/ candidate match the directory and stop there, returning 403 instead of
127
+ # falling through to the SPA shell. Serve the shell for that case too.
128
+ error_page 403 = /index.html;
129
+
115
130
  location = /404.html {
116
131
  internal;
117
132
  try_files /404.html =404;
@@ -1,3 +1,4 @@
1
+ import fs from 'fs';
1
2
  import path from 'path';
2
3
  import { errorExit } from '../../utils/index.js';
3
4
  import nutinConfig from '../../../nutin.config.js';
@@ -6,6 +7,16 @@ import { renderFromTemplate } from './render-docker-template.js';
6
7
 
7
8
  const DOCKER_DIR = path.join(process.cwd(), 'tools', 'docker');
8
9
  const PLACEHOLDER = '__PORTS_PLACEHOLDER__';
10
+ const ROOT_REDIRECT_PLACEHOLDER = '__ROOT_REDIRECT_PLACEHOLDER__';
11
+
12
+ // Only i18n builds serve every page under a language prefix — without it, "/" is a real page.
13
+ function rootRedirect() {
14
+ if (!nutinConfig.i18n) return '';
15
+
16
+ const languagesConfigPath = path.join(process.cwd(), 'config', 'languages.json');
17
+ const { defaultLanguage } = JSON.parse(fs.readFileSync(languagesConfigPath, 'utf-8'));
18
+ return `location = / {\n return 301 /${defaultLanguage}/$is_args$args;\n }`;
19
+ }
9
20
 
10
21
  async function validateDocker() {
11
22
  let ports;
@@ -22,6 +33,7 @@ async function validateDocker() {
22
33
 
23
34
  await renderFromTemplate(DOCKER_DIR, 'nginx.conf.template', 'nginx.conf', [
24
35
  [PLACEHOLDER, ports.map((port) => `listen ${port};`).join('\n ')],
36
+ [ROOT_REDIRECT_PLACEHOLDER, rootRedirect()],
25
37
  ]).catch((err) => errorExit(err.message));
26
38
  }
27
39
 
@@ -1 +0,0 @@
1
- // global components export
@@ -1 +0,0 @@
1
- // global services export
@@ -1,3 +0,0 @@
1
- // global views export
2
- export * from './home/home.view.js';
3
- export * from './not-found/not-found.view.js';