orionplace-nuxt 0.1.2 → 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 (39) hide show
  1. package/README.md +52 -2
  2. package/bin/orionplace-nuxt.mjs +24 -9
  3. package/package.json +3 -2
  4. package/src/project-readme.mjs +64 -29
  5. package/src/setup.mjs +37 -7
  6. package/src/update.mjs +36 -0
  7. package/templates/app/app.vue +90 -0
  8. package/templates/app/composables/useOrionplaceAppLoad.ts +51 -0
  9. package/templates/app/interfaces/orionplace/apiBase/TMethod.ts +1 -0
  10. package/templates/app/interfaces/orionplace/appBase/IAppLoad.ts +15 -0
  11. package/templates/app/interfaces/orionplace/appBase/IAppLoadCommon.ts +54 -0
  12. package/templates/app/interfaces/orionplace/appBase/IApploadUserProfile.ts +13 -0
  13. package/templates/app/interfaces/orionplace/appBase/IForm.ts +21 -0
  14. package/templates/app/interfaces/orionplace/appBase/ISEO.ts +21 -0
  15. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonBanner.ts +11 -0
  16. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonContact.ts +8 -0
  17. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonCurrency.ts +15 -0
  18. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonEvents.ts +18 -0
  19. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLocale.ts +14 -0
  20. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLogos.ts +6 -0
  21. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonPath.ts +22 -0
  22. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSEO.ts +13 -0
  23. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSocial.ts +15 -0
  24. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonTariff.ts +7 -0
  25. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonUnit.ts +12 -0
  26. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVariables.ts +7 -0
  27. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVisualSettings.ts +11 -0
  28. package/templates/app/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonWalletValue.ts +3 -0
  29. package/templates/app/interfaces/orionplace/category/ICategory.ts +40 -0
  30. package/templates/app/interfaces/orionplace/domains/IDomain.ts +13 -0
  31. package/templates/app/interfaces/orionplace/index.ts +29 -0
  32. package/templates/app/interfaces/orionplace/products/IProduct.ts +235 -0
  33. package/templates/app/interfaces/orionplace/shared/IGeoCoordinates.ts +4 -0
  34. package/templates/app/interfaces/orionplace/shared/ITransformValue.ts +3 -0
  35. package/templates/app/interfaces/orionplace/subscribeOptions/ISubscribeOption.ts +9 -0
  36. package/templates/app/interfaces/orionplace/subscribeTariff/ISubscribeTariff.ts +46 -0
  37. package/templates/app/interfaces/orionplace/subscribes/ISubscribe.ts +28 -0
  38. package/templates/app/interfaces/orionplace/transport.ts +30 -0
  39. package/templates/app/utils/orionplaceAppLoad.ts +33 -0
package/README.md CHANGED
@@ -62,6 +62,7 @@ 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
@@ -79,6 +80,7 @@ npm run orionplace:add-frontend -- shop.example.com
79
80
  npm run orionplace:remove-frontend -- www.shop.example.com
80
81
  npm run orionplace:mimic -- shop.example.com
81
82
  npm run orionplace:help
83
+ npm run orionplace:update
82
84
  ```
83
85
 
84
86
  `npm set` belongs to npm's own configuration system; use the commands above.
@@ -102,7 +104,7 @@ change your account, create DNS records, configure TLS, or deploy a VPS.
102
104
  The module executes the generated local runtime files. You can read and customize
103
105
  them. Later setup runs preserve edited runtime files and the project README,
104
106
  reporting which files were preserved. Unchanged generated files can be refreshed
105
- 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
106
108
  never silently overwritten. Keep the hash manifest in version control.
107
109
 
108
110
  ## Multiple frontend domains
@@ -174,4 +176,52 @@ npm pack --dry-run
174
176
  ```
175
177
 
176
178
  The published tarball includes only the CLI, module, runtime source, README,
177
- 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,5 +1,6 @@
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';
@@ -8,6 +9,7 @@ const help = `Orionplace Nuxt setup
8
9
 
9
10
  npx orionplace-nuxt help
10
11
  npx orionplace-nuxt init [directory]
12
+ npx orionplace-nuxt update
11
13
  npx orionplace-nuxt set backend <personal-backend.orionplace.app>
12
14
  npx orionplace-nuxt add frontend <shop.example.com>
13
15
  npx orionplace-nuxt remove frontend <shop.example.com>
@@ -15,7 +17,10 @@ const help = `Orionplace Nuxt setup
15
17
  npx orionplace-nuxt set frontend <shop.example.com>
16
18
 
17
19
  Options: --cwd <directory> --dry-run --help --version
20
+ Update option: --local (apply this CLI version instead of fetching the latest release)
18
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.
19
24
  Set backend or add frontend also initializes the integration; either can come first.
20
25
  MIMIC starts empty. The first added frontend sets it; later additions preserve it.
21
26
  Set mimic selects an existing frontend without changing any domain mappings.
@@ -33,8 +38,10 @@ try {
33
38
  const positional = [];
34
39
  let cwd = process.cwd();
35
40
  let dryRun = false;
41
+ let local = false;
36
42
  for (let index = 0; index < argv.length; index++) {
37
43
  if (argv[index] === '--dry-run') dryRun = true;
44
+ else if (argv[index] === '--local') local = true;
38
45
  else if (argv[index] === '--cwd') {
39
46
  if (!argv[index + 1]) throw new Error('--cwd needs a directory');
40
47
  cwd = resolve(argv[++index]);
@@ -42,9 +49,12 @@ try {
42
49
  else positional.push(argv[index]);
43
50
  }
44
51
  const [command, target] = positional;
52
+ if (local && command !== 'update') throw new Error('--local is only supported by update.');
45
53
  let result;
46
54
  if (command === 'init' && positional.length <= 2) {
47
55
  result = await setupProject({ cwd: target ? resolve(cwd, target) : cwd, dryRun });
56
+ } else if (command === 'update' && positional.length === 1) {
57
+ result = await updateProject({ cwd, dryRun, local });
48
58
  } else if (((command === 'set' && ['backend', 'frontend', 'mimic'].includes(target)) ||
49
59
  (['add', 'remove'].includes(command) && target === 'frontend')) && positional.length <= 3) {
50
60
  let value = positional[2];
@@ -56,15 +66,20 @@ try {
56
66
  if (!value) throw new Error(`Provide the ${target} domain. See --help.`);
57
67
  result = await setupProject({ cwd, action: command, field: target, value, dryRun });
58
68
  } else throw new Error('Unknown command. See --help.');
59
- console.log(`${dryRun ? 'Planned' : 'Updated'} ${result.root}`);
60
- for (const path of result.changed) console.log(` ${result.removed.includes(path) ? 'Remove: ' : ''}${path}`);
61
- for (const path of result.preserved) console.log(` Preserved your edits: ${path}`);
62
- console.log(result.settings.backend && result.settings.frontend
63
- ? `Ready: ${result.settings.frontends.join(', ')} -> ${result.settings.backend}\nLocal MIMIC: ${result.settings.frontend}`
64
- : `Next: ${result.settings.backend ? 'add frontend' : 'set backend'} to complete the domain mapping.`);
65
- 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.');
66
- if (result.isNewNuxt) console.log('Run npm install in the project, then npm run dev.');
67
- 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
+ }
68
83
  }
69
84
  } catch (error) {
70
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.2",
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"
@@ -98,53 +98,88 @@ In another terminal:
98
98
  \`\`\`bash
99
99
  curl -i http://localhost:3000/api/appload \\
100
100
  -H 'Content-Type: application/json' \\
101
- --data '{"ignore_cache":true,"detalize_mode":"full"}'
101
+ --data '{"ignore_cache":true,"detalize_mode":"light"}'
102
102
  \`\`\`
103
103
 
104
104
  Check both the HTTP status and \`form_errors\`. Successful initialization returns
105
105
  \`page_data\`; HTTP 200 by itself is insufficient. Documentation and public requests
106
106
  do not require a login prompt. Protected actions still require backend permission.
107
107
 
108
- Browser requests use the storefront's own API path:
109
-
110
- \`\`\`javascript
111
- const response = await fetch('/api/appload', {
112
- method: 'POST',
113
- credentials: 'same-origin',
114
- headers: { 'Content-Type': 'application/json' },
115
- body: JSON.stringify({ ignore_cache: true, detalize_mode: 'full' }),
116
- });
117
- const result = await response.json();
118
- console.log(response.status, result);
119
- \`\`\`
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.
126
+
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.
120
135
 
121
- For SSR, use Nuxt's \`useRequestFetch()\` so the request's domain, cookie and
122
- authorization headers reach the same gateway. For example, in \`app/app.vue\`:
136
+ If your project already has a custom \`app/app.vue\`, the CLI preserves it.
137
+ Integrate the generated composable explicitly:
123
138
 
124
139
  \`\`\`vue
125
- <script setup>
126
- const requestFetch = useRequestFetch();
127
- const { data, error } = await useAsyncData('marketplace-initialization', () =>
128
- requestFetch('/api/appload', {
129
- method: 'POST',
130
- body: { ignore_cache: true, detalize_mode: 'full' },
131
- }),
132
- );
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.
133
144
  </script>
134
145
 
135
146
  <template>
136
147
  <main>
137
- <p v-if="error">The marketplace API is currently unavailable.</p>
138
- <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>
139
152
  </main>
140
153
  </template>
141
154
  \`\`\`
142
155
 
143
- Keep private data out of shared caches. This SSR example forwards incoming cookies;
144
- it does not relay new upstream cookies to the browser. Make session-changing calls
145
- from the browser, or explicitly append each returned Set-Cookie header to the SSR
146
- 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
+ \`\`\`
147
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.
148
183
 
149
184
  ## Deploy on your VPS
150
185
 
package/src/setup.mjs CHANGED
@@ -57,7 +57,8 @@ 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')) {
60
+ if (!['set', 'add', 'remove', 'update'].includes(action) ||
61
+ (['add', 'remove'].includes(action) && field !== 'frontend') || (action === 'update' && field)) {
61
62
  throw new Error('Use set backend, add frontend, remove frontend, set mimic, or set frontend.');
62
63
  }
63
64
  const root = resolve(cwd);
@@ -68,6 +69,9 @@ export async function setupProject({ cwd = process.cwd(), action = 'set', field,
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.');
@@ -151,27 +155,53 @@ export async function setupProject({ cwd = process.cwd(), action = 'set', field,
151
155
  pkg.scripts['orionplace:remove-frontend'] ||= 'orionplace-nuxt remove frontend';
152
156
  pkg.scripts['orionplace:mimic'] ||= 'orionplace-nuxt set mimic';
153
157
  pkg.scripts['orionplace:help'] ||= 'orionplace-nuxt help';
158
+ pkg.scripts['orionplace:update'] ||= 'orionplace-nuxt update';
154
159
  pkg.dependencies ||= {};
155
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
+ }
156
165
  if (isNewNuxt) {
157
166
  pkg.dependencies.nuxt = '^4.5.0';
158
167
  pkg.dependencies.vue ||= '^3.5.0';
159
168
  pkg.dependencies['vue-router'] ||= '^5.0.0';
160
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
+ }
161
194
  await plan('package.json', JSON.stringify(pkg, null, 2) + '\n');
162
195
  for (const file of ['marketplace.mjs', 'api.mjs', 'domain.mjs']) {
163
196
  await plan(`orionplace/runtime/${file}`, await readFile(new URL(`../runtime/${file}`, import.meta.url), 'utf8'), { managed: true, preserve: true });
164
197
  }
165
198
  await plan('orionplace/README.md', projectReadme(settings), { managed: true, preserve: true });
166
- if (isNewNuxt && !(await read(await safePath(root, 'app/app.vue')))) {
167
- 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 });
168
- }
169
199
  // A new project starts explicitly empty; keep a pre-existing manual setting on init.
170
200
  const hasMimicSetting = /^\s*(?:export\s+)?MIMIC_FRONT_DOMAIN\s*=/m.test(current['.env'] || '');
171
- if (settings.frontend || oldFrontend || !hasMimicSetting) {
201
+ if (action !== 'update' && (settings.frontend || oldFrontend || !hasMimicSetting)) {
172
202
  await plan('.env', updateEnvironment(current['.env'], settings.frontend || ''));
173
203
  }
174
- if (settings.backend) {
204
+ if (settings.backend && action !== 'update') {
175
205
  const mappings = action === 'remove' || field === 'mimic' ? []
176
206
  : field === 'frontend' ? [setting] : settings.frontends;
177
207
  for (const domain of mappings) {
@@ -184,7 +214,7 @@ export async function setupProject({ cwd = process.cwd(), action = 'set', field,
184
214
  if (!ignore.split(/\r?\n/).includes(entry)) ignore = ignore.replace(/\n*$/, '\n') + entry + '\n';
185
215
  }
186
216
  await plan('.gitignore', ignore);
187
- 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');
188
218
  await plan('orionplace/.generated.json', JSON.stringify(manifest, null, 2) + '\n');
189
219
  if (!dryRun) {
190
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>
@@ -0,0 +1,51 @@
1
+ import { createError, useAsyncData, useRequestFetch, useResponseHeader } from '#imports';
2
+ import type { AppLoadFailure, AppLoadRequest, AppLoadResult } from '../interfaces/orionplace/transport';
3
+ import { appLoadResult, inspectAppLoad } from '../utils/orionplaceAppLoad';
4
+
5
+ /** Call once in app.vue. Nuxt transfers the SSR result to the browser during hydration. */
6
+ export function useOrionplaceAppLoad() {
7
+ // SSR forwards the current request's domain, cookies and authorization to our own gateway.
8
+ const requestFetch = useRequestFetch();
9
+ const responseCookies = import.meta.server ? useResponseHeader('set-cookie') : undefined;
10
+ if (import.meta.server) useResponseHeader('cache-control').value = 'private, no-store';
11
+
12
+ return useAsyncData<AppLoadResult, AppLoadFailure>(
13
+ 'orionplace:appload',
14
+ async (_app, { signal }) => {
15
+ let received: { body: unknown; status: number; cookies: string[] } | undefined;
16
+ try {
17
+ await requestFetch<unknown>('/api/appload', {
18
+ method: 'POST',
19
+ body: { detalize_mode: 'light', ignore_cache: true } satisfies AppLoadRequest,
20
+ credentials: 'same-origin',
21
+ cache: 'no-store',
22
+ retry: 0,
23
+ timeout: 35_000,
24
+ ignoreResponseError: true,
25
+ signal,
26
+ onResponse({ response }) {
27
+ received = { body: response._data, status: response.status,
28
+ cookies: import.meta.server ? response.headers.getSetCookie() : [] };
29
+ },
30
+ });
31
+ } catch {
32
+ throw createError({ message: 'Could not reach the marketplace API.', data: {
33
+ kind: 'network', httpStatus: null, fields: [],
34
+ } satisfies AppLoadFailure });
35
+ }
36
+ if (!received) throw createError({ message: 'The API returned no response.', data: {
37
+ kind: 'network', httpStatus: null, fields: [],
38
+ } satisfies AppLoadFailure });
39
+ // AppLoad can establish a CSRF/session cookie during SSR. Relay each cookie separately.
40
+ if (responseCookies) {
41
+ const cookies = received.cookies;
42
+ const existing = responseCookies.value;
43
+ if (cookies.length) responseCookies.value = [...(Array.isArray(existing) ? existing : existing ? [String(existing)] : []), ...cookies];
44
+ }
45
+ const failure = inspectAppLoad(received.body, received.status);
46
+ if (failure) throw createError({ message: 'Marketplace initialization failed.', data: failure });
47
+ return appLoadResult(received.body, received.status);
48
+ },
49
+ { deep: false, dedupe: 'defer' },
50
+ );
51
+ }
@@ -0,0 +1 @@
1
+ export type TMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
@@ -0,0 +1,15 @@
1
+ import type { IAppLoadCommon } from './IAppLoadCommon';
2
+ import type { IApploadUserProfile } from './IApploadUserProfile';
3
+ import type { IAppLoadPathsRecord } from './appLoadCommon/IAppLoadCommonPath';
4
+
5
+ export interface IAppLoad {
6
+ init: IAppLoadCommon;
7
+ unauth: IAppLoadPathsRecord;
8
+ user?: IApploadUserProfile;
9
+ auth?: IAppLoadPathsRecord;
10
+ view_key?: string;
11
+ spent_total?: number;
12
+ spent_init?: number;
13
+ unauth_spent?: number;
14
+ auth_spent?: number;
15
+ }
@@ -0,0 +1,54 @@
1
+ import type { IAppLoadCommonLocale } from './appLoadCommon/IAppLoadCommonLocale';
2
+ import type { IAppLoadCommonCurrency } from './appLoadCommon/IAppLoadCommonCurrency';
3
+ import type { IAppLoadCommonUnit } from './appLoadCommon/IAppLoadCommonUnit';
4
+ import type { IAppLoadCommonLogos } from './appLoadCommon/IAppLoadCommonLogos';
5
+ import type { IGeoCoordinates } from '../shared/IGeoCoordinates';
6
+ import type { IAppLoadCommonSeo } from './appLoadCommon/IAppLoadCommonSEO';
7
+ import type { IAppLoadCommonSocial } from './appLoadCommon/IAppLoadCommonSocial';
8
+ import type { IAppLoadCommonVisualSettings } from './appLoadCommon/IAppLoadCommonVisualSettings';
9
+ import type { IAppLoadCommonEvents } from './appLoadCommon/IAppLoadCommonEvents';
10
+ import type { IAppLoadCommonBanner } from './appLoadCommon/IAppLoadCommonBanner';
11
+ import type { ICategory } from '../category/ICategory';
12
+ import type { IProduct } from '../products/IProduct';
13
+ import type { IAppLoadCommonTariff } from './appLoadCommon/IAppLoadCommonTariff';
14
+ import type { IAppLoadCommonWalletValue } from './appLoadCommon/IAppLoadCommonWalletValue';
15
+ import type { IAppLoadCommonVariables } from './appLoadCommon/IAppLoadCommonVariables';
16
+ import type { IAppLoadCommonContact } from './appLoadCommon/IAppLoadCommonContact';
17
+
18
+ export interface IAppLoadCommon {
19
+ locale: Record<string, IAppLoadCommonLocale>;
20
+ currencies: Record<string, IAppLoadCommonCurrency>;
21
+ weights: Record<string, IAppLoadCommonUnit>;
22
+ lengths: Record<string, IAppLoadCommonUnit>;
23
+ geo_location_enabled: boolean;
24
+ is_authenticated: boolean;
25
+ wallet?: Record<string, IAppLoadCommonWalletValue>;
26
+ logos: IAppLoadCommonLogos;
27
+ public_keys: Record<string, string>;
28
+ default_map: IGeoCoordinates;
29
+ seo?: IAppLoadCommonSeo[];
30
+ sm?: IAppLoadCommonSocial[];
31
+ variables?: IAppLoadCommonVariables;
32
+ contact?: IAppLoadCommonContact;
33
+ visual_settings?: IAppLoadCommonVisualSettings;
34
+ events?: IAppLoadCommonEvents[];
35
+ main_banner?: Record<string, IAppLoadCommonBanner[]>;
36
+ catalog?: ICategory[];
37
+ products?: IProduct[];
38
+ featured?: IProduct[];
39
+ search_hints?: Record<string, string>[];
40
+ tariffs?: IAppLoadCommonTariff;
41
+ stock?: { mode: 'bar' | 'qr'; simple: boolean };
42
+ order?: { order_auto_register_with_carrier: boolean };
43
+ promo?: { create_benefits: boolean };
44
+ product?: { common_properties: boolean };
45
+ app?: { backend: boolean };
46
+ domains?: IAppLoadDomain[];
47
+ }
48
+
49
+ export interface IAppLoadDomain {
50
+ id: number;
51
+ domain_group: number | null;
52
+ domain: string;
53
+ is_active: boolean;
54
+ }