@simplelocalize/cli 2.10.0 → 3.0.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
@@ -10,9 +10,9 @@ in their workflows.
10
10
  npm install @simplelocalize/cli
11
11
  ```
12
12
 
13
- After installing the package, it should automatically choose the right binary for
14
- your system and install it into `node_modules/.bin` directory, to make it available for your
15
- scripts in `package.json`.
13
+ The package installs a small `simplelocalize` launcher into `node_modules/.bin`, so it's available for your
14
+ scripts in `package.json`. The launcher downloads the right SimpleLocalize CLI binary for your system
15
+ on first run (or during installation, when install scripts are allowed) and caches it per version.
16
16
 
17
17
  ```json
18
18
  {
@@ -20,7 +20,7 @@ scripts in `package.json`.
20
20
  "version": "1.0.0",
21
21
  "private": true,
22
22
  "dependencies": {
23
- "@simplelocalize/cli": "^2.9.2",
23
+ "@simplelocalize/cli": "^3.0.0"
24
24
  },
25
25
  "scripts": {
26
26
  "start": "react-scripts start",
@@ -28,6 +28,9 @@ scripts in `package.json`.
28
28
  "sl:upload": "simplelocalize upload",
29
29
  "sl:autotranslate": "simplelocalize auto-translate"
30
30
  },
31
+ "simplelocalize": {
32
+ "cliVersion": "2.12.0"
33
+ }
31
34
  }
32
35
  ```
33
36
 
@@ -43,13 +46,23 @@ globally:
43
46
  npx @simplelocalize/cli download
44
47
  ```
45
48
 
46
- ## Versioning explanation
49
+ ## Choosing the CLI version
47
50
 
48
- `@simplelocalize/cli` versioning is synced with https://github.com/simplelocalize/simplelocalize-cli versioning.
51
+ The npm package version is independent of the SimpleLocalize CLI version. The CLI version is resolved in this order:
49
52
 
50
- `@simplelocalize/cli@2.9.1` will install SimpleLocalize CLI 2.9.x and this npm package version is 1.
51
- `@simplelocalize/cli@2.9.1` will install SimpleLocalize CLI 2.9.x and this npm package version is 2.
53
+ 1. `SIMPLELOCALIZE_CLI_VERSION` environment variable, e.g. `SIMPLELOCALIZE_CLI_VERSION=2.12.0 npx @simplelocalize/cli download`
54
+ 2. `simplelocalize.cliVersion` in the nearest `package.json` that defines it (searched from the current directory upwards)
55
+ 3. the default CLI version bundled with the installed package (`cliVersion` in its `package.json`)
52
56
 
53
- By using this versioning scheme, we can release new versions of this npm package
54
- without changing the SimpleLocalize CLI version, for example, to fix bugs in this package,
55
- and follow semantic versioning rules.
57
+ Use a full version, e.g. `2.12.0`. See [CLI releases](https://github.com/simplelocalize/simplelocalize-cli/releases) for available versions.
58
+ We recommend pinning `simplelocalize.cliVersion` in your project, so every developer and CI run uses the same CLI.
59
+
60
+ Other environment variables:
61
+
62
+ - `SIMPLELOCALIZE_CLI_CACHE_DIR` – where binaries are cached (default: `~/.cache/simplelocalize/cli`, `%LOCALAPPDATA%\simplelocalize\cli` on Windows). Cache this directory in CI to skip downloads.
63
+ - `SIMPLELOCALIZE_CLI_BINARY` – path to an already installed CLI binary; skips the download entirely.
64
+
65
+ ## Upgrading from 2.x
66
+
67
+ `@simplelocalize/cli@2.x` versions were tied to the CLI version (`2.10.x` installed CLI `2.10.0`).
68
+ Since `3.0.0`, pin the CLI version with `simplelocalize.cliVersion` instead. The launcher requires Node.js 18 or newer.
package/index.cjs CHANGED
@@ -1,164 +1,159 @@
1
1
  #!/usr/bin/env node
2
- const { execSync } = require("child_process");
2
+ const { spawnSync } = require("child_process");
3
3
  const path = require("path");
4
4
  const fs = require('fs');
5
5
  const axios = require('axios');
6
6
  const os = require('os');
7
7
 
8
- let binaryPath = path.join(__dirname, 'bin', 'simplelocalize');
9
- if (os.platform() === 'win32') {
10
- binaryPath += '.exe';
11
- }
12
-
13
- const getExpectedCliVersion = () => {
14
- const packageVersion = require('./package.json').version;
15
- const parts = packageVersion.split('.');
16
- return parts[0] + '.' + parts[1] + '.0';
8
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
9
+
10
+ const isValidVersion = (version) => typeof version === 'string' && VERSION_PATTERN.test(version);
11
+
12
+ // Walks up from the project directory and returns the first "simplelocalize.cliVersion" found in package.json.
13
+ const findPinnedVersion = (startDir) => {
14
+ let dir = path.resolve(startDir);
15
+ while (true) {
16
+ const packageJsonPath = path.join(dir, 'package.json');
17
+ if (fs.existsSync(packageJsonPath)) {
18
+ try {
19
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
20
+ const cliVersion = packageJson.simplelocalize && packageJson.simplelocalize.cliVersion;
21
+ if (cliVersion) {
22
+ return { version: String(cliVersion), source: packageJsonPath };
23
+ }
24
+ } catch (error) {
25
+ // ignore unreadable package.json and keep looking
26
+ }
27
+ }
28
+ const parent = path.dirname(dir);
29
+ if (parent === dir) {
30
+ return null;
31
+ }
32
+ dir = parent;
33
+ }
17
34
  };
18
35
 
19
- const isBinaryInstalled = () => {
20
- try {
21
- const output = execSync(binaryPath + ' --version').toString().trim();
22
- const expectedVersion = getExpectedCliVersion();
23
- return output.includes(expectedVersion);
24
- }
25
- catch (error) {
26
- return false;
27
- }
28
- }
36
+ const resolveCliVersion = (projectDir) => {
37
+ const fromEnv = process.env.SIMPLELOCALIZE_CLI_VERSION;
38
+ const resolved = fromEnv
39
+ ? { version: fromEnv, source: 'SIMPLELOCALIZE_CLI_VERSION' }
40
+ : findPinnedVersion(projectDir) || { version: require('./package.json').cliVersion, source: 'default' };
29
41
 
30
- const printBinaryVersion = () => {
31
- try {
32
- console.log(execSync(binaryPath + ' --version').toString());
42
+ if (!isValidVersion(resolved.version)) {
43
+ throw new Error(`Invalid SimpleLocalize CLI version '${resolved.version}' (from ${resolved.source}), use a full version, e.g. 2.12.0`);
33
44
  }
34
- catch (error) {
35
- console.error('Error running SimpleLocalize CLI');
36
- process.exit(1);
37
- }
38
- }
45
+ return resolved.version;
46
+ };
39
47
 
40
- const buildDownloadBinaryUrl = (version) => {
41
- let platform = "";
42
- if (os.platform() === "win32") {
43
- platform = "windows.exe"
48
+ const getPlatformSuffix = () => {
49
+ const arch = os.arch() === "arm64" ? "-arm64" : "";
50
+ switch (os.platform()) {
51
+ case "win32":
52
+ return "windows.exe";
53
+ case "darwin":
54
+ return "mac" + arch;
55
+ case "linux":
56
+ return "linux" + arch;
57
+ default:
58
+ throw new Error(`Unsupported platform: ${os.platform()} ${os.arch()}, set SIMPLELOCALIZE_CLI_BINARY to a binary path or use the JAR`);
44
59
  }
60
+ };
45
61
 
46
- if (os.platform() === "darwin") {
47
- platform = "mac"
62
+ const getCacheDir = () => {
63
+ if (process.env.SIMPLELOCALIZE_CLI_CACHE_DIR) {
64
+ return process.env.SIMPLELOCALIZE_CLI_CACHE_DIR;
48
65
  }
49
-
50
- if (os.platform() === "linux") {
51
- platform = "linux"
66
+ if (os.platform() === 'win32' && process.env.LOCALAPPDATA) {
67
+ return path.join(process.env.LOCALAPPDATA, 'simplelocalize', 'cli');
52
68
  }
69
+ const xdgCache = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
70
+ return path.join(xdgCache, 'simplelocalize', 'cli');
71
+ };
53
72
 
54
- let arch = "";
55
- if (os.arch() === "arm64") {
56
- arch = "-arm64"
57
- }
58
- console.log(`Downloading SimpleLocalize CLI ${version} for ${platform}${arch}...`);
59
- return `https://get.simplelocalize.io/binaries/${version}/simplelocalize-cli-${platform}${arch}`
60
- }
73
+ const getBinaryPath = (version) => {
74
+ const fileName = 'simplelocalize-cli' + (os.platform() === 'win32' ? '.exe' : '');
75
+ return path.join(getCacheDir(), version, fileName);
76
+ };
61
77
 
62
- async function installBinary() {
63
- const cliVersion = getExpectedCliVersion();
64
- if (fs.existsSync(binaryPath)) {
65
- console.log('Removing existing binary...');
66
- fs.unlinkSync(binaryPath);
67
- }
68
- const downloadUrl = buildDownloadBinaryUrl(cliVersion);
69
- if (!fs.existsSync(path.join(__dirname, 'bin'))) {
70
- fs.mkdirSync(path.join(__dirname, 'bin'));
71
- }
78
+ async function installBinary(version, binaryPath) {
79
+ const downloadUrl = `https://get.simplelocalize.io/binaries/${version}/simplelocalize-cli-${getPlatformSuffix()}`;
80
+ console.error(`Downloading SimpleLocalize CLI ${version} from ${downloadUrl}...`);
81
+ fs.mkdirSync(path.dirname(binaryPath), { recursive: true });
72
82
 
83
+ // Download to a temporary file and rename, so parallel runs never see a partial binary
84
+ const tempPath = `${binaryPath}.${process.pid}.tmp`;
73
85
  try {
74
86
  const response = await axios({
75
87
  url: downloadUrl,
76
88
  method: 'GET',
77
89
  responseType: 'stream'
78
90
  });
79
-
80
- const writer = fs.createWriteStream(binaryPath);
91
+ const writer = fs.createWriteStream(tempPath);
81
92
  response.data.pipe(writer);
82
93
  await new Promise((resolve, reject) => {
83
94
  writer.on('finish', resolve);
84
95
  writer.on('error', reject);
96
+ response.data.on('error', reject);
85
97
  });
98
+ if (os.platform() !== 'win32') {
99
+ fs.chmodSync(tempPath, 0o755);
100
+ }
101
+ fs.renameSync(tempPath, binaryPath);
86
102
  } catch (error) {
87
- console.error('Error downloading SimpleLocalize CLI binary:', error.message);
88
- process.exit(1);
103
+ fs.rmSync(tempPath, { force: true });
104
+ try {
105
+ fs.rmdirSync(path.dirname(binaryPath));
106
+ } catch (ignored) {
107
+ // not empty, another run already installed this version
108
+ }
109
+ const status = error.response && error.response.status;
110
+ const reason = status === 404 ? `version ${version} not found` : error.message;
111
+ throw new Error(`Error downloading SimpleLocalize CLI binary: ${reason}`);
89
112
  }
113
+ }
90
114
 
91
- // Make it executable (only on Unix-based systems)
92
- if (os.platform() !== 'win32') {
93
- console.log('Making binary executable...');
94
- fs.chmodSync(binaryPath, 0o755);
115
+ const ensureBinary = async (projectDir) => {
116
+ if (process.env.SIMPLELOCALIZE_CLI_BINARY) {
117
+ return process.env.SIMPLELOCALIZE_CLI_BINARY;
95
118
  }
96
-
97
- // Ensure binary exists and is executable before continuing
119
+ const version = resolveCliVersion(projectDir);
120
+ const binaryPath = getBinaryPath(version);
98
121
  if (!fs.existsSync(binaryPath)) {
99
- throw new Error('Binary file was not created!');
100
- }
101
- }
102
-
103
- const linkToNodeModulesBin = () => {
104
- let nodeModulesBinPath = path.join(__dirname, '..', '..', '.bin', 'simplelocalize');
105
-
106
- if (os.platform() === 'win32') {
107
- nodeModulesBinPath += '.exe';
122
+ await installBinary(version, binaryPath);
108
123
  }
124
+ return binaryPath;
125
+ };
109
126
 
110
- // Ensure the .bin directory exists
111
- const binDir = path.dirname(nodeModulesBinPath);
112
- if (!fs.existsSync(binDir)) {
113
- fs.mkdirSync(binDir, { recursive: true });
127
+ const run = async () => {
128
+ let binaryPath;
129
+ try {
130
+ binaryPath = await ensureBinary(process.cwd());
131
+ } catch (error) {
132
+ console.error(error.message);
133
+ process.exit(1);
114
134
  }
115
-
116
- if (fs.existsSync(nodeModulesBinPath)) {
117
- fs.unlinkSync(nodeModulesBinPath);
135
+ const result = spawnSync(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
136
+ if (result.error) {
137
+ console.error(`Error running SimpleLocalize CLI (${binaryPath}): ${result.error.message}`);
138
+ process.exit(1);
118
139
  }
119
- fs.symlinkSync(binaryPath, nodeModulesBinPath);
120
- console.log('Link to node_modules/.bin created.');
121
- }
122
-
123
- const init = async () => {
124
- console.log("Checking if SimpleLocalize CLI is installed...");
125
- if (!isBinaryInstalled()) {
126
- console.log("SimpleLocalize CLI not installed. Installing...");
127
- await installBinary();
128
- if (!isBinaryInstalled()) {
129
- console.error('Error installing SimpleLocalize CLI');
130
- process.exit(1);
131
- }
132
- console.log("SimpleLocalize CLI installed successfully!");
133
- } else {
134
- console.log("SimpleLocalize CLI already installed.");
140
+ if (result.signal) {
141
+ process.kill(process.pid, result.signal);
135
142
  }
136
- printBinaryVersion();
137
- linkToNodeModulesBin();
138
- process.exit(0);
139
- }
143
+ process.exit(result.status ?? 1);
144
+ };
140
145
 
141
- // Only run install logic if called with 'install' argument (for postinstall)
142
- if (process.argv[2] === 'install') {
143
- (async () => {
144
- await init();
145
- })();
146
- } else {
147
- // Forward all other CLI commands to the downloaded binary
148
- if (!isBinaryInstalled()) {
149
- console.error('SimpleLocalize CLI binary is not installed. Please run: npm install');
150
- process.exit(1);
151
- }
152
- // Build the command to forward
153
- const args = process.argv.slice(2).map(arg => {
154
- // Quote arguments with spaces for safety
155
- if (/\s/.test(arg)) return `"${arg}"`;
156
- return arg;
157
- }).join(' ');
146
+ // Optional prefetch during installation; never fails the install (the binary is downloaded on first run anyway)
147
+ const prefetch = async () => {
158
148
  try {
159
- execSync(`${binaryPath} ${args}`, { stdio: 'inherit' });
160
- process.exit(0);
149
+ await ensureBinary(process.env.INIT_CWD || process.cwd());
161
150
  } catch (error) {
162
- process.exit(error.status || 1);
151
+ console.warn(`${error.message}. SimpleLocalize CLI will be downloaded on first run.`);
163
152
  }
153
+ };
154
+
155
+ if (process.argv[2] === 'install' && process.env.npm_lifecycle_event === 'postinstall') {
156
+ prefetch();
157
+ } else {
158
+ run();
164
159
  }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@simplelocalize/cli",
3
- "version": "2.10.0",
3
+ "version": "3.0.0",
4
+ "cliVersion": "2.12.0",
4
5
  "description": "SimpleLocalize CLI for NPM",
5
6
  "main": "index.cjs",
6
7
  "type": "module",
@@ -9,11 +10,17 @@
9
10
  },
10
11
  "repository": {
11
12
  "type": "git",
12
- "url": "https://github.com/simplelocalize/simplelicalize-cli-npm.git"
13
+ "url": "git+https://github.com/simplelocalize/simplelocalize-cli-npm.git"
13
14
  },
14
15
  "bin": {
15
16
  "simplelocalize": "index.cjs"
16
17
  },
18
+ "files": [
19
+ "index.cjs"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
17
24
  "dependencies": {
18
25
  "axios": "^1.8.3"
19
26
  },
@@ -1,35 +0,0 @@
1
- name: CI & Publish
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
- workflow_dispatch:
9
-
10
- jobs:
11
- test:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
- - uses: actions/setup-node@v4
16
- with:
17
- node-version: '20'
18
- cache: 'npm'
19
- - run: npm ci
20
-
21
- publish:
22
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
23
- needs: test
24
- runs-on: ubuntu-latest
25
- steps:
26
- - uses: actions/checkout@v4
27
- - uses: actions/setup-node@v4
28
- with:
29
- node-version: '20'
30
- registry-url: 'https://registry.npmjs.org/'
31
- - run: npm ci
32
- - run: npm run build --if-present
33
- - run: npm publish --access public
34
- env:
35
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}