scjson 0.1.7 → 0.1.8

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/browser.cjs ADDED
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Agent Name: js-cli
3
+ *
4
+ * Part of the scjson project.
5
+ * Developed by Softoboros Technology Inc.
6
+ * Licensed under the BSD 1-Clause License.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { Command } = require('commander');
12
+ const { XMLParser, XMLBuilder } = require('fast-xml-parser');
13
+ const Ajv = require('ajv');
14
+
15
+ const program = new Command();
16
+ const schema = require('./scjson.schema.json');
17
+
18
+ /**
19
+ * Remove nulls and empty containers from values recursively.
20
+ *
21
+ * @param {*} value - Candidate value.
22
+ * @returns {*} Sanitised value.
23
+ */
24
+ function removeEmpty(value) {
25
+ if (Array.isArray(value)) {
26
+ const arr = value.map(removeEmpty).filter(v => v !== undefined);
27
+ return arr.length > 0 ? arr : undefined;
28
+ }
29
+ if (value && typeof value === 'object') {
30
+ const obj = {};
31
+ for (const [k, v] of Object.entries(value)) {
32
+ const r = removeEmpty(v);
33
+ if (r !== undefined) obj[k] = r;
34
+ }
35
+ return Object.keys(obj).length > 0 ? obj : undefined;
36
+ }
37
+ if (value === null) {
38
+ return undefined;
39
+ }
40
+ if (value === '') {
41
+ return undefined;
42
+ }
43
+ return value;
44
+ }
45
+
46
+ const ajv = new Ajv({ useDefaults: true, strict: false });
47
+ const validate = ajv.compile(schema);
48
+
49
+ /**
50
+ * Convert an SCXML string to scjson.
51
+ *
52
+ * @param {string} xmlStr - XML input.
53
+ * @param {boolean} [omitEmpty=true] - Remove empty values when true.
54
+ * @returns {string} JSON representation.
55
+ *
56
+ * Removes the XML namespace attribute and injects default values
57
+ * expected by the schema.
58
+ */
59
+ function xmlToJson(xmlStr, omitEmpty = true) {
60
+ const parser = new XMLParser({ ignoreAttributes: false });
61
+ let obj = parser.parse(xmlStr);
62
+ if (obj.scxml) {
63
+ obj = obj.scxml;
64
+ }
65
+ if (omitEmpty) {
66
+ obj = removeEmpty(obj) || {};
67
+ }
68
+ if (obj['@_xmlns']) {
69
+ delete obj['@_xmlns'];
70
+ }
71
+ if (obj.version === undefined) {
72
+ obj.version = 1.0;
73
+ }
74
+ if (obj.datamodel_attribute === undefined) {
75
+ obj.datamodel_attribute = 'null';
76
+ }
77
+ if (!validate(obj)) {
78
+ throw new Error('Invalid scjson');
79
+ }
80
+ if (omitEmpty) {
81
+ obj = removeEmpty(obj) || {};
82
+ }
83
+ return JSON.stringify(obj, null, 2);
84
+ }
85
+
86
+ /**
87
+ * Convert a scjson string to SCXML.
88
+ *
89
+ * @param {string} jsonStr - JSON input.
90
+ * @returns {string} XML output.
91
+ */
92
+ function jsonToXml(jsonStr) {
93
+ const builder = new XMLBuilder({ ignoreAttributes: false, format: true });
94
+ const obj = JSON.parse(jsonStr);
95
+ if (!validate(obj)) {
96
+ throw new Error('Invalid scjson');
97
+ }
98
+ return builder.build({ scxml: obj });
99
+ }
100
+
101
+ program
102
+ .name('scjson')
103
+ .description('SCXML <-> scjson converter and validator');
104
+
105
+ program
106
+ .command('validate')
107
+ .description('validate a scjson or SCXML file by round-tripping it')
108
+ .argument('<file>', 'file path')
109
+ .option('-r, --recursive', 'recurse into directories')
110
+ .action((file, options) => {
111
+ const src = path.resolve(file);
112
+ let success = true;
113
+
114
+ function validateFile(p) {
115
+ const data = fs.readFileSync(p, 'utf8');
116
+ try {
117
+ if (p.endsWith('.scxml')) {
118
+ const json = xmlToJson(data);
119
+ jsonToXml(json);
120
+ } else if (p.endsWith('.scjson')) {
121
+ const xml = jsonToXml(data);
122
+ xmlToJson(xml);
123
+ } else {
124
+ return;
125
+ }
126
+ } catch (e) {
127
+ console.error(`Validation failed for ${p}: ${e.message}`);
128
+ success = false;
129
+ }
130
+ }
131
+
132
+ if (fs.statSync(src).isDirectory()) {
133
+ const pattern = options.recursive ? '**/*' : '*';
134
+ const files = require('glob').sync(pattern, { cwd: src, nodir: true });
135
+ files.forEach(f => validateFile(path.join(src, f)));
136
+ } else {
137
+ validateFile(src);
138
+ }
139
+
140
+ if (!success) {
141
+ process.exitCode = 1;
142
+ }
143
+ });
144
+
145
+ function convertDirectoryJson(inputDir, outputDir, recursive, verify, keepEmpty) {
146
+ const pattern = recursive ? '**/*.scxml' : '*.scxml';
147
+ const files = require('glob').sync(pattern, { cwd: inputDir, nodir: true });
148
+ files.forEach(f => {
149
+ const src = path.join(inputDir, f);
150
+ const dest = path.join(outputDir, f.replace(/\.scxml$/, '.scjson'));
151
+ convertScxmlFile(src, dest, verify, keepEmpty);
152
+ });
153
+ }
154
+
155
+ function convertDirectoryXml(inputDir, outputDir, recursive, verify, keepEmpty) {
156
+ const pattern = recursive ? '**/*.scjson' : '*.scjson';
157
+ const files = require('glob').sync(pattern, { cwd: inputDir, nodir: true });
158
+ files.forEach(f => {
159
+ const src = path.join(inputDir, f);
160
+ const dest = path.join(outputDir, f.replace(/\.scjson$/, '.scxml'));
161
+ convertScjsonFile(src, dest, verify, keepEmpty);
162
+ });
163
+ }
164
+
165
+ function convertScxmlFile(src, dest, verify, keepEmpty) {
166
+ const xmlStr = fs.readFileSync(src, 'utf8');
167
+ try {
168
+ const jsonStr = xmlToJson(xmlStr, !keepEmpty);
169
+ if (verify) {
170
+ jsonToXml(jsonStr);
171
+ } else {
172
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
173
+ fs.writeFileSync(dest, jsonStr);
174
+ }
175
+ if (verify) console.log(`Verified ${src}`);
176
+ } catch (e) {
177
+ console.error(`Failed to convert ${src}: ${e.message}`);
178
+ }
179
+ }
180
+
181
+ function convertScjsonFile(src, dest, verify) {
182
+ const jsonStr = fs.readFileSync(src, 'utf8');
183
+ try {
184
+ const xmlStr = jsonToXml(jsonStr);
185
+ if (verify) {
186
+ xmlToJson(xmlStr);
187
+ } else {
188
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
189
+ fs.writeFileSync(dest, xmlStr);
190
+ }
191
+ if (verify) console.log(`Verified ${src}`);
192
+ } catch (e) {
193
+ console.error(`Failed to convert ${src}: ${e.message}`);
194
+ }
195
+ }
196
+
197
+ program
198
+ .command('json')
199
+ .argument('<path>', 'SCXML file or directory')
200
+ .option('-o, --output <path>', 'output file or directory')
201
+ .option('-r, --recursive', 'recurse into directories')
202
+ .option('-v, --verify', 'verify conversion without writing output')
203
+ .option('--keep-empty', 'keep null or empty items when producing JSON')
204
+ .action((p, opts) => {
205
+ const src = path.resolve(p);
206
+ const out = opts.output ? path.resolve(opts.output) : src;
207
+
208
+ if (fs.statSync(src).isDirectory()) {
209
+ convertDirectoryJson(src, out, opts.recursive, opts.verify, opts.keepEmpty);
210
+ } else {
211
+ const dest = opts.output && !opts.output.endsWith('.json') && !opts.output.endsWith('.scjson')
212
+ ? path.join(out, path.basename(src).replace(/\.scxml$/, '.scjson'))
213
+ : (opts.output || src.replace(/\.scxml$/, '.scjson'));
214
+ convertScxmlFile(src, dest, opts.verify, opts.keepEmpty);
215
+ }
216
+ });
217
+
218
+ program
219
+ .command('xml')
220
+ .argument('<path>', 'scjson file or directory')
221
+ .option('-o, --output <path>', 'output file or directory')
222
+ .option('-r, --recursive', 'recurse into directories')
223
+ .option('-v, --verify', 'verify conversion without writing output')
224
+ .option('--keep-empty', 'keep null or empty items when producing JSON')
225
+ .action((p, opts) => {
226
+ const src = path.resolve(p);
227
+ const out = opts.output ? path.resolve(opts.output) : src;
228
+
229
+ if (fs.statSync(src).isDirectory()) {
230
+ convertDirectoryXml(src, out, opts.recursive, opts.verify, opts.keepEmpty);
231
+ } else {
232
+ const dest = opts.output && !opts.output.endsWith('.xml') && !opts.output.endsWith('.scxml')
233
+ ? path.join(out, path.basename(src).replace(/\.scjson$/, '.scxml'))
234
+ : (opts.output || src.replace(/\.scjson$/, '.scxml'));
235
+ convertScjsonFile(src, dest, opts.verify, opts.keepEmpty);
236
+ }
237
+ });
238
+
239
+ if (require.main === module) {
240
+ program.parse(process.argv);
241
+ }
242
+
243
+ module.exports = { program, xmlToJson, jsonToXml };
package/browser.mjs CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { XMLParser, XMLBuilder } from 'fast-xml-parser';
14
14
  import Ajv from 'ajv';
15
- import schema from '../scjson.schema.json' assert { type: 'json' };
15
+ import schema from './scjson.schema.json' assert { type: 'json' };
16
16
 
17
17
  const ajv = new Ajv({ useDefaults: true, strict: false });
18
18
  const validate = ajv.compile(schema);
package/index.js CHANGED
@@ -13,7 +13,7 @@ const { XMLParser, XMLBuilder } = require('fast-xml-parser');
13
13
  const Ajv = require('ajv');
14
14
 
15
15
  const program = new Command();
16
- const schema = require('../scjson.schema.json');
16
+ const schema = require('./scjson.schema.json');
17
17
 
18
18
  /**
19
19
  * Remove nulls and empty containers from values recursively.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scjson",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "A JSON-based serialization of SCXML (State Chart XML) for modern tooling, interoperability, and education.",
5
5
  "keywords": [
6
6
  "scjson",
@@ -31,7 +31,10 @@
31
31
  "type": "commonjs",
32
32
  "exports": {
33
33
  ".": "./index.js",
34
- "./browser": "./browser.mjs"
34
+ "./browser": {
35
+ "import": "./browser.mjs",
36
+ "require": "./browser.cjs"
37
+ }
35
38
  },
36
39
  "typesVersions": {
37
40
  "*": {
@@ -44,8 +47,10 @@
44
47
  "bin/",
45
48
  "index.js",
46
49
  "browser.mjs",
50
+ "browser.cjs",
47
51
  "types/scjson-browser.d.ts",
48
52
  "tests/",
53
+ "scjson.schema.json",
49
54
  "README.md",
50
55
  "LEGAL.md",
51
56
  "LICENSE"