flex-md 1.0.0
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 +36 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +151 -0
- package/dist/stringify.d.ts +2 -0
- package/dist/stringify.js +110 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.js +64 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# flex-md
|
|
2
|
+
|
|
3
|
+
Parse and stringify **FlexMD Frames**: semi-structured Markdown that is easy for humans + deterministic for machines.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i flex-md
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { parseFlexMd, stringifyFlexMd } from "flex-md";
|
|
15
|
+
|
|
16
|
+
const md = `[[message role=user id=m1]]
|
|
17
|
+
@tags: auth, login
|
|
18
|
+
Hello
|
|
19
|
+
|
|
20
|
+
@payload:name: input
|
|
21
|
+
\`\`\`json
|
|
22
|
+
{"a":1}
|
|
23
|
+
\`\`\`
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const doc = parseFlexMd(md);
|
|
27
|
+
console.log(doc.frames[0]?.type); // "message"
|
|
28
|
+
|
|
29
|
+
const back = stringifyFlexMd(doc, { skipEmpty: true });
|
|
30
|
+
console.log(back);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Notes
|
|
34
|
+
- Frames start with: `[[type key=value ...]]`
|
|
35
|
+
- Metadata lines: `@key: value`
|
|
36
|
+
- Payload binding: `@payload:name: X` then the next fenced block is payload X
|
package/dist/index.cjs
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/parser.d.ts
ADDED
package/dist/parser.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { splitTokensPreservingQuotes, unquote } from "./util.js";
|
|
2
|
+
const HEADER_RE = /^\[\[(.+)\]\]\s*$/;
|
|
3
|
+
const META_RE = /^@([^:]+):\s*(.*)$/;
|
|
4
|
+
const PAYLOAD_DECL_RE = /^@payload:name:\s*(.+)\s*$/;
|
|
5
|
+
function parseHeader(inner) {
|
|
6
|
+
// Supports:
|
|
7
|
+
// 1) [[message role=user id=m1 ts=...]]
|
|
8
|
+
// 2) shorthand: [[user m1]] => type=message role=user id=m1
|
|
9
|
+
const tokens = splitTokensPreservingQuotes(inner.trim()).map(unquote);
|
|
10
|
+
if (tokens.length === 0)
|
|
11
|
+
return { type: "message" };
|
|
12
|
+
// shorthand: [[user m1]] or [[assistant m2]]
|
|
13
|
+
if (tokens.length === 2 && !tokens[0].includes("=") && !tokens[1].includes("=")) {
|
|
14
|
+
return { type: "message", role: tokens[0], id: tokens[1] };
|
|
15
|
+
}
|
|
16
|
+
const type = tokens[0];
|
|
17
|
+
const out = { type };
|
|
18
|
+
for (const t of tokens.slice(1)) {
|
|
19
|
+
const idx = t.indexOf("=");
|
|
20
|
+
if (idx <= 0)
|
|
21
|
+
continue;
|
|
22
|
+
const key = t.slice(0, idx).trim();
|
|
23
|
+
const val = unquote(t.slice(idx + 1).trim());
|
|
24
|
+
out[key] = val;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function parseMetaValue(key, value, arrayKeys) {
|
|
29
|
+
const v = value.trim();
|
|
30
|
+
if (!arrayKeys.has(key))
|
|
31
|
+
return v;
|
|
32
|
+
// comma-separated
|
|
33
|
+
const parts = v
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((p) => p.trim())
|
|
36
|
+
.filter((p) => p.length > 0);
|
|
37
|
+
return parts;
|
|
38
|
+
}
|
|
39
|
+
function tryParsePayload(lang, raw) {
|
|
40
|
+
const l = (lang ?? "").toLowerCase();
|
|
41
|
+
if (l === "json") {
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(raw);
|
|
44
|
+
return { lang, value, raw };
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return { lang, value: raw, raw, parseError: String(e?.message ?? e) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { lang, value: raw, raw };
|
|
51
|
+
}
|
|
52
|
+
export function parseFlexMd(input, options = {}) {
|
|
53
|
+
const arrayKeys = new Set((options.arrayMetaKeys ?? ["tags", "refs"]).map((s) => s.trim()));
|
|
54
|
+
const lines = input.split("\n");
|
|
55
|
+
const endsWithNewline = input.endsWith("\n");
|
|
56
|
+
const frames = [];
|
|
57
|
+
let cur = null;
|
|
58
|
+
// body accumulator for current frame (excluding meta and payload blocks)
|
|
59
|
+
let bodyLines = [];
|
|
60
|
+
// payload state: if declared, next code fence becomes its payload
|
|
61
|
+
let pendingPayloadName = null;
|
|
62
|
+
function flushCurrent() {
|
|
63
|
+
if (!cur)
|
|
64
|
+
return;
|
|
65
|
+
const body = bodyLines.join("\n");
|
|
66
|
+
// Preserve original trailing newline behavior inside body (best-effort):
|
|
67
|
+
cur.body_md = body.length ? body + "\n" : "";
|
|
68
|
+
// Trim to empty if it's only whitespace/newlines
|
|
69
|
+
if (cur.body_md.trim().length === 0)
|
|
70
|
+
delete cur.body_md;
|
|
71
|
+
frames.push(cur);
|
|
72
|
+
cur = null;
|
|
73
|
+
bodyLines = [];
|
|
74
|
+
pendingPayloadName = null;
|
|
75
|
+
}
|
|
76
|
+
let i = 0;
|
|
77
|
+
while (i < lines.length) {
|
|
78
|
+
const line = lines[i];
|
|
79
|
+
// Frame header
|
|
80
|
+
const hm = line.match(HEADER_RE);
|
|
81
|
+
if (hm) {
|
|
82
|
+
flushCurrent();
|
|
83
|
+
cur = { ...parseHeader(hm[1]) };
|
|
84
|
+
i++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// If we haven't seen a header yet, start an implicit frame.
|
|
88
|
+
cur ??= { type: "message" };
|
|
89
|
+
// Payload declaration
|
|
90
|
+
const pm = line.match(PAYLOAD_DECL_RE);
|
|
91
|
+
if (pm) {
|
|
92
|
+
pendingPayloadName = pm[1].trim();
|
|
93
|
+
i++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// If a payload was declared, capture the next fenced code block
|
|
97
|
+
if (pendingPayloadName) {
|
|
98
|
+
const fenceStart = line.match(/^(```|~~~)\s*([A-Za-z0-9_-]+)?\s*$/);
|
|
99
|
+
if (fenceStart) {
|
|
100
|
+
const fence = fenceStart[1];
|
|
101
|
+
const lang = fenceStart[2]?.trim();
|
|
102
|
+
const rawLines = [];
|
|
103
|
+
i++;
|
|
104
|
+
while (i < lines.length && lines[i].trimEnd() !== fence) {
|
|
105
|
+
rawLines.push(lines[i]);
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
// consume closing fence if present
|
|
109
|
+
if (i < lines.length && lines[i].trimEnd() === fence)
|
|
110
|
+
i++;
|
|
111
|
+
const raw = rawLines.join("\n");
|
|
112
|
+
cur.payloads ??= {};
|
|
113
|
+
cur.payloads[pendingPayloadName] = tryParsePayload(lang, raw);
|
|
114
|
+
pendingPayloadName = null;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// payload declared but no fence; treat as body line
|
|
119
|
+
bodyLines.push(line);
|
|
120
|
+
pendingPayloadName = null;
|
|
121
|
+
i++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Metadata line
|
|
126
|
+
const mm = line.match(META_RE);
|
|
127
|
+
if (mm) {
|
|
128
|
+
const key = mm[1].trim();
|
|
129
|
+
const value = mm[2] ?? "";
|
|
130
|
+
cur.meta ??= {};
|
|
131
|
+
cur.meta[key] = parseMetaValue(key, value, arrayKeys);
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Default: part of body
|
|
136
|
+
bodyLines.push(line);
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
flushCurrent();
|
|
140
|
+
// Preserve overall trailing newline more closely: if input had no final newline,
|
|
141
|
+
// avoid forcing one by trimming last frame body newline.
|
|
142
|
+
if (!endsWithNewline && frames.length > 0) {
|
|
143
|
+
const last = frames[frames.length - 1];
|
|
144
|
+
if (typeof last.body_md === "string" && last.body_md.endsWith("\n")) {
|
|
145
|
+
last.body_md = last.body_md.slice(0, -1);
|
|
146
|
+
if (last.body_md.trim().length === 0)
|
|
147
|
+
delete last.body_md;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { frames };
|
|
151
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isBlank, isEmptyObject, quoteIfNeeded } from "./util.js";
|
|
2
|
+
function metaValueToString(key, v, arrayKeys) {
|
|
3
|
+
if (v === null)
|
|
4
|
+
return null;
|
|
5
|
+
if (Array.isArray(v)) {
|
|
6
|
+
if (v.length === 0)
|
|
7
|
+
return null;
|
|
8
|
+
// Render arrays as comma-separated by default for known keys, otherwise JSON
|
|
9
|
+
if (arrayKeys.has(key))
|
|
10
|
+
return v.join(", ");
|
|
11
|
+
return JSON.stringify(v);
|
|
12
|
+
}
|
|
13
|
+
if (typeof v === "string")
|
|
14
|
+
return v;
|
|
15
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
16
|
+
return String(v);
|
|
17
|
+
// Fallback
|
|
18
|
+
return String(v);
|
|
19
|
+
}
|
|
20
|
+
function shouldSkipMetaValue(v) {
|
|
21
|
+
if (v === null || v === undefined)
|
|
22
|
+
return true;
|
|
23
|
+
if (typeof v === "string" && v.trim() === "")
|
|
24
|
+
return true;
|
|
25
|
+
if (Array.isArray(v) && v.length === 0)
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function buildHeader(frame) {
|
|
30
|
+
// [[type key=value ...]]
|
|
31
|
+
const parts = [`[[${frame.type}`];
|
|
32
|
+
// include common attrs if present (role/id/ts) and any other top-level string attrs
|
|
33
|
+
const knownOrder = ["role", "id", "ts"];
|
|
34
|
+
for (const k of knownOrder) {
|
|
35
|
+
const val = frame[k];
|
|
36
|
+
if (typeof val === "string" && val.trim().length) {
|
|
37
|
+
parts.push(`${String(k)}=${quoteIfNeeded(val)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// If the user adds extra header-like attributes, you can support them by convention:
|
|
41
|
+
// we intentionally do NOT auto-include unknown keys to keep JSON schema clean.
|
|
42
|
+
return parts.join(" ") + "]]";
|
|
43
|
+
}
|
|
44
|
+
export function stringifyFlexMd(doc, options = {}) {
|
|
45
|
+
const skipEmpty = options.skipEmpty ?? true;
|
|
46
|
+
const fence = options.fence ?? "```";
|
|
47
|
+
const arrayKeys = new Set((options.arrayMetaKeys ?? ["tags", "refs"]).map((s) => s.trim()));
|
|
48
|
+
const out = [];
|
|
49
|
+
for (let idx = 0; idx < doc.frames.length; idx++) {
|
|
50
|
+
const frame = doc.frames[idx];
|
|
51
|
+
out.push(buildHeader(frame));
|
|
52
|
+
// META
|
|
53
|
+
if (!isEmptyObject(frame.meta)) {
|
|
54
|
+
const keys = Object.keys(frame.meta ?? {});
|
|
55
|
+
for (const k of keys) {
|
|
56
|
+
const v = frame.meta[k];
|
|
57
|
+
if (skipEmpty && shouldSkipMetaValue(v))
|
|
58
|
+
continue;
|
|
59
|
+
const rendered = metaValueToString(k, v, arrayKeys);
|
|
60
|
+
if (skipEmpty && (rendered === null || rendered.trim() === ""))
|
|
61
|
+
continue;
|
|
62
|
+
out.push(`@${k}: ${rendered ?? ""}`.trimEnd());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// BODY
|
|
66
|
+
if (typeof frame.body_md === "string") {
|
|
67
|
+
const body = frame.body_md;
|
|
68
|
+
if (!(skipEmpty && isBlank(body))) {
|
|
69
|
+
// Keep body verbatim, but avoid double-blanking: ensure we don't accidentally
|
|
70
|
+
// merge with next header by always preserving its internal newlines.
|
|
71
|
+
const normalized = body.endsWith("\n") ? body.slice(0, -1) : body;
|
|
72
|
+
if (normalized.length > 0)
|
|
73
|
+
out.push(normalized);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// PAYLOADS
|
|
77
|
+
const payloads = frame.payloads ?? {};
|
|
78
|
+
const payloadNames = Object.keys(payloads);
|
|
79
|
+
if (!(skipEmpty && payloadNames.length === 0)) {
|
|
80
|
+
for (const name of payloadNames) {
|
|
81
|
+
const p = payloads[name];
|
|
82
|
+
// Skip empty payload if requested
|
|
83
|
+
if (skipEmpty &&
|
|
84
|
+
(p == null ||
|
|
85
|
+
(typeof p.raw === "string" && p.raw.trim() === "") ||
|
|
86
|
+
(p.value == null && (p.raw ?? "").trim() === ""))) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
out.push(`@payload:name: ${name}`);
|
|
90
|
+
const lang = (p.lang ?? "").trim();
|
|
91
|
+
const header = lang ? `${fence}${lang}` : fence;
|
|
92
|
+
out.push(header);
|
|
93
|
+
// Prefer raw if present; if missing raw but value exists, serialize value
|
|
94
|
+
const raw = typeof p.raw === "string" && p.raw.length > 0
|
|
95
|
+
? p.raw
|
|
96
|
+
: p.value !== undefined
|
|
97
|
+
? typeof p.value === "string"
|
|
98
|
+
? p.value
|
|
99
|
+
: JSON.stringify(p.value, null, 2)
|
|
100
|
+
: "";
|
|
101
|
+
out.push(raw);
|
|
102
|
+
out.push(fence);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Frame separator: blank line between frames (readable) unless last
|
|
106
|
+
if (idx !== doc.frames.length - 1)
|
|
107
|
+
out.push("");
|
|
108
|
+
}
|
|
109
|
+
return out.join("\n") + "\n";
|
|
110
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type FlexMetaValue = string | string[] | number | boolean | null;
|
|
2
|
+
export interface FlexPayload {
|
|
3
|
+
/** Language from the fenced block, e.g. "json", "yaml", "text" */
|
|
4
|
+
lang?: string;
|
|
5
|
+
/** Parsed value for json fences; otherwise a string raw value */
|
|
6
|
+
value: unknown;
|
|
7
|
+
/** Raw text inside the fence (no surrounding ``` lines) */
|
|
8
|
+
raw: string;
|
|
9
|
+
/** If lang=json but parsing failed */
|
|
10
|
+
parseError?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FlexFrame {
|
|
13
|
+
type: string;
|
|
14
|
+
role?: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
ts?: string;
|
|
17
|
+
/** Metadata from @key: lines */
|
|
18
|
+
meta?: Record<string, FlexMetaValue>;
|
|
19
|
+
/** Body markdown between header/meta and next frame header */
|
|
20
|
+
body_md?: string;
|
|
21
|
+
/** Payloads keyed by @payload:name: X */
|
|
22
|
+
payloads?: Record<string, FlexPayload>;
|
|
23
|
+
}
|
|
24
|
+
export interface FlexDocument {
|
|
25
|
+
frames: FlexFrame[];
|
|
26
|
+
}
|
|
27
|
+
export interface ParseOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Which metadata keys should be split by commas into string[]
|
|
30
|
+
* Default: ["tags", "refs"]
|
|
31
|
+
*/
|
|
32
|
+
arrayMetaKeys?: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface StringifyOptions {
|
|
35
|
+
/**
|
|
36
|
+
* If true, omit empty sections when converting JSON -> FlexMD.
|
|
37
|
+
* Default: true
|
|
38
|
+
*/
|
|
39
|
+
skipEmpty?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Which meta keys should be rendered as comma-separated lists if arrays.
|
|
42
|
+
* Default: ["tags", "refs"]
|
|
43
|
+
*/
|
|
44
|
+
arrayMetaKeys?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Preferred fence marker. Default: "```"
|
|
47
|
+
*/
|
|
48
|
+
fence?: "```" | "~~~";
|
|
49
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function isBlank(s: unknown): boolean;
|
|
2
|
+
export declare function isEmptyObject(o: unknown): boolean;
|
|
3
|
+
export declare function splitTokensPreservingQuotes(input: string): string[];
|
|
4
|
+
export declare function unquote(s: string): string;
|
|
5
|
+
export declare function quoteIfNeeded(value: string): string;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function isBlank(s) {
|
|
2
|
+
return typeof s !== "string" || s.trim().length === 0;
|
|
3
|
+
}
|
|
4
|
+
export function isEmptyObject(o) {
|
|
5
|
+
if (!o || typeof o !== "object")
|
|
6
|
+
return true;
|
|
7
|
+
return Object.keys(o).length === 0;
|
|
8
|
+
}
|
|
9
|
+
export function splitTokensPreservingQuotes(input) {
|
|
10
|
+
// Splits by whitespace, but keeps "quoted strings" and 'quoted strings'
|
|
11
|
+
// Example: key="hello world" -> single token
|
|
12
|
+
const tokens = [];
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < input.length) {
|
|
15
|
+
while (i < input.length && /\s/.test(input[i]))
|
|
16
|
+
i++;
|
|
17
|
+
if (i >= input.length)
|
|
18
|
+
break;
|
|
19
|
+
const ch = input[i];
|
|
20
|
+
if (ch === '"' || ch === "'") {
|
|
21
|
+
const quote = ch;
|
|
22
|
+
i++;
|
|
23
|
+
let buf = "";
|
|
24
|
+
while (i < input.length) {
|
|
25
|
+
const c = input[i];
|
|
26
|
+
if (c === "\\" && i + 1 < input.length) {
|
|
27
|
+
// minimal escape support
|
|
28
|
+
buf += input[i + 1];
|
|
29
|
+
i += 2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (c === quote) {
|
|
33
|
+
i++;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
buf += c;
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
tokens.push(quote + buf + quote);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// unquoted token
|
|
43
|
+
let start = i;
|
|
44
|
+
while (i < input.length && !/\s/.test(input[i]))
|
|
45
|
+
i++;
|
|
46
|
+
tokens.push(input.slice(start, i));
|
|
47
|
+
}
|
|
48
|
+
return tokens;
|
|
49
|
+
}
|
|
50
|
+
export function unquote(s) {
|
|
51
|
+
const t = s.trim();
|
|
52
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
53
|
+
return t.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
return t;
|
|
56
|
+
}
|
|
57
|
+
export function quoteIfNeeded(value) {
|
|
58
|
+
// Only quote if whitespace or special chars likely to break header tokenization
|
|
59
|
+
if (/[\s\]]/.test(value) || value.includes('"') || value.includes("'")) {
|
|
60
|
+
// Use JSON-style quoting for safety
|
|
61
|
+
return JSON.stringify(value);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flex-md",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Parse and stringify FlexMD Frames (semi-structured Markdown) to/from JSON.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "",
|
|
7
|
+
"keywords": ["markdown", "parser", "serialization", "llm", "prompting", "semi-structured"],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": ["dist"],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc && node ./scripts/cjs-bridge.mjs",
|
|
22
|
+
"test": "node ./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.6.3"
|
|
26
|
+
}
|
|
27
|
+
}
|