orionplace-nuxt 0.1.1 → 0.1.3

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 (40) hide show
  1. package/README.md +64 -7
  2. package/bin/orionplace-nuxt.mjs +32 -13
  3. package/package.json +3 -2
  4. package/src/module.mjs +2 -0
  5. package/src/project-readme.mjs +78 -33
  6. package/src/setup.mjs +52 -10
  7. package/src/update.mjs +36 -0
  8. package/templates/app/app.vue +90 -0
  9. package/templates/app/composables/useOrionplaceAppLoad.ts +51 -0
  10. package/templates/app/interfaces/orionplace/apiBase/TMethod.ts +1 -0
  11. package/templates/app/interfaces/orionplace/appBase/IAppLoad.ts +15 -0
  12. package/templates/app/interfaces/orionplace/appBase/IAppLoadCommon.ts +54 -0
  13. package/templates/app/interfaces/orionplace/appBase/IApploadUserProfile.ts +13 -0
  14. package/templates/app/interfaces/orionplace/appBase/IForm.ts +21 -0
  15. package/templates/app/interfaces/orionplace/appBase/ISEO.ts +21 -0
  16. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonBanner.ts +11 -0
  17. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonContact.ts +8 -0
  18. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonCurrency.ts +15 -0
  19. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonEvents.ts +18 -0
  20. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLocale.ts +14 -0
  21. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLogos.ts +6 -0
  22. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonPath.ts +22 -0
  23. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSEO.ts +13 -0
  24. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSocial.ts +15 -0
  25. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonTariff.ts +7 -0
  26. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonUnit.ts +12 -0
  27. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVariables.ts +7 -0
  28. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVisualSettings.ts +11 -0
  29. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonWalletValue.ts +3 -0
  30. package/templates/app/interfaces/orionplace/category/ICategory.ts +40 -0
  31. package/templates/app/interfaces/orionplace/domains/IDomain.ts +13 -0
  32. package/templates/app/interfaces/orionplace/index.ts +29 -0
  33. package/templates/app/interfaces/orionplace/products/IProduct.ts +235 -0
  34. package/templates/app/interfaces/orionplace/shared/IGeoCoordinates.ts +4 -0
  35. package/templates/app/interfaces/orionplace/shared/ITransformValue.ts +3 -0
  36. package/templates/app/interfaces/orionplace/subscribeOptions/ISubscribeOption.ts +9 -0
  37. package/templates/app/interfaces/orionplace/subscribeTariff/ISubscribeTariff.ts +46 -0
  38. package/templates/app/interfaces/orionplace/subscribes/ISubscribe.ts +28 -0
  39. package/templates/app/interfaces/orionplace/transport.ts +30 -0
  40. package/templates/app/utils/orionplaceAppLoad.ts +33 -0
package/README.md CHANGED
@@ -62,12 +62,13 @@ to inspect planned changes without writing them.
62
62
 
63
63
  ```bash
64
64
  npx orionplace-nuxt init [directory]
65
+ npx orionplace-nuxt update
65
66
  npx orionplace-nuxt set backend your-project.orionplace.app
66
67
  npx orionplace-nuxt add frontend shop.example.com
67
68
  npx orionplace-nuxt add frontend www.shop.example.com
68
69
  npx orionplace-nuxt remove frontend www.shop.example.com
69
- npx orionplace-nuxt set frontend shop.example.com
70
- npx orionplace-nuxt --help
70
+ npx orionplace-nuxt set mimic shop.example.com
71
+ npx orionplace-nuxt help
71
72
  ```
72
73
 
73
74
  Either domain command can run first. Both settings are needed for a usable mapping.
@@ -77,6 +78,9 @@ After setup, these npm script aliases are also available:
77
78
  npm run orionplace:backend -- your-project.orionplace.app
78
79
  npm run orionplace:add-frontend -- shop.example.com
79
80
  npm run orionplace:remove-frontend -- www.shop.example.com
81
+ npm run orionplace:mimic -- shop.example.com
82
+ npm run orionplace:help
83
+ npm run orionplace:update
80
84
  ```
81
85
 
82
86
  `npm set` belongs to npm's own configuration system; use the commands above.
@@ -100,7 +104,7 @@ change your account, create DNS records, configure TLS, or deploy a VPS.
100
104
  The module executes the generated local runtime files. You can read and customize
101
105
  them. Later setup runs preserve edited runtime files and the project README,
102
106
  reporting which files were preserved. Unchanged generated files can be refreshed
103
- by running `init` after upgrading the package. A manually edited domain file is
107
+ with `npx orionplace-nuxt update`. A manually edited domain file is
104
108
  never silently overwritten. Keep the hash manifest in version control.
105
109
 
106
110
  ## Multiple frontend domains
@@ -109,9 +113,14 @@ Each frontend gets its own `sitemaps/<domain>.cfg`. `add frontend` preserves oth
109
113
  domains and can be repeated safely. `set backend` updates every CLI-managed mapping
110
114
  to the new personal API; it refuses to overwrite a manually edited mapping.
111
115
 
112
- The first added frontend becomes `MIMIC_FRONT_DOMAIN` in `.env`. Further additions
113
- preserve that local development choice. `set frontend <domain>` adds the domain
114
- if needed and selects it for MIMIC; it does not remove other domains.
116
+ A new project starts with `MIMIC_FRONT_DOMAIN=""` in `.env`. Setting a backend
117
+ alone leaves it empty. The first added frontend becomes MIMIC; further additions
118
+ preserve that choice. `set mimic <domain>` selects an already-added frontend and
119
+ updates MIMIC without changing any domain mappings. Unknown domains are rejected;
120
+ add them with `add frontend` first.
121
+
122
+ `help`, `--help`, and `-h` display commands without writing files. The legacy
123
+ `set frontend <domain>` shortcut remains supported: it adds and selects a domain.
115
124
 
116
125
  `remove frontend <domain>` removes that domain from the list and deletes only its
117
126
  unchanged generated `.cfg`. Removing the MIMIC domain selects the first remaining
@@ -167,4 +176,52 @@ npm pack --dry-run
167
176
  ```
168
177
 
169
178
  The published tarball includes only the CLI, module, runtime source, README,
170
- license, and package metadata.
179
+ license, starter templates, and package metadata.
180
+
181
+ ## AppLoad starter
182
+
183
+ The CLI generates an AppLoad debug `app/app.vue`, an SSR-aware
184
+ `useOrionplaceAppLoad()` composable, validation helpers and the full nested
185
+ TypeScript contract under `app/interfaces/orionplace/`. The first page loads
186
+ `POST /api/appload`, shows request state and section counts, and supports reload.
187
+ It preserves custom apps and edited generated files. Generated instructions in
188
+ `orionplace/README.md` explain how to integrate the composable in an existing app.
189
+
190
+ ## Update an existing project
191
+
192
+ After the initial setup, use one command to get the latest package and components:
193
+
194
+ ```bash
195
+ npx orionplace-nuxt update
196
+ ```
197
+
198
+ It runs the latest CLI, adds new template files, refreshes unchanged generated
199
+ files, updates the package dependency and runs `npm install` to synchronize the
200
+ lockfile and installed dependencies. Edited files are preserved and listed in the
201
+ output. Review their new versions under `node_modules/orionplace-nuxt/templates`
202
+ or `runtime` and merge your changes manually when needed. Removed upstream files
203
+ are retained locally; the updater does not delete customer source.
204
+
205
+ Domain mappings, domain settings and `.env` remain unchanged. The command does
206
+ not deploy your application: restart development, or rebuild and redeploy the
207
+ production frontend after updating.
208
+
209
+ ```bash
210
+ npx orionplace-nuxt update --dry-run
211
+ npm run orionplace:update
212
+ ```
213
+
214
+ Dry-run downloads the latest CLI into npm's cache to inspect its templates but
215
+ leaves project files and dependencies unchanged. `update --local` applies the
216
+ version of the CLI being executed instead of fetching the latest CLI; it still
217
+ installs project dependencies. Use this only when intentionally selecting a version.
218
+
219
+ For a project on 0.1.2 or older, bootstrap the new command once:
220
+
221
+ ```bash
222
+ npx --yes orionplace-nuxt@latest update
223
+ ```
224
+
225
+ Updates require the existing `orionplace/settings.json` and `.generated.json`.
226
+ Conflicting untracked files stop the source update before any writes. If dependency
227
+ installation fails after source generation, run `npm install` and retry `update`.
@@ -1,27 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import { setupProject } from '../src/setup.mjs';
3
+ import { updateProject } from '../src/update.mjs';
3
4
  import { readFile } from 'node:fs/promises';
4
5
  import { resolve } from 'node:path';
5
6
  import { createInterface } from 'node:readline/promises';
6
7
 
7
8
  const help = `Orionplace Nuxt setup
8
9
 
10
+ npx orionplace-nuxt help
9
11
  npx orionplace-nuxt init [directory]
12
+ npx orionplace-nuxt update
10
13
  npx orionplace-nuxt set backend <personal-backend.orionplace.app>
11
14
  npx orionplace-nuxt add frontend <shop.example.com>
12
15
  npx orionplace-nuxt remove frontend <shop.example.com>
16
+ npx orionplace-nuxt set mimic <existing-frontend.example.com>
13
17
  npx orionplace-nuxt set frontend <shop.example.com>
14
18
 
15
19
  Options: --cwd <directory> --dry-run --help --version
20
+ Update option: --local (apply this CLI version instead of fetching the latest release)
16
21
 
22
+ Update downloads the latest release, refreshes unedited source, and installs dependencies.
23
+ It preserves edited source, domain mappings and MIMIC. --dry-run leaves project files unchanged.
17
24
  Set backend or add frontend also initializes the integration; either can come first.
18
- Add frontend keeps other domains and the current MIMIC domain. Set frontend selects MIMIC.
25
+ MIMIC starts empty. The first added frontend sets it; later additions preserve it.
26
+ Set mimic selects an existing frontend without changing any domain mappings.
27
+ Set frontend is the legacy shortcut to add and select a frontend.
19
28
  Set backend updates every managed frontend mapping. Remove frontend deletes only that mapping.
20
29
  After creating a new project, run npm install and npm run dev in that directory.
21
30
  Documentation and editable server source are generated in orionplace/.`;
22
31
  try {
23
32
  const argv = process.argv.slice(2);
24
- if (!argv.length || argv.includes('--help') || argv.includes('-h')) {
33
+ if (!argv.length || argv[0] === 'help' || argv.includes('--help') || argv.includes('-h')) {
25
34
  console.log(help);
26
35
  } else if (argv.includes('--version')) {
27
36
  console.log(JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version);
@@ -29,8 +38,10 @@ try {
29
38
  const positional = [];
30
39
  let cwd = process.cwd();
31
40
  let dryRun = false;
41
+ let local = false;
32
42
  for (let index = 0; index < argv.length; index++) {
33
43
  if (argv[index] === '--dry-run') dryRun = true;
44
+ else if (argv[index] === '--local') local = true;
34
45
  else if (argv[index] === '--cwd') {
35
46
  if (!argv[index + 1]) throw new Error('--cwd needs a directory');
36
47
  cwd = resolve(argv[++index]);
@@ -38,29 +49,37 @@ try {
38
49
  else positional.push(argv[index]);
39
50
  }
40
51
  const [command, target] = positional;
52
+ if (local && command !== 'update') throw new Error('--local is only supported by update.');
41
53
  let result;
42
54
  if (command === 'init' && positional.length <= 2) {
43
55
  result = await setupProject({ cwd: target ? resolve(cwd, target) : cwd, dryRun });
44
- } else if (((command === 'set' && ['backend', 'frontend'].includes(target)) ||
56
+ } else if (command === 'update' && positional.length === 1) {
57
+ result = await updateProject({ cwd, dryRun, local });
58
+ } else if (((command === 'set' && ['backend', 'frontend', 'mimic'].includes(target)) ||
45
59
  (['add', 'remove'].includes(command) && target === 'frontend')) && positional.length <= 3) {
46
60
  let value = positional[2];
47
61
  if (!value && process.stdin.isTTY) {
48
62
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
49
- try { value = await prompt.question(`${target === 'backend' ? 'Personal backend API' : 'Public frontend domain'}: `); }
63
+ try { value = await prompt.question(`${target === 'backend' ? 'Personal backend API' : target === 'mimic' ? 'Existing frontend to use for MIMIC' : 'Public frontend domain'}: `); }
50
64
  finally { prompt.close(); }
51
65
  }
52
66
  if (!value) throw new Error(`Provide the ${target} domain. See --help.`);
53
67
  result = await setupProject({ cwd, action: command, field: target, value, dryRun });
54
68
  } else throw new Error('Unknown command. See --help.');
55
- console.log(`${dryRun ? 'Planned' : 'Updated'} ${result.root}`);
56
- for (const path of result.changed) console.log(` ${result.removed.includes(path) ? 'Remove: ' : ''}${path}`);
57
- for (const path of result.preserved) console.log(` Preserved your edits: ${path}`);
58
- console.log(result.settings.backend && result.settings.frontend
59
- ? `Ready: ${result.settings.frontends.join(', ')} -> ${result.settings.backend}\nLocal MIMIC: ${result.settings.frontend}`
60
- : `Next: ${result.settings.backend ? 'add frontend' : 'set backend'} to complete the domain mapping.`);
61
- if (result.changed.some(path => path.endsWith('.cfg') || path === '.env')) console.log('Restart a running development server to refresh allowed hosts and MIMIC. Reload your production environment when changing MIMIC.');
62
- if (result.isNewNuxt) console.log('Run npm install in the project, then npm run dev.');
63
- console.log('Project documentation: orionplace/README.md');
69
+ if (result) {
70
+ console.log(`${dryRun ? 'Planned' : 'Updated'} ${result.root}`);
71
+ for (const path of result.changed) console.log(` ${result.removed.includes(path) ? 'Remove: ' : ''}${path}`);
72
+ for (const path of result.preserved) console.log(` Preserved your edits: ${path}`);
73
+ console.log(result.settings.backend && result.settings.frontend
74
+ ? `Ready: ${result.settings.frontends.join(', ')} -> ${result.settings.backend}\nLocal MIMIC: ${result.settings.frontend}`
75
+ : `Next: ${result.settings.backend ? 'add frontend' : 'set backend'} to complete the domain mapping.`);
76
+ if (result.changed.some(path => path.endsWith('.cfg') || path === '.env')) console.log('Restart a running development server to refresh allowed hosts and MIMIC. Reload your production environment when changing MIMIC.');
77
+ if (result.isNewNuxt) console.log('Run npm install in the project, then npm run dev.');
78
+ console.log('Project documentation: orionplace/README.md');
79
+ if (command === 'update') console.log(dryRun
80
+ ? 'Dependency installation would run after applying these changes. Project files were not changed.'
81
+ : 'Update complete. Restart development, or rebuild and redeploy your production frontend.');
82
+ }
64
83
  }
65
84
  } catch (error) {
66
85
  console.error(`Orionplace: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orionplace-nuxt",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Nuxt integration and setup CLI for your Orionplace personal marketplace backend.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,11 +31,12 @@
31
31
  "bin",
32
32
  "src",
33
33
  "runtime",
34
+ "templates",
34
35
  "README.md",
35
36
  "LICENSE"
36
37
  ],
37
38
  "scripts": {
38
- "test": "node --test test/*.test.mjs"
39
+ "test": "node --experimental-strip-types --test test/*.test.mjs"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "nuxt": "^4.0.0"
package/src/module.mjs CHANGED
@@ -29,6 +29,8 @@ export default defineNuxtModule({
29
29
  // Do not inherit page caching for authenticated API requests.
30
30
  routes['/api/**'] = { ...routes['/api/**'], cache: false, swr: false,
31
31
  headers: { ...routes['/api/**']?.headers, 'cache-control': 'no-store' } };
32
+ // MIMIC defaults to empty. The CLI sets .env on the first "add frontend"
33
+ // or an explicit "set mimic"; never infer it from the domain-file directory.
32
34
  // Values stay private to the server. MIMIC_FRONT_DOMAIN is also read at runtime.
33
35
  nuxt.options.runtimeConfig.mimicFrontDomain ??= '';
34
36
  nuxt.options.runtimeConfig.mimicFrontDomain = process.env.MIMIC_FRONT_DOMAIN
@@ -10,7 +10,7 @@ You can change that backend domain in your account, then update this project.
10
10
  ## Current setup
11
11
 
12
12
  - Frontend domains: ${settings.frontends?.length ? settings.frontends.map(domain => '\`' + domain + '\`').join(', ') : 'not configured yet'}
13
- - Local MIMIC domain: \`${settings.frontend || 'not configured yet'}\`
13
+ - Local MIMIC domain: \`${settings.frontend || 'empty'}\`
14
14
  - Personal API: \`${settings.backend || 'not configured yet'}\`
15
15
 
16
16
  Set or change these values from the project root:
@@ -26,9 +26,19 @@ Set the backend or add a frontend first; either order works. Each frontend has i
26
26
  own \`sitemaps/<domain>.cfg\` mapping to your personal API. Adding a domain again
27
27
  is safe. Changing the backend updates every CLI-managed frontend mapping.
28
28
 
29
- The first added frontend becomes \`MIMIC_FRONT_DOMAIN\` in \`.env\`. Adding more
30
- domains preserves that choice. Use \`npx orionplace-nuxt set frontend <domain>\`
31
- to add and select a different local domain.
29
+ MIMIC is empty in a new project: \`MIMIC_FRONT_DOMAIN=""\`. Setting the backend
30
+ alone leaves it empty. The first added frontend sets MIMIC; adding more domains
31
+ preserves that choice.
32
+
33
+ Use \`npx orionplace-nuxt set mimic <domain>\` to select an already-added frontend
34
+ for localhost. It changes MIMIC without editing domain mappings. Add an unknown
35
+ domain with \`add frontend\` first. The legacy \`set frontend\` shortcut still
36
+ adds and selects a frontend.
37
+
38
+ \`\`\`bash
39
+ npx orionplace-nuxt help
40
+ npx orionplace-nuxt set mimic ${frontend}
41
+ \`\`\`
32
42
 
33
43
  Removing a frontend deletes only its unchanged generated \`.cfg\` file. If it was
34
44
  the selected MIMIC domain, the first remaining domain takes its place. Removing
@@ -88,53 +98,88 @@ In another terminal:
88
98
  \`\`\`bash
89
99
  curl -i http://localhost:3000/api/appload \\
90
100
  -H 'Content-Type: application/json' \\
91
- --data '{"ignore_cache":true,"detalize_mode":"full"}'
101
+ --data '{"ignore_cache":true,"detalize_mode":"light"}'
92
102
  \`\`\`
93
103
 
94
104
  Check both the HTTP status and \`form_errors\`. Successful initialization returns
95
105
  \`page_data\`; HTTP 200 by itself is insufficient. Documentation and public requests
96
106
  do not require a login prompt. Protected actions still require backend permission.
97
107
 
98
- Browser requests use the storefront's own API path:
99
-
100
- \`\`\`javascript
101
- const response = await fetch('/api/appload', {
102
- method: 'POST',
103
- credentials: 'same-origin',
104
- headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify({ ignore_cache: true, detalize_mode: 'full' }),
106
- });
107
- const result = await response.json();
108
- console.log(response.status, result);
109
- \`\`\`
108
+ ## AppLoad initialization
109
+
110
+ The generated \`app/app.vue\` calls \`useOrionplaceAppLoad()\` and displays loading,
111
+ success or failure, HTTP status, session state and counts of returned sections.
112
+ Use **Reload AppLoad** to issue a fresh request. A failed request never opens a
113
+ login modal. Once the connection works, replace this debug page with your storefront.
114
+
115
+ | Project file | Purpose |
116
+ | --- | --- |
117
+ | \`app/composables/useOrionplaceAppLoad.ts\` | SSR-aware initialization and manual reload. |
118
+ | \`app/utils/orionplaceAppLoad.ts\` | HTTP, form-error and bootstrap-envelope checks. |
119
+ | \`app/interfaces/orionplace/index.ts\` | All AppLoad interfaces and nested model exports. |
120
+ | \`app/interfaces/orionplace/transport.ts\` | Request, response, result and error contracts. |
121
+
122
+ The interfaces cover initialization settings, locales, currencies, units, catalog,
123
+ products, banners, events, tariffs and subscriptions, API discovery and forms,
124
+ and the optional signed-in profile. Plugin-defined JSON remains explicitly typed
125
+ as JSON; interfaces are not a full runtime schema validator.
110
126
 
111
- For SSR, use Nuxt's \`useRequestFetch()\` so the request's domain, cookie and
112
- authorization headers reach the same gateway. For example, in \`app/app.vue\`:
127
+ The server uses the current request's domain and session. Its result is reused
128
+ during browser hydration, and new response cookies are relayed to the browser.
129
+ The page response uses \`private, no-store\`; do not put personalized pages behind
130
+ shared HTML caches. Reloading replaces the current session's discovery data.
131
+
132
+ Requests use \`detalize_mode: "light"\` and \`ignore_cache: true\`. Tenant settings
133
+ can override these options, and individual initialization sections have separate
134
+ backend caches. Full endpoint descriptions are therefore optional.
135
+
136
+ If your project already has a custom \`app/app.vue\`, the CLI preserves it.
137
+ Integrate the generated composable explicitly:
113
138
 
114
139
  \`\`\`vue
115
- <script setup>
116
- const requestFetch = useRequestFetch();
117
- const { data, error } = await useAsyncData('marketplace-initialization', () =>
118
- requestFetch('/api/appload', {
119
- method: 'POST',
120
- body: { ignore_cache: true, detalize_mode: 'full' },
121
- }),
122
- );
140
+ <script setup lang="ts">
141
+ import { useOrionplaceAppLoad } from './composables/useOrionplaceAppLoad';
142
+ const { data, status, error, refresh } = await useOrionplaceAppLoad();
143
+ // data.value?.response.page_data contains the typed initialization and discovery.
123
144
  </script>
124
145
 
125
146
  <template>
126
147
  <main>
127
- <p v-if="error">The marketplace API is currently unavailable.</p>
128
- <pre v-else>{{ data }}</pre>
148
+ <p role="status">AppLoad: {{ status }}</p>
149
+ <p v-if="error" role="alert">Marketplace initialization failed.</p>
150
+ <button type="button" :disabled="status === 'pending'" @click="refresh()">Reload AppLoad</button>
151
+ <p v-if="data">HTTP {{ data.httpStatus }}</p>
129
152
  </main>
130
153
  </template>
131
154
  \`\`\`
132
155
 
133
- Keep private data out of shared caches. This SSR example forwards incoming cookies;
134
- it does not relay new upstream cookies to the browser. Make session-changing calls
135
- from the browser, or explicitly append each returned Set-Cookie header to the SSR
136
- response in your server integration.
156
+ Call the composable once in your app root and pass the data to your components.
157
+ The generated debug view displays section summaries; it does not render profile
158
+ fields, keys or backend exception stacks. The typed response is still available
159
+ to your application, so avoid logging or publishing it indiscriminately.
160
+
161
+ CLI reruns preserve edited generated source. Review updated package templates
162
+ manually when you have customized a file. Existing custom apps are not replaced.
163
+
164
+ ## Update the package and components
165
+
166
+ Run this from your project root whenever you want the latest release:
167
+
168
+ \`\`\`bash
169
+ npx orionplace-nuxt update
170
+ \`\`\`
137
171
 
172
+ The command fetches the latest CLI, adds new template files, refreshes unchanged
173
+ generated source, updates the package dependency, and runs \`npm install\`.
174
+ It preserves edited files and lists them for manual review. Find their incoming
175
+ versions under \`node_modules/orionplace-nuxt/templates\` or \`runtime\`.
176
+ Domain mappings, domain settings and \`.env\` are preserved. Upstream file removals
177
+ do not delete local source. Restart development or rebuild and redeploy afterward.
178
+
179
+ Use \`npx orionplace-nuxt update --dry-run\` to review changes without modifying
180
+ project files or installed dependencies; downloading the CLI may update npm's
181
+ cache. The alias \`npm run orionplace:update\` performs the same normal update.
182
+ If installation fails after source generation, run \`npm install\` and retry.
138
183
 
139
184
  ## Deploy on your VPS
140
185
 
package/src/setup.mjs CHANGED
@@ -57,17 +57,21 @@ function updateEnvironment(source, domain) {
57
57
  }
58
58
 
59
59
  export async function setupProject({ cwd = process.cwd(), action = 'set', field, value, dryRun = false } = {}) {
60
- if (!['set', 'add', 'remove'].includes(action) || (action !== 'set' && field !== 'frontend')) {
61
- throw new Error('Use set backend, add frontend, remove frontend, or set frontend.');
60
+ if (!['set', 'add', 'remove', 'update'].includes(action) ||
61
+ (['add', 'remove'].includes(action) && field !== 'frontend') || (action === 'update' && field)) {
62
+ throw new Error('Use set backend, add frontend, remove frontend, set mimic, or set frontend.');
62
63
  }
63
64
  const root = resolve(cwd);
64
- const setting = field === 'backend' ? backendUrl(value) : field === 'frontend' ? frontendDomain(value) : null;
65
- if (field && !setting) throw new Error('Choose backend or frontend.');
65
+ const setting = field === 'backend' ? backendUrl(value) : ['frontend', 'mimic'].includes(field) ? frontendDomain(value) : null;
66
+ if (field && !setting) throw new Error('Choose backend, frontend, or mimic.');
66
67
  const names = ['package.json', 'nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs', 'orionplace/settings.json', 'orionplace/.generated.json', '.env', '.gitignore'];
67
68
  const current = {};
68
69
  for (const name of names) current[name] = await read(await safePath(root, name));
69
70
  const pkg = current['package.json'] ? JSON.parse(current['package.json']) : { name: 'orionplace-storefront', private: true, type: 'module' };
70
71
  const dependencies = { ...pkg.devDependencies, ...pkg.dependencies };
72
+ if (action === 'update' && (!dependencies.nuxt || !current['orionplace/settings.json'] || !current['orionplace/.generated.json'])) {
73
+ throw new Error('Update requires an initialized Orionplace Nuxt project. Run init first.');
74
+ }
71
75
  if (!dependencies.nuxt && ['next', '@sveltejs/kit'].some(name => dependencies[name])) throw new Error('This package sets up Nuxt projects only.');
72
76
  if (dependencies.nuxt && !/^(?:[~^]|>=)?4(?:\.|$)/.test(dependencies.nuxt)) {
73
77
  throw new Error('This release supports Nuxt 4. Upgrade your Nuxt dependency before setup.');
@@ -98,6 +102,12 @@ export async function setupProject({ cwd = process.cwd(), action = 'set', field,
98
102
  settings.frontends = [...new Set(frontends.map(frontendDomain))];
99
103
  if (settings.frontend && !settings.frontends.includes(settings.frontend)) settings.frontends.unshift(settings.frontend);
100
104
  if (field === 'backend') settings.backend = setting;
105
+ if (field === 'mimic') {
106
+ if (!settings.frontends.includes(setting)) {
107
+ throw new Error(`Add this frontend before selecting it for MIMIC: ${setting}`);
108
+ }
109
+ settings.frontend = setting;
110
+ }
101
111
  if (field === 'frontend') {
102
112
  if (action === 'remove') {
103
113
  if (!settings.frontends.includes(setting)) throw new Error(`Frontend is not managed by this project: ${setting}`);
@@ -143,24 +153,56 @@ export async function setupProject({ cwd = process.cwd(), action = 'set', field,
143
153
  pkg.scripts['orionplace:frontend'] ||= 'orionplace-nuxt set frontend';
144
154
  pkg.scripts['orionplace:add-frontend'] ||= 'orionplace-nuxt add frontend';
145
155
  pkg.scripts['orionplace:remove-frontend'] ||= 'orionplace-nuxt remove frontend';
156
+ pkg.scripts['orionplace:mimic'] ||= 'orionplace-nuxt set mimic';
157
+ pkg.scripts['orionplace:help'] ||= 'orionplace-nuxt help';
158
+ pkg.scripts['orionplace:update'] ||= 'orionplace-nuxt update';
146
159
  pkg.dependencies ||= {};
147
160
  pkg.dependencies['orionplace-nuxt'] ||= dependencies['orionplace-nuxt'] || `^${metadata.version}`;
161
+ if (action === 'update') {
162
+ pkg.dependencies['orionplace-nuxt'] = metadata.version;
163
+ if (pkg.devDependencies) delete pkg.devDependencies['orionplace-nuxt'];
164
+ }
148
165
  if (isNewNuxt) {
149
166
  pkg.dependencies.nuxt = '^4.5.0';
150
167
  pkg.dependencies.vue ||= '^3.5.0';
151
168
  pkg.dependencies['vue-router'] ||= '^5.0.0';
152
169
  }
170
+ // Install the complete editable AppLoad source. Never replace a customer's app.
171
+ const appPath = 'app/app.vue';
172
+ const existingApp = await read(await safePath(root, appPath));
173
+ const installApp = existingApp === null || Boolean(previous[appPath]);
174
+ const templateRoot = new URL('../templates/', import.meta.url);
175
+ async function planTemplates(directory = '') {
176
+ for (const entry of await readdir(new URL(directory, templateRoot), { withFileTypes: true })) {
177
+ const name = directory + entry.name;
178
+ if (entry.isDirectory()) { await planTemplates(name + '/'); continue; }
179
+ if (name === appPath && !installApp) continue;
180
+ await plan(name, await readFile(new URL(name, templateRoot), 'utf8'), { managed: true, preserve: true });
181
+ }
182
+ }
183
+ await planTemplates();
184
+ if (installApp && !dependencies.sass && !dependencies['sass-embedded']) {
185
+ pkg.devDependencies ||= {};
186
+ pkg.devDependencies.sass = '^1.102.0';
187
+ }
188
+ if (!await read(await safePath(root, 'tsconfig.json'))) {
189
+ await plan('tsconfig.json', JSON.stringify({ files: [], references: [
190
+ { path: './.nuxt/tsconfig.app.json' }, { path: './.nuxt/tsconfig.server.json' },
191
+ { path: './.nuxt/tsconfig.shared.json' }, { path: './.nuxt/tsconfig.node.json' },
192
+ ] }, null, 2) + '\n', { managed: true });
193
+ }
153
194
  await plan('package.json', JSON.stringify(pkg, null, 2) + '\n');
154
195
  for (const file of ['marketplace.mjs', 'api.mjs', 'domain.mjs']) {
155
196
  await plan(`orionplace/runtime/${file}`, await readFile(new URL(`../runtime/${file}`, import.meta.url), 'utf8'), { managed: true, preserve: true });
156
197
  }
157
198
  await plan('orionplace/README.md', projectReadme(settings), { managed: true, preserve: true });
158
- if (isNewNuxt && !(await read(await safePath(root, 'app/app.vue')))) {
159
- await plan('app/app.vue', '<template>\n <main>\n <h1>Your marketplace</h1>\n <p>Domain setup and API instructions are in orionplace/README.md.</p>\n </main>\n</template>\n', { managed: true });
199
+ // A new project starts explicitly empty; keep a pre-existing manual setting on init.
200
+ const hasMimicSetting = /^\s*(?:export\s+)?MIMIC_FRONT_DOMAIN\s*=/m.test(current['.env'] || '');
201
+ if (action !== 'update' && (settings.frontend || oldFrontend || !hasMimicSetting)) {
202
+ await plan('.env', updateEnvironment(current['.env'], settings.frontend || ''));
160
203
  }
161
- if (settings.frontend || oldFrontend) await plan('.env', updateEnvironment(current['.env'], settings.frontend || ''));
162
- if (settings.backend) {
163
- const mappings = action === 'remove' ? []
204
+ if (settings.backend && action !== 'update') {
205
+ const mappings = action === 'remove' || field === 'mimic' ? []
164
206
  : field === 'frontend' ? [setting] : settings.frontends;
165
207
  for (const domain of mappings) {
166
208
  await plan(`sitemaps/${domain}.cfg`, settings.backend + '\n', { managed: true });
@@ -172,7 +214,7 @@ export async function setupProject({ cwd = process.cwd(), action = 'set', field,
172
214
  if (!ignore.split(/\r?\n/).includes(entry)) ignore = ignore.replace(/\n*$/, '\n') + entry + '\n';
173
215
  }
174
216
  await plan('.gitignore', ignore);
175
- await plan('orionplace/settings.json', JSON.stringify(settings, null, 2) + '\n');
217
+ if (action !== 'update') await plan('orionplace/settings.json', JSON.stringify(settings, null, 2) + '\n');
176
218
  await plan('orionplace/.generated.json', JSON.stringify(manifest, null, 2) + '\n');
177
219
  if (!dryRun) {
178
220
  for (const [name, content] of changes) {
package/src/update.mjs ADDED
@@ -0,0 +1,36 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { resolve } from 'node:path';
3
+ import { setupProject } from './setup.mjs';
4
+
5
+ export function runNpm(args, cwd) {
6
+ const npmCli = process.env.npm_execpath;
7
+ const command = npmCli ? process.execPath : 'npm';
8
+ const commandArgs = npmCli ? [npmCli, ...args] : args;
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(command, commandArgs, { cwd, stdio: 'inherit', shell: false });
11
+ child.once('error', reject);
12
+ child.once('exit', (code, signal) => code === 0 ? resolve() : reject(new Error(
13
+ `npm ${args[0]} failed (${signal || code}).`,
14
+ )));
15
+ });
16
+ }
17
+
18
+ export async function updateProject({ cwd = process.cwd(), dryRun = false, local = false, npm = runNpm } = {}) {
19
+ cwd = resolve(cwd);
20
+ // Validate before downloading or writing; the latest CLI repeats preflight with its own templates.
21
+ const preview = await setupProject({ cwd, action: 'update', dryRun: true });
22
+ if (!local) {
23
+ console.log('Checking the latest Orionplace Nuxt release…');
24
+ await npm(['exec', '--yes', '--prefer-online', '--package=orionplace-nuxt@latest', '--',
25
+ 'orionplace-nuxt', 'update', '--local', '--cwd', cwd, ...(dryRun ? ['--dry-run'] : [])], cwd);
26
+ return null; // The latest CLI reports its own changes.
27
+ }
28
+ if (dryRun) return preview;
29
+ const result = await setupProject({ cwd, action: 'update' });
30
+ try {
31
+ await npm(['install', '--no-audit', '--no-fund'], cwd);
32
+ } catch (error) {
33
+ throw new Error(`Source files were updated, but dependency installation failed. Run npm install in the project, then retry update. ${error.message}`);
34
+ }
35
+ return result;
36
+ }
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { useHead } from '#imports';
4
+ import { useOrionplaceAppLoad } from './composables/useOrionplaceAppLoad';
5
+ import type { IAppLoadPathsRecord } from './interfaces/orionplace';
6
+
7
+ useHead({ title: 'Marketplace · AppLoad debug' });
8
+ const { data, status, error, refresh } = await useOrionplaceAppLoad();
9
+ const page = computed(() => data.value?.response.page_data);
10
+ const init = computed(() => page.value?.init);
11
+ const countEndpoints = (routes?: IAppLoadPathsRecord) =>
12
+ Object.values(routes?.paths ?? {}).reduce((total, section) => total + Object.keys(section).length, 0);
13
+ const stateLabel = computed(() => ({ idle: 'Not started', pending: 'Loading', success: 'Loaded', error: 'Failed' })[status.value]);
14
+ const failure = computed(() => error.value?.data);
15
+ </script>
16
+
17
+ <template>
18
+ <main class="bootstrap">
19
+ <header>
20
+ <p class="eyebrow">Orionplace · Nuxt starter</p>
21
+ <h1>Your marketplace connection</h1>
22
+ <p>This first page checks initialization through <code>POST /api/appload</code>.
23
+ Replace it with your storefront when the connection is ready.</p>
24
+ </header>
25
+ <section aria-labelledby="status-title" :aria-busy="status === 'pending'">
26
+ <div class="section-heading">
27
+ <h2 id="status-title">AppLoad state</h2>
28
+ <button type="button" :disabled="status === 'pending'" @click="refresh()">Reload AppLoad</button>
29
+ </div>
30
+ <p role="status" aria-live="polite" class="state" :data-state="status">{{ stateLabel }}</p>
31
+ <p v-if="status === 'pending'">Loading initialization data and available API endpoints…</p>
32
+ <div v-else-if="error" role="alert">
33
+ <p>AppLoad could not initialize. {{ failure?.kind === 'validation' ? 'The backend returned form errors.' :
34
+ failure?.kind === 'invalid-response' ? 'The response is missing required initialization data.' :
35
+ failure?.kind === 'network' ? 'The API request did not complete.' : 'The API returned an HTTP error.' }}</p>
36
+ <p v-if="failure?.httpStatus">HTTP status: <code>{{ failure.httpStatus }}</code></p>
37
+ <p>Check your backend, frontend domain and <code>MIMIC_FRONT_DOMAIN</code> using
38
+ <code>npx orionplace-nuxt help</code>. Setup instructions are in <code>orionplace/README.md</code>.</p>
39
+ </div>
40
+ <dl v-else-if="data && init">
41
+ <div><dt>HTTP status</dt><dd>{{ data.httpStatus }}</dd></div>
42
+ <div><dt>Loaded at (UTC)</dt><dd><time :datetime="data.loadedAt">{{ data.loadedAt }}</time></dd></div>
43
+ <div><dt>Session</dt><dd>{{ init.is_authenticated ? 'Signed in' : 'Guest' }}</dd></div>
44
+ <div><dt>Languages</dt><dd>{{ Object.keys(init.locale).length }}</dd></div>
45
+ <div><dt>Currencies</dt><dd>{{ Object.keys(init.currencies).length }}</dd></div>
46
+ <div><dt>Public endpoints</dt><dd>{{ countEndpoints(page?.unauth) }}</dd></div>
47
+ <div><dt>Authenticated endpoints</dt><dd>{{ countEndpoints(page?.auth) }}</dd></div>
48
+ <div><dt>Catalog roots</dt><dd>{{ init.catalog?.length ?? 'Not included' }}</dd></div>
49
+ <div><dt>Initial products</dt><dd>{{ init.products?.length ?? 'Not included' }}</dd></div>
50
+ <div><dt>Tariffs</dt><dd>{{ init.tariffs?.data.length ?? 'Not included' }}</dd></div>
51
+ </dl>
52
+ </section>
53
+ <section aria-labelledby="next-title">
54
+ <h2 id="next-title">Build from typed initialization data</h2>
55
+ <p>The request lives in <code>app/composables/useOrionplaceAppLoad.ts</code>.
56
+ The response and its nested interfaces are in <code>app/interfaces/orionplace/</code>.</p>
57
+ <p>This debug page shows section counts. Account details, keys and backend diagnostics are not displayed.</p>
58
+ <a href="https://orionmarket.place/sandbox">Read the marketplace documentation</a>
59
+ </section>
60
+ </main>
61
+ </template>
62
+
63
+ <style lang="scss">
64
+ body { margin: 0; background: #f5f6f8; color: #202b38; font-family: system-ui, sans-serif; }
65
+ .bootstrap {
66
+ max-width: 860px; margin: 0 auto; padding: 48px 24px;
67
+ h1, h2 { font-weight: 400; line-height: 1.25; }
68
+ h1 { font-size: 32px; margin: 12px 0; }
69
+ h2 { font-size: 21px; margin: 0; }
70
+ p { line-height: 1.7; }
71
+ .eyebrow { color: #596777; font-size: 14px; }
72
+ section { margin-top: 28px; padding: 24px; border: 1px solid #d9dfe6; border-radius: 10px; background: #fff; }
73
+ .section-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
74
+ .state { display: inline-block; padding: 4px 12px; border-radius: 4px; background: #eaf0f5;
75
+ &[data-state='success'] { background: #e6f2ec; color: #245c40; }
76
+ &[data-state='error'] { background: #fbeceb; color: #8c302a; }
77
+ }
78
+ button { font: inherit; color: inherit; background: #f5f6f8; border: 1px solid #bfc9d4; border-radius: 5px; padding: 9px 14px; cursor: pointer;
79
+ &:disabled { opacity: .6; cursor: wait; }
80
+ &:hover:not(:disabled) { background: #eaf0f5; }
81
+ }
82
+ a { color: #245786; text-underline-offset: 3px; }
83
+ :focus-visible { outline: 2px solid #245786; outline-offset: 4px; }
84
+ code { font-size: .9em; overflow-wrap: anywhere; }
85
+ dl { margin-bottom: 0; div { display: grid; grid-template-columns: minmax(140px, 1fr) 2fr; gap: 12px; padding: 12px 0; border-top: 1px solid #e8ecf0; }
86
+ dt { color: #596777; } dd { margin: 0; overflow-wrap: anywhere; }
87
+ }
88
+ @media (max-width: 520px) { padding: 24px 16px; section { padding: 18px; } dl div { grid-template-columns: 1fr; gap: 4px; } }
89
+ }
90
+ </style>