●BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the output●NATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other builders●PLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screen●COMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to end●PRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that back●DEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify●BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the output●NATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other builders●PLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screen●COMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to end●PRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that back●DEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify
Your lockfile's dev/prod split won't tell you which licenses your app must credit
A record of classifying every dependency in a production project to decide what belongs on an app's license screen, and where the copyleft findings and the actual shipped artifact turned out to disagree.
I stalled for half a day over whether to put a "Licenses" row at the bottom of a settings screen.
Adding it was clearly the right thing. What I could not decide was what to list there. A project exported by an AI builder carries hundreds of packages you never picked yourself. Do you list all of them, or only what actually ships? Pasting a template without answering that question is not disclosure. It is the appearance of disclosure.
So I classified the lockfile of a Next.js 16 project I run in production. The short version: the fourteen LGPL packages that made my stomach drop were not shipping at all. And the dependencies that genuinely needed crediting could not be identified from the dev / prod flags alone.
Start by counting the lockfile as it is
package-lock.json at lockfileVersion: 3 records a license field per package. You can inventory every dependency straight from the lockfile, without expanding node_modules.
Here is the script I actually ran. Standard library only.
# license_inventory.py - classify dependency licenses from the lockfile aloneimport json, collections, syspath = sys.argv[1] if len(sys.argv) > 1 else "package-lock.json"with open(path, encoding="utf-8") as f: data = json.load(f)packages = data.get("packages", {})if not packages: # lockfileVersion 1 has no "packages" map, so fail loudly here raise SystemExit("No packages map. Regenerate the lockfile at version 2 or later.")def classify(meta): # Order matters. Check dev first, or devOptional leaks into prod. if meta.get("dev"): return "dev" if meta.get("devOptional"): return "devOptional" if meta.get("optional"): return "optional" return "prod"counts = collections.Counter()by_class = collections.defaultdict(collections.Counter)unknown = []for name, meta in packages.items(): if not name: # the empty key is the root project itself continue kind = classify(meta) counts[kind] += 1 lic = meta.get("license") if isinstance(lic, list): # older packages sometimes use an array lic = " / ".join(lic) if lic is None: unknown.append(name) lic = "(none recorded)" by_class[kind][lic] += 1print("total:", sum(counts.values()), dict(counts))for kind in ("prod", "optional", "dev", "devOptional"): if by_class[kind]: print(f"--- {kind} ---") for lic, n in by_class[kind].most_common(): print(f" {n:4d} {lic}")print("missing license field:", len(unknown))for name in unknown[:20]: print(" ", name)
The output looked like this.
Class
Count
What it means
prod
430
Resolved as a production dependency
dev
429
Used only by builds, tests, and linters
optional
70
Platform-specific binaries and similar, installed conditionally
devOptional
3
Development-side and optional
932 packages in total. prod and dev split almost evenly at 46% each, and the entries carrying copyleft terms came to 27 packages, or 2.9% of the tree. Not one package was missing a license field.
That last part surprised me. With several hundred dependencies I expected a handful of gaps, and there were none. Even at the scale an indie developer works at, the provenance of the tree is tidier than I assumed.
Every copyleft hit was build-only
Scanning the breakdown, the first thing that caught my eye was ten packages under LGPL-3.0-or-later plus four more under Apache-2.0 AND LGPL-3.0-or-later. Fourteen in total. Enough to stop working and start reading.
Listing them showed all fourteen were platform binaries for the image library sharp (@img/sharp-libvips-*). All fourteen classified as dev.
The same pattern held for the thirteen MPL-2.0 packages: lightningcss with its platform binaries, plus the accessibility checker axe-core. Every one of them dev.
License
Count
Class
What they actually are
LGPL-3.0-or-later (alone and in combination)
14
all dev
sharp's libvips binaries
MPL-2.0
13
all dev
lightningcss / axe-core
CC-BY-4.0
1
prod
caniuse-lite
The fourteen packages I braced myself for never reach a user's device. The image conversion happened on a build machine, and only the converted output is distributed.
That points at something about method. If you begin a license inventory as a hunt for scary names, the first things you find will almost always be build tooling. Build tools run on developer machines, which makes them likely to ship native binaries, and native binaries are the most likely place for copyleft to appear. Searching in that order spends your attention exactly where it matters least.
MPL-2.0 asks that when you distribute covered software in executable form, you tell recipients how to obtain the corresponding source form (Mozilla Public License 2.0). Read the other way around: if you are not distributing it, that clause is not your problem yet. Establishing what you distribute has to come first.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to separate dependencies that reach your shipped artifact from the ones that only run on your build machine, and decide on your own terms what belongs on a license screen
✦You will have a checking order that stops you from freezing the moment you spot a copyleft package name, before confirming whether it ever reaches users at all
✦You will see how 932 dependencies in a production project broke down by class, so you know where to look first in your own project
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The dev/prod split is not the same question as "does this ship"
So can you just list the 430 prod packages? Not that either.
caniuse-lite, classified as prod, is CC-BY-4.0. It is a browser-support dataset consulted at build time, and only its conclusions survive into the output. The npm dependency graph still resolves it as a production dependency.
The prod flag means "present in the production dependency graph," not "present in the bytes you ship." Treating those as one thing makes you credit packages that never ship while missing ones that do.
I ended up thinking in three layers.
Layer
What tells you
How it affects the license screen
1. Exists in the dependency graph
Lockfile classification
Candidates only. Nothing is decided here
2. Present in the shipped bytes
Bundle output and native dependency records
This is the set you credit
3. Loaded at runtime
Behavior on device
Already covered by layer 2
Getting from layer 1 to layer 2 is the part that requires actually building something. I suspect that is why so many projects settle for "list the entire lockfile." Over-listing is not a violation, but a screen filled with the copyright notices of build tooling gives a reader nothing.
Pinning down layer 2 in an Expo export
Projects exported by Rork and Rork Max live in the same npm world. What differs is that the artifact splits in two: a JS bundle and a native binary.
For the JS side, export the bundle and look inside it.
# 1) Export with the same settings you shipnpx expo export --platform ios --output-dir ./dist-audit# 2) Read which modules made it in, from the source map# The "sources" array lists every file that was pulled into the bundlenode -e "const fs=require('fs');const path=process.argv[1];const map=JSON.parse(fs.readFileSync(path,'utf8'));const pkgs=new Set();for (const s of map.sources) { const m = s.match(/node_modules\/((?:@[^/]+\/)?[^/]+)/); if (m) pkgs.add(m[1]);}console.log([...pkgs].sort().join('\n'));console.error('packages bundled: ' + pkgs.size);" ./dist-audit/_expo/static/js/ios/*.map
That list is the JS half of layer 2, and it will not match the 430 prod entries. A package can be resolved as a dependency and still never be imported.
The native half comes from elsewhere: ios/Podfile.lock on iOS, and Gradle's resolved configuration on Android.
# iOS: pods that were actually linkedgrep -A200 '^PODS:' ios/Podfile.lock | sed -n 's/^ - \([^ (]*\).*/\1/p' | sort -u# Android: dependencies on the release runtime classpath onlycd android && ./gradlew app:dependencies --configuration releaseRuntimeClasspath
If you ship to both the App Store and Google Play, both native trees are in scope. Counting one and calling it done leaves the other artifact's dependencies off your screen.
The numbers here shift with your SDK version and installed modules, so take them in your own project. Copying mine would tell you nothing useful.
Three things that tripped me up
First, without source maps there is no JS side of layer 2. Some production export configurations disable them, so you have to re-enable them for the audit run. I write the audit build to a separate directory and delete it once I am done, to keep it away from anything shippable.
Second, optional dependencies change with the environment. Platform binaries resolve differently on my macOS machine than on a Linux CI runner. Counting locally alone misses whatever CI pulled in. Running the inventory where the shipped artifact is actually built turned out to be the only reliable option.
Third, an old lockfileVersion will stop the script with an error, because version 1 has no packages map. Regenerating the lockfile works around it, but that can also change how dependencies resolve, which is not something to do days before a release. I pushed that audit to the following release cycle instead.
Decide how you read the obligations before you write the screen
Once layer 2 is settled, the remaining question is what each license asks for. Reading the actual text turned out to be the fastest route.
License
What distribution requires
Where it lands in practice
MIT / ISC / BSD family
Retain the copyright notice and permission text
Full text on the license screen
Apache-2.0
The license, plus the attributions from any NOTICE file in readable form
License screen, with NOTICE contents included
MPL-2.0
When distributing in executable form, inform recipients how to obtain the source form
Needs individual handling if it reaches layer 2
CC-BY-4.0
Credit
Credit if it reaches layer 2, otherwise nothing
The NOTICE handling sits in section 4(d) of the Apache text. When you distribute derivative works, the attributions from the NOTICE file must appear in readable form in at least one of three places: a NOTICE file, the source or documentation, or a display generated by the work itself (Apache License, Version 2.0). An app's license screen is that third option. That single clause is the practical reason the row belongs in your settings.
One caveat: I am not a lawyer. What is written here is how I read the texts against my own project, not legal advice. Where a judgment call is involved, ask someone qualified, and especially so if a copyleft library does turn up in layer 2.
What went on the settings screen in the end
The rules I settled on:
Credit only what reached layer 2. Anything that stopped at layer 1 stays off the screen
Never hand-write the list. Generate it from the build output every time
If copyleft appears in layer 2, review it individually before release, and do not wave that one through on your own judgment
The second rule carried the most weight. A hand-written list becomes false the week after you add one dependency. If skipping the work of generating it means publishing an inaccurate notice, not publishing one is arguably more honest. So I refused to add the row until I could generate the list.
The generator ended up as a build step that writes the layer 2 inventory to JSON and bundles it as an app asset. The screen just reads the JSON and renders it. If you are still deciding where such a screen belongs structurally, building a proper settings screen for a Rork app covers that side and makes the connection easier to see.
The third rule lives in CI as detection only. A human still makes the call, because a change that pulls copyleft into layer 2 usually means more than "one more dependency."
If you take one step from here
Run the inventory script against your own package-lock.json once. The class breakdown alone tells you a great deal about what you are carrying. Layer 2 can wait until after that.
For a long time I filed licensing under "tedious work to deal with later." Counting it showed me the tedious part was never the work. It was that I had never decided where the line was.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.