@gxp-dev/tools 2.0.71 → 2.0.72

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.
@@ -0,0 +1,284 @@
1
+ /**
2
+ * GxP JSON Config Linter
3
+ *
4
+ * Validates `configuration.json` and `app-manifest.json` against JSON Schemas
5
+ * derived from the GxP templating system documentation. Schemas live in
6
+ * ./schemas/ and are composed via $ref.
7
+ *
8
+ * Public API:
9
+ * - detectSchema(filePath): pick the correct schema for a file, or null.
10
+ * - lintFile(filePath): return { file, ok, errors[] }.
11
+ * - lintFiles(files): return aggregated results.
12
+ */
13
+
14
+ const fs = require("fs")
15
+ const path = require("path")
16
+ const Ajv = require("ajv/dist/2020")
17
+ const addFormats = require("ajv-formats")
18
+
19
+ const SCHEMA_DIR = path.join(__dirname, "schemas")
20
+
21
+ const SCHEMA_FILES = [
22
+ "common.schema.json",
23
+ "field.schema.json",
24
+ "card.schema.json",
25
+ "configuration.schema.json",
26
+ "app-manifest.schema.json",
27
+ ]
28
+
29
+ let cachedAjv = null
30
+
31
+ function getAjv() {
32
+ if (cachedAjv) {
33
+ return cachedAjv
34
+ }
35
+ const ajv = new Ajv({
36
+ allErrors: true,
37
+ strict: false,
38
+ allowUnionTypes: true,
39
+ verbose: true,
40
+ })
41
+ addFormats(ajv)
42
+
43
+ for (const fileName of SCHEMA_FILES) {
44
+ const schemaPath = path.join(SCHEMA_DIR, fileName)
45
+ const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"))
46
+ // Reference schemas by their bare filename (how siblings $ref each other).
47
+ ajv.addSchema(schema, fileName)
48
+ }
49
+
50
+ cachedAjv = ajv
51
+ return ajv
52
+ }
53
+
54
+ /**
55
+ * Given a file path, decide which root schema should validate it.
56
+ * Matches exact filenames `configuration.json` / `app-manifest.json` and also
57
+ * suffixed variants like `broken-configuration.json` so users can keep multiple
58
+ * samples around during development. Returns the schema filename or null.
59
+ */
60
+ function detectSchema(filePath) {
61
+ const base = path.basename(filePath)
62
+ if (base === "configuration.json" || /configuration\.json$/i.test(base)) {
63
+ return "configuration.schema.json"
64
+ }
65
+ if (base === "app-manifest.json" || /app-manifest\.json$/i.test(base)) {
66
+ return "app-manifest.schema.json"
67
+ }
68
+ return null
69
+ }
70
+
71
+ /**
72
+ * Walk a JSON document by dotted/bracketed pointer to locate a value.
73
+ * Falls back to the document start if the path is unresolvable — good enough
74
+ * for error location heuristics; the AJV path is always printed too.
75
+ */
76
+ function locateInSource(source, instancePath) {
77
+ if (!instancePath || instancePath === "") {
78
+ return { line: 1, column: 1 }
79
+ }
80
+
81
+ // Split AJV 2020-12 JSON pointer: "/additionalTabs/0/cards/1/fieldsList/0/type"
82
+ const segments = instancePath.split("/").filter(Boolean)
83
+ let cursor = 0
84
+ let line = 1
85
+ let column = 1
86
+
87
+ // Simple forward scanner: find each segment's appearance after current cursor.
88
+ // Good enough for error pinpointing; not a full JSON parser.
89
+ for (const rawSegment of segments) {
90
+ const segment = decodeURIComponent(
91
+ rawSegment.replace(/~1/g, "/").replace(/~0/g, "~"),
92
+ )
93
+ let needle
94
+ if (/^\d+$/.test(segment)) {
95
+ // Array index — scan forward to the Nth top-level "," or "[" after cursor.
96
+ // Cheap approximation: skip.
97
+ needle = null
98
+ } else {
99
+ needle = `"${segment}"`
100
+ }
101
+ if (needle) {
102
+ const next = source.indexOf(needle, cursor)
103
+ if (next >= 0) {
104
+ cursor = next + needle.length
105
+ }
106
+ }
107
+ }
108
+
109
+ for (let i = 0; i < cursor && i < source.length; i++) {
110
+ if (source[i] === "\n") {
111
+ line++
112
+ column = 1
113
+ } else {
114
+ column++
115
+ }
116
+ }
117
+ return { line, column }
118
+ }
119
+
120
+ /**
121
+ * Lint a single file. Returns a result object; never throws for validation or
122
+ * JSON-parse errors (those become errors in the result).
123
+ */
124
+ function lintFile(filePath) {
125
+ const absPath = path.resolve(filePath)
126
+ const result = {
127
+ file: absPath,
128
+ ok: true,
129
+ skipped: false,
130
+ reason: null,
131
+ errors: [],
132
+ }
133
+
134
+ if (!fs.existsSync(absPath)) {
135
+ result.ok = false
136
+ result.errors.push({
137
+ code: "file-not-found",
138
+ message: `File not found: ${absPath}`,
139
+ line: 1,
140
+ column: 1,
141
+ instancePath: "",
142
+ })
143
+ return result
144
+ }
145
+
146
+ const schemaKey = detectSchema(absPath)
147
+ if (!schemaKey) {
148
+ result.skipped = true
149
+ result.reason = "no-schema-for-filename"
150
+ return result
151
+ }
152
+
153
+ const source = fs.readFileSync(absPath, "utf-8")
154
+ let data
155
+ try {
156
+ data = JSON.parse(source)
157
+ } catch (e) {
158
+ result.ok = false
159
+ result.errors.push({
160
+ code: "json-parse-error",
161
+ message: `Invalid JSON: ${e.message}`,
162
+ line: 1,
163
+ column: 1,
164
+ instancePath: "",
165
+ })
166
+ return result
167
+ }
168
+
169
+ const ajv = getAjv()
170
+ const validate = ajv.getSchema(schemaKey)
171
+ const valid = validate(data)
172
+ if (!valid) {
173
+ result.ok = false
174
+ const seen = new Set()
175
+ for (const err of validate.errors || []) {
176
+ // Drop AJV's meta-errors that just restate "a nested if/then failed" —
177
+ // the real cause is the nested error itself, which we also have.
178
+ if (err.keyword === "if" || err.keyword === "allOf") {
179
+ continue
180
+ }
181
+ // Collapse duplicates that AJV emits when an inner schema is reached
182
+ // via multiple schemaPaths (e.g. through if/then and directly).
183
+ const dedupeKey = `${err.keyword}|${err.instancePath}|${err.params?.missingProperty || ""}|${err.params?.additionalProperty || ""}|${err.params?.allowedValue || ""}`
184
+ if (seen.has(dedupeKey)) {
185
+ continue
186
+ }
187
+ seen.add(dedupeKey)
188
+
189
+ const { line, column } = locateInSource(source, err.instancePath)
190
+ result.errors.push({
191
+ code: err.keyword,
192
+ message: formatAjvMessage(err),
193
+ line,
194
+ column,
195
+ instancePath: err.instancePath || "/",
196
+ schemaPath: err.schemaPath,
197
+ params: err.params,
198
+ })
199
+ }
200
+ }
201
+ return result
202
+ }
203
+
204
+ function formatAjvMessage(err) {
205
+ const at = err.instancePath || "(root)"
206
+ const core = err.message || "failed validation"
207
+ const hints = []
208
+ if (err.keyword === "enum" && err.params?.allowedValues) {
209
+ hints.push(`allowed: ${err.params.allowedValues.join(", ")}`)
210
+ }
211
+ if (err.keyword === "required" && err.params?.missingProperty) {
212
+ return `${at} missing required property "${err.params.missingProperty}"`
213
+ }
214
+ if (err.keyword === "type" && err.params?.type) {
215
+ return `${at} must be ${err.params.type}`
216
+ }
217
+ if (
218
+ err.keyword === "additionalProperties" &&
219
+ err.params?.additionalProperty
220
+ ) {
221
+ return `${at} has unexpected property "${err.params.additionalProperty}"`
222
+ }
223
+ const tail = hints.length ? ` (${hints.join("; ")})` : ""
224
+ return `${at} ${core}${tail}`
225
+ }
226
+
227
+ function lintFiles(files) {
228
+ const results = files.map((f) => lintFile(f))
229
+ const summary = {
230
+ totalFiles: results.length,
231
+ filesWithErrors: results.filter((r) => !r.ok).length,
232
+ skipped: results.filter((r) => r.skipped).length,
233
+ totalErrors: results.reduce((n, r) => n + r.errors.length, 0),
234
+ }
235
+ return { results, summary }
236
+ }
237
+
238
+ /**
239
+ * Validate an already-parsed JSON value. `pathHint` is only used to pick the
240
+ * right root schema (via detectSchema). Useful for MCP tools that want to
241
+ * validate a prospective edit before touching disk.
242
+ */
243
+ function lintData(data, pathHint) {
244
+ const result = { ok: true, skipped: false, reason: null, errors: [] }
245
+ const schemaKey = detectSchema(pathHint)
246
+ if (!schemaKey) {
247
+ result.skipped = true
248
+ result.reason = "no-schema-for-filename"
249
+ return result
250
+ }
251
+
252
+ const ajv = getAjv()
253
+ const validate = ajv.getSchema(schemaKey)
254
+ const valid = validate(data)
255
+ if (!valid) {
256
+ result.ok = false
257
+ const seen = new Set()
258
+ for (const err of validate.errors || []) {
259
+ if (err.keyword === "if" || err.keyword === "allOf") continue
260
+ const dedupeKey = `${err.keyword}|${err.instancePath}|${err.params?.missingProperty || ""}|${err.params?.additionalProperty || ""}`
261
+ if (seen.has(dedupeKey)) continue
262
+ seen.add(dedupeKey)
263
+
264
+ result.errors.push({
265
+ code: err.keyword,
266
+ message: formatAjvMessage(err),
267
+ line: 1,
268
+ column: 1,
269
+ instancePath: err.instancePath || "/",
270
+ schemaPath: err.schemaPath,
271
+ params: err.params,
272
+ })
273
+ }
274
+ }
275
+ return result
276
+ }
277
+
278
+ module.exports = {
279
+ detectSchema,
280
+ lintFile,
281
+ lintFiles,
282
+ lintData,
283
+ SCHEMA_DIR,
284
+ }
@@ -0,0 +1,124 @@
1
+ {
2
+ "$id": "https://gxp.dev/schemas/app-manifest.schema.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "title": "GxP App Manifest",
5
+ "description": "Plugin metadata + default datastore values. Read by the platform at install time and by the toolkit dev server.",
6
+ "type": "object",
7
+ "required": ["name", "version"],
8
+ "properties": {
9
+ "name": {
10
+ "type": "string",
11
+ "minLength": 1,
12
+ "description": "Plugin display name."
13
+ },
14
+ "version": {
15
+ "type": "string",
16
+ "pattern": "^[0-9]+(\\.[0-9]+){0,2}([-+][A-Za-z0-9.-]+)?$",
17
+ "description": "Semver-like plugin version."
18
+ },
19
+ "description": { "type": "string" },
20
+ "manifest_version": {
21
+ "type": "integer",
22
+ "enum": [1, 2, 3]
23
+ },
24
+ "asset_dir": {
25
+ "type": "string",
26
+ "description": "Relative path to the asset directory, e.g. /src/public.",
27
+ "pattern": "^/?[A-Za-z0-9._-][A-Za-z0-9/._-]*$"
28
+ },
29
+ "configurationFile": { "type": "string" },
30
+ "appInstructionsFile": { "type": "string" },
31
+ "defaultStylingFile": { "type": "string" },
32
+ "appInstructions": { "type": "string" },
33
+ "defaultStyling": { "type": "string" },
34
+ "configuration": {
35
+ "type": ["object", "string"]
36
+ },
37
+ "settings": {
38
+ "type": "object",
39
+ "description": "Default key/value settings (pluginVars). Keys should match identifiers used in gxp-settings directives.",
40
+ "patternProperties": {
41
+ "^[A-Za-z_][A-Za-z0-9_]*$": {
42
+ "type": ["string", "number", "boolean", "null", "array", "object"]
43
+ }
44
+ },
45
+ "additionalProperties": false
46
+ },
47
+ "strings": {
48
+ "type": "object",
49
+ "description": "i18n strings. Must contain a `default` locale map; other keys are locale codes.",
50
+ "properties": {
51
+ "default": {
52
+ "type": "object",
53
+ "patternProperties": {
54
+ "^[A-Za-z_][A-Za-z0-9_]*$": { "type": "string" }
55
+ },
56
+ "additionalProperties": false
57
+ }
58
+ },
59
+ "patternProperties": {
60
+ "^[a-z]{2}(-[A-Z]{2})?$": {
61
+ "type": "object",
62
+ "additionalProperties": { "type": "string" }
63
+ }
64
+ },
65
+ "required": ["default"],
66
+ "additionalProperties": true
67
+ },
68
+ "assets": {
69
+ "type": "object",
70
+ "description": "Default asset URLs/paths keyed by gxp-src identifier.",
71
+ "patternProperties": {
72
+ "^[A-Za-z_][A-Za-z0-9_]*$": {
73
+ "$ref": "common.schema.json#/$defs/urlOrPath"
74
+ }
75
+ },
76
+ "additionalProperties": false
77
+ },
78
+ "triggerState": {
79
+ "type": "object",
80
+ "description": "Default values for dynamic state updated by sockets/CLI.",
81
+ "patternProperties": {
82
+ "^[A-Za-z_][A-Za-z0-9_]*$": {
83
+ "type": ["string", "number", "boolean", "null"]
84
+ }
85
+ },
86
+ "additionalProperties": false
87
+ },
88
+ "dependencies": {
89
+ "type": "array",
90
+ "items": {
91
+ "type": "object",
92
+ "required": ["identifier"],
93
+ "properties": {
94
+ "identifier": {
95
+ "$ref": "common.schema.json#/$defs/identifier"
96
+ },
97
+ "model": { "type": "string" },
98
+ "events": { "type": "object" }
99
+ },
100
+ "additionalProperties": true
101
+ }
102
+ },
103
+ "permissions": {
104
+ "type": "array",
105
+ "items": {
106
+ "oneOf": [
107
+ { "$ref": "common.schema.json#/$defs/identifier" },
108
+ {
109
+ "type": "object",
110
+ "required": ["identifier"],
111
+ "properties": {
112
+ "identifier": {
113
+ "$ref": "common.schema.json#/$defs/identifier"
114
+ },
115
+ "description": { "type": "string" }
116
+ },
117
+ "additionalProperties": true
118
+ }
119
+ ]
120
+ }
121
+ }
122
+ },
123
+ "additionalProperties": true
124
+ }
@@ -0,0 +1,165 @@
1
+ {
2
+ "$id": "https://gxp.dev/schemas/card.schema.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "title": "GxP Form Card",
5
+ "description": "A card inside additionalTabs.cards or a nested card_list/tabs_list.",
6
+ "type": "object",
7
+ "required": ["type"],
8
+ "properties": {
9
+ "type": {
10
+ "type": "string",
11
+ "enum": [
12
+ "fields_list",
13
+ "data_table",
14
+ "card_list",
15
+ "tabs_list",
16
+ "info_card",
17
+ "render_list",
18
+ "carousel",
19
+ "grid_view",
20
+ "component",
21
+ "data_display",
22
+ "button_group",
23
+ "form_builder"
24
+ ]
25
+ },
26
+ "title": { "type": ["string", "null"] },
27
+ "tabId": { "type": ["string", "null"] },
28
+ "cols": { "$ref": "common.schema.json#/$defs/gridCols" },
29
+ "colsMd": { "$ref": "common.schema.json#/$defs/gridCols" },
30
+ "colsLg": { "$ref": "common.schema.json#/$defs/gridCols" },
31
+ "marginBottom": { "type": "integer", "minimum": 0 },
32
+ "showCard": { "type": "boolean" },
33
+ "border": { "type": "boolean" },
34
+ "listKey": { "type": ["string", "null"] },
35
+ "condition": { "type": ["string", "null"] },
36
+ "conditionParams": {
37
+ "$ref": "common.schema.json#/$defs/conditionParams"
38
+ },
39
+ "fieldsList": {
40
+ "type": "array",
41
+ "items": { "$ref": "field.schema.json" }
42
+ },
43
+ "cards": {
44
+ "type": "array",
45
+ "items": { "$ref": "card.schema.json" }
46
+ },
47
+ "tabsList": {
48
+ "type": "array",
49
+ "items": {
50
+ "type": "object",
51
+ "required": ["title"],
52
+ "properties": {
53
+ "title": { "type": "string" },
54
+ "tabId": { "type": ["string", "null"] },
55
+ "cards": {
56
+ "type": "array",
57
+ "items": { "$ref": "card.schema.json" }
58
+ }
59
+ },
60
+ "additionalProperties": true
61
+ }
62
+ },
63
+ "buttons": {
64
+ "type": "array",
65
+ "items": {
66
+ "type": "object",
67
+ "required": ["text"],
68
+ "properties": {
69
+ "text": { "type": "string" },
70
+ "type": { "type": "string" },
71
+ "icon": { "type": ["string", "null"] },
72
+ "href": { "type": ["string", "null"] },
73
+ "action": { "type": ["string", "null"] },
74
+ "condition": { "type": ["string", "null"] },
75
+ "conditionParams": {
76
+ "$ref": "common.schema.json#/$defs/conditionParams"
77
+ }
78
+ },
79
+ "additionalProperties": true
80
+ }
81
+ },
82
+ "component": { "type": ["string", "null"] },
83
+ "componentProps": { "type": "object" },
84
+ "headers": {
85
+ "type": "array",
86
+ "items": { "type": ["string", "object"] }
87
+ },
88
+ "rows": { "type": "array" },
89
+ "options": { "type": "object" },
90
+ "tableTitle": { "type": ["string", "null"] }
91
+ },
92
+ "allOf": [
93
+ {
94
+ "if": {
95
+ "properties": { "type": { "const": "fields_list" } },
96
+ "required": ["type"]
97
+ },
98
+ "then": {
99
+ "required": ["fieldsList"],
100
+ "properties": {
101
+ "fieldsList": {
102
+ "type": "array",
103
+ "items": { "$ref": "field.schema.json" }
104
+ }
105
+ }
106
+ }
107
+ },
108
+ {
109
+ "if": {
110
+ "properties": { "type": { "const": "card_list" } },
111
+ "required": ["type"]
112
+ },
113
+ "then": {
114
+ "required": ["cards"],
115
+ "properties": {
116
+ "cards": {
117
+ "type": "array",
118
+ "minItems": 1,
119
+ "items": { "$ref": "card.schema.json" }
120
+ }
121
+ }
122
+ }
123
+ },
124
+ {
125
+ "if": {
126
+ "properties": { "type": { "const": "tabs_list" } },
127
+ "required": ["type"]
128
+ },
129
+ "then": {
130
+ "required": ["tabsList"]
131
+ }
132
+ },
133
+ {
134
+ "if": {
135
+ "properties": { "type": { "const": "button_group" } },
136
+ "required": ["type"]
137
+ },
138
+ "then": {
139
+ "required": ["buttons"]
140
+ }
141
+ },
142
+ {
143
+ "if": {
144
+ "properties": { "type": { "const": "component" } },
145
+ "required": ["type"]
146
+ },
147
+ "then": {
148
+ "required": ["component"],
149
+ "properties": {
150
+ "component": { "type": "string", "minLength": 1 }
151
+ }
152
+ }
153
+ },
154
+ {
155
+ "if": {
156
+ "properties": { "type": { "const": "data_table" } },
157
+ "required": ["type"]
158
+ },
159
+ "then": {
160
+ "anyOf": [{ "required": ["headers"] }, { "required": ["rows"] }]
161
+ }
162
+ }
163
+ ],
164
+ "additionalProperties": true
165
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "$id": "https://gxp.dev/schemas/common.schema.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "title": "GxP Common Definitions",
5
+ "description": "Shared sub-schemas referenced by card and field schemas.",
6
+ "$defs": {
7
+ "conditionParam": {
8
+ "type": "object",
9
+ "properties": {
10
+ "name": { "type": "string" },
11
+ "value": {
12
+ "type": ["string", "number", "boolean", "null"]
13
+ },
14
+ "logic": {
15
+ "type": "string",
16
+ "enum": ["==", "!=", ">", ">=", "<", "<=", "in", "not_in", "includes"]
17
+ },
18
+ "column": { "type": ["string", "null"] }
19
+ },
20
+ "required": ["name", "logic"],
21
+ "additionalProperties": true
22
+ },
23
+ "conditionParams": {
24
+ "type": "array",
25
+ "items": { "$ref": "#/$defs/conditionParam" }
26
+ },
27
+ "gridCols": {
28
+ "type": "integer",
29
+ "minimum": 1,
30
+ "maximum": 12
31
+ },
32
+ "colorHex": {
33
+ "type": "string",
34
+ "pattern": "^#(?:[0-9a-fA-F]{3}){1,2}$"
35
+ },
36
+ "urlOrPath": {
37
+ "type": "string",
38
+ "description": "Absolute URL or site-relative path",
39
+ "pattern": "^(https?:)?(//)?/?[^\\s]*$"
40
+ },
41
+ "identifier": {
42
+ "type": "string",
43
+ "pattern": "^[A-Za-z_][A-Za-z0-9_-]*$"
44
+ },
45
+ "option": {
46
+ "type": "object",
47
+ "properties": {
48
+ "label": { "type": "string" },
49
+ "value": {
50
+ "type": ["string", "number", "boolean", "null"]
51
+ },
52
+ "disabled": { "type": "boolean" }
53
+ },
54
+ "required": ["label", "value"],
55
+ "additionalProperties": true
56
+ },
57
+ "options": {
58
+ "type": "array",
59
+ "items": { "$ref": "#/$defs/option" }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$id": "https://gxp.dev/schemas/configuration.schema.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "title": "GxP Plugin Configuration Form",
5
+ "description": "Defines the configuration form shown to admins in the GxP admin panel when they install or configure a plugin. Follows the GxP templating system (ShowPage > tabs > cards > fields).",
6
+ "type": "object",
7
+ "required": ["additionalTabs"],
8
+ "properties": {
9
+ "additionalTabs": {
10
+ "type": "array",
11
+ "description": "Top-level array of tab definitions. Each item is a card (usually card_list or fields_list) rendered under the plugin's configuration panel.",
12
+ "items": { "$ref": "card.schema.json" }
13
+ },
14
+ "title": { "type": ["string", "null"] },
15
+ "description": { "type": ["string", "null"] },
16
+ "version": { "type": ["string", "integer", "null"] }
17
+ },
18
+ "additionalProperties": true
19
+ }