@webority/mobile-tools 0.18.0 → 0.19.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 +15 -1
- package/lib/cli.js +1 -1
- package/lib/codemod.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @webority/mobile-tools
|
|
2
2
|
|
|
3
|
-
The dev-time CLI for Webority React Native apps. It covers
|
|
3
|
+
The dev-time CLI for Webority React Native apps. It covers six jobs: compiling the environment config into the native projects, catching a stale `node_modules` before a 20-minute build, building and launching on one device, rendering per-environment launcher icons, publishing over-the-air updates, and moving an app's theme onto a new `@webority/mobile-ui` release's key names. It behaves the same on Windows (Git Bash, PowerShell, cmd) and macOS (zsh).
|
|
4
4
|
|
|
5
5
|
It is a devDependency. App code never imports it, and it adds nothing to the app bundle.
|
|
6
6
|
|
|
@@ -142,6 +142,20 @@ Publishes a JavaScript-only change over the air to the phones already on the cur
|
|
|
142
142
|
|
|
143
143
|
`--dry-run` prints the commands after the store check, and changes nothing. `weborityos` must be on `PATH` and signed in (`weborityos login`). Development is refused: it runs from Metro.
|
|
144
144
|
|
|
145
|
+
### `codemod theme-keys`
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx mobile-tools codemod theme-keys [--dry-run]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Moves the app's theme overrides onto the key names of the `@webority/mobile-ui` it has installed. Run it after upgrading mobile-ui, because the app stops at startup while a theme override still sets a retired key.
|
|
152
|
+
|
|
153
|
+
- Reads the retired keys from the app's own copy of mobile-ui (`@webority/mobile-ui/retired-theme-keys`), so it always matches the installed release. It fails when mobile-ui is older than 0.19.0.
|
|
154
|
+
- Reads every `.js`, `.jsx`, `.ts` and `.tsx` file under the app folder, except `node_modules`, `lib`, `build`, `dist`, `android`, `ios`, `coverage` and dot-folders. A file that does not parse is named and skipped.
|
|
155
|
+
- A theme object is an object literal with at least two of `colors`, `spacing`, `typography`, `components`, `radius` and `shadows`. Inside one, it renames each renamed key in place and deletes each removed key. Values are never changed.
|
|
156
|
+
- It lists, with file and line, what needs a person: a renamed key whose value must now be a scale name (`'md'`, not `12`), each removed key with what to set instead, and code that reads a retired radius (`theme.radius.full`).
|
|
157
|
+
- Exits 1 while anything is listed, and 0 when nothing is left. `--dry-run` reports the same list and writes nothing.
|
|
158
|
+
|
|
145
159
|
## `mobile-tools.config.json`
|
|
146
160
|
|
|
147
161
|
Optional, at the app root. Every key is optional. An unknown key or a wrong type fails, naming the key.
|
package/lib/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{parseArgs}from"node:util";import{parseEnvironment}from"./config.js";import{installHooks}from"./hooks.js";import{icons}from"./icons.js";import{otaDeploy}from"./ota.js";import{CliError,fail,say,warn}from"./output.js";import{prebuild}from"./prebuild.js";import{run}from"./run.js";import{checkSync,sync,writeStamp}from"./sync.js";const USAGE="mobile-tools <command> [options] (run from the app's root folder)\n\n prebuild --env <Development|Staging|Production> [--platform android|ios]\n prebuild --same-env [--platform android|ios]\n Compile appsettings into appsettings.Compiled.json and stamp the native projects.\n\n sync [--check | --stamp]\n --check: exit 1 with the fix command when node_modules (or ios/Pods) is stale.\n --stamp: record that node_modules matches the lockfile now (run it from postinstall).\n Without either: install from the lockfile, stamp, and reset native codegen when needed.\n\n hooks install\n Add git post-merge and post-checkout hooks that run sync --check.\n\n run --env <Environment> [--platform android|ios] [--device <serial|name>]\n Build for one device's ABI, install it there, reuse or start Metro, and launch.\n\n icons [--source <png|svg>]\n Render per-environment launcher icons for Android flavors and iOS AppIcon-<Env> sets.\n\n ota deploy --env <Staging|Production> [--platform android|ios] [--rollout <0-100>]\n [--notes <text>] [--critical] [--approval-id <id>] [--force] [--no-store-check]\n [--store-profile <name>] [--dry-run]\n ota deploy --complete\n Prebuild, expo export and weborityos ota deploy per platform. Production skips a platform\n whose live store build is not VERSION_INTERNAL. The first run raises an approval card per\n platform; --complete uploads the bundles once approved.",parse=(o,
|
|
2
|
+
import{parseArgs}from"node:util";import{codemodThemeKeys}from"./codemod.js";import{parseEnvironment}from"./config.js";import{installHooks}from"./hooks.js";import{icons}from"./icons.js";import{otaDeploy}from"./ota.js";import{CliError,fail,say,warn}from"./output.js";import{prebuild}from"./prebuild.js";import{run}from"./run.js";import{checkSync,sync,writeStamp}from"./sync.js";const USAGE="mobile-tools <command> [options] (run from the app's root folder)\n\n prebuild --env <Development|Staging|Production> [--platform android|ios]\n prebuild --same-env [--platform android|ios]\n Compile appsettings into appsettings.Compiled.json and stamp the native projects.\n\n sync [--check | --stamp]\n --check: exit 1 with the fix command when node_modules (or ios/Pods) is stale.\n --stamp: record that node_modules matches the lockfile now (run it from postinstall).\n Without either: install from the lockfile, stamp, and reset native codegen when needed.\n\n hooks install\n Add git post-merge and post-checkout hooks that run sync --check.\n\n run --env <Environment> [--platform android|ios] [--device <serial|name>]\n Build for one device's ABI, install it there, reuse or start Metro, and launch.\n\n icons [--source <png|svg>]\n Render per-environment launcher icons for Android flavors and iOS AppIcon-<Env> sets.\n\n ota deploy --env <Staging|Production> [--platform android|ios] [--rollout <0-100>]\n [--notes <text>] [--critical] [--approval-id <id>] [--force] [--no-store-check]\n [--store-profile <name>] [--dry-run]\n ota deploy --complete\n Prebuild, expo export and weborityos ota deploy per platform. Production skips a platform\n whose live store build is not VERSION_INTERNAL. The first run raises an approval card per\n platform; --complete uploads the bundles once approved.\n\n codemod theme-keys [--dry-run]\n Rewrite theme overrides onto the installed mobile-ui's key names; list what needs a person.",parse=(e,o,t=!1)=>{try{const{values:n,positionals:s}=parseArgs({args:e,options:o,allowPositionals:t,strict:!0});return{values:n,positionals:s}}catch(e){return fail(`${e.message}\n\n${USAGE}`,2)}},envOption=e=>"string"==typeof e?parseEnvironment(e):void 0,platformOption=e=>{if(void 0!==e)return"android"!==e&&"ios"!==e?fail(`--platform must be android or ios, not ${String(e)}`,2):e},main=async e=>{const[o,...t]=e,n=process.cwd();switch(o){case"prebuild":{const{values:e}=parse(t,{env:{type:"string"},"same-env":{type:"boolean"},platform:{type:"string"}});return void prebuild({appRoot:n,env:envOption(e.env),sameEnv:!0===e["same-env"],platform:platformOption(e.platform)})}case"sync":{const{values:e}=parse(t,{check:{type:"boolean"},stamp:{type:"boolean"}});if(e.check&&e.stamp&&fail("use --check or --stamp, not both",2),e.stamp)return writeStamp(n),void say("sync --stamp: recorded node_modules as matching the lockfile");if(e.check){const e=checkSync(n,process.platform);return e.length>0&&fail(e.join("\n")),void say("sync --check: node_modules matches the lockfile")}return void sync(n,process.platform)}case"hooks":{const{positionals:e}=parse(t,{},!0);return"install"===e[0]&&1===e.length||fail(`usage: mobile-tools hooks install\n\n${USAGE}`,2),void installHooks(n)}case"run":{const{values:e}=parse(t,{env:{type:"string"},platform:{type:"string"},device:{type:"string"}}),o=envOption(e.env)??fail("run needs --env <Development|Staging|Production>",2);return void await run({appRoot:n,env:o,platform:platformOption(e.platform)??"android",device:"string"==typeof e.device?e.device:void 0})}case"codemod":{const{values:e,positionals:o}=parse(t,{"dry-run":{type:"boolean"}},!0);1===o.length&&"theme-keys"===o[0]||fail(`usage: mobile-tools codemod theme-keys [--dry-run]\n\n${USAGE}`,2);const s=!0===e["dry-run"],r=codemodThemeKeys({appRoot:n,dryRun:s});for(const e of r.changedFiles)say(`${s?"would update":"updated"} ${e}`);for(const e of r.findings)say(`${e.file}:${e.line} ${e.path}: ${e.message}`);return r.findings.length>0&&fail(`${r.findings.length} item(s) above need a person; the app refuses retired keys at startup until they are fixed`),void say("codemod theme-keys: done")}case"icons":{const{values:e}=parse(t,{source:{type:"string"}});return void await icons({appRoot:n,source:"string"==typeof e.source?e.source:void 0})}case"ota":{const{values:e,positionals:o}=parse(t,{env:{type:"string"},platform:{type:"string"},rollout:{type:"string"},notes:{type:"string"},critical:{type:"boolean"},"approval-id":{type:"string"},complete:{type:"boolean"},force:{type:"boolean"},"no-store-check":{type:"boolean"},"store-profile":{type:"string"},"dry-run":{type:"boolean"}},!0);"deploy"===o[0]&&1===o.length||fail(`usage: mobile-tools ota deploy --env <Staging|Production>\n\n${USAGE}`,2);const text=e=>"string"==typeof e?e:void 0;return void otaDeploy({appRoot:n,env:envOption(e.env),platform:platformOption(e.platform),rollout:text(e.rollout),notes:text(e.notes),critical:!0===e.critical,approvalId:text(e["approval-id"]),complete:!0===e.complete,force:!0===e.force,storeCheck:!0!==e["no-store-check"],storeProfile:text(e["store-profile"]),dryRun:!0===e["dry-run"]})}case void 0:case"help":case"--help":case"-h":return void say(USAGE);default:fail(`unknown command "${o}"\n\n${USAGE}`,2)}};main(process.argv.slice(2)).catch(e=>{e instanceof CliError&&(warn(`mobile-tools: ${e.message}`),process.exit(e.exitCode)),warn(e instanceof Error?e.stack??e.message:String(e)),process.exit(1)});
|
package/lib/codemod.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readdirSync,readFileSync,writeFileSync}from"node:fs";import{createRequire}from"node:module";import path from"node:path";import{fail,warn}from"./output.js";const THEME_KEYS=new Set(["colors","spacing","typography","components","radius","shadows"]),SIZES=new Set(["xs","sm","md","lg","xl"]),SKIP_DIRS=new Set(["node_modules","lib","build","dist","android","ios","coverage"]),SKIP_KEYS=new Set(["loc","start","end","extra","comments","leadingComments","trailingComments","innerComments"]),PLUGINS={".js":["jsx"],".jsx":["jsx"],".ts":["typescript"],".tsx":["typescript","jsx"]},requireFrom=(e,t)=>{try{return createRequire(path.join(e,"package.json"))(t)}catch{return}},loadTable=e=>{const t=requireFrom(e,"@webority/mobile-ui/retired-theme-keys"),s=t?.RETIRED_THEME_KEYS;return Array.isArray(s)?s:fail("codemod theme-keys needs @webority/mobile-ui 0.19.0 or later installed in this app")},loadParser=e=>{const t=requireFrom(e,"@babel/parser");return"function"!=typeof t?.parse?fail("codemod theme-keys needs @babel/parser, which every React Native app has; run pnpm install first"):t.parse},isAstNode=e=>"object"==typeof e&&null!==e&&"string"==typeof e.type,visit=(e,t)=>{if(Array.isArray(e))for(const s of e)visit(s,t);else if(isAstNode(e)){t(e);for(const[s,r]of Object.entries(e))SKIP_KEYS.has(s)||"object"!=typeof r||null===r||visit(r,t)}},propsOf=e=>e.properties??[],keyName=e=>{if("ObjectProperty"!==e.type||!0===e.computed)return;const t=e.key;return"Identifier"===t?.type?t.name:"StringLiteral"===t?.type&&"string"==typeof t.value?t.value:void 0},isThemeObject=e=>propsOf(e).filter(e=>{const t=keyName(e);return void 0!==t&&THEME_KEYS.has(t)}).length>=2,matchPath=(e,t,s,r)=>{const[o,...n]=t;for(const t of propsOf(e)){const i=keyName(t);if(void 0===i||("<size>"===o?!SIZES.has(i):i!==o))continue;const a=[...s,i];0===n.length?r.push({prop:t,parent:e,path:a.join(".")}):isAstNode(t.value)&&"ObjectExpression"===t.value.type&&matchPath(t.value,n,a,r)}},deleteRange=(e,t)=>{const s=propsOf(e),r=s.indexOf(t),o=s[r+1];if(void 0!==o)return{start:t.start,end:o.start};const n=s[r-1];return void 0!==n?{start:n.end,end:t.end}:{start:e.start+1,end:e.end-1}},applyEdits=(e,t,s)=>{const r=[...t].sort((e,t)=>e.start-t.start||e.end-t.end),o=[];for(const e of r){const t=o.at(-1);void 0===t||e.start>=t.end?o.push({...e}):(""===t.text&&""===e.text||fail(`codemod theme-keys: two edits overlap in ${s}; fix it by hand`),t.end=Math.max(t.end,e.end))}let n=e;for(const e of o.reverse())n=n.slice(0,e.start)+e.text+n.slice(e.end);return n},retiredRadiusRead=e=>{if("MemberExpression"!==e.type)return;const t=e.object;if("MemberExpression"!==t?.type||"Identifier"!==t.property?.type||"radius"!==t.property.name)return;const s=e.property;return!0!==e.computed&&"Identifier"===s?.type?"full"===s.name||"xs"===s.name?s.name:void 0:!0===e.computed&&"StringLiteral"===s?.type&&"2xl"===s.value?"2xl":void 0},listSourceFiles=e=>{const t=[],s=readdirSync(e,{withFileTypes:!0}).sort((e,t)=>e.name.localeCompare(t.name));for(const r of s){const s=path.join(e,r.name);r.isDirectory()?SKIP_DIRS.has(r.name)||r.name.startsWith(".")||t.push(...listSourceFiles(s)):r.isFile()&&void 0!==PLUGINS[path.extname(r.name)]&&t.push(s)}return t},firstLine=e=>(e instanceof Error?e.message:String(e)).split("\n",1)[0];export const codemodThemeKeys=e=>{const{appRoot:t,dryRun:s}=e,r=e.table??loadTable(t),o=e.parse??loadParser(t),n=[],i=[];for(const e of listSourceFiles(t)){const a=path.relative(t,e).split(path.sep).join("/"),p=readFileSync(e,"utf8");let l;try{l=o(p,{sourceType:"module",plugins:PLUGINS[path.extname(e)]})}catch(e){warn(`codemod theme-keys: skipped ${a}, it does not parse (${firstLine(e)})`);continue}const d=[],c=[],u=new Set;if(visit(l,e=>{if("ObjectExpression"===e.type&&isThemeObject(e))for(const t of r){const s=[];matchPath(e,t.path.split("."),[],s);for(const e of s){if(u.has(e.prop))continue;u.add(e.prop);const{prop:s}=e,r=s.loc.start.line,o=t.to;if(null===o){d.push({...deleteRange(e.parent,s),text:""}),c.push({file:a,line:r,path:e.path,message:`removed; ${t.hint}`});continue}const n=s.key,i=s.value;if(void 0!==n&&isAstNode(i)){if(!0===s.shorthand)d.push({start:s.start,end:s.end,text:`${o}: ${p.slice(i.start,i.end)}`});else{const e="StringLiteral"===n.type?p[n.start]:"";d.push({start:n.start,end:n.end,text:`${e}${o}${e}`})}if(void 0!==t.scale&&"StringLiteral"!==i.type){const s="fontSize"===t.scale?"base":"md";c.push({file:a,line:r,path:e.path,message:`set a ${t.scale} scale name such as '${s}' (the value was ${p.slice(i.start,i.end)})`})}}}}const t=retiredRadiusRead(e);void 0!==t&&c.push({file:a,line:e.loc.start.line,path:`radius.${t}`,message:"reads a retired radius key; use pill for full, sm for xs, xl for 2xl"})}),d.length>0){const t=applyEdits(p,d,a);i.push(a),s||writeFileSync(e,t,"utf8")}n.push(...c)}return{changedFiles:i,findings:n}};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/mobile-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Dev-time CLI for Webority React Native apps: one prebuild, a dependency guard, a device-aware run, per-environment launcher icons and over-the-air deploys, the same on Windows and macOS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|