@lingva/build-tools 0.0.0-stage → 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,24 @@
1
+ # Lingva SDK License 1.0
2
+
3
+ Copyright (c) Yury Saukou. All rights reserved.
4
+
5
+ Permission is granted, without charge, to install, execute, and internally modify
6
+ this Lingva SDK package for the purpose of integrating applications with Lingva
7
+ or using its local localization features.
8
+
9
+ You may redistribute the unmodified package, or compiled portions of it, only as
10
+ a dependency or component of an application that provides substantial
11
+ functionality beyond the SDK itself. You may not sell, sublicense, republish, or
12
+ distribute the SDK as a standalone product; remove or alter copyright and
13
+ license notices; use the SDK to provide a competing localization platform or
14
+ SDK; or circumvent access controls, usage limits, or commercial entitlements of
15
+ Lingva services.
16
+
17
+ Access to hosted Lingva services, paid features, support, and self-hosted Lingva
18
+ software is governed separately and is not granted by this license.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING
24
+ FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # Lingva build tools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared package build lock, package-local tsdown launcher, and production source-map removal. Both Lingva repositories consume the same release. `LINGVA_DEV_BUILD=true` enables developer source maps; production builds remove map files and references.
4
+
5
+ Run `lingva-build` from a package directory. Import `defineLingvaPackageBuildConfig` from `@lingva/build-tools/config` for tsdown configuration.
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { randomUUID } from 'node:crypto';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { spawn } from 'node:child_process';
8
+ import { createRequire } from 'node:module';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const BUILD_LOCK_FILE = '.lingva-build.lock';
12
+ const BUILD_LOCK_RETRY_MS = 100;
13
+ const BUILD_LOCK_METADATA = 'owner.json';
14
+ const STALE_LOCK_METADATA_MS = 60_000;
15
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
16
+
17
+ function wait(durationMs) {
18
+ return new Promise((resolve) => setTimeout(resolve, durationMs));
19
+ }
20
+
21
+ async function isProcessRunning(pid) {
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch (cause) {
26
+ return cause?.code !== 'ESRCH';
27
+ }
28
+ }
29
+
30
+ async function clearStaleLock(lockPath) {
31
+ try {
32
+ const value = JSON.parse(await fs.readFile(path.join(lockPath, BUILD_LOCK_METADATA), 'utf8'));
33
+
34
+ if (!Number.isInteger(value.pid) || !(await isProcessRunning(value.pid))) {
35
+ await fs.rm(lockPath, { force: true, recursive: true });
36
+ }
37
+ } catch (cause) {
38
+ if (cause?.code === 'ENOENT') {
39
+ return;
40
+ }
41
+
42
+ const stats = await fs.stat(lockPath).catch(() => null);
43
+ if (stats && Date.now() - stats.mtimeMs > STALE_LOCK_METADATA_MS) {
44
+ await fs.rm(lockPath, { force: true, recursive: true });
45
+ }
46
+ }
47
+ }
48
+
49
+ async function acquireBuildLock(lockPath) {
50
+ const token = randomUUID();
51
+ const value = JSON.stringify({ pid: process.pid, token });
52
+
53
+ while (true) {
54
+ try {
55
+ await fs.mkdir(lockPath);
56
+ await fs.writeFile(path.join(lockPath, BUILD_LOCK_METADATA), value, 'utf8');
57
+ return token;
58
+ } catch (cause) {
59
+ if (cause?.code !== 'EEXIST') {
60
+ throw cause;
61
+ }
62
+
63
+ await clearStaleLock(lockPath);
64
+ await wait(BUILD_LOCK_RETRY_MS);
65
+ }
66
+ }
67
+ }
68
+
69
+ async function releaseBuildLock(lockPath, token) {
70
+ try {
71
+ const current = JSON.parse(await fs.readFile(path.join(lockPath, BUILD_LOCK_METADATA), 'utf8'));
72
+ if (current.token === token) {
73
+ await fs.rm(lockPath, { force: true, recursive: true });
74
+ }
75
+ } catch (cause) {
76
+ if (cause?.code !== 'ENOENT') {
77
+ throw cause;
78
+ }
79
+ }
80
+ }
81
+
82
+ function run(command, args) {
83
+ return new Promise((resolve, reject) => {
84
+ const child = spawn(command, args, { cwd: process.cwd(), stdio: 'inherit' });
85
+ child.once('error', reject);
86
+ child.once('exit', (code, signal) => {
87
+ if (code === 0) {
88
+ resolve();
89
+ return;
90
+ }
91
+
92
+ reject(new Error(`${command} exited with ${signal ?? `code ${code}`}.`));
93
+ });
94
+ });
95
+ }
96
+
97
+ export async function runPackageBuild({ copyTemplates } = {}) {
98
+ const lockPath = path.join(process.cwd(), BUILD_LOCK_FILE);
99
+ const token = await acquireBuildLock(lockPath);
100
+
101
+ try {
102
+ // Launch the package's own CLI with Node: Windows cannot directly spawn
103
+ // a .cmd shim, and shell interpolation would break paths containing spaces.
104
+ const require = createRequire(path.join(process.cwd(), 'package.json'));
105
+ await run(process.execPath, [require.resolve('tsdown/run')]);
106
+
107
+ if (copyTemplates) await copyTemplates();
108
+
109
+ await run(process.execPath, [path.join(scriptDirectory, 'protect-build.mjs'), 'dist']);
110
+ } finally {
111
+ await releaseBuildLock(lockPath, token);
112
+ }
113
+ }
114
+
115
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
116
+ void runPackageBuild().catch((cause) => {
117
+ console.error(cause);
118
+ process.exitCode = 1;
119
+ });
120
+ }
package/config.d.mts ADDED
@@ -0,0 +1,4 @@
1
+ import type { Options } from 'tsdown';
2
+ export declare function defineLingvaPackageBuildConfig(
3
+ config: Omit<Options, 'minify' | 'sourcemap' | 'clean'>
4
+ ): Options;
package/config.mjs ADDED
@@ -0,0 +1,9 @@
1
+ import process from 'node:process';
2
+ import { defineConfig } from 'tsdown';
3
+
4
+ const isDeveloperBuild = process.env.LINGVA_DEV_BUILD === 'true';
5
+
6
+ /** @param {import('tsdown').Options} config */
7
+ export function defineLingvaPackageBuildConfig(config) {
8
+ return defineConfig({ ...config, clean: true, minify: !isDeveloperBuild, sourcemap: isDeveloperBuild });
9
+ }
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@lingva/build-tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Shared protected package builds for Lingva workspaces",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE.md",
7
+ "files": [
8
+ "build-package.mjs",
9
+ "protect-build.mjs",
10
+ "config.mjs",
11
+ "config.d.mts"
12
+ ],
13
+ "bin": {
14
+ "lingva-build": "./build-package.mjs"
15
+ },
16
+ "exports": {
17
+ "./build": "./build-package.mjs",
18
+ "./config": {
19
+ "types": "./config.d.mts",
20
+ "import": "./config.mjs"
21
+ }
22
+ },
23
+ "peerDependencies": {
24
+ "tsdown": "^0.21.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "provenance": true
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/usausoft/lingva.git",
33
+ "directory": "packages/build-tools"
34
+ }
6
35
  }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ async function collectFiles(targetPath) {
8
+ const stats = await fs.stat(targetPath);
9
+
10
+ if (stats.isFile()) {
11
+ return [targetPath];
12
+ }
13
+
14
+ if (!stats.isDirectory()) {
15
+ return [];
16
+ }
17
+
18
+ const entries = await fs.readdir(targetPath, { withFileTypes: true });
19
+ const files = await Promise.all(
20
+ entries.map(async (entry) => {
21
+ const entryPath = path.join(targetPath, entry.name);
22
+
23
+ if (entry.isDirectory()) {
24
+ return collectFiles(entryPath);
25
+ }
26
+
27
+ if (entry.isFile()) {
28
+ return [entryPath];
29
+ }
30
+
31
+ return [];
32
+ })
33
+ );
34
+
35
+ return files.flat();
36
+ }
37
+
38
+ async function stripSourceMapReference(filePath) {
39
+ let content = await fs.readFile(filePath, 'utf8');
40
+ content = content.replace(/\n\/\/# sourceMappingURL=.*$/u, '');
41
+ await fs.writeFile(filePath, content, 'utf8');
42
+ }
43
+
44
+ async function main() {
45
+ if (process.env.LINGVA_DEV_BUILD === 'true') {
46
+ process.exit(0);
47
+ }
48
+
49
+ const distPath = process.argv[2];
50
+
51
+ if (!distPath) {
52
+ console.error('Usage: node scripts/protect-build.mjs <dist-dir>');
53
+ process.exit(1);
54
+ }
55
+
56
+ const absoluteDistPath = path.resolve(process.cwd(), distPath);
57
+ const files = await collectFiles(absoluteDistPath);
58
+
59
+ await Promise.all(
60
+ files.map(async (filePath) => {
61
+ if (filePath.endsWith('.map')) {
62
+ await fs.rm(filePath, { force: true });
63
+ return;
64
+ }
65
+
66
+ if (/\.(?:[cm]?[jt]s)$/u.test(filePath)) {
67
+ await stripSourceMapReference(filePath);
68
+ }
69
+ })
70
+ );
71
+ }
72
+
73
+ void main();