@cashscript/utils 0.13.0-next.8 → 0.13.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/dist/artifact.js CHANGED
@@ -1,37 +1,65 @@
1
1
  export function formatArtifact(artifact, format) {
2
2
  if (format === 'ts') {
3
- // We remove any undefined values to make the artifact serializable using stringifyAsTs
3
+ // We remove any undefined values to make the artifact serializable
4
4
  const normalisedArtifact = JSON.parse(JSON.stringify(artifact));
5
- return `export default ${stringifyAsTs(normalisedArtifact)} as const;\n`;
5
+ return `export default ${stringify(normalisedArtifact, 'ts')} as const;\n`;
6
6
  }
7
- return JSON.stringify(artifact, null, 2);
7
+ return stringify(artifact, 'json');
8
8
  }
9
9
  const indent = (level) => ' '.repeat(level);
10
- function stringifyAsTs(obj, indentationLevel = 1) {
11
- // For strings we use JSON.stringify to handle escaping, but we want to use single quotes instead of double quotes
12
- // around string values inside objects, to match regular TS style
13
- if (typeof obj === 'string') {
14
- return `'${JSON.stringify(obj).replace(/'/g, "\\'").replace(/\\"/g, '"').slice(1, -1)}'`;
15
- }
16
- // Numbers and booleans are just converted to strings
17
- if (typeof obj === 'number' || typeof obj === 'boolean') {
10
+ // Objects with this many or more properties are always expanded onto multiple lines, to keep
11
+ // generated `.artifact.ts` files compliant with the `object-curly-newline` lint rule (minProperties: 4)
12
+ const MAX_INLINE_OBJECT_PROPERTIES = 3;
13
+ // Recursively serialises an artifact to JSON or TS. Small records that are elements of an array
14
+ // (e.g. `requires`, `constructorInputs`, `abi` inputs) are kept on a single line so list-like data
15
+ // stays compact, while standalone objects assigned to a property (e.g. `compiler.options`) keep
16
+ // their values expanded one per line.
17
+ function stringify(obj, format, indentationLevel = 1, isArrayElement = false) {
18
+ if (typeof obj === 'string')
19
+ return formatString(obj, format);
20
+ // Numbers, booleans and null are represented identically in JSON and TS
21
+ if (obj === null || typeof obj === 'number' || typeof obj === 'boolean') {
18
22
  return JSON.stringify(obj);
19
23
  }
20
- // Arrays are recursively formatted with indentation
21
- if (Array.isArray(obj)) {
22
- if (obj.length === 0)
23
- return '[]';
24
- const formattedItems = obj.map((item) => `${indent(indentationLevel)}${stringifyAsTs(item, indentationLevel + 1)}`);
25
- return `[\n${formattedItems.join(',\n')},\n${indent(indentationLevel - 1)}]`;
26
- }
27
- // Objects are recursively formatted with indentation
28
- if (typeof obj === 'object') {
29
- const entries = Object.entries(obj);
30
- if (entries.length === 0)
31
- return '{}';
32
- const formattedEntries = entries.map(([key, value]) => (`${indent(indentationLevel)}${key}: ${stringifyAsTs(value, indentationLevel + 1)}`));
33
- return `{\n${formattedEntries.join(',\n')},\n${indent(indentationLevel - 1)}}`;
34
- }
24
+ if (Array.isArray(obj))
25
+ return formatArray(obj, format, indentationLevel, isArrayElement);
26
+ if (typeof obj === 'object')
27
+ return formatObject(obj, format, indentationLevel, isArrayElement);
35
28
  throw new Error(`Unsupported type: ${typeof obj}`);
36
29
  }
30
+ function formatString(value, format) {
31
+ // JSON strings use standard double-quoted escaping
32
+ if (format === 'json')
33
+ return JSON.stringify(value);
34
+ // TS strings use single quotes instead of double quotes around string values, to match regular TS style
35
+ return `'${JSON.stringify(value).replace(/'/g, "\\'").replace(/\\"/g, '"').slice(1, -1)}'`;
36
+ }
37
+ function formatArray(array, format, indentationLevel, isArrayElement) {
38
+ if (array.length === 0)
39
+ return '[]';
40
+ const items = array.map((item) => stringify(item, format, indentationLevel + 1, true));
41
+ if (isArrayElement && canInline(array))
42
+ return `[${items.join(', ')}]`;
43
+ const lines = items.map((item) => `${indent(indentationLevel)}${item}`).join(',\n');
44
+ const trailingComma = format === 'ts' ? ',' : '';
45
+ return `[\n${lines}${trailingComma}\n${indent(indentationLevel - 1)}]`;
46
+ }
47
+ function formatObject(obj, format, indentationLevel, isArrayElement) {
48
+ const entries = Object.entries(obj).filter(([, value]) => value !== undefined);
49
+ if (entries.length === 0)
50
+ return '{}';
51
+ const formatKey = (key) => (format === 'json' ? JSON.stringify(key) : key);
52
+ const formatted = entries.map(([key, value]) => `${formatKey(key)}: ${stringify(value, format, indentationLevel + 1)}`);
53
+ if (isArrayElement && entries.length <= MAX_INLINE_OBJECT_PROPERTIES && canInline(obj)) {
54
+ return `{ ${formatted.join(', ')} }`;
55
+ }
56
+ const lines = formatted.map((entry) => `${indent(indentationLevel)}${entry}`).join(',\n');
57
+ const trailingComma = format === 'ts' ? ',' : '';
58
+ return `{\n${lines}${trailingComma}\n${indent(indentationLevel - 1)}}`;
59
+ }
60
+ // A container can be inlined when none of its values are themselves a non-empty object or array
61
+ function canInline(container) {
62
+ const values = Array.isArray(container) ? container : Object.values(container);
63
+ return values.every((value) => (value === undefined || value === null || typeof value !== 'object' || Object.keys(value).length === 0));
64
+ }
37
65
  //# sourceMappingURL=artifact.js.map
@@ -1,7 +1,7 @@
1
1
  import { range } from './data.js';
2
2
  import { scriptToBitAuthAsm } from './script.js';
3
3
  import { parseSourceTags, sourceMapToLocationData } from './source-map.js';
4
- import { PositionHint } from './types.js';
4
+ import { PositionHint, SourceTagKind } from './types.js';
5
5
  export function buildLineToOpcodesMap(bytecode, sourceMapOrLocationData) {
6
6
  const locationData = typeof sourceMapOrLocationData === 'string' ? sourceMapToLocationData(sourceMapOrLocationData) : sourceMapOrLocationData;
7
7
  return locationData.reduce((lineToOpcodeMap, singleLocation, index) => {
@@ -43,14 +43,38 @@ function getDisplayLine(singleLocation) {
43
43
  function escapeCommentChars(text) {
44
44
  return text.replaceAll('/*', '\\/*').replaceAll('*/', '*\\/');
45
45
  }
46
+ // Compiler-injected prologue checks: emitted before the function body but executing at its top.
47
+ const PROLOGUE_KINDS = [
48
+ SourceTagKind.LOCKTIME_GUARD,
49
+ SourceTagKind.PARAMETER_VALIDATION,
50
+ ];
46
51
  function buildInsertions(locationData, sourceLines, sourceTags) {
47
52
  const tags = (sourceTags ? parseSourceTags(sourceTags) : []);
48
53
  return tags.map((tag) => {
49
- const annotation = deriveTagLabel(tag, locationData, sourceLines);
50
- const insertAfterLine = getDisplayLine(locationData[Math.max(tag.startIndex - 1, 0)]);
54
+ const { insertAfterLine, indent } = deriveAnchor(tag, tags, locationData, sourceLines);
55
+ const annotation = `${indent}${tagDescription(tag, locationData, sourceLines)}`;
51
56
  return { insertAfterLine, annotation, startIndex: tag.startIndex, endIndex: tag.endIndex };
52
57
  });
53
58
  }
59
+ function deriveAnchor(tag, tags, locationData, sourceLines) {
60
+ if (PROLOGUE_KINDS.includes(tag.kind)) {
61
+ const prologueTags = tags.filter((t) => PROLOGUE_KINDS.includes(t.kind));
62
+ // The prologue is one contiguous opcode block at the function's start
63
+ const lastPrologueOpcode = Math.max(...prologueTags.map((t) => t.endIndex));
64
+ const firstBodyOpcode = lastPrologueOpcode + 1;
65
+ const firstBodyLine = getDisplayLine(locationData[firstBodyOpcode]);
66
+ return {
67
+ // `insertAfterLine` splices *after* a line, so `firstBodyLine - 1` lands all the prologue
68
+ // annotations directly above the first body statement, at its indentation.
69
+ insertAfterLine: firstBodyLine - 1,
70
+ indent: lineIndent(sourceLines, firstBodyLine),
71
+ };
72
+ }
73
+ return {
74
+ insertAfterLine: getDisplayLine(locationData[Math.max(tag.startIndex - 1, 0)]),
75
+ indent: deriveIndent(getDisplayLine(locationData[tag.startIndex]), sourceLines),
76
+ };
77
+ }
54
78
  function spliceSyntheticSourceLines(sourceLines, insertions) {
55
79
  return insertions.reduceRight((lines, ins) => [...lines.slice(0, ins.insertAfterLine), ins.annotation, ...lines.slice(ins.insertAfterLine)], sourceLines);
56
80
  }
@@ -80,19 +104,36 @@ const getUpdatedLineNumber = (currentLineNumber, insertion, opcodeIndex) => {
80
104
  return currentLineNumber + 1;
81
105
  return currentLineNumber;
82
106
  };
83
- /** Derive the annotation text (e.g. ">>> for-loop update (i = i + 1)") for a source tag. */
84
- function deriveTagLabel(tag, locationData, sourceLines) {
107
+ // e.g. ">>> for-loop update (i = i + 1)"
108
+ function tagDescription(tag, locationData, sourceLines) {
109
+ switch (tag.kind) {
110
+ case SourceTagKind.LOCKTIME_GUARD:
111
+ return '>>> tx.locktime guard (auto-injected)';
112
+ case SourceTagKind.PARAMETER_VALIDATION: {
113
+ const parameter = deriveSourceText(tag, locationData, sourceLines);
114
+ return `>>> parameter type check${parameter ? ` (${parameter})` : ''}`;
115
+ }
116
+ case SourceTagKind.FOR_UPDATE:
117
+ default:
118
+ return `>>> for-loop update (${deriveSourceText(tag, locationData, sourceLines)})`;
119
+ }
120
+ }
121
+ function deriveIndent(headerLine, sourceLines) {
122
+ const bodyLine = sourceLines.slice(headerLine).find((l) => l.trim().length > 0);
123
+ return bodyLine?.match(/^(\s*)/)?.[1] ?? '';
124
+ }
125
+ function lineIndent(sourceLines, line) {
126
+ return sourceLines[line - 1]?.match(/^(\s*)/)?.[1] ?? '';
127
+ }
128
+ function deriveSourceText(tag, locationData, sourceLines) {
85
129
  const headerLine = getDisplayLine(locationData[tag.startIndex]);
86
- // Find the column span of the update expression using single-line locations in the tag range
87
130
  const singleLineLocations = range(tag.startIndex, tag.endIndex)
88
131
  .map((idx) => locationData[idx].location)
89
- .filter((loc) => loc.start.line === loc.end.line);
132
+ .filter((loc) => loc.start.line === loc.end.line && loc.start.column !== loc.end.column);
133
+ if (singleLineLocations.length === 0)
134
+ return '';
90
135
  const startCol = Math.min(...singleLineLocations.map((loc) => loc.start.column));
91
136
  const endCol = Math.max(...singleLineLocations.map((loc) => loc.end.column));
92
- const expression = sourceLines[headerLine - 1].substring(startCol, endCol);
93
- // Indentation: use first non-empty line after header
94
- const bodyLine = sourceLines.slice(headerLine).find((l) => l.trim().length > 0);
95
- const indent = bodyLine?.match(/^(\s*)/)?.[1] ?? '';
96
- return `${indent}>>> for-loop update (${expression})`;
137
+ return sourceLines[headerLine - 1].substring(startCol, endCol).trim();
97
138
  }
98
139
  //# sourceMappingURL=bitauth-script.js.map
@@ -14,7 +14,7 @@ export function computeBytecodePattern(bytecode) {
14
14
  function groupByPushRun(opcodes) {
15
15
  return opcodes.reduce((opcodeRuns, opcode) => {
16
16
  const lastOpcodeRun = opcodeRuns.at(-1);
17
- // If ther are no runs yet, start a new run with the current opcode
17
+ // If there are no runs yet, start a new run with the current opcode
18
18
  if (!lastOpcodeRun)
19
19
  return [[opcode]];
20
20
  // If the last run is the same type of opcode as the current opcode, add the current opcode to the last run
package/dist/types.d.ts CHANGED
@@ -52,7 +52,9 @@ export declare enum PositionHint {
52
52
  END = 1
53
53
  }
54
54
  export declare enum SourceTagKind {
55
- FOR_UPDATE = "fu"
55
+ FOR_UPDATE = "fu",
56
+ LOCKTIME_GUARD = "lg",
57
+ PARAMETER_VALIDATION = "pv"
56
58
  }
57
59
  export interface SourceTagEntry {
58
60
  startIndex: number;
package/dist/types.js CHANGED
@@ -191,10 +191,11 @@ export var PositionHint;
191
191
  PositionHint[PositionHint["START"] = 0] = "START";
192
192
  PositionHint[PositionHint["END"] = 1] = "END";
193
193
  })(PositionHint || (PositionHint = {}));
194
- // Semantic tags for opcodes that need special treatment in debugging output (e.g. synthetic labels).
195
- // Currently used for loop constructs where opcode order diverges from source line order.
194
+ // Semantic tags for compiler-injected opcodes that have no (or misleading) user source
196
195
  export var SourceTagKind;
197
196
  (function (SourceTagKind) {
198
197
  SourceTagKind["FOR_UPDATE"] = "fu";
198
+ SourceTagKind["LOCKTIME_GUARD"] = "lg";
199
+ SourceTagKind["PARAMETER_VALIDATION"] = "pv";
199
200
  })(SourceTagKind || (SourceTagKind = {}));
200
201
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cashscript/utils",
3
- "version": "0.13.0-next.8",
3
+ "version": "0.13.0",
4
4
  "description": "CashScript utilities and types",
5
5
  "keywords": [
6
6
  "bitcoin cash",
@@ -48,5 +48,5 @@
48
48
  "typescript": "^5.9.2",
49
49
  "vitest": "^4.0.15"
50
50
  },
51
- "gitHead": "579998062f2f9c52393568b09b580407228cb79f"
51
+ "gitHead": "a3461662016a2f3dad36431ba5a2b148bcc4dc75"
52
52
  }