rork-xcode 0.8.0 → 0.10.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 +59 -11
- package/dist/index.d.ts +379 -51
- package/dist/index.js +662 -124
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -206,6 +206,17 @@ if (widget) project.removeTarget(widget);
|
|
|
206
206
|
project.referrersOf(app.id); // every object referencing an id, for custom teardowns
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
+
### Renaming
|
|
210
|
+
|
|
211
|
+
`renameTarget` renames a target and every place the document knows it by name. That covers the target's `name` and `productName`, its product file reference, the `remoteInfo` of container proxies pointing at it, `TEST_TARGET_NAME` values naming it, and the path segments of `TEST_HOST` and `BUNDLE_LOADER` that name the target or its product. Names match whole, so renaming `DemoApp` leaves `DemoAppTests` alone, and a `PRODUCT_NAME` of `$(TARGET_NAME)` follows by itself. On-disk renames (source folders, entitlements files) and the group paths pointing at those folders stay with the caller.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const app = project.findMainAppTarget("ios");
|
|
215
|
+
if (app) project.renameTarget(app, "RenamedApp");
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Scheme files live next to the project rather than inside it, so the scheme model carries the counterpart (see [Schemes](#schemes)).
|
|
219
|
+
|
|
209
220
|
### Escape hatch
|
|
210
221
|
|
|
211
222
|
Every view exposes its raw dictionary, so anything the typed surface does not cover stays one property away, and `project.objects()` iterates every object with its typed view:
|
|
@@ -235,25 +246,21 @@ for (const [id, object] of project.objects()) {
|
|
|
235
246
|
|
|
236
247
|
`.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 the scheme module covers 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.
|
|
237
248
|
|
|
238
|
-
`Xcscheme` is the model.
|
|
249
|
+
`Xcscheme` is the model. Renames are one call per move. `renameTarget` is the scheme-file side of the project model's `renameTarget` and touches only the named target's references, and `renameContainer` follows a rename of the `.xcodeproj` directory itself. Both return whether anything changed, so callers can skip rewriting untouched files:
|
|
239
250
|
|
|
240
251
|
```ts
|
|
241
252
|
import { Xcscheme } from "rork-xcode";
|
|
242
253
|
|
|
243
254
|
const scheme = Xcscheme.parse(xcschemeText);
|
|
244
255
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
for (const reference of scheme.buildableReferences()) {
|
|
248
|
-
const { blueprintName, buildableName } = reference;
|
|
249
|
-
if (blueprintName) reference.blueprintName = blueprintName.replace("DemoApp", "RenamedApp");
|
|
250
|
-
if (buildableName) reference.buildableName = buildableName.replace("DemoApp", "RenamedApp");
|
|
251
|
-
reference.referencedContainer = "container:RenamedApp.xcodeproj";
|
|
252
|
-
}
|
|
256
|
+
scheme.renameTarget("DemoApp", "RenamedApp"); // DemoAppTests stays untouched
|
|
257
|
+
scheme.renameContainer("DemoApp", "RenamedApp"); // container:RenamedApp.xcodeproj
|
|
253
258
|
|
|
254
259
|
const text = scheme.build();
|
|
255
260
|
```
|
|
256
261
|
|
|
262
|
+
For anything else, buildable references come back as typed views through `scheme.buildableReferences()`, with property access to the blueprint name, buildable name, container, and identifier.
|
|
263
|
+
|
|
257
264
|
`Xcscheme.create` 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:
|
|
258
265
|
|
|
259
266
|
```ts
|
|
@@ -266,6 +273,47 @@ const text = scheme.build();
|
|
|
266
273
|
|
|
267
274
|
Underneath, the document is a plain tree of elements with ordered attributes and children, reachable through `scheme.root` and `scheme.elements(name)`, so anything the typed surface does not cover stays one property away. `parseXcscheme` and `buildXcscheme` remain available for working with the tree directly. Attribute order is preserved and meaningful, 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.
|
|
268
275
|
|
|
276
|
+
## Xcconfig files
|
|
277
|
+
|
|
278
|
+
Build settings do not only live in the pbxproj. Projects push them into `.xcconfig` files referenced through `baseConfigurationReference`, and the xcconfig module reads and writes that format with the fidelity the rest of the library promises. The format is hand-authored with no canonical layout, so the contract here is losslessness. Parsing and building an untouched file reproduces it byte for byte, including comments, blank lines, column alignment, and line endings. Malformed lines fail loudly with a typed error carrying line and column rather than being dropped, so a file the parser accepts is a file it fully understood.
|
|
279
|
+
|
|
280
|
+
`Xcconfig` is the model. Reads follow the file top to bottom the way Xcode does, and writes edit single lines while leaving every other byte alone:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { Xcconfig } from "rork-xcode";
|
|
284
|
+
|
|
285
|
+
const config = Xcconfig.parse(xcconfigText);
|
|
286
|
+
|
|
287
|
+
config.get("PRODUCT_BUNDLE_IDENTIFIER"); // last unconditional assignment wins
|
|
288
|
+
config.set("MARKETING_VERSION", "1.2.0"); // rewrites in place, appends when new
|
|
289
|
+
const settings = config.settings(); // flattened, the way Xcode reads the file
|
|
290
|
+
|
|
291
|
+
const text = config.build();
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`#include` directives are exposed as data because the library never touches the filesystem. Flattening resolves them through a caller-supplied lookup and applies each file at its directive's position, cycle-safe, so lines after an include override it exactly like textual inclusion. Position matters: an include hoisted or reordered changes what the file means, so the model never moves one.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const settings = config.settings({
|
|
298
|
+
resolveInclude: (path, optional) => loadedConfigs.get(path),
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Conditional assignments like `OTHER_LDFLAGS[sdk=iphoneos*][arch=arm64]` are parsed structurally with their conditions preserved verbatim on round-trip, unknown condition names included. Passing a build context applies them during flattening, with every condition required to match and trailing `*` wildcards honored; without a context they stay out, which mirrors reading the file with no build in mind. `$(inherited)` references splice in the value accumulated earlier in the chain and stay literal when there is none, so lower layers can still resolve them:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
const settings = config.settings({
|
|
306
|
+
context: { sdk: "iphoneos", arch: "arm64", config: "Release" },
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Registering a file on the project makes `getBuildSetting` resolve through it in Xcode's order of target settings, then the target's xcconfig, then project settings, then the project's xcconfig:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
project.registerXcconfig(reference, Xcconfig.parse(text));
|
|
314
|
+
app.getBuildSetting("SDKROOT"); // now sees values the xcconfig defines
|
|
315
|
+
```
|
|
316
|
+
|
|
269
317
|
## Performance
|
|
270
318
|
|
|
271
319
|
`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.
|
|
@@ -295,9 +343,9 @@ Measured on an Apple M5 Max, Node.js 24, single thread, with `@bacons/xcode` 1.0
|
|
|
295
343
|
## Verification
|
|
296
344
|
|
|
297
345
|
- 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.
|
|
298
|
-
- Documents already in current Xcode's layout must round-trip byte for byte
|
|
346
|
+
- Documents already in current Xcode's layout must round-trip byte for byte, and documents from other tool generations must normalize to a byte-stable fixed point with unchanged values.
|
|
299
347
|
- 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.
|
|
300
|
-
- A corpus sweep (`pnpm corpus`) walks every Xcode project and
|
|
348
|
+
- A corpus sweep (`pnpm corpus`) walks every Xcode project, scheme, and xcconfig on the machine, verifies each one parses and reaches a byte-stable fixed point (byte-exact losslessness for xcconfig, which has no canonical layout), exercises the object model against every project, and cross-validates a sample against plutil's own reading.
|
|
301
349
|
- CI runs the full gate on Linux and macOS, and executes the built artifact on the oldest supported Node to enforce the `engines` floor.
|
|
302
350
|
|
|
303
351
|
## Releasing
|