rork-xcode 0.2.0 → 0.3.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  [![CI](https://github.com/rorkai/rork-xcode/actions/workflows/ci.yml/badge.svg)](https://github.com/rorkai/rork-xcode/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/rork-xcode)](https://www.npmjs.com/package/rork-xcode)
5
5
 
6
- The [fastest](#performance) zero-dependency Xcode project (`project.pbxproj`) parser, builder, and object model for any JavaScript runtime: browsers, Node.js, Bun, Electron, Cloudflare Workers, and React Native.
6
+ The [fastest](#performance) zero-dependency Xcode project (`project.pbxproj`) parser, builder, and object model for any JavaScript runtime: browsers, Node.js, Bun, Electron, Cloudflare Workers, and React Native. [Scheme files](#schemes) (`.xcscheme`) are covered with the same round-trip guarantees.
7
7
 
8
8
  ```ts
9
9
  import { parsePbxproj, buildPbxproj } from "rork-xcode";
@@ -229,6 +229,39 @@ for (const [id, object] of project.objects()) {
229
229
  - **Soft reads, loud writes.** Real-world projects can be malformed, so lookups return `undefined` where a document could omit something. Operations that cannot proceed without structure (no root project object, an unknown product type, a view whose object was deleted) throw `XcodeModelError`.
230
230
  - **Identity-mapped views.** Two lookups of the same id return the same instance, so views compare with `===`.
231
231
 
232
+ ## Schemes
233
+
234
+ `.xcscheme` files describe how Xcode builds, runs, tests, and archives a target. They are not property lists but a small XML dialect of their own, and `parseXcscheme` and `buildXcscheme` cover it with the same contract as the pbxproj functions: an Xcode-written scheme rebuilds byte for byte, any other input reaches Xcode's canonical layout in one build, and malformed input fails with a typed error carrying line and column.
235
+
236
+ ```ts
237
+ import { buildXcscheme, parseXcscheme, xcschemeElements } from "rork-xcode";
238
+
239
+ const scheme = parseXcscheme(xcschemeText);
240
+
241
+ // The tree is plain data: elements with ordered attributes and children.
242
+ for (const reference of xcschemeElements(scheme.root, "BuildableReference")) {
243
+ reference.attributes["BlueprintName"] = "RenamedApp";
244
+ reference.attributes["BuildableName"] = "RenamedApp.app";
245
+ reference.attributes["ReferencedContainer"] = "container:RenamedApp.xcodeproj";
246
+ }
247
+
248
+ const text = buildXcscheme(scheme);
249
+ ```
250
+
251
+ `createXcscheme` produces the scheme Xcode's own "New Scheme" action writes for an application target, wired to the target's object id from the project document:
252
+
253
+ ```ts
254
+ import { createXcscheme } from "rork-xcode";
255
+
256
+ const scheme = createXcscheme({
257
+ appName: "DemoApp",
258
+ blueprintIdentifier: app.id,
259
+ });
260
+ const text = buildXcscheme(scheme);
261
+ ```
262
+
263
+ Attribute order is preserved and meaningful: the writer emits attributes in insertion order, which is how byte-identical round-trips fall out. Comments are kept, attribute values resolve the character references Xcode writes (`"`, `&`, `
` and friends), and the writer re-escapes them identically.
264
+
232
265
  ## Performance
233
266
 
234
267
  `rork-xcode` is measured against the pbxproj parsers on npm, [`@bacons/xcode`](https://www.npmjs.com/package/@bacons/xcode) (its `/json` parse/build entry point) and [`xcode`](https://www.npmjs.com/package/xcode) (the long-standing package used by native build tooling), on three documents: two real Xcode-written projects from the test suite and a deterministically generated five-target app with 800 source files. It is the fastest at both operations on every document, with zero dependencies.
@@ -260,7 +293,7 @@ Measured on an Apple M5 Max, Node.js 24, single thread, with `@bacons/xcode` 1.0
260
293
  - The committed fixture corpus spans project generations from Xcode 3 to Xcode 16, captured from real projects with identifiers neutralized: synchronized folders with both exception-set kinds, classic groups, variant groups, aggregate and legacy targets, reference proxies, build rules, Swift packages, and a ~100 KiB multiplatform framework project.
261
294
  - Documents already in current Xcode's layout must round-trip byte for byte; documents from other tool generations must normalize to a byte-stable fixed point with unchanged values.
262
295
  - On macOS, the suite cross-validates every fixture and its rebuilt form with `plutil`, Apple's own property list parser and the empirical ground truth for what Apple tooling accepts.
263
- - A corpus sweep (`pnpm corpus`) walks every Xcode project on the machine, verifies each one parses and reaches a byte-stable fixed point, exercises the object model against it, and cross-validates a sample against plutil's own reading.
296
+ - A corpus sweep (`pnpm corpus`) walks every Xcode project and scheme on the machine, verifies each one parses and reaches a byte-stable fixed point, exercises the object model against every project, and cross-validates a sample against plutil's own reading.
264
297
  - CI runs the full gate on Linux and macOS, and executes the built artifact on the oldest supported Node to enforce the `engines` floor.
265
298
 
266
299
  ## Releasing
package/dist/index.d.ts CHANGED
@@ -114,6 +114,42 @@ declare class PbxprojParseError extends Error {
114
114
  */
115
115
  constructor(message: string, source: string, offset: number);
116
116
  }
117
+ /**
118
+ * Thrown when the source text is not a well-formed scheme document (the
119
+ * XML dialect of `.xcscheme` files).
120
+ *
121
+ * The message always embeds the line and column of the failure, and the
122
+ * same information is available in structured form on {@link position}
123
+ * for programmatic use.
124
+ */
125
+ declare class XcschemeParseError extends Error {
126
+ /** Where in the source text parsing failed. */
127
+ readonly position: PbxprojErrorPosition;
128
+ /**
129
+ * @param message Failure description without location; the location is
130
+ * appended automatically.
131
+ * @param source Full source text, used to compute the position.
132
+ * @param offset Character offset of the failure inside `source`.
133
+ */
134
+ constructor(message: string, source: string, offset: number);
135
+ }
136
+ /**
137
+ * Thrown when a scheme element cannot be written as XML.
138
+ *
139
+ * Raised for element and attribute names that are not valid XML names and
140
+ * for attribute values carrying control characters XML 1.0 cannot encode.
141
+ * The {@link path} pinpoints the offending element inside the tree.
142
+ */
143
+ declare class XcschemeBuildError extends Error {
144
+ /** Path to the offending element from the root, e.g. `Scheme.BuildAction[0]`. */
145
+ readonly path: string;
146
+ /**
147
+ * @param message Failure description without location; the element path
148
+ * is appended automatically.
149
+ * @param path Path to the offending element from the root.
150
+ */
151
+ constructor(message: string, path: string);
152
+ }
117
153
  /**
118
154
  * Thrown by the object model when an operation cannot proceed: the
119
155
  * document lacks the structure the operation needs (no objects dictionary,
@@ -1245,6 +1281,122 @@ declare class XcodeObject<Properties extends PbxprojObject = PbxprojObject> {
1245
1281
  */
1246
1282
  declare function parsePbxproj(text: string): PbxprojValue;
1247
1283
  //#endregion
1284
+ //#region src/scheme/types.d.ts
1285
+ /**
1286
+ * The node model of a scheme document.
1287
+ *
1288
+ * A parsed `.xcscheme` is a tree of plain objects: elements with ordered
1289
+ * attributes and child nodes, plus the occasional comment. All state lives
1290
+ * in this tree. There is no wrapper class to keep in sync, so callers
1291
+ * mutate nodes directly and serialize whatever the tree currently says.
1292
+ *
1293
+ * @module
1294
+ */
1295
+ /**
1296
+ * An XML element of a scheme document.
1297
+ *
1298
+ * Attribute order is preserved and meaningful: the writer emits attributes
1299
+ * in insertion order, which is how byte-identical round-trips of
1300
+ * Xcode-written files fall out. Assigning an existing key keeps its
1301
+ * position, and adding a new key appends it.
1302
+ */
1303
+ interface XcschemeElement {
1304
+ /** Tag name, for example `Scheme` or `BuildableReference`. */
1305
+ name: string;
1306
+ /** Attribute values by name, in document order. */
1307
+ attributes: Record<string, string>;
1308
+ /** Child elements and comments, in document order. */
1309
+ children: XcschemeNode[];
1310
+ }
1311
+ /**
1312
+ * A comment inside a scheme document. Xcode never writes comments, but
1313
+ * hand-edited and tool-generated schemes can carry them, and dropping
1314
+ * them would make round-trips lossy.
1315
+ */
1316
+ interface XcschemeComment {
1317
+ /** The comment text between `<!--` and `-->`, verbatim. */
1318
+ comment: string;
1319
+ }
1320
+ /**
1321
+ * Any node a scheme element can contain.
1322
+ */
1323
+ type XcschemeNode = XcschemeElement | XcschemeComment;
1324
+ /**
1325
+ * A parsed scheme file: its root element plus any comments sitting
1326
+ * outside it. Xcode writes only the root, so the comment lists are almost
1327
+ * always empty, but files that carry them round-trip without loss.
1328
+ */
1329
+ interface XcschemeDocument {
1330
+ /** Comments before the root element. */
1331
+ leading: XcschemeComment[];
1332
+ /** The `Scheme` element. */
1333
+ root: XcschemeElement;
1334
+ /** Comments after the root element. */
1335
+ trailing: XcschemeComment[];
1336
+ }
1337
+ /**
1338
+ * Whether a node is an element rather than a comment.
1339
+ */
1340
+ declare function isXcschemeElement(node: XcschemeNode): node is XcschemeElement;
1341
+ //#endregion
1342
+ //#region src/scheme/build.d.ts
1343
+ /**
1344
+ * Serializes a scheme document to the text of a `.xcscheme` file.
1345
+ *
1346
+ * @throws XcschemeBuildError for element or attribute names that are not
1347
+ * XML names and for attribute values carrying unencodable control
1348
+ * characters, with the path of the offending node.
1349
+ */
1350
+ declare function buildXcscheme(document: XcschemeDocument): string;
1351
+ //#endregion
1352
+ //#region src/scheme/model.d.ts
1353
+ /**
1354
+ * Collects elements of the given name anywhere in the tree, in document
1355
+ * order, the subtree root included. Passing no name collects every
1356
+ * element.
1357
+ *
1358
+ * ```ts
1359
+ * for (const reference of xcschemeElements(scheme.root, "BuildableReference")) {
1360
+ * reference.attributes["BlueprintName"] = "RenamedApp";
1361
+ * }
1362
+ * ```
1363
+ */
1364
+ declare function xcschemeElements(root: XcschemeElement, name?: string): XcschemeElement[];
1365
+ /**
1366
+ * Options for {@link createXcscheme}.
1367
+ */
1368
+ interface CreateXcschemeOptions {
1369
+ /** The application target's name, which also names the scheme. */
1370
+ appName: string;
1371
+ /**
1372
+ * The `.xcodeproj` directory name the buildable references point at.
1373
+ * Defaults to `<appName>.xcodeproj`.
1374
+ */
1375
+ xcodeprojName?: string;
1376
+ /**
1377
+ * The application target's object id in the project document. Xcode
1378
+ * repairs an empty identifier on first open, so omitting it still
1379
+ * produces a working scheme.
1380
+ */
1381
+ blueprintIdentifier?: string;
1382
+ }
1383
+ /**
1384
+ * Creates the scheme Xcode writes for an application target: build,
1385
+ * launch, profile, analyze, and archive actions wired to the app product,
1386
+ * with Xcode's default configuration choices (Debug for development
1387
+ * actions, Release for profiling and archiving).
1388
+ */
1389
+ declare function createXcscheme(options: CreateXcschemeOptions): XcschemeDocument;
1390
+ //#endregion
1391
+ //#region src/scheme/parse.d.ts
1392
+ /**
1393
+ * Parses the text of a `.xcscheme` file into its node tree.
1394
+ *
1395
+ * @throws XcschemeParseError when the text is not a well-formed scheme
1396
+ * document, with the line and column of the failure.
1397
+ */
1398
+ declare function parseXcscheme(text: string): XcschemeDocument;
1399
+ //#endregion
1248
1400
  //#region src/uuid.d.ts
1249
1401
  /**
1250
1402
  * Deterministic object identifiers for generated pbxproj objects.
@@ -1272,4 +1424,4 @@ declare function parsePbxproj(text: string): PbxprojValue;
1272
1424
  */
1273
1425
  declare function generateObjectId(seed: string, existing: ReadonlySet<string>): string;
1274
1426
  //#endregion
1275
- export { type AddNativeTargetOptions, AggregateTarget, type ApplePlatform, type BuildConfigurationProperties, type BuildFileProperties, BuildPhase, type BuildPhaseProperties, BuildRule, type BuildRuleProperties, type BuildSettings, type ConfigurationListProperties, type ContainerItemProxyProperties, CopyFilesDestination, type ExceptionSetProperties, type FileReferenceProperties, Group, type GroupProperties, Isa, LegacyTarget, type LegacyTargetProperties, NativeTarget, type NativeTargetProperties, type PbxprojArray, PbxprojBuildError, type PbxprojErrorPosition, type PbxprojObject, PbxprojParseError, type PbxprojValue, ProductType, type ProjectIssue, type ProjectIssueKind, ReferenceProxy, type ReferenceProxyProperties, RootProject, type RootProjectProperties, type SwiftPackageProductDependencyProperties, type SwiftPackageReferenceProperties, SyncRootGroup, type SyncRootGroupProperties, Target, type TargetDependencyProperties, type TargetProperties, VersionGroup, type VersionGroupProperties, XcodeModelError, XcodeObject, XcodeProject, buildPbxproj, generateObjectId, parsePbxproj };
1427
+ export { type AddNativeTargetOptions, AggregateTarget, type ApplePlatform, type BuildConfigurationProperties, type BuildFileProperties, BuildPhase, type BuildPhaseProperties, BuildRule, type BuildRuleProperties, type BuildSettings, type ConfigurationListProperties, type ContainerItemProxyProperties, CopyFilesDestination, type CreateXcschemeOptions, type ExceptionSetProperties, type FileReferenceProperties, Group, type GroupProperties, Isa, LegacyTarget, type LegacyTargetProperties, NativeTarget, type NativeTargetProperties, type PbxprojArray, PbxprojBuildError, type PbxprojErrorPosition, type PbxprojObject, PbxprojParseError, type PbxprojValue, ProductType, type ProjectIssue, type ProjectIssueKind, ReferenceProxy, type ReferenceProxyProperties, RootProject, type RootProjectProperties, type SwiftPackageProductDependencyProperties, type SwiftPackageReferenceProperties, SyncRootGroup, type SyncRootGroupProperties, Target, type TargetDependencyProperties, type TargetProperties, VersionGroup, type VersionGroupProperties, XcodeModelError, XcodeObject, XcodeProject, XcschemeBuildError, type XcschemeComment, type XcschemeDocument, type XcschemeElement, type XcschemeNode, XcschemeParseError, buildPbxproj, buildXcscheme, createXcscheme, generateObjectId, isXcschemeElement, parsePbxproj, parseXcscheme, xcschemeElements };
package/dist/index.js CHANGED
@@ -259,6 +259,51 @@ var PbxprojParseError = class extends Error {
259
259
  }
260
260
  };
261
261
  /**
262
+ * Thrown when the source text is not a well-formed scheme document (the
263
+ * XML dialect of `.xcscheme` files).
264
+ *
265
+ * The message always embeds the line and column of the failure, and the
266
+ * same information is available in structured form on {@link position}
267
+ * for programmatic use.
268
+ */
269
+ var XcschemeParseError = class extends Error {
270
+ /** Where in the source text parsing failed. */
271
+ position;
272
+ /**
273
+ * @param message Failure description without location; the location is
274
+ * appended automatically.
275
+ * @param source Full source text, used to compute the position.
276
+ * @param offset Character offset of the failure inside `source`.
277
+ */
278
+ constructor(message, source, offset) {
279
+ const position = positionAt(source, offset);
280
+ super(`${message} (line ${position.line}, column ${position.column})`);
281
+ this.name = "XcschemeParseError";
282
+ this.position = position;
283
+ }
284
+ };
285
+ /**
286
+ * Thrown when a scheme element cannot be written as XML.
287
+ *
288
+ * Raised for element and attribute names that are not valid XML names and
289
+ * for attribute values carrying control characters XML 1.0 cannot encode.
290
+ * The {@link path} pinpoints the offending element inside the tree.
291
+ */
292
+ var XcschemeBuildError = class extends Error {
293
+ /** Path to the offending element from the root, e.g. `Scheme.BuildAction[0]`. */
294
+ path;
295
+ /**
296
+ * @param message Failure description without location; the element path
297
+ * is appended automatically.
298
+ * @param path Path to the offending element from the root.
299
+ */
300
+ constructor(message, path) {
301
+ super(`${message} (at ${path})`);
302
+ this.name = "XcschemeBuildError";
303
+ this.path = path;
304
+ }
305
+ };
306
+ /**
262
307
  * Thrown by the object model when an operation cannot proceed: the
263
308
  * document lacks the structure the operation needs (no objects dictionary,
264
309
  * no root project object), a view's object was removed from the document,
@@ -1595,11 +1640,11 @@ const IS_LITERAL_CHAR = (() => {
1595
1640
  for (const ch of "_$/:.-") table[ch.charCodeAt(0)] = 1;
1596
1641
  return table;
1597
1642
  })();
1598
- const CODE_TAB = 9;
1599
- const CODE_LINE_FEED = 10;
1600
- const CODE_CARRIAGE_RETURN = 13;
1601
- const CODE_SPACE = 32;
1602
- const CODE_QUOTE = 34;
1643
+ const CODE_TAB$1 = 9;
1644
+ const CODE_LINE_FEED$1 = 10;
1645
+ const CODE_CARRIAGE_RETURN$1 = 13;
1646
+ const CODE_SPACE$1 = 32;
1647
+ const CODE_QUOTE$1 = 34;
1603
1648
  const CODE_SINGLE_QUOTE = 39;
1604
1649
  const CODE_OPEN_PAREN = 40;
1605
1650
  const CODE_CLOSE_PAREN = 41;
@@ -1607,13 +1652,13 @@ const CODE_ASTERISK = 42;
1607
1652
  const CODE_COMMA = 44;
1608
1653
  const CODE_MINUS = 45;
1609
1654
  const CODE_DOT = 46;
1610
- const CODE_SLASH = 47;
1655
+ const CODE_SLASH$1 = 47;
1611
1656
  const CODE_ZERO = 48;
1612
1657
  const CODE_NINE = 57;
1613
1658
  const CODE_SEMICOLON = 59;
1614
- const CODE_LESS_THAN = 60;
1615
- const CODE_EQUALS = 61;
1616
- const CODE_GREATER_THAN = 62;
1659
+ const CODE_LESS_THAN$1 = 60;
1660
+ const CODE_EQUALS$1 = 61;
1661
+ const CODE_GREATER_THAN$1 = 62;
1617
1662
  const CODE_BACKSLASH = 92;
1618
1663
  const CODE_OPEN_BRACE = 123;
1619
1664
  const CODE_CLOSE_BRACE = 125;
@@ -1625,10 +1670,10 @@ const CODE_CLOSE_BRACE = 125;
1625
1670
  */
1626
1671
  const IS_WHITESPACE = (() => {
1627
1672
  const table = /* @__PURE__ */ new Uint8Array(256);
1628
- table[CODE_SPACE] = 1;
1629
- table[CODE_TAB] = 1;
1630
- table[CODE_CARRIAGE_RETURN] = 1;
1631
- table[CODE_LINE_FEED] = 1;
1673
+ table[CODE_SPACE$1] = 1;
1674
+ table[CODE_TAB$1] = 1;
1675
+ table[CODE_CARRIAGE_RETURN$1] = 1;
1676
+ table[CODE_LINE_FEED$1] = 1;
1632
1677
  return table;
1633
1678
  })();
1634
1679
  /**
@@ -1644,7 +1689,7 @@ function isDigit(code) {
1644
1689
  * through the `read*` and `parse*` methods; there is no separate tokenizer
1645
1690
  * stage and no token objects.
1646
1691
  */
1647
- var Parser = class {
1692
+ var Parser$1 = class {
1648
1693
  /** Source text of the document being parsed. */
1649
1694
  input;
1650
1695
  /** Cursor position as a UTF-16 code unit offset into {@link input}. */
@@ -1691,7 +1736,7 @@ var Parser = class {
1691
1736
  const length = input.length;
1692
1737
  let pos = this.pos;
1693
1738
  while (pos < length && IS_WHITESPACE[input.charCodeAt(pos)] === 1) pos++;
1694
- if (pos < length && input.charCodeAt(pos) === CODE_SLASH) pos = this.skipCommentedTrivia(pos);
1739
+ if (pos < length && input.charCodeAt(pos) === CODE_SLASH$1) pos = this.skipCommentedTrivia(pos);
1695
1740
  this.pos = pos;
1696
1741
  }
1697
1742
  /**
@@ -1711,7 +1756,7 @@ var Parser = class {
1711
1756
  const length = input.length;
1712
1757
  for (;;) {
1713
1758
  const next = input.charCodeAt(pos + 1);
1714
- if (next === CODE_SLASH) {
1759
+ if (next === CODE_SLASH$1) {
1715
1760
  const lineEnd = input.indexOf("\n", pos + 2);
1716
1761
  pos = lineEnd === -1 ? length : lineEnd;
1717
1762
  } else if (next === CODE_ASTERISK) {
@@ -1723,7 +1768,7 @@ var Parser = class {
1723
1768
  pos = commentEnd + 2;
1724
1769
  } else return pos;
1725
1770
  while (pos < length && IS_WHITESPACE[input.charCodeAt(pos)] === 1) pos++;
1726
- if (pos >= length || input.charCodeAt(pos) !== CODE_SLASH) return pos;
1771
+ if (pos >= length || input.charCodeAt(pos) !== CODE_SLASH$1) return pos;
1727
1772
  }
1728
1773
  }
1729
1774
  /**
@@ -1806,7 +1851,7 @@ var Parser = class {
1806
1851
  const input = this.input;
1807
1852
  const length = input.length;
1808
1853
  const start = ++this.pos;
1809
- while (this.pos < length && input.charCodeAt(this.pos) !== CODE_GREATER_THAN) this.pos++;
1854
+ while (this.pos < length && input.charCodeAt(this.pos) !== CODE_GREATER_THAN$1) this.pos++;
1810
1855
  if (this.pos >= length) this.fail("Unterminated data run", start - 1);
1811
1856
  let hex = "";
1812
1857
  for (let i = start; i < this.pos; i++) {
@@ -1849,10 +1894,10 @@ var Parser = class {
1849
1894
  return result;
1850
1895
  }
1851
1896
  let key;
1852
- if (code === CODE_QUOTE || code === CODE_SINGLE_QUOTE) key = this.readQuotedString();
1897
+ if (code === CODE_QUOTE$1 || code === CODE_SINGLE_QUOTE) key = this.readQuotedString();
1853
1898
  else if (IS_LITERAL_CHAR[code] === 1) key = this.readLiteral();
1854
1899
  else this.fail(`Expected a key but found '${input[this.pos]}'`);
1855
- this.expect(CODE_EQUALS, "=");
1900
+ this.expect(CODE_EQUALS$1, "=");
1856
1901
  const value = this.parseValue();
1857
1902
  this.expect(CODE_SEMICOLON, ";");
1858
1903
  if (key === "__proto__") Object.defineProperty(result, key, {
@@ -1905,9 +1950,9 @@ var Parser = class {
1905
1950
  const code = this.input.charCodeAt(this.pos);
1906
1951
  if (code === CODE_OPEN_BRACE) return this.parseObject();
1907
1952
  if (code === CODE_OPEN_PAREN) return this.parseArray();
1908
- if (code === CODE_QUOTE || code === CODE_SINGLE_QUOTE) return this.readQuotedString();
1953
+ if (code === CODE_QUOTE$1 || code === CODE_SINGLE_QUOTE) return this.readQuotedString();
1909
1954
  if (IS_LITERAL_CHAR[code] === 1) return interpretLiteral(this.readLiteral());
1910
- if (code === CODE_LESS_THAN) return this.readData();
1955
+ if (code === CODE_LESS_THAN$1) return this.readData();
1911
1956
  this.fail(`Expected a value but found '${this.input[this.pos]}'`);
1912
1957
  }
1913
1958
  };
@@ -1961,7 +2006,7 @@ function interpretLiteral(literal) {
1961
2006
  * carries the line and column of the failure.
1962
2007
  */
1963
2008
  function parsePbxproj(text) {
1964
- return new Parser(text).parseDocument();
2009
+ return new Parser$1(text).parseDocument();
1965
2010
  }
1966
2011
  //#endregion
1967
2012
  //#region src/md5.ts
@@ -3397,4 +3442,482 @@ function stripReferences(properties, id) {
3397
3442
  }
3398
3443
  }
3399
3444
  //#endregion
3400
- export { AggregateTarget, BuildPhase, BuildRule, CopyFilesDestination, Group, Isa, LegacyTarget, NativeTarget, PbxprojBuildError, PbxprojParseError, ProductType, ReferenceProxy, RootProject, SyncRootGroup, Target, VersionGroup, XcodeModelError, XcodeObject, XcodeProject, buildPbxproj, generateObjectId, parsePbxproj };
3445
+ //#region src/scheme/types.ts
3446
+ /**
3447
+ * Whether a node is an element rather than a comment.
3448
+ */
3449
+ function isXcschemeElement(node) {
3450
+ return "name" in node;
3451
+ }
3452
+ //#endregion
3453
+ //#region src/scheme/build.ts
3454
+ /**
3455
+ * Serializer for the `.xcscheme` XML dialect.
3456
+ *
3457
+ * The output reproduces Xcode's own layout byte for byte: the UTF-8
3458
+ * declaration, three-space indentation, one attribute per line with
3459
+ * spaces around the equals sign, the closing angle bracket glued to the
3460
+ * last attribute, and an explicit close tag on every element. Parsing an
3461
+ * Xcode-written scheme and building it back yields the identical file,
3462
+ * and any input reaches that canonical form in one build.
3463
+ *
3464
+ * @module
3465
+ */
3466
+ const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
3467
+ /** One indentation step of Xcode's scheme writer. */
3468
+ const INDENT = " ";
3469
+ /**
3470
+ * Matches the names Xcode's scheme vocabulary uses; a stray non-name
3471
+ * (an empty string, spaces, XML syntax) must fail rather than produce a
3472
+ * document no parser accepts.
3473
+ */
3474
+ const NAME_PATTERN = /^[A-Za-z_:][A-Za-z0-9_:.-]*$/u;
3475
+ /**
3476
+ * Matches characters that cannot appear in an attribute value: XML 1.0
3477
+ * has no representation for control characters other than tab, line
3478
+ * feed, and carriage return.
3479
+ */
3480
+ const UNENCODABLE_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/u;
3481
+ /**
3482
+ * Serializes a scheme document to the text of a `.xcscheme` file.
3483
+ *
3484
+ * @throws XcschemeBuildError for element or attribute names that are not
3485
+ * XML names and for attribute values carrying unencodable control
3486
+ * characters, with the path of the offending node.
3487
+ */
3488
+ function buildXcscheme(document) {
3489
+ let output = XML_DECLARATION;
3490
+ for (const comment of document.leading) output += `<!--${comment.comment}-->\n`;
3491
+ output += renderElement(document.root, 0, document.root.name);
3492
+ for (const comment of document.trailing) output += `<!--${comment.comment}-->\n`;
3493
+ return output;
3494
+ }
3495
+ /**
3496
+ * Renders one element with Xcode's layout: attributes each on their own
3497
+ * line indented one step past the tag, `>` glued to the last attribute,
3498
+ * children indented one step, and always an explicit close tag.
3499
+ */
3500
+ function renderElement(element, depth, path) {
3501
+ if (!NAME_PATTERN.test(element.name)) throw new XcschemeBuildError(`Element name ${JSON.stringify(element.name)} is not a valid XML name`, path);
3502
+ const indent = INDENT.repeat(depth);
3503
+ const names = Object.keys(element.attributes);
3504
+ let output;
3505
+ if (names.length === 0) output = `${indent}<${element.name}>\n`;
3506
+ else {
3507
+ output = `${indent}<${element.name}\n`;
3508
+ const attributeIndent = indent + INDENT;
3509
+ for (let i = 0; i < names.length; i++) {
3510
+ const name = names[i];
3511
+ if (!NAME_PATTERN.test(name)) throw new XcschemeBuildError(`Attribute name ${JSON.stringify(name)} is not a valid XML name`, path);
3512
+ const value = escapeAttribute(element.attributes[name], path, name);
3513
+ const terminator = i === names.length - 1 ? ">" : "";
3514
+ output += `${attributeIndent}${name} = "${value}"${terminator}\n`;
3515
+ }
3516
+ }
3517
+ output += renderChildren(element.children, depth + 1, path);
3518
+ output += `${indent}</${element.name}>\n`;
3519
+ return output;
3520
+ }
3521
+ /**
3522
+ * Renders an element's children in document order, numbering repeated
3523
+ * element names in the error path so a failure points at one node.
3524
+ */
3525
+ function renderChildren(children, depth, path) {
3526
+ let output = "";
3527
+ const seen = /* @__PURE__ */ new Map();
3528
+ for (const child of children) if (isXcschemeElement(child)) {
3529
+ const index = seen.get(child.name) ?? 0;
3530
+ seen.set(child.name, index + 1);
3531
+ output += renderElement(child, depth, `${path}.${child.name}[${index}]`);
3532
+ } else output += `${INDENT.repeat(depth)}<!--${child.comment}-->\n`;
3533
+ return output;
3534
+ }
3535
+ /**
3536
+ * Escapes an attribute value the way Xcode's writer does: the five named
3537
+ * entities for XML syntax characters, and character references for tab,
3538
+ * line feed, and carriage return so they survive attribute-value
3539
+ * normalization on the next parse.
3540
+ */
3541
+ function escapeAttribute(value, path, attributeName) {
3542
+ const unencodable = UNENCODABLE_PATTERN.exec(value);
3543
+ if (unencodable != null) throw new XcschemeBuildError(`Attribute ${attributeName} contains the control character U+${unencodable[0].charCodeAt(0).toString(16).padStart(4, "0").toUpperCase()}, which XML cannot encode`, path);
3544
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;").replaceAll(" ", "&#9;").replaceAll("\n", "&#10;").replaceAll("\r", "&#13;");
3545
+ }
3546
+ //#endregion
3547
+ //#region src/scheme/model.ts
3548
+ /**
3549
+ * Helpers over the scheme node tree: element queries and the default
3550
+ * scheme factory.
3551
+ *
3552
+ * The tree itself is plain data, so most edits are direct property
3553
+ * writes. What this module adds is the recursive query that editing
3554
+ * flows start from and a factory producing the scheme Xcode's own "New
3555
+ * Scheme" action writes for an application target.
3556
+ *
3557
+ * @module
3558
+ */
3559
+ /**
3560
+ * Collects elements of the given name anywhere in the tree, in document
3561
+ * order, the subtree root included. Passing no name collects every
3562
+ * element.
3563
+ *
3564
+ * ```ts
3565
+ * for (const reference of xcschemeElements(scheme.root, "BuildableReference")) {
3566
+ * reference.attributes["BlueprintName"] = "RenamedApp";
3567
+ * }
3568
+ * ```
3569
+ */
3570
+ function xcschemeElements(root, name) {
3571
+ const found = [];
3572
+ const visit = (element) => {
3573
+ if (name == null || element.name === name) found.push(element);
3574
+ for (const child of element.children) if (isXcschemeElement(child)) visit(child);
3575
+ };
3576
+ visit(root);
3577
+ return found;
3578
+ }
3579
+ /**
3580
+ * Creates the scheme Xcode writes for an application target: build,
3581
+ * launch, profile, analyze, and archive actions wired to the app product,
3582
+ * with Xcode's default configuration choices (Debug for development
3583
+ * actions, Release for profiling and archiving).
3584
+ */
3585
+ function createXcscheme(options) {
3586
+ const xcodeprojName = options.xcodeprojName ?? `${options.appName}.xcodeproj`;
3587
+ /**
3588
+ * Buildable references appear once per action; each site gets its own
3589
+ * element so a mutation through one action cannot alias into another.
3590
+ */
3591
+ const buildableReference = () => ({
3592
+ name: "BuildableReference",
3593
+ attributes: {
3594
+ BuildableIdentifier: "primary",
3595
+ BlueprintIdentifier: options.blueprintIdentifier ?? "",
3596
+ BuildableName: `${options.appName}.app`,
3597
+ BlueprintName: options.appName,
3598
+ ReferencedContainer: `container:${xcodeprojName}`
3599
+ },
3600
+ children: []
3601
+ });
3602
+ return {
3603
+ leading: [],
3604
+ root: {
3605
+ name: "Scheme",
3606
+ attributes: { version: "1.7" },
3607
+ children: [
3608
+ {
3609
+ name: "BuildAction",
3610
+ attributes: {
3611
+ parallelizeBuildables: "YES",
3612
+ buildImplicitDependencies: "YES"
3613
+ },
3614
+ children: [{
3615
+ name: "BuildActionEntries",
3616
+ attributes: {},
3617
+ children: [{
3618
+ name: "BuildActionEntry",
3619
+ attributes: {
3620
+ buildForTesting: "YES",
3621
+ buildForRunning: "YES",
3622
+ buildForProfiling: "YES",
3623
+ buildForArchiving: "YES",
3624
+ buildForAnalyzing: "YES"
3625
+ },
3626
+ children: [buildableReference()]
3627
+ }]
3628
+ }]
3629
+ },
3630
+ {
3631
+ name: "LaunchAction",
3632
+ attributes: {
3633
+ buildConfiguration: "Debug",
3634
+ selectedDebuggerIdentifier: "Xcode.DebuggerFoundation.Debugger.LLDB",
3635
+ selectedLauncherIdentifier: "Xcode.DebuggerFoundation.Launcher.LLDB",
3636
+ launchStyle: "0",
3637
+ useCustomWorkingDirectory: "NO",
3638
+ ignoresPersistentStateOnLaunch: "NO",
3639
+ debugDocumentVersioning: "YES",
3640
+ debugServiceExtension: "internal",
3641
+ allowLocationSimulation: "YES"
3642
+ },
3643
+ children: [{
3644
+ name: "BuildableProductRunnable",
3645
+ attributes: { runnableDebuggingMode: "0" },
3646
+ children: [buildableReference()]
3647
+ }]
3648
+ },
3649
+ {
3650
+ name: "ProfileAction",
3651
+ attributes: {
3652
+ buildConfiguration: "Release",
3653
+ shouldUseLaunchSchemeArgsEnv: "YES",
3654
+ savedToolIdentifier: "",
3655
+ useCustomWorkingDirectory: "NO",
3656
+ debugDocumentVersioning: "YES"
3657
+ },
3658
+ children: [{
3659
+ name: "BuildableProductRunnable",
3660
+ attributes: { runnableDebuggingMode: "0" },
3661
+ children: [buildableReference()]
3662
+ }]
3663
+ },
3664
+ {
3665
+ name: "AnalyzeAction",
3666
+ attributes: { buildConfiguration: "Debug" },
3667
+ children: []
3668
+ },
3669
+ {
3670
+ name: "ArchiveAction",
3671
+ attributes: {
3672
+ buildConfiguration: "Release",
3673
+ revealArchiveInOrganizer: "YES"
3674
+ },
3675
+ children: []
3676
+ }
3677
+ ]
3678
+ },
3679
+ trailing: []
3680
+ };
3681
+ }
3682
+ //#endregion
3683
+ //#region src/scheme/parse.ts
3684
+ /**
3685
+ * Parser for the `.xcscheme` XML dialect.
3686
+ *
3687
+ * Scheme files are XML with a narrow shape: elements with attributes and
3688
+ * child elements, no text content, no namespaces, no DOCTYPE. The parser
3689
+ * accepts that shape from any writer (attribute order, quoting style, and
3690
+ * whitespace vary across tools), resolves character and entity references
3691
+ * in attribute values, and preserves comments. Anything outside the shape
3692
+ * fails loudly with a position, in line with the pbxproj parser.
3693
+ *
3694
+ * @module
3695
+ */
3696
+ const CODE_TAB = 9;
3697
+ const CODE_LINE_FEED = 10;
3698
+ const CODE_CARRIAGE_RETURN = 13;
3699
+ const CODE_SPACE = 32;
3700
+ const CODE_BANG = 33;
3701
+ const CODE_QUOTE = 34;
3702
+ const CODE_AMPERSAND = 38;
3703
+ const CODE_APOSTROPHE = 39;
3704
+ const CODE_HYPHEN = 45;
3705
+ const CODE_SLASH = 47;
3706
+ const CODE_LESS_THAN = 60;
3707
+ const CODE_EQUALS = 61;
3708
+ const CODE_GREATER_THAN = 62;
3709
+ const CODE_QUESTION = 63;
3710
+ const CODE_HASH = 35;
3711
+ const CODE_BOM = 65279;
3712
+ /**
3713
+ * Whether a code unit is XML whitespace (space, tab, line feed, or
3714
+ * carriage return).
3715
+ */
3716
+ function isWhitespace(code) {
3717
+ return code === CODE_SPACE || code === CODE_TAB || code === CODE_LINE_FEED || code === CODE_CARRIAGE_RETURN;
3718
+ }
3719
+ /**
3720
+ * Whether a code unit can start an XML name. The scheme vocabulary is
3721
+ * ASCII, so the accepted alphabet is letters, underscore, and colon.
3722
+ */
3723
+ function isNameStart(code) {
3724
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code === 58;
3725
+ }
3726
+ /**
3727
+ * Whether a code unit can continue an XML name.
3728
+ */
3729
+ function isNameChar(code) {
3730
+ return isNameStart(code) || code >= 48 && code <= 57 || code === CODE_HYPHEN || code === 46;
3731
+ }
3732
+ /** The named character entities XML 1.0 predefines. */
3733
+ const NAMED_ENTITIES = {
3734
+ amp: "&",
3735
+ apos: "'",
3736
+ gt: ">",
3737
+ lt: "<",
3738
+ quot: "\""
3739
+ };
3740
+ /**
3741
+ * Parses the text of a `.xcscheme` file into its node tree.
3742
+ *
3743
+ * @throws XcschemeParseError when the text is not a well-formed scheme
3744
+ * document, with the line and column of the failure.
3745
+ */
3746
+ function parseXcscheme(text) {
3747
+ return new Parser(text).parseDocument();
3748
+ }
3749
+ var Parser = class {
3750
+ input;
3751
+ pos = 0;
3752
+ constructor(input) {
3753
+ this.input = input;
3754
+ if (input.charCodeAt(0) === CODE_BOM) this.pos = 1;
3755
+ }
3756
+ parseDocument() {
3757
+ this.skipWhitespace();
3758
+ this.skipDeclaration();
3759
+ const leading = [];
3760
+ const trailing = [];
3761
+ this.skipWhitespace();
3762
+ while (this.peekIsCommentStart()) {
3763
+ leading.push(this.parseComment());
3764
+ this.skipWhitespace();
3765
+ }
3766
+ const root = this.parseElement();
3767
+ this.skipWhitespace();
3768
+ while (this.peekIsCommentStart()) {
3769
+ trailing.push(this.parseComment());
3770
+ this.skipWhitespace();
3771
+ }
3772
+ if (this.pos < this.input.length) this.fail("Expected end of document after the root element");
3773
+ return {
3774
+ leading,
3775
+ root,
3776
+ trailing
3777
+ };
3778
+ }
3779
+ /**
3780
+ * Skips the `<?xml ... ?>` declaration when present. Its attributes are
3781
+ * not retained: the writer always emits the canonical UTF-8 declaration.
3782
+ */
3783
+ skipDeclaration() {
3784
+ if (this.input.charCodeAt(this.pos) !== CODE_LESS_THAN || this.input.charCodeAt(this.pos + 1) !== CODE_QUESTION) return;
3785
+ const end = this.input.indexOf("?>", this.pos + 2);
3786
+ if (end === -1) this.fail("Unterminated XML declaration");
3787
+ this.pos = end + 2;
3788
+ }
3789
+ parseElement() {
3790
+ if (this.input.charCodeAt(this.pos) !== CODE_LESS_THAN) this.fail("Expected an element");
3791
+ this.pos++;
3792
+ const name = this.parseName("element name");
3793
+ const attributes = Object.create(null);
3794
+ for (;;) {
3795
+ const hadWhitespace = this.skipWhitespace();
3796
+ const code = this.input.charCodeAt(this.pos);
3797
+ if (code === CODE_GREATER_THAN) {
3798
+ this.pos++;
3799
+ break;
3800
+ }
3801
+ if (code === CODE_SLASH && this.input.charCodeAt(this.pos + 1) === CODE_GREATER_THAN) {
3802
+ this.pos += 2;
3803
+ return {
3804
+ name,
3805
+ attributes,
3806
+ children: []
3807
+ };
3808
+ }
3809
+ if (Number.isNaN(code)) this.fail(`Unterminated <${name}> tag`);
3810
+ if (!hadWhitespace) this.fail("Expected whitespace before an attribute");
3811
+ const attributeName = this.parseName("attribute name");
3812
+ this.skipWhitespace();
3813
+ if (this.input.charCodeAt(this.pos) !== CODE_EQUALS) this.fail(`Expected = after attribute ${attributeName}`);
3814
+ this.pos++;
3815
+ this.skipWhitespace();
3816
+ if (attributeName in attributes) this.fail(`Duplicate attribute ${attributeName} on <${name}>`);
3817
+ attributes[attributeName] = this.parseAttributeValue();
3818
+ }
3819
+ const children = [];
3820
+ for (;;) {
3821
+ this.skipWhitespace();
3822
+ const code = this.input.charCodeAt(this.pos);
3823
+ if (Number.isNaN(code)) this.fail(`Missing </${name}> close tag`);
3824
+ if (code !== CODE_LESS_THAN) this.fail("Unexpected text content inside an element");
3825
+ if (this.input.charCodeAt(this.pos + 1) === CODE_SLASH) {
3826
+ this.pos += 2;
3827
+ const closeName = this.parseName("close tag name");
3828
+ if (closeName !== name) this.fail(`Expected </${name}> but found </${closeName}>`);
3829
+ this.skipWhitespace();
3830
+ if (this.input.charCodeAt(this.pos) !== CODE_GREATER_THAN) this.fail(`Malformed </${closeName}> close tag`);
3831
+ this.pos++;
3832
+ return {
3833
+ name,
3834
+ attributes,
3835
+ children
3836
+ };
3837
+ }
3838
+ if (this.peekIsCommentStart()) children.push(this.parseComment());
3839
+ else children.push(this.parseElement());
3840
+ }
3841
+ }
3842
+ peekIsCommentStart() {
3843
+ return this.input.charCodeAt(this.pos) === CODE_LESS_THAN && this.input.charCodeAt(this.pos + 1) === CODE_BANG && this.input.charCodeAt(this.pos + 2) === CODE_HYPHEN && this.input.charCodeAt(this.pos + 3) === CODE_HYPHEN;
3844
+ }
3845
+ parseComment() {
3846
+ const end = this.input.indexOf("-->", this.pos + 4);
3847
+ if (end === -1) this.fail("Unterminated comment");
3848
+ const comment = this.input.slice(this.pos + 4, end);
3849
+ this.pos = end + 3;
3850
+ return { comment };
3851
+ }
3852
+ parseName(what) {
3853
+ const start = this.pos;
3854
+ if (!isNameStart(this.input.charCodeAt(this.pos))) this.fail(`Expected ${what}`);
3855
+ this.pos++;
3856
+ while (isNameChar(this.input.charCodeAt(this.pos))) this.pos++;
3857
+ return this.input.slice(start, this.pos);
3858
+ }
3859
+ parseAttributeValue() {
3860
+ const quote = this.input.charCodeAt(this.pos);
3861
+ if (quote !== CODE_QUOTE && quote !== CODE_APOSTROPHE) this.fail("Expected a quoted attribute value");
3862
+ this.pos++;
3863
+ let value = "";
3864
+ let runStart = this.pos;
3865
+ for (;;) {
3866
+ const code = this.input.charCodeAt(this.pos);
3867
+ if (Number.isNaN(code)) this.fail("Unterminated attribute value");
3868
+ if (code === quote) {
3869
+ value += this.input.slice(runStart, this.pos);
3870
+ this.pos++;
3871
+ return value;
3872
+ }
3873
+ if (code === CODE_LESS_THAN) this.fail("Attribute values cannot contain a raw <");
3874
+ if (code === CODE_AMPERSAND) {
3875
+ value += this.input.slice(runStart, this.pos);
3876
+ value += this.parseReference();
3877
+ runStart = this.pos;
3878
+ } else this.pos++;
3879
+ }
3880
+ }
3881
+ /**
3882
+ * Resolves one `&...;` reference with the cursor on the ampersand. The
3883
+ * accepted forms are the five XML named entities and decimal or hex
3884
+ * character references, which covers everything observed in
3885
+ * Xcode-written schemes (`&quot;`, `&amp;`, `&apos;`, `&lt;`, `&gt;`,
3886
+ * and `&#10;`-style whitespace).
3887
+ */
3888
+ parseReference() {
3889
+ const start = this.pos;
3890
+ const end = this.input.indexOf(";", start + 1);
3891
+ if (end === -1 || end - start > 12) this.fail("Unterminated character reference");
3892
+ const body = this.input.slice(start + 1, end);
3893
+ this.pos = end + 1;
3894
+ if (body.charCodeAt(0) === CODE_HASH) {
3895
+ const isHex = body.charCodeAt(1) === 120 || body.charCodeAt(1) === 88;
3896
+ const digits = body.slice(isHex ? 2 : 1);
3897
+ const radix = isHex ? 16 : 10;
3898
+ const codePoint = Number.parseInt(digits, radix);
3899
+ if (digits.length === 0 || Number.isNaN(codePoint) || codePoint > 1114111) this.failAt(`Invalid character reference &${body};`, start);
3900
+ return String.fromCodePoint(codePoint);
3901
+ }
3902
+ const named = Object.hasOwn(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : void 0;
3903
+ if (named == null) this.failAt(`Unknown entity &${body};`, start);
3904
+ return named;
3905
+ }
3906
+ /**
3907
+ * Skips whitespace and reports whether any was consumed, which attribute
3908
+ * parsing uses to require separation between attributes.
3909
+ */
3910
+ skipWhitespace() {
3911
+ const start = this.pos;
3912
+ while (isWhitespace(this.input.charCodeAt(this.pos))) this.pos++;
3913
+ return this.pos > start;
3914
+ }
3915
+ fail(message) {
3916
+ this.failAt(message, this.pos);
3917
+ }
3918
+ failAt(message, offset) {
3919
+ throw new XcschemeParseError(message, this.input, offset);
3920
+ }
3921
+ };
3922
+ //#endregion
3923
+ export { AggregateTarget, BuildPhase, BuildRule, CopyFilesDestination, Group, Isa, LegacyTarget, NativeTarget, PbxprojBuildError, PbxprojParseError, ProductType, ReferenceProxy, RootProject, SyncRootGroup, Target, VersionGroup, XcodeModelError, XcodeObject, XcodeProject, XcschemeBuildError, XcschemeParseError, buildPbxproj, buildXcscheme, createXcscheme, generateObjectId, isXcschemeElement, parsePbxproj, parseXcscheme, xcschemeElements };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rork-xcode",
3
- "version": "0.2.0",
4
- "description": "Zero-dependency Xcode project (pbxproj) parser and builder that runs identically in every JavaScript runtime",
3
+ "version": "0.3.0",
4
+ "description": "Zero-dependency Xcode project (pbxproj) and scheme (xcscheme) parser, builder, and object model that runs identically in every JavaScript runtime",
5
5
  "keywords": [
6
6
  "apple",
7
7
  "builder",