configuration-management 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,22 +82,25 @@ async function main (params) {
82
82
  Spectrum-based Commerce Admin extension UI for schema-driven system configuration.
83
83
 
84
84
  ```js
85
+ import React from 'react'
85
86
  import { createRoot } from 'react-dom/client'
86
87
  import {
87
88
  ConfigurationManagementApp,
88
- configureWeb,
89
- SystemConfig
89
+ configureWeb
90
90
  } from 'configuration-management/web'
91
- import actionUrls from './config.json' // deploy-time URLs from aio app deploy
91
+ import actionUrls from './config.json'
92
92
  import 'configuration-management/web/styles.css'
93
93
 
94
94
  configureWeb({ actionUrls })
95
95
 
96
96
  createRoot(document.getElementById('root')).render(
97
- <ConfigurationManagementApp runtime={runtime} ims={ims} />
97
+ React.createElement(ConfigurationManagementApp, { runtime, ims })
98
98
  )
99
99
  ```
100
100
 
101
+ The web UI is **pre-built** in the package (`web/dist/index.js`), so no Parcel
102
+ `includeNodeModules` or webpack aliases are required in your host `package.json`.
103
+
101
104
  | Export | Description |
102
105
  |--------|-------------|
103
106
  | `ConfigurationManagementApp` | Full app shell (router + Spectrum provider + UIX registration) |
@@ -114,8 +117,9 @@ OpenWhisk runtime actions and the Commerce Admin extension manifest ship with th
114
117
 
115
118
  #### Automatic wiring on `npm install`
116
119
 
117
- The package runs a **postinstall** script that patches your project's `app.config.yaml`
118
- (if it exists) with:
120
+ The package runs a **postinstall** script that:
121
+
122
+ 1. Patches your project's `app.config.yaml` (if present) with:
119
123
 
120
124
  ```yaml
121
125
  extensions:
@@ -123,7 +127,20 @@ extensions:
123
127
  $include: node_modules/configuration-management/actions/configurations/ext.config.yaml
124
128
  ```
125
129
 
126
- Requirements:
130
+ 2. Scaffolds a minimal `web-src/` bootstrap (if missing or previously auto-generated):
131
+
132
+ ```
133
+ web-src/
134
+ ├── index.html
135
+ └── src/
136
+ ├── index.js ← imports ConfigurationManagementApp from the package
137
+ ├── exc-runtime.js
138
+ └── config.json ← created empty; filled by aio app deploy
139
+ ```
140
+
141
+ Custom `web-src` files are left untouched unless they were created by a prior install
142
+ (files containing the `configuration-management: auto-generated bootstrap` marker, or
143
+ an older bootstrap that imports `configuration-management/web`).
127
144
 
128
145
  - Run `npm install` from your App Builder project root (where `app.config.yaml` lives).
129
146
  - Do not use `npm install --ignore-scripts` (that skips postinstall).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configuration-management",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Schema-driven system configuration for Adobe Commerce App Builder sync apps. Magento-style scoped config in Adobe App Builder Database (ABDB) with encryption, Commerce REST helpers, and React Admin UI.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Adobe Inc.",
@@ -16,10 +16,12 @@
16
16
  ],
17
17
  "main": "./src/index.js",
18
18
  "bin": {
19
- "configuration-management-setup": "./scripts/setup-app-config.js"
19
+ "configuration-management-setup": "./scripts/setup.js"
20
20
  },
21
21
  "scripts": {
22
- "postinstall": "node ./scripts/setup-app-config.js"
22
+ "build": "node ./scripts/build-web.js",
23
+ "prepublishOnly": "node ./scripts/build-web.js",
24
+ "postinstall": "node ./scripts/build-web.js && node ./scripts/setup.js"
23
25
  },
24
26
  "exports": {
25
27
  ".": "./src/index.js",
@@ -28,8 +30,8 @@
28
30
  "./crypto": "./src/system-config-crypto.js",
29
31
  "./shared": "./src/system-config-shared.js",
30
32
  "./oauth1a": "./src/oauth1a.js",
31
- "./web": "./web/index.js",
32
- "./web/index.js": "./web/index.js",
33
+ "./web": "./web/dist/index.js",
34
+ "./web/index.js": "./web/dist/index.js",
33
35
  "./web/styles.css": "./web/src/styles/index.css",
34
36
  "./web/src/styles/index.css": "./web/src/styles/index.css",
35
37
  "./actions/utils": "./actions/utils.js",
@@ -61,6 +63,7 @@
61
63
  "react-router-dom": "^6.8.1"
62
64
  },
63
65
  "dependencies": {
66
+ "esbuild": "^0.25.12",
64
67
  "got": "^11.8.5",
65
68
  "oauth-1.0a": "^2.2.6"
66
69
  },
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ Copyright 2025 Adobe. All rights reserved.
4
+ Licensed under the Apache License, Version 2.0
5
+ */
6
+
7
+ const fs = require('fs')
8
+ const path = require('path')
9
+
10
+ async function main () {
11
+ let esbuild
12
+ try {
13
+ esbuild = require('esbuild')
14
+ } catch {
15
+ console.error('[configuration-management] esbuild is required to build the web UI. Run npm install in the package directory.')
16
+ process.exit(1)
17
+ }
18
+
19
+ const pkgRoot = path.join(__dirname, '..')
20
+ const entry = path.join(pkgRoot, 'web/src/index.js')
21
+ const outdir = path.join(pkgRoot, 'web/dist')
22
+ const outfile = path.join(outdir, 'index.js')
23
+
24
+ fs.mkdirSync(outdir, { recursive: true })
25
+
26
+ await esbuild.build({
27
+ entryPoints: [entry],
28
+ bundle: true,
29
+ format: 'esm',
30
+ platform: 'browser',
31
+ outfile,
32
+ packages: 'external',
33
+ jsx: 'automatic',
34
+ loader: { '.js': 'jsx' },
35
+ target: ['chrome79', 'firefox85', 'safari13'],
36
+ logLevel: 'info'
37
+ })
38
+
39
+ console.log('[configuration-management] built web/dist/index.js')
40
+ }
41
+
42
+ if (require.main === module) {
43
+ main().catch((err) => {
44
+ console.error('[configuration-management] build-web failed:', err.message)
45
+ process.exit(1)
46
+ })
47
+ }
48
+
49
+ module.exports = { main }
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ Copyright 2025 Adobe. All rights reserved.
4
+ Licensed under the Apache License, Version 2.0
5
+ */
6
+
7
+ const fs = require('fs')
8
+ const path = require('path')
9
+
10
+ const EXTENSION_POINT = 'commerce/backend-ui/1'
11
+ const INCLUDE_REL = 'node_modules/configuration-management/actions/configurations/ext.config.yaml'
12
+ const MARKER = '# configuration-management (auto-linked on npm install)'
13
+ const WEB_BOOTSTRAP_MARKER = 'configuration-management: auto-generated bootstrap'
14
+
15
+ const TEMPLATES_DIR = path.join(__dirname, 'templates')
16
+
17
+ function escapeRe (str) {
18
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
19
+ }
20
+
21
+ function findProjectRoot (startDir) {
22
+ let dir = startDir
23
+ while (dir && dir !== path.dirname(dir)) {
24
+ if (
25
+ fs.existsSync(path.join(dir, 'app.config.yaml')) ||
26
+ fs.existsSync(path.join(dir, 'web-src'))
27
+ ) {
28
+ return dir
29
+ }
30
+ dir = path.dirname(dir)
31
+ }
32
+ return null
33
+ }
34
+
35
+ function resolveProjectRoot () {
36
+ const initCwd = process.env.INIT_CWD
37
+ if (initCwd) {
38
+ const fromInit = findProjectRoot(initCwd)
39
+ if (fromInit) return fromInit
40
+ }
41
+ return findProjectRoot(process.cwd())
42
+ }
43
+
44
+ function readTemplate (name) {
45
+ return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf8')
46
+ }
47
+
48
+ function writeIfMissingOrManaged (filePath, content, marker) {
49
+ if (fs.existsSync(filePath)) {
50
+ const existing = fs.readFileSync(filePath, 'utf8')
51
+ if (existing.includes(marker)) {
52
+ const dir = path.dirname(filePath)
53
+ fs.mkdirSync(dir, { recursive: true })
54
+ const changed = existing !== content
55
+ if (changed) {
56
+ fs.writeFileSync(filePath, content, 'utf8')
57
+ }
58
+ return { changed, reason: changed ? 'updated-managed' : 'unchanged-managed', path: filePath }
59
+ }
60
+ return { changed: false, reason: 'skipped-custom-file', path: filePath }
61
+ }
62
+ const dir = path.dirname(filePath)
63
+ fs.mkdirSync(dir, { recursive: true })
64
+ fs.writeFileSync(filePath, content, 'utf8')
65
+ return { changed: true, reason: 'written', path: filePath }
66
+ }
67
+
68
+ function writeWebSrcIndex (projectRoot) {
69
+ const filePath = path.join(projectRoot, 'web-src', 'src', 'index.js')
70
+ const content = readTemplate('web-src-index.js')
71
+
72
+ if (!fs.existsSync(filePath)) {
73
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
74
+ fs.writeFileSync(filePath, content, 'utf8')
75
+ return { changed: true, reason: 'written', path: filePath }
76
+ }
77
+
78
+ const existing = fs.readFileSync(filePath, 'utf8')
79
+ if (existing.includes(WEB_BOOTSTRAP_MARKER)) {
80
+ const changed = existing !== content
81
+ if (changed) {
82
+ fs.writeFileSync(filePath, content, 'utf8')
83
+ }
84
+ return { changed, reason: changed ? 'updated-managed' : 'unchanged-managed', path: filePath }
85
+ }
86
+
87
+ if (existing.includes('configuration-management/web')) {
88
+ fs.writeFileSync(filePath, content, 'utf8')
89
+ return { changed: true, reason: 'migrated-stale-bootstrap', path: filePath }
90
+ }
91
+
92
+ return { changed: false, reason: 'skipped-custom-file', path: filePath }
93
+ }
94
+
95
+ function alreadyLinked (content) {
96
+ return content.includes('configuration-management/actions/configurations/ext.config.yaml')
97
+ }
98
+
99
+ function hasExtensionPoint (content) {
100
+ return /^[ \t]*commerce\/backend-ui\/1:/m.test(content)
101
+ }
102
+
103
+ function buildExtensionBlock () {
104
+ return [
105
+ MARKER,
106
+ 'extensions:',
107
+ ` ${EXTENSION_POINT}:`,
108
+ ` $include: ${INCLUDE_REL}`
109
+ ].join('\n')
110
+ }
111
+
112
+ function updateExistingExtensionBlock (content) {
113
+ const match = content.match(/^([ \t]*)commerce\/backend-ui\/1:/m)
114
+ if (!match) return null
115
+
116
+ const indent = match[1]
117
+ const includeIndent = `${indent} `
118
+ const blockRe = new RegExp(
119
+ `^${escapeRe(indent)}commerce/backend-ui/1:[ \\t]*\\n` +
120
+ `(?:${escapeRe(includeIndent)}\\$include:[^\\n]*\\n)?`,
121
+ 'm'
122
+ )
123
+ const replacement =
124
+ `${indent}${EXTENSION_POINT}:\n${includeIndent}$include: ${INCLUDE_REL}\n`
125
+ const next = content.replace(blockRe, replacement)
126
+ return next !== content ? next : null
127
+ }
128
+
129
+ function patchAppConfig (content) {
130
+ if (alreadyLinked(content)) {
131
+ return { content, changed: false, reason: 'already-linked' }
132
+ }
133
+
134
+ if (hasExtensionPoint(content)) {
135
+ const updated = updateExistingExtensionBlock(content)
136
+ if (updated) {
137
+ return { content: updated, changed: true, reason: 'updated-existing-extension' }
138
+ }
139
+ return { content, changed: false, reason: 'extension-exists-unmodified' }
140
+ }
141
+
142
+ if (/^extensions:[ \t]*\n/m.test(content)) {
143
+ const injection = ` ${EXTENSION_POINT}:\n $include: ${INCLUDE_REL}\n`
144
+ const next = content.replace(/^extensions:[ \t]*\n/m, `extensions:\n${injection}`)
145
+ if (next !== content) {
146
+ return { content: next, changed: true, reason: 'added-under-extensions' }
147
+ }
148
+ }
149
+
150
+ if (!/^extensions:/m.test(content)) {
151
+ const trimmed = content.replace(/\s+$/, '')
152
+ const separator = trimmed.length > 0 ? '\n\n' : ''
153
+ return {
154
+ content: `${trimmed}${separator}${buildExtensionBlock()}\n`,
155
+ changed: true,
156
+ reason: 'appended'
157
+ }
158
+ }
159
+
160
+ return { content, changed: false, reason: 'no-change' }
161
+ }
162
+
163
+ function setupAppConfig (projectRoot) {
164
+ const appConfigPath = path.join(projectRoot, 'app.config.yaml')
165
+ if (!fs.existsSync(appConfigPath)) {
166
+ return { changed: false, reason: 'no-app-config' }
167
+ }
168
+
169
+ const original = fs.readFileSync(appConfigPath, 'utf8')
170
+ const { content, changed, reason } = patchAppConfig(original)
171
+ if (!changed) {
172
+ return { changed: false, reason }
173
+ }
174
+
175
+ fs.writeFileSync(appConfigPath, content, 'utf8')
176
+ return { changed: true, reason, detail: INCLUDE_REL }
177
+ }
178
+
179
+ function setupWebSrc (projectRoot) {
180
+ const webSrcDir = path.join(projectRoot, 'web-src')
181
+ const results = []
182
+
183
+ const indexJs = writeWebSrcIndex(projectRoot)
184
+ results.push(indexJs)
185
+
186
+ const indexHtml = writeIfMissingOrManaged(
187
+ path.join(webSrcDir, 'index.html'),
188
+ readTemplate('web-src-index.html'),
189
+ 'configuration-management'
190
+ )
191
+ results.push(indexHtml)
192
+
193
+ const excRuntime = writeIfMissingOrManaged(
194
+ path.join(webSrcDir, 'src', 'exc-runtime.js'),
195
+ readTemplate('exc-runtime.js'),
196
+ 'Module Runtime: Needs to be within an iframe'
197
+ )
198
+ results.push(excRuntime)
199
+
200
+ const configPath = path.join(webSrcDir, 'src', 'config.json')
201
+ if (!fs.existsSync(configPath)) {
202
+ fs.mkdirSync(path.dirname(configPath), { recursive: true })
203
+ fs.writeFileSync(configPath, '{}\n', 'utf8')
204
+ results.push({ changed: true, reason: 'created-config-json', path: configPath })
205
+ }
206
+
207
+ const changed = results.some((r) => r.changed)
208
+ return { changed, results }
209
+ }
210
+
211
+ function main () {
212
+ if (process.env.CONFIGURATION_MANAGEMENT_SKIP_SETUP === '1') {
213
+ return
214
+ }
215
+
216
+ const projectRoot = resolveProjectRoot()
217
+ if (!projectRoot) {
218
+ console.log(
219
+ '[configuration-management] No App Builder project found — skip setup. ' +
220
+ 'Run `npx configuration-management-setup` from your project root after `aio app init`.'
221
+ )
222
+ return
223
+ }
224
+
225
+ const app = setupAppConfig(projectRoot)
226
+ if (app.changed) {
227
+ console.log(
228
+ `[configuration-management] Updated app.config.yaml (${app.reason}):\n` +
229
+ ` $include: ${app.detail}`
230
+ )
231
+ }
232
+
233
+ const web = setupWebSrc(projectRoot)
234
+ for (const r of web.results || []) {
235
+ if (r.changed) {
236
+ console.log(`[configuration-management] Wrote ${r.path}`)
237
+ }
238
+ }
239
+
240
+ if (!app.changed && !web.changed) {
241
+ console.log('[configuration-management] Project already configured.')
242
+ }
243
+ }
244
+
245
+ if (require.main === module) {
246
+ try {
247
+ main()
248
+ } catch (err) {
249
+ console.error('[configuration-management] setup failed:', err.message)
250
+ process.exitCode = 1
251
+ }
252
+ }
253
+
254
+ module.exports = {
255
+ patchAppConfig,
256
+ setupAppConfig,
257
+ setupWebSrc,
258
+ INCLUDE_REL,
259
+ EXTENSION_POINT
260
+ }
@@ -0,0 +1,14 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ /* eslint-disable-next-line */
14
+ (function(e,t){if(t.location===t.parent.location)throw new Error("Module Runtime: Needs to be within an iframe!");var o=function(e){var t=new URL(e.location.href).searchParams.get("_mr");return t||!e.EXC_US_HMR?t:e.sessionStorage.getItem("unifiedShellMRScript")}(t);if(!o)throw new Error("Module Runtime: Missing script!");if("https:"!==(o=new URL(decodeURIComponent(o))).protocol)throw new Error("Module Runtime: Must be HTTPS!");if(!/^(exc-unifiedcontent\.)?experience(-qa|-stage|-cdn|-cdn-stage)?\.adobe\.(com|net)$/.test(o.hostname)&&!/localhost\.corp\.adobe\.com$/.test(o.hostname))throw new Error("Module Runtime: Invalid domain!");if(!/\.js$/.test(o.pathname))throw new Error("Module Runtime: Must be a JavaScript file!");t.EXC_US_HMR&&t.sessionStorage.setItem("unifiedShellMRScript",o.toString());var n=e.createElement("script");n.async=1,n.src=o.toString(),n.onload=n.onreadystatechange=function(){n.readyState&&!/loaded|complete/.test(n.readyState)||(n.onload=n.onreadystatechange=null,n=void 0,"EXC_MR_READY"in t&&t.EXC_MR_READY())},e.head.appendChild(n)})(document,window);
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
6
+ <meta name="theme-color" content="#1473e6">
7
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
8
+ <link rel="icon" type="image/svg+xml" href="./favicon.svg">
9
+ <title>Sync Management</title>
10
+ </head>
11
+ <body>
12
+ <noscript>You need to enable JavaScript to run this app.</noscript>
13
+ <div id="root"></div>
14
+ <script src="./src/index.js" async="true" type="module"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,61 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ */
7
+ // configuration-management: auto-generated bootstrap (updated on npm install)
8
+
9
+ import 'core-js/stable'
10
+ import 'regenerator-runtime/runtime'
11
+
12
+ import React from 'react'
13
+ import { createRoot } from 'react-dom/client'
14
+
15
+ import Runtime, { init } from '@adobe/exc-app'
16
+ import { ConfigurationManagementApp as App, configureWeb } from 'configuration-management/web'
17
+ import actions from './config.json'
18
+ import 'configuration-management/web/styles.css'
19
+
20
+ configureWeb({ actionUrls: actions })
21
+
22
+ window.React = React
23
+
24
+ try {
25
+ require('./exc-runtime')
26
+ init(bootstrapInExcShell)
27
+ } catch (e) {
28
+ console.log('application not running in Adobe Experience Cloud Shell')
29
+ bootstrapRaw()
30
+ }
31
+
32
+ function renderApp (runtime, ims) {
33
+ createRoot(document.getElementById('root')).render(
34
+ React.createElement(App, { runtime, ims })
35
+ )
36
+ }
37
+
38
+ function bootstrapRaw () {
39
+ renderApp({ on: () => {} }, {})
40
+ }
41
+
42
+ function bootstrapInExcShell () {
43
+ const runtime = Runtime()
44
+ runtime.favicon = './favicon.svg'
45
+
46
+ runtime.on('ready', ({ imsOrg, imsToken, imsProfile }) => {
47
+ runtime.done()
48
+ renderApp(runtime, {
49
+ profile: imsProfile,
50
+ org: imsOrg,
51
+ token: imsToken
52
+ })
53
+ })
54
+
55
+ runtime.solution = {
56
+ icon: 'AdobeExperienceCloud',
57
+ title: 'Sync Management',
58
+ shortTitle: 'Sync'
59
+ }
60
+ runtime.title = 'Sync Management'
61
+ }