@webority/mobile-tools 0.88.0 → 0.89.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
@@ -1,6 +1,6 @@
1
1
  # @webority/mobile-tools
2
2
 
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).
3
+ The dev-time CLI for Webority React Native apps. It covers five 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, and publishing over-the-air updates. 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
 
@@ -122,20 +122,6 @@ Renders every environment's launcher icons from one square source: an SVG, or a
122
122
  - Production is clean. Staging carries an orange `STAGING` ribbon across the bottom-right corner, and Development a green `DEV` one. The ribbon is SVG text in a generic sans-serif, with no font file.
123
123
  - The same source gives the same bytes on the same machine.
124
124
 
125
- ### `codemod theme-keys`
126
-
127
- ```bash
128
- npx mobile-tools codemod theme-keys [--dry-run]
129
- ```
130
-
131
- 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.
132
-
133
- - 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.
134
- - 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.
135
- - 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.
136
- - 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`).
137
- - Exits 1 while anything is listed, and 0 when nothing is left. `--dry-run` reports the same list and writes nothing.
138
-
139
125
  ## `mobile-tools.config.json`
140
126
 
141
127
  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{codemodThemeKeys}from"./codemod.js";import{parseEnvironment}from"./config.js";import{installHooks}from"./hooks.js";import{icons}from"./icons.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 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,n=!1)=>{try{const{values:s,positionals:t}=parseArgs({args:e,options:o,allowPositionals:n,strict:!0});return{values:s,positionals:t}}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,...n]=e,s=process.cwd();switch(o){case"prebuild":{const{values:e}=parse(n,{env:{type:"string"},"same-env":{type:"boolean"},platform:{type:"string"}});return void prebuild({appRoot:s,env:envOption(e.env),sameEnv:!0===e["same-env"],platform:platformOption(e.platform)})}case"sync":{const{values:e}=parse(n,{check:{type:"boolean"},stamp:{type:"boolean"}});if(e.check&&e.stamp&&fail("use --check or --stamp, not both",2),e.stamp)return writeStamp(s),void say("sync --stamp: recorded node_modules as matching the lockfile");if(e.check){const e=checkSync(s,process.platform);return e.length>0&&fail(e.join("\n")),void say("sync --check: node_modules matches the lockfile")}return void sync(s,process.platform)}case"hooks":{const{positionals:e}=parse(n,{},!0);return"install"===e[0]&&1===e.length||fail(`usage: mobile-tools hooks install\n\n${USAGE}`,2),void installHooks(s)}case"run":{const{values:e}=parse(n,{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:s,env:o,platform:platformOption(e.platform)??"android",device:"string"==typeof e.device?e.device:void 0})}case"codemod":{const{values:e,positionals:o}=parse(n,{"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 t=!0===e["dry-run"],r=codemodThemeKeys({appRoot:s,dryRun:t});for(const e of r.changedFiles)say(`${t?"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(n,{source:{type:"string"}});return void await icons({appRoot:s,source:"string"==typeof e.source?e.source:void 0})}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)});
2
+ import{parseArgs}from"node:util";import{parseEnvironment}from"./config.js";import{installHooks}from"./hooks.js";import{icons}from"./icons.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.",parse=(e,o,n=!1)=>{try{const{values:s,positionals:t}=parseArgs({args:e,options:o,allowPositionals:n,strict:!0});return{values:s,positionals:t}}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,...n]=e,s=process.cwd();switch(o){case"prebuild":{const{values:e}=parse(n,{env:{type:"string"},"same-env":{type:"boolean"},platform:{type:"string"}});return void prebuild({appRoot:s,env:envOption(e.env),sameEnv:!0===e["same-env"],platform:platformOption(e.platform)})}case"sync":{const{values:e}=parse(n,{check:{type:"boolean"},stamp:{type:"boolean"}});if(e.check&&e.stamp&&fail("use --check or --stamp, not both",2),e.stamp)return writeStamp(s),void say("sync --stamp: recorded node_modules as matching the lockfile");if(e.check){const e=checkSync(s,process.platform);return e.length>0&&fail(e.join("\n")),void say("sync --check: node_modules matches the lockfile")}return void sync(s,process.platform)}case"hooks":{const{positionals:e}=parse(n,{},!0);return"install"===e[0]&&1===e.length||fail(`usage: mobile-tools hooks install\n\n${USAGE}`,2),void installHooks(s)}case"run":{const{values:e}=parse(n,{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:s,env:o,platform:platformOption(e.platform)??"android",device:"string"==typeof e.device?e.device:void 0})}case"icons":{const{values:e}=parse(n,{source:{type:"string"}});return void await icons({appRoot:s,source:"string"==typeof e.source?e.source:void 0})}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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-tools",
3
- "version": "0.88.0",
3
+ "version": "0.89.0",
4
4
  "description": "Dev-time CLI for Webority React Native apps: one prebuild, a dependency guard, a device-aware run and per-environment launcher icons, the same on Windows and macOS.",
5
5
  "keywords": [
6
6
  "react-native",
package/lib/codemod.js DELETED
@@ -1 +0,0 @@
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}};