@scalebun/cli 1.0.0 → 1.4.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/lib/api/client.d.ts +8 -0
- package/lib/api/client.d.ts.map +1 -1
- package/lib/api/client.js +70 -2
- package/lib/api/client.js.map +1 -1
- package/lib/commands/ci-init.d.ts +0 -6
- package/lib/commands/ci-init.d.ts.map +1 -1
- package/lib/commands/ci-init.js +172 -28
- package/lib/commands/ci-init.js.map +1 -1
- package/lib/commands/doctor.d.ts +8 -0
- package/lib/commands/doctor.d.ts.map +1 -0
- package/lib/commands/doctor.js +252 -0
- package/lib/commands/doctor.js.map +1 -0
- package/lib/commands/login.d.ts +28 -4
- package/lib/commands/login.d.ts.map +1 -1
- package/lib/commands/login.js +132 -51
- package/lib/commands/login.js.map +1 -1
- package/lib/commands/ota-bundle.d.ts.map +1 -1
- package/lib/commands/ota-bundle.js +2 -0
- package/lib/commands/ota-bundle.js.map +1 -1
- package/lib/commands/ota-delete-bundle.d.ts +8 -0
- package/lib/commands/ota-delete-bundle.d.ts.map +1 -1
- package/lib/commands/ota-delete-bundle.js +60 -0
- package/lib/commands/ota-delete-bundle.js.map +1 -1
- package/lib/commands/ota-keys.d.ts +14 -0
- package/lib/commands/ota-keys.d.ts.map +1 -0
- package/lib/commands/ota-keys.js +200 -0
- package/lib/commands/ota-keys.js.map +1 -0
- package/lib/commands/ota-publish.d.ts +10 -0
- package/lib/commands/ota-publish.d.ts.map +1 -1
- package/lib/commands/ota-publish.js +251 -34
- package/lib/commands/ota-publish.js.map +1 -1
- package/lib/commands/ota-rollout.d.ts +3 -0
- package/lib/commands/ota-rollout.d.ts.map +1 -0
- package/lib/commands/ota-rollout.js +235 -0
- package/lib/commands/ota-rollout.js.map +1 -0
- package/lib/commands/quickstart.d.ts +4 -0
- package/lib/commands/quickstart.d.ts.map +1 -0
- package/lib/commands/quickstart.js +274 -0
- package/lib/commands/quickstart.js.map +1 -0
- package/lib/index.js +20 -1
- package/lib/index.js.map +1 -1
- package/lib/utils/bundle.d.ts +86 -0
- package/lib/utils/bundle.d.ts.map +1 -1
- package/lib/utils/bundle.js +244 -14
- package/lib/utils/bundle.js.map +1 -1
- package/lib/utils/credentials.d.ts +12 -1
- package/lib/utils/credentials.d.ts.map +1 -1
- package/lib/utils/credentials.js +40 -1
- package/lib/utils/credentials.js.map +1 -1
- package/lib/utils/transportSecurity.d.ts +36 -0
- package/lib/utils/transportSecurity.d.ts.map +1 -0
- package/lib/utils/transportSecurity.js +74 -0
- package/lib/utils/transportSecurity.js.map +1 -0
- package/lib/utils/ui.d.ts.map +1 -1
- package/lib/utils/ui.js +2 -0
- package/lib/utils/ui.js.map +1 -1
- package/package.json +1 -1
package/lib/utils/bundle.d.ts
CHANGED
|
@@ -19,13 +19,78 @@ export interface BundleOptions {
|
|
|
19
19
|
* step will be mandatory when the patch engine ships.
|
|
20
20
|
*/
|
|
21
21
|
hermes?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Permit shipping a plain-JS bundle when `--hermes` was asked for but
|
|
24
|
+
* hermesc could not run. Default false: a failed compile is an ERROR.
|
|
25
|
+
*
|
|
26
|
+
* It used to warn and ship the uncompiled bundle, which is a silent
|
|
27
|
+
* downgrade of the artifact — the publish "succeeds" and nothing downstream
|
|
28
|
+
* can tell that what shipped is not what was requested. Two concrete costs:
|
|
29
|
+
* cold start is 20-30% slower for every user on that release, and the delta
|
|
30
|
+
* engine diffs the new bundle against the previous one, so a channel that
|
|
31
|
+
* flips between bytecode and plain JS produces patches with nothing in
|
|
32
|
+
* common and every device falls back to a full download.
|
|
33
|
+
*
|
|
34
|
+
* A broken toolchain should stop the publish, not quietly change what it
|
|
35
|
+
* publishes. The escape hatch stays available for anyone who genuinely
|
|
36
|
+
* wants the uncompiled artifact — it just has to be asked for.
|
|
37
|
+
*/
|
|
38
|
+
allowPlainJs?: boolean;
|
|
22
39
|
/** If true, generate a sourcemap alongside the bundle. */
|
|
23
40
|
sourcemap?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Identity marker to compile into the bundle. Injected into the JS source
|
|
43
|
+
* BEFORE hermesc runs — Hermes bytecode is a binary format, so text cannot be
|
|
44
|
+
* prepended after compilation without corrupting the file.
|
|
45
|
+
*
|
|
46
|
+
* At runtime the SDK reads `globalThis.__SCALEBUN_OTA_ID__` and compares it
|
|
47
|
+
* against the slot it believes is active. That comparison is the only way to
|
|
48
|
+
* prove from JS that the bundle we installed is the bundle actually running —
|
|
49
|
+
* which catches a host app that never wired `getJSBundleFile()`, and the
|
|
50
|
+
* bridgeless runtimes below RN 0.76.1 that ignore it entirely.
|
|
51
|
+
*/
|
|
52
|
+
identityToken?: string;
|
|
24
53
|
}
|
|
25
54
|
export interface BundleResult {
|
|
26
55
|
bundlePath: string;
|
|
27
56
|
assetsPath: string;
|
|
57
|
+
/**
|
|
58
|
+
* Asset files Metro emitted for this bundle, relative to `assetsPath`.
|
|
59
|
+
* OTA ships the JS artifact only — these resolve from the installed binary,
|
|
60
|
+
* so the list exists to WARN about assets added since the last store build.
|
|
61
|
+
*/
|
|
62
|
+
assets: string[];
|
|
63
|
+
/** True when the artifact really is Hermes bytecode (verified from the header). */
|
|
64
|
+
hermesCompiled: boolean;
|
|
65
|
+
/** HBC format version read from the compiled header, or null for plain JS. */
|
|
66
|
+
hermesBytecodeVersion: number | null;
|
|
28
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Read the HBC format version straight out of the compiled file header.
|
|
70
|
+
*
|
|
71
|
+
* Parsed from the artifact rather than from `hermesc --version` output, because
|
|
72
|
+
* the header is what the device's VM actually compares against — and its format
|
|
73
|
+
* is stable, whereas the CLI's version banner is not.
|
|
74
|
+
*
|
|
75
|
+
* Returns null when the file is not Hermes bytecode.
|
|
76
|
+
*/
|
|
77
|
+
export declare function readHermesBytecodeVersion(filePath: string): number | null;
|
|
78
|
+
/**
|
|
79
|
+
* Locate the `hermesc` binary that ships inside React Native.
|
|
80
|
+
*
|
|
81
|
+
* `hermesc` is NOT an npm package — `npx hermesc` fails with
|
|
82
|
+
* "No versions available for hermesc" on every project, which meant `--hermes`
|
|
83
|
+
* silently degraded to a plain JS bundle every single time it was used. React
|
|
84
|
+
* Native ships a prebuilt binary per host platform under `sdks/hermesc/`.
|
|
85
|
+
*
|
|
86
|
+
* Using the copy inside the project's own `react-native` is also the correct
|
|
87
|
+
* choice for a second reason: it is the compiler matched to the Hermes runtime
|
|
88
|
+
* in that app's binary. Compiling with any other hermesc is what produces
|
|
89
|
+
* "Wrong bytecode version. Expected 96 but got 98" on device.
|
|
90
|
+
*/
|
|
91
|
+
export declare function resolveHermesc(cwd?: string): string | null;
|
|
92
|
+
/** React Native version of the project being published, from its node_modules. */
|
|
93
|
+
export declare function detectProjectRnVersion(cwd?: string): string | null;
|
|
29
94
|
/**
|
|
30
95
|
* Shell out to `npx react-native bundle` to produce a JS bundle.
|
|
31
96
|
*
|
|
@@ -33,5 +98,26 @@ export interface BundleResult {
|
|
|
33
98
|
* The native runtime detects the format from magic bytes (\x1f\x\xbc —
|
|
34
99
|
* Hermes bytecode header), so the same .jsbundle extension works for both.
|
|
35
100
|
*/
|
|
101
|
+
/**
|
|
102
|
+
* True when this project is an Expo app.
|
|
103
|
+
*
|
|
104
|
+
* Expo projects do not ship `@react-native-community/cli`, so `npx react-native
|
|
105
|
+
* bundle` fails outright with a "react-native depends on
|
|
106
|
+
* @react-native-community/cli" error. Expo's equivalent is `expo export:embed`,
|
|
107
|
+
* which takes the same flags and produces the same artifact. Detecting this is
|
|
108
|
+
* not a nicety — without it `ota publish` cannot build a bundle in an Expo app
|
|
109
|
+
* at all, which is a large share of the React Native ecosystem.
|
|
110
|
+
*/
|
|
111
|
+
export declare function isExpoProject(cwd?: string): boolean;
|
|
36
112
|
export declare function buildBundle(opts: BundleOptions): BundleResult;
|
|
113
|
+
/**
|
|
114
|
+
* Every asset file Metro emitted for this bundle, relative to the assets root.
|
|
115
|
+
*
|
|
116
|
+
* OTA ships ONE artifact: the JS/bytecode bundle. Assets are resolved from what
|
|
117
|
+
* the installed binary already contains, so an image or font added since the
|
|
118
|
+
* last store build is referenced by the new JS and is simply not there — the
|
|
119
|
+
* update installs cleanly and renders a missing image. `ota publish` uses this
|
|
120
|
+
* list to say so out loud rather than discarding the directory in silence.
|
|
121
|
+
*/
|
|
122
|
+
export declare function listAssets(root: string): string[];
|
|
37
123
|
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,SAAS,GAAG,KAAK,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,OAAO,CAAC;IACb;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,SAAS,GAAG,KAAK,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,OAAO,CAAC;IACb;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,mFAAmF;IACnF,cAAc,EAAE,OAAO,CAAC;IACxB,8EAA8E;IAC9E,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAQD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAkBzE;AAED,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CASjF;AAED;;;;;;GAMG;AACH;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAelE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAwK7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBjD"}
|
package/lib/utils/bundle.js
CHANGED
|
@@ -33,11 +33,92 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.readHermesBytecodeVersion = readHermesBytecodeVersion;
|
|
37
|
+
exports.resolveHermesc = resolveHermesc;
|
|
38
|
+
exports.detectProjectRnVersion = detectProjectRnVersion;
|
|
39
|
+
exports.isExpoProject = isExpoProject;
|
|
36
40
|
exports.buildBundle = buildBundle;
|
|
41
|
+
exports.listAssets = listAssets;
|
|
37
42
|
const childProcess = __importStar(require("child_process"));
|
|
38
43
|
const fs = __importStar(require("fs"));
|
|
39
44
|
const path = __importStar(require("path"));
|
|
40
45
|
const os = __importStar(require("os"));
|
|
46
|
+
/**
|
|
47
|
+
* Hermes bytecode file magic, little-endian on disk.
|
|
48
|
+
* Source: `const uint64_t MAGIC = 0x1F1903C103BC1FC6` in Hermes.
|
|
49
|
+
*/
|
|
50
|
+
const HBC_MAGIC = Buffer.from([0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03, 0x19, 0x1f]);
|
|
51
|
+
/**
|
|
52
|
+
* Read the HBC format version straight out of the compiled file header.
|
|
53
|
+
*
|
|
54
|
+
* Parsed from the artifact rather than from `hermesc --version` output, because
|
|
55
|
+
* the header is what the device's VM actually compares against — and its format
|
|
56
|
+
* is stable, whereas the CLI's version banner is not.
|
|
57
|
+
*
|
|
58
|
+
* Returns null when the file is not Hermes bytecode.
|
|
59
|
+
*/
|
|
60
|
+
function readHermesBytecodeVersion(filePath) {
|
|
61
|
+
let fd;
|
|
62
|
+
try {
|
|
63
|
+
fd = fs.openSync(filePath, 'r');
|
|
64
|
+
const header = Buffer.alloc(12);
|
|
65
|
+
const read = fs.readSync(fd, header, 0, 12, 0);
|
|
66
|
+
if (read < 12)
|
|
67
|
+
return null;
|
|
68
|
+
if (!header.subarray(0, 8).equals(HBC_MAGIC))
|
|
69
|
+
return null;
|
|
70
|
+
return header.readUInt32LE(8);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
if (fd !== undefined) {
|
|
77
|
+
try {
|
|
78
|
+
fs.closeSync(fd);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* ignore */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Locate the `hermesc` binary that ships inside React Native.
|
|
88
|
+
*
|
|
89
|
+
* `hermesc` is NOT an npm package — `npx hermesc` fails with
|
|
90
|
+
* "No versions available for hermesc" on every project, which meant `--hermes`
|
|
91
|
+
* silently degraded to a plain JS bundle every single time it was used. React
|
|
92
|
+
* Native ships a prebuilt binary per host platform under `sdks/hermesc/`.
|
|
93
|
+
*
|
|
94
|
+
* Using the copy inside the project's own `react-native` is also the correct
|
|
95
|
+
* choice for a second reason: it is the compiler matched to the Hermes runtime
|
|
96
|
+
* in that app's binary. Compiling with any other hermesc is what produces
|
|
97
|
+
* "Wrong bytecode version. Expected 96 but got 98" on device.
|
|
98
|
+
*/
|
|
99
|
+
function resolveHermesc(cwd = process.cwd()) {
|
|
100
|
+
const dir = process.platform === 'win32'
|
|
101
|
+
? 'win64-bin'
|
|
102
|
+
: process.platform === 'darwin'
|
|
103
|
+
? 'osx-bin'
|
|
104
|
+
: 'linux64-bin';
|
|
105
|
+
const bin = process.platform === 'win32' ? 'hermesc.exe' : 'hermesc';
|
|
106
|
+
const candidate = path.join(cwd, 'node_modules', 'react-native', 'sdks', 'hermesc', dir, bin);
|
|
107
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
108
|
+
}
|
|
109
|
+
/** React Native version of the project being published, from its node_modules. */
|
|
110
|
+
function detectProjectRnVersion(cwd = process.cwd()) {
|
|
111
|
+
try {
|
|
112
|
+
const pkgPath = path.join(cwd, 'node_modules', 'react-native', 'package.json');
|
|
113
|
+
if (!fs.existsSync(pkgPath))
|
|
114
|
+
return null;
|
|
115
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
116
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
41
122
|
/**
|
|
42
123
|
* Shell out to `npx react-native bundle` to produce a JS bundle.
|
|
43
124
|
*
|
|
@@ -45,23 +126,49 @@ const os = __importStar(require("os"));
|
|
|
45
126
|
* The native runtime detects the format from magic bytes (\x1f\x\xbc —
|
|
46
127
|
* Hermes bytecode header), so the same .jsbundle extension works for both.
|
|
47
128
|
*/
|
|
129
|
+
/**
|
|
130
|
+
* True when this project is an Expo app.
|
|
131
|
+
*
|
|
132
|
+
* Expo projects do not ship `@react-native-community/cli`, so `npx react-native
|
|
133
|
+
* bundle` fails outright with a "react-native depends on
|
|
134
|
+
* @react-native-community/cli" error. Expo's equivalent is `expo export:embed`,
|
|
135
|
+
* which takes the same flags and produces the same artifact. Detecting this is
|
|
136
|
+
* not a nicety — without it `ota publish` cannot build a bundle in an Expo app
|
|
137
|
+
* at all, which is a large share of the React Native ecosystem.
|
|
138
|
+
*/
|
|
139
|
+
function isExpoProject(cwd = process.cwd()) {
|
|
140
|
+
try {
|
|
141
|
+
if (fs.existsSync(path.join(cwd, 'node_modules', 'expo', 'package.json'))) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
145
|
+
if (!fs.existsSync(pkgPath))
|
|
146
|
+
return false;
|
|
147
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
148
|
+
return Boolean(pkg.dependencies?.expo ?? pkg.devDependencies?.expo);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
48
154
|
function buildBundle(opts) {
|
|
49
155
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scalebun-bundle-'));
|
|
50
156
|
const bundlePath = path.join(tmpDir, 'bundle.jsbundle');
|
|
51
157
|
const assetsPath = path.join(tmpDir, 'assets');
|
|
52
158
|
fs.mkdirSync(assetsPath, { recursive: true });
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
|
|
159
|
+
// `expo export:embed` is the Expo-side equivalent of `react-native bundle`
|
|
160
|
+
// and accepts the same flags. Both emit a plain JS bundle; Hermes compilation
|
|
161
|
+
// is a separate step below in either case.
|
|
162
|
+
const expo = isExpoProject();
|
|
163
|
+
const args = expo
|
|
164
|
+
? ['expo', 'export:embed']
|
|
165
|
+
: ['react-native', 'bundle'];
|
|
166
|
+
args.push('--platform', opts.platform, '--dev', String(opts.dev), '--entry-file', opts.entryFile, '--bundle-output', bundlePath, '--assets-dest', assetsPath);
|
|
61
167
|
// Sprint 8: Generate sourcemap if requested
|
|
62
168
|
if (opts.sourcemap) {
|
|
63
169
|
args.push('--sourcemap-output', bundlePath + '.map');
|
|
64
170
|
}
|
|
171
|
+
console.log(` Detected ${expo ? 'Expo' : 'bare React Native'} project`);
|
|
65
172
|
console.log(` Running: npx ${args.join(' ')}`);
|
|
66
173
|
const result = childProcess.spawnSync('npx', args, {
|
|
67
174
|
stdio: 'inherit',
|
|
@@ -69,26 +176,149 @@ function buildBundle(opts) {
|
|
|
69
176
|
timeout: 300000, // 5 minutes
|
|
70
177
|
});
|
|
71
178
|
if (result.status !== 0) {
|
|
72
|
-
throw new Error(
|
|
179
|
+
throw new Error(`${expo ? 'expo export:embed' : 'react-native bundle'} failed with exit code ${result.status}`);
|
|
73
180
|
}
|
|
74
181
|
if (!fs.existsSync(bundlePath)) {
|
|
75
182
|
throw new Error(`Bundle file not found at ${bundlePath}`);
|
|
76
183
|
}
|
|
184
|
+
// Identity marker — injected into the JS source, before any Hermes step
|
|
185
|
+
// (after hermesc the artifact is binary and text would corrupt the header).
|
|
186
|
+
//
|
|
187
|
+
// APPENDED, not prepended. Metro has already written the sourcemap by this
|
|
188
|
+
// point, and a prepended line shifts every generated line down by one —
|
|
189
|
+
// silently corrupting the line numbers of every symbolicated stack trace
|
|
190
|
+
// from this bundle. An appended statement adds no mappings and moves none;
|
|
191
|
+
// it still executes during bundle evaluation, and the SDK reads the global
|
|
192
|
+
// at init(), long after evaluation completes.
|
|
193
|
+
if (opts.identityToken) {
|
|
194
|
+
const marker = `\nglobalThis.__SCALEBUN_OTA_ID__=${JSON.stringify(opts.identityToken)};\n`;
|
|
195
|
+
fs.appendFileSync(bundlePath, marker, 'utf-8');
|
|
196
|
+
}
|
|
77
197
|
// Hermes bytecode compilation (opt-in)
|
|
78
198
|
if (opts.hermes) {
|
|
79
199
|
console.log(' Compiling to Hermes bytecode…');
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
200
|
+
// Compile to a sibling path, then rename. hermesc reading and writing the
|
|
201
|
+
// same path in one invocation is not guaranteed to be safe, and a truncated
|
|
202
|
+
// in-place write would produce a bundle that hashes fine and never loads.
|
|
203
|
+
const hbcPath = bundlePath + '.hbc';
|
|
204
|
+
const hermesc = resolveHermesc();
|
|
205
|
+
// A missing hermesc is handled with the compile failures below, so that
|
|
206
|
+
// "cannot compile" has exactly one outcome regardless of the reason.
|
|
207
|
+
// Output is CAPTURED, not inherited. hermesc emits one warning per
|
|
208
|
+
// undeclared global per module ("the variable Buffer was not declared…"),
|
|
209
|
+
// and each warning quotes the offending source line — which in a minified
|
|
210
|
+
// bundle is a single line of 100KB+. Two consequences shaped this code:
|
|
211
|
+
// 1. Inherited stdio drowned the publish summary in megabytes of noise.
|
|
212
|
+
// 2. spawnSync's DEFAULT maxBuffer is 1MB; the quoted warnings blow past
|
|
213
|
+
// it, Node kills hermesc (ENOBUFS), and `--hermes` silently degrades
|
|
214
|
+
// to plain JS — the same bug as `npx hermesc`, via a different door.
|
|
215
|
+
// Hit live on the very first publish after the capture change.
|
|
216
|
+
const hermescResult = hermesc
|
|
217
|
+
? childProcess.spawnSync(hermesc, ['-emit-binary', '-out', hbcPath, bundlePath], {
|
|
218
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
219
|
+
shell: false,
|
|
220
|
+
timeout: 120000,
|
|
221
|
+
encoding: 'utf-8',
|
|
222
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
223
|
+
})
|
|
224
|
+
: { status: 1 };
|
|
225
|
+
if (hermesc && hermescResult.status === 0) {
|
|
226
|
+
const warnings = `${hermescResult.stdout ?? ''}${hermescResult.stderr ?? ''}`
|
|
227
|
+
.split('\n')
|
|
228
|
+
.filter((l) => l.includes('warning:')).length;
|
|
229
|
+
if (warnings > 0) {
|
|
230
|
+
console.log(` (${warnings} hermesc warnings suppressed — expected for RN bundles)`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (hermescResult.status !== 0 || !fs.existsSync(hbcPath)) {
|
|
234
|
+
// Collect the reason before deciding what to do with it — the same
|
|
235
|
+
// detail is worth printing whether this ends as an error or a warning.
|
|
236
|
+
const reason = hermesc
|
|
237
|
+
? `hermesc failed (exit ${hermescResult.status}` +
|
|
238
|
+
`${hermescResult.error ? `, ${hermescResult.error.message}` : ''})`
|
|
239
|
+
: 'hermesc not found under node_modules/react-native/sdks/hermesc';
|
|
240
|
+
// Only lines that carry the error itself, each clipped: a quoted
|
|
241
|
+
// minified source line is 100KB of noise per line.
|
|
242
|
+
const errLines = `${hermescResult.stderr ?? ''}`
|
|
243
|
+
.split('\n')
|
|
244
|
+
.filter((l) => l.includes('error:'))
|
|
245
|
+
.slice(0, 5)
|
|
246
|
+
.map((l) => (l.length > 200 ? l.slice(0, 200) + '…' : l));
|
|
247
|
+
try {
|
|
248
|
+
if (fs.existsSync(hbcPath))
|
|
249
|
+
fs.unlinkSync(hbcPath);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
/* ignore */
|
|
253
|
+
}
|
|
254
|
+
if (!opts.allowPlainJs) {
|
|
255
|
+
// STOP. --hermes was explicitly requested; shipping the uncompiled
|
|
256
|
+
// bundle instead would publish a different artifact than the one
|
|
257
|
+
// asked for, and nothing downstream could tell.
|
|
258
|
+
for (const l of errLines)
|
|
259
|
+
console.error(` ${l}`);
|
|
260
|
+
throw new Error(`${reason}.\n` +
|
|
261
|
+
' --hermes was requested, so this is a hard failure rather than a silent\n' +
|
|
262
|
+
' downgrade: shipping plain JS would slow every user\'s cold start by\n' +
|
|
263
|
+
' 20-30% and break delta patching against previous bytecode releases.\n' +
|
|
264
|
+
' Fix the toolchain, or pass --allow-plain-js to publish uncompiled.');
|
|
265
|
+
}
|
|
266
|
+
console.warn(` ⚠️ ${reason} — shipping plain JS bundle (--allow-plain-js)`);
|
|
267
|
+
for (const l of errLines)
|
|
268
|
+
console.warn(` ${l}`);
|
|
84
269
|
}
|
|
85
270
|
else {
|
|
271
|
+
fs.renameSync(hbcPath, bundlePath);
|
|
86
272
|
console.log(' ✅ Hermes bytecode compiled');
|
|
87
273
|
}
|
|
88
274
|
}
|
|
89
275
|
else {
|
|
90
276
|
console.log(' ℹ️ Shipping plain JS (no hermesc). Use --hermes for bytecode compilation.');
|
|
91
277
|
}
|
|
92
|
-
|
|
278
|
+
// Read the format back off the artifact rather than trusting the flag. When
|
|
279
|
+
// hermesc fails we fall back to plain JS, and recording `hermes: true` anyway
|
|
280
|
+
// would tell the backend to gate devices on a bytecode version this bundle
|
|
281
|
+
// does not have.
|
|
282
|
+
const hermesBytecodeVersion = readHermesBytecodeVersion(bundlePath);
|
|
283
|
+
if (opts.hermes && hermesBytecodeVersion === null) {
|
|
284
|
+
console.warn(' ⚠️ Artifact is not Hermes bytecode — recording it as plain JS.');
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
bundlePath,
|
|
288
|
+
assetsPath,
|
|
289
|
+
assets: listAssets(assetsPath),
|
|
290
|
+
hermesCompiled: hermesBytecodeVersion !== null,
|
|
291
|
+
hermesBytecodeVersion,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Every asset file Metro emitted for this bundle, relative to the assets root.
|
|
296
|
+
*
|
|
297
|
+
* OTA ships ONE artifact: the JS/bytecode bundle. Assets are resolved from what
|
|
298
|
+
* the installed binary already contains, so an image or font added since the
|
|
299
|
+
* last store build is referenced by the new JS and is simply not there — the
|
|
300
|
+
* update installs cleanly and renders a missing image. `ota publish` uses this
|
|
301
|
+
* list to say so out loud rather than discarding the directory in silence.
|
|
302
|
+
*/
|
|
303
|
+
function listAssets(root) {
|
|
304
|
+
const out = [];
|
|
305
|
+
const walk = (dir, prefix) => {
|
|
306
|
+
let entries;
|
|
307
|
+
try {
|
|
308
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return; // no assets dir at all is the common, healthy case
|
|
312
|
+
}
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
315
|
+
if (entry.isDirectory())
|
|
316
|
+
walk(path.join(dir, entry.name), rel);
|
|
317
|
+
else
|
|
318
|
+
out.push(rel);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
walk(root, '');
|
|
322
|
+
return out;
|
|
93
323
|
}
|
|
94
324
|
//# sourceMappingURL=bundle.js.map
|
package/lib/utils/bundle.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,7 +8,18 @@ export interface Credentials {
|
|
|
8
8
|
* 2. ~/.scalebun/credentials.json (from `scalebun login`)
|
|
9
9
|
*/
|
|
10
10
|
export declare function readCredentials(): Credentials | null;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Store credentials to ~/.scalebun/credentials.json.
|
|
13
|
+
*
|
|
14
|
+
* The `mode: 0o600` below is real on macOS and Linux and a NO-OP ON WINDOWS —
|
|
15
|
+
* Node cannot express a Windows ACL through a Unix mode, so it silently drops
|
|
16
|
+
* it and the file lands with whatever the parent directory grants. That is how
|
|
17
|
+
* a token ends up readable by every other account on a shared machine while
|
|
18
|
+
* the code looks like it locked the file down. So on Windows the ACL is set
|
|
19
|
+
* explicitly afterwards, and a failure to do so is reported rather than
|
|
20
|
+
* swallowed: the user is entitled to know their token is sitting in a
|
|
21
|
+
* world-readable file.
|
|
22
|
+
*/
|
|
12
23
|
export declare function writeCredentials(creds: Credentials): void;
|
|
13
24
|
/** Delete stored credentials. */
|
|
14
25
|
export declare function clearCredentials(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CAkBpD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAUzD;AAgCD,iCAAiC;AACjC,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC"}
|
package/lib/utils/credentials.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.clearCredentials = clearCredentials;
|
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const os = __importStar(require("os"));
|
|
42
|
+
const childProcess = __importStar(require("child_process"));
|
|
42
43
|
const CREDENTIALS_DIR = path.join(os.homedir(), '.scalebun');
|
|
43
44
|
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
|
|
44
45
|
/**
|
|
@@ -67,12 +68,50 @@ function readCredentials() {
|
|
|
67
68
|
return null;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Store credentials to ~/.scalebun/credentials.json.
|
|
73
|
+
*
|
|
74
|
+
* The `mode: 0o600` below is real on macOS and Linux and a NO-OP ON WINDOWS —
|
|
75
|
+
* Node cannot express a Windows ACL through a Unix mode, so it silently drops
|
|
76
|
+
* it and the file lands with whatever the parent directory grants. That is how
|
|
77
|
+
* a token ends up readable by every other account on a shared machine while
|
|
78
|
+
* the code looks like it locked the file down. So on Windows the ACL is set
|
|
79
|
+
* explicitly afterwards, and a failure to do so is reported rather than
|
|
80
|
+
* swallowed: the user is entitled to know their token is sitting in a
|
|
81
|
+
* world-readable file.
|
|
82
|
+
*/
|
|
71
83
|
function writeCredentials(creds) {
|
|
72
84
|
if (!fs.existsSync(CREDENTIALS_DIR)) {
|
|
73
85
|
fs.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
74
86
|
}
|
|
75
87
|
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2) + '\n', { mode: 0o600 });
|
|
88
|
+
restrictToCurrentUser(CREDENTIALS_FILE);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Windows-only ACL tightening: drop inherited ACEs and grant the current user
|
|
92
|
+
* alone full control. On POSIX the file mode already did this and this is a
|
|
93
|
+
* no-op.
|
|
94
|
+
*/
|
|
95
|
+
function restrictToCurrentUser(file) {
|
|
96
|
+
if (process.platform !== 'win32')
|
|
97
|
+
return;
|
|
98
|
+
const user = process.env.USERNAME
|
|
99
|
+
? `${process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\` : ''}${process.env.USERNAME}`
|
|
100
|
+
: null;
|
|
101
|
+
if (!user)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
// /inheritance:r removes inherited permissions — without it the grant is
|
|
105
|
+
// added ON TOP of whatever the profile directory already allows, which is
|
|
106
|
+
// usually the point being missed.
|
|
107
|
+
childProcess.execFileSync('icacls', [file, '/inheritance:r', '/grant:r', `${user}:F`], { stdio: 'ignore', timeout: 10000 });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
console.warn(`⚠️ Could not restrict permissions on ${file}.\n` +
|
|
111
|
+
' It contains an access token in plaintext and may be readable by other\n' +
|
|
112
|
+
' accounts on this machine. Fix it with:\n' +
|
|
113
|
+
` icacls "${file}" /inheritance:r /grant:r "%USERNAME%:F"`);
|
|
114
|
+
}
|
|
76
115
|
}
|
|
77
116
|
/** Delete stored credentials. */
|
|
78
117
|
function clearCredentials() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTPS enforcement for everything the CLI authenticates to.
|
|
3
|
+
*
|
|
4
|
+
* WHAT WAS WRONG. The API client picked its transport straight off the URL
|
|
5
|
+
* scheme — `parsed.protocol === 'https:' ? https : http` — with nothing
|
|
6
|
+
* requiring HTTPS in the first place, and every request attaches
|
|
7
|
+
* `Authorization: Bearer <token>`. `scalebun login` accepts an arbitrary
|
|
8
|
+
* `--api-url` and POSTs an email and password to it. So a CLI pointed at an
|
|
9
|
+
* `http://` URL — by a typo, a copied CI snippet, or a tampered config —
|
|
10
|
+
* would put a personal access token, or the user's actual password, on the
|
|
11
|
+
* wire in cleartext, with no warning and no failure.
|
|
12
|
+
*
|
|
13
|
+
* THE RULE. Credentials only travel over TLS. The single exception is
|
|
14
|
+
* loopback, because local development against `http://localhost:3000` is a
|
|
15
|
+
* real workflow and traffic that never leaves the machine cannot be
|
|
16
|
+
* intercepted on the network. Anything else is refused before a socket is
|
|
17
|
+
* opened, rather than warned about — a warning printed above a successful
|
|
18
|
+
* request is not a control.
|
|
19
|
+
*
|
|
20
|
+
* Loopback is matched on resolved literals, not on name. `localhost` and the
|
|
21
|
+
* reserved 127/8 and ::1 literals are loopback by definition; an arbitrary
|
|
22
|
+
* hostname that merely happens to resolve to 127.0.0.1 today is not, because
|
|
23
|
+
* whoever controls the DNS answer controls where the token goes tomorrow.
|
|
24
|
+
*/
|
|
25
|
+
export declare class InsecureApiUrlError extends Error {
|
|
26
|
+
constructor(url: string, hostname: string);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate an API base URL before it is used or persisted.
|
|
30
|
+
*
|
|
31
|
+
* @returns the normalised URL (trailing slashes stripped)
|
|
32
|
+
* @throws InsecureApiUrlError for a non-loopback `http://` URL
|
|
33
|
+
* @throws Error for a URL that is malformed or uses a non-HTTP scheme
|
|
34
|
+
*/
|
|
35
|
+
export declare function assertSecureApiUrl(rawUrl: string): string;
|
|
36
|
+
//# sourceMappingURL=transportSecurity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transportSecurity.d.ts","sourceRoot":"","sources":["../../src/utils/transportSecurity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAYH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAW1C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAkBzD"}
|