@pipelab/plugin-electron 1.0.0-beta.27 → 1.0.0-beta.31

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/dist/index.cjs CHANGED
@@ -89582,7 +89582,7 @@ const runWithLiveLogs = async (command, args, execaOptions, log, hooks, abortSig
89582
89582
  TERM: "xterm-256color",
89583
89583
  FORCE_STDERR_LOGGING: "1"
89584
89584
  },
89585
- cancelSignal: abortSignal
89585
+ cancelSignal: abortSignal ?? execaOptions.cancelSignal
89586
89586
  });
89587
89587
  hooks?.onCreated?.(subprocess);
89588
89588
  subprocess.stdout?.on("data", (data) => {
@@ -151288,6 +151288,17 @@ async function isOnline() {
151288
151288
  lastCheckTime = now;
151289
151289
  return isOnlineCached;
151290
151290
  }
151291
+ function allowPrereleaseInRange(rangeStr) {
151292
+ try {
151293
+ return new import_semver$1.default.Range(rangeStr).set.map((conj) => conj.map((c) => {
151294
+ const v = c.semver;
151295
+ if (v.prerelease && v.prerelease.length) return c.operator + v.format();
151296
+ return c.operator + v.major + "." + v.minor + "." + v.patch + "-0";
151297
+ }).join(" ")).join(" || ");
151298
+ } catch {
151299
+ return rangeStr;
151300
+ }
151301
+ }
151291
151302
  /**
151292
151303
  * Robust utility to fetch, cache, and resolve an NPM package.
151293
151304
  * Centralized in core-node to avoid circular dependencies.
@@ -151310,6 +151321,7 @@ async function fetchPackage(packageName, versionOrRange, options) {
151310
151321
  const ctx = options.context;
151311
151322
  const baseDir = ctx.getPackagesPath(packageName);
151312
151323
  let resolvedVersion;
151324
+ const includePrerelease = !!(ctx.releaseTag && ctx.releaseTag !== "latest");
151313
151325
  let resolvedVersionOrRange = versionOrRange;
151314
151326
  if (resolvedVersionOrRange === "local") resolvedVersionOrRange = "latest";
151315
151327
  console.log(`[Fetcher] Resolving ${packageName}@${resolvedVersionOrRange || "latest"}...`);
@@ -151317,7 +151329,7 @@ async function fetchPackage(packageName, versionOrRange, options) {
151317
151329
  if (!await isOnline()) {
151318
151330
  console.warn(`[Fetcher] ${packageName}: offline mode detected (${Date.now() - resolveStart}ms), trying local fallback...`);
151319
151331
  const fallbackStart = Date.now();
151320
- const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, /* @__PURE__ */ new Error("Offline"), baseDir, packageName);
151332
+ const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, /* @__PURE__ */ new Error("Offline"), baseDir, packageName, includePrerelease);
151321
151333
  if (fallbackVersion) {
151322
151334
  resolvedVersion = fallbackVersion;
151323
151335
  console.log(`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`);
@@ -151332,7 +151344,27 @@ async function fetchPackage(packageName, versionOrRange, options) {
151332
151344
  const packument = await packumentPromise;
151333
151345
  const versions = Object.keys(packument.versions);
151334
151346
  const range = resolvedVersionOrRange || "latest";
151335
- let foundVersion = packument["dist-tags"]?.[range] || import_semver$1.default.maxSatisfying(versions, range);
151347
+ let foundVersion = packument["dist-tags"]?.[range] || import_semver$1.default.maxSatisfying(versions, range, { includePrerelease });
151348
+ if (!foundVersion && ctx.releaseTag && ctx.releaseTag !== "latest") {
151349
+ const releaseTagVersion = packument["dist-tags"]?.[ctx.releaseTag];
151350
+ if (releaseTagVersion && import_semver$1.default.valid(releaseTagVersion)) {
151351
+ const rewrittenRangeForCheck = allowPrereleaseInRange(range);
151352
+ if (import_semver$1.default.satisfies(releaseTagVersion, rewrittenRangeForCheck, { includePrerelease: true })) {
151353
+ console.log(`[Fetcher] Using release tag "${ctx.releaseTag}" (${releaseTagVersion}) for ${packageName}@${range} because it satisfies the range`);
151354
+ foundVersion = releaseTagVersion;
151355
+ }
151356
+ }
151357
+ }
151358
+ if (!foundVersion && includePrerelease) {
151359
+ const rewrittenRange = allowPrereleaseInRange(range);
151360
+ if (rewrittenRange !== range) {
151361
+ const matched = import_semver$1.default.maxSatisfying(versions, rewrittenRange, { includePrerelease });
151362
+ if (matched) {
151363
+ console.log(`[Fetcher] Resolved ${packageName}@${range} to ${matched} via rewritten range ${rewrittenRange}`);
151364
+ foundVersion = matched;
151365
+ }
151366
+ }
151367
+ }
151336
151368
  if (range === "latest" && ctx.releaseTag && ctx.releaseTag !== "latest") {
151337
151369
  const releaseTagVersion = packument["dist-tags"]?.[ctx.releaseTag];
151338
151370
  if (releaseTagVersion && import_semver$1.default.valid(releaseTagVersion)) {
@@ -151348,7 +151380,7 @@ async function fetchPackage(packageName, versionOrRange, options) {
151348
151380
  } catch (error) {
151349
151381
  console.warn(`[Fetcher] ${packageName}: remote resolution failed (${Date.now() - resolveStart}ms), trying local fallback...`);
151350
151382
  const fallbackStart = Date.now();
151351
- const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, error, baseDir, packageName);
151383
+ const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, error, baseDir, packageName, includePrerelease);
151352
151384
  if (fallbackVersion) {
151353
151385
  resolvedVersion = fallbackVersion;
151354
151386
  console.log(`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`);
@@ -151530,35 +151562,24 @@ async function installDependencies(packageDir, packageName, options) {
151530
151562
  console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
151531
151563
  return;
151532
151564
  }
151533
- const tempDir = `${packageDir}.tmp-deps-${Math.random().toString(36).slice(2)}`;
151534
- await (0, node_fs_promises.mkdir)(tempDir, { recursive: true });
151535
151565
  try {
151536
- await (0, node_fs_promises.cp)((0, node_path.join)(packageDir, "package.json"), (0, node_path.join)(tempDir, "package.json"));
151537
151566
  console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
151538
151567
  const pnpmStart = Date.now();
151539
- const { all } = await runPnpm(tempDir, {
151568
+ const { all } = await runPnpm(packageDir, {
151540
151569
  signal: options.signal,
151541
151570
  context: options.context
151542
151571
  });
151543
151572
  console.log(`[Fetcher] ${packageName}: pnpm install command took ${Date.now() - pnpmStart}ms`);
151544
151573
  if (all) console.log(`[Fetcher] ${packageName}: Installation trace:\n${all}`);
151545
- const tempNodeModules = (0, node_path.join)(tempDir, "node_modules");
151546
- if ((0, node_fs.existsSync)(nodeModulesPath)) await (0, node_fs_promises.rm)(nodeModulesPath, {
151547
- recursive: true,
151548
- force: true
151549
- }).catch(() => {});
151550
- const renameStart = Date.now();
151551
- await (0, node_fs_promises.rename)(tempNodeModules, nodeModulesPath);
151552
- console.log(`[Fetcher] ${packageName}: Dependencies installed successfully (rename took ${Date.now() - renameStart}ms, total installDependencies took ${Date.now() - start}ms).`);
151574
+ console.log(`[Fetcher] ${packageName}: Dependencies installed successfully (total installDependencies took ${Date.now() - start}ms).`);
151553
151575
  } catch (err) {
151554
151576
  console.error(`[Fetcher] ${packageName}: CRITICAL ERROR during dependency installation: ${err.message}`);
151555
151577
  if (err.all) console.error(`[Fetcher] ${packageName}: Error details:\n${err.all}`);
151556
- throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
151557
- } finally {
151558
- await (0, node_fs_promises.rm)(tempDir, {
151578
+ await (0, node_fs_promises.rm)(nodeModulesPath, {
151559
151579
  recursive: true,
151560
151580
  force: true
151561
151581
  }).catch(() => {});
151582
+ throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
151562
151583
  }
151563
151584
  }
151564
151585
  async function fetchPipelabAsset(packageName, versionOrRange, options) {
@@ -151622,7 +151643,7 @@ async function crawlMonorepoPackages() {
151622
151643
  }
151623
151644
  return cache;
151624
151645
  }
151625
- async function tryLocalFallback(versionOrRange, _error, baseDir, logPrefix) {
151646
+ async function tryLocalFallback(versionOrRange, _error, baseDir, logPrefix, includePrerelease = false) {
151626
151647
  if (!(0, node_fs.existsSync)(baseDir)) return null;
151627
151648
  try {
151628
151649
  const localVersions = (await (0, node_fs_promises.readdir)(baseDir, { withFileTypes: true })).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name).filter((name) => !!import_semver$1.default.valid(name));
@@ -151635,7 +151656,7 @@ async function tryLocalFallback(versionOrRange, _error, baseDir, logPrefix) {
151635
151656
  return latestLocal;
151636
151657
  }
151637
151658
  } else {
151638
- const matched = import_semver$1.default.maxSatisfying(localVersions, range);
151659
+ const matched = import_semver$1.default.maxSatisfying(localVersions, range, { includePrerelease });
151639
151660
  if (matched) {
151640
151661
  console.info(`[Fetcher] ${logPrefix}: Using locally cached matching version: ${matched} for range ${range}`);
151641
151662
  return matched;
@@ -152519,14 +152540,14 @@ const defaultElectronConfig = {
152519
152540
  const makeRunner = createActionRunner(async (options) => {
152520
152541
  const appFolder = options.inputs["input-folder"];
152521
152542
  if (!options.inputs.configuration) throw new Error("Missing electron configuration");
152522
- return await forge("make", appFolder, options, merge(defaultElectronConfig, options.inputs.configuration));
152543
+ await forge("make", appFolder, options, merge(defaultElectronConfig, options.inputs.configuration));
152523
152544
  });
152524
152545
  //#endregion
152525
152546
  //#region src/package.ts
152526
152547
  const packageRunner = createActionRunner(async (options) => {
152527
152548
  const appFolder = options.inputs["input-folder"];
152528
152549
  if (!options.inputs.configuration) throw new Error("Missing electron configuration");
152529
- return await forge("package", appFolder, options, merge(defaultElectronConfig, options.inputs.configuration));
152550
+ await forge("package", appFolder, options, merge(defaultElectronConfig, options.inputs.configuration));
152530
152551
  });
152531
152552
  //#endregion
152532
152553
  //#region src/preview.ts
@@ -152535,6 +152556,7 @@ const previewRunner = createActionRunner(async (options) => {
152535
152556
  if (url === "") throw new Error("URL can't be empty");
152536
152557
  if (!options.inputs.configuration) throw new Error("Missing electron configuration");
152537
152558
  const output = await forge("package", void 0, options, merge(defaultElectronConfig, options.inputs.configuration));
152559
+ if (!output || !output.binary) throw new Error("Failed to build electron package for preview");
152538
152560
  options.log("Opening preview", JSON.stringify(output));
152539
152561
  options.log("Opening url", url);
152540
152562
  await runWithLiveLogs(output.binary, ["--url", url], { cancelSignal: options.abortSignal }, options.log);
@@ -152598,7 +152620,7 @@ const packageV2Runner = createActionRunner(async (options) => {
152598
152620
  serverMode: options.inputs["serverMode"]
152599
152621
  });
152600
152622
  options.log("completeConfiguration", completeConfiguration);
152601
- return await forge("package", appFolder, options, completeConfiguration);
152623
+ await forge("package", appFolder, options, completeConfiguration);
152602
152624
  });
152603
152625
  //#endregion
152604
152626
  //#region src/index.ts
package/dist/index.mjs CHANGED
@@ -89575,7 +89575,7 @@ const runWithLiveLogs = async (command, args, execaOptions, log, hooks, abortSig
89575
89575
  TERM: "xterm-256color",
89576
89576
  FORCE_STDERR_LOGGING: "1"
89577
89577
  },
89578
- cancelSignal: abortSignal
89578
+ cancelSignal: abortSignal ?? execaOptions.cancelSignal
89579
89579
  });
89580
89580
  hooks?.onCreated?.(subprocess);
89581
89581
  subprocess.stdout?.on("data", (data) => {
@@ -151282,6 +151282,17 @@ async function isOnline() {
151282
151282
  lastCheckTime = now;
151283
151283
  return isOnlineCached;
151284
151284
  }
151285
+ function allowPrereleaseInRange(rangeStr) {
151286
+ try {
151287
+ return new import_semver$1.default.Range(rangeStr).set.map((conj) => conj.map((c) => {
151288
+ const v = c.semver;
151289
+ if (v.prerelease && v.prerelease.length) return c.operator + v.format();
151290
+ return c.operator + v.major + "." + v.minor + "." + v.patch + "-0";
151291
+ }).join(" ")).join(" || ");
151292
+ } catch {
151293
+ return rangeStr;
151294
+ }
151295
+ }
151285
151296
  /**
151286
151297
  * Robust utility to fetch, cache, and resolve an NPM package.
151287
151298
  * Centralized in core-node to avoid circular dependencies.
@@ -151304,6 +151315,7 @@ async function fetchPackage(packageName, versionOrRange, options) {
151304
151315
  const ctx = options.context;
151305
151316
  const baseDir = ctx.getPackagesPath(packageName);
151306
151317
  let resolvedVersion;
151318
+ const includePrerelease = !!(ctx.releaseTag && ctx.releaseTag !== "latest");
151307
151319
  let resolvedVersionOrRange = versionOrRange;
151308
151320
  if (resolvedVersionOrRange === "local") resolvedVersionOrRange = "latest";
151309
151321
  console.log(`[Fetcher] Resolving ${packageName}@${resolvedVersionOrRange || "latest"}...`);
@@ -151311,7 +151323,7 @@ async function fetchPackage(packageName, versionOrRange, options) {
151311
151323
  if (!await isOnline()) {
151312
151324
  console.warn(`[Fetcher] ${packageName}: offline mode detected (${Date.now() - resolveStart}ms), trying local fallback...`);
151313
151325
  const fallbackStart = Date.now();
151314
- const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, /* @__PURE__ */ new Error("Offline"), baseDir, packageName);
151326
+ const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, /* @__PURE__ */ new Error("Offline"), baseDir, packageName, includePrerelease);
151315
151327
  if (fallbackVersion) {
151316
151328
  resolvedVersion = fallbackVersion;
151317
151329
  console.log(`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`);
@@ -151326,7 +151338,27 @@ async function fetchPackage(packageName, versionOrRange, options) {
151326
151338
  const packument = await packumentPromise;
151327
151339
  const versions = Object.keys(packument.versions);
151328
151340
  const range = resolvedVersionOrRange || "latest";
151329
- let foundVersion = packument["dist-tags"]?.[range] || import_semver$1.default.maxSatisfying(versions, range);
151341
+ let foundVersion = packument["dist-tags"]?.[range] || import_semver$1.default.maxSatisfying(versions, range, { includePrerelease });
151342
+ if (!foundVersion && ctx.releaseTag && ctx.releaseTag !== "latest") {
151343
+ const releaseTagVersion = packument["dist-tags"]?.[ctx.releaseTag];
151344
+ if (releaseTagVersion && import_semver$1.default.valid(releaseTagVersion)) {
151345
+ const rewrittenRangeForCheck = allowPrereleaseInRange(range);
151346
+ if (import_semver$1.default.satisfies(releaseTagVersion, rewrittenRangeForCheck, { includePrerelease: true })) {
151347
+ console.log(`[Fetcher] Using release tag "${ctx.releaseTag}" (${releaseTagVersion}) for ${packageName}@${range} because it satisfies the range`);
151348
+ foundVersion = releaseTagVersion;
151349
+ }
151350
+ }
151351
+ }
151352
+ if (!foundVersion && includePrerelease) {
151353
+ const rewrittenRange = allowPrereleaseInRange(range);
151354
+ if (rewrittenRange !== range) {
151355
+ const matched = import_semver$1.default.maxSatisfying(versions, rewrittenRange, { includePrerelease });
151356
+ if (matched) {
151357
+ console.log(`[Fetcher] Resolved ${packageName}@${range} to ${matched} via rewritten range ${rewrittenRange}`);
151358
+ foundVersion = matched;
151359
+ }
151360
+ }
151361
+ }
151330
151362
  if (range === "latest" && ctx.releaseTag && ctx.releaseTag !== "latest") {
151331
151363
  const releaseTagVersion = packument["dist-tags"]?.[ctx.releaseTag];
151332
151364
  if (releaseTagVersion && import_semver$1.default.valid(releaseTagVersion)) {
@@ -151342,7 +151374,7 @@ async function fetchPackage(packageName, versionOrRange, options) {
151342
151374
  } catch (error) {
151343
151375
  console.warn(`[Fetcher] ${packageName}: remote resolution failed (${Date.now() - resolveStart}ms), trying local fallback...`);
151344
151376
  const fallbackStart = Date.now();
151345
- const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, error, baseDir, packageName);
151377
+ const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, error, baseDir, packageName, includePrerelease);
151346
151378
  if (fallbackVersion) {
151347
151379
  resolvedVersion = fallbackVersion;
151348
151380
  console.log(`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`);
@@ -151524,35 +151556,24 @@ async function installDependencies(packageDir, packageName, options) {
151524
151556
  console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
151525
151557
  return;
151526
151558
  }
151527
- const tempDir = `${packageDir}.tmp-deps-${Math.random().toString(36).slice(2)}`;
151528
- await mkdir(tempDir, { recursive: true });
151529
151559
  try {
151530
- await cp(join(packageDir, "package.json"), join(tempDir, "package.json"));
151531
151560
  console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
151532
151561
  const pnpmStart = Date.now();
151533
- const { all } = await runPnpm(tempDir, {
151562
+ const { all } = await runPnpm(packageDir, {
151534
151563
  signal: options.signal,
151535
151564
  context: options.context
151536
151565
  });
151537
151566
  console.log(`[Fetcher] ${packageName}: pnpm install command took ${Date.now() - pnpmStart}ms`);
151538
151567
  if (all) console.log(`[Fetcher] ${packageName}: Installation trace:\n${all}`);
151539
- const tempNodeModules = join(tempDir, "node_modules");
151540
- if (existsSync(nodeModulesPath)) await rm(nodeModulesPath, {
151541
- recursive: true,
151542
- force: true
151543
- }).catch(() => {});
151544
- const renameStart = Date.now();
151545
- await rename(tempNodeModules, nodeModulesPath);
151546
- console.log(`[Fetcher] ${packageName}: Dependencies installed successfully (rename took ${Date.now() - renameStart}ms, total installDependencies took ${Date.now() - start}ms).`);
151568
+ console.log(`[Fetcher] ${packageName}: Dependencies installed successfully (total installDependencies took ${Date.now() - start}ms).`);
151547
151569
  } catch (err) {
151548
151570
  console.error(`[Fetcher] ${packageName}: CRITICAL ERROR during dependency installation: ${err.message}`);
151549
151571
  if (err.all) console.error(`[Fetcher] ${packageName}: Error details:\n${err.all}`);
151550
- throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
151551
- } finally {
151552
- await rm(tempDir, {
151572
+ await rm(nodeModulesPath, {
151553
151573
  recursive: true,
151554
151574
  force: true
151555
151575
  }).catch(() => {});
151576
+ throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
151556
151577
  }
151557
151578
  }
151558
151579
  async function fetchPipelabAsset(packageName, versionOrRange, options) {
@@ -151616,7 +151637,7 @@ async function crawlMonorepoPackages() {
151616
151637
  }
151617
151638
  return cache;
151618
151639
  }
151619
- async function tryLocalFallback(versionOrRange, _error, baseDir, logPrefix) {
151640
+ async function tryLocalFallback(versionOrRange, _error, baseDir, logPrefix, includePrerelease = false) {
151620
151641
  if (!existsSync(baseDir)) return null;
151621
151642
  try {
151622
151643
  const localVersions = (await readdir(baseDir, { withFileTypes: true })).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name).filter((name) => !!import_semver$1.default.valid(name));
@@ -151629,7 +151650,7 @@ async function tryLocalFallback(versionOrRange, _error, baseDir, logPrefix) {
151629
151650
  return latestLocal;
151630
151651
  }
151631
151652
  } else {
151632
- const matched = import_semver$1.default.maxSatisfying(localVersions, range);
151653
+ const matched = import_semver$1.default.maxSatisfying(localVersions, range, { includePrerelease });
151633
151654
  if (matched) {
151634
151655
  console.info(`[Fetcher] ${logPrefix}: Using locally cached matching version: ${matched} for range ${range}`);
151635
151656
  return matched;
@@ -152513,14 +152534,14 @@ const defaultElectronConfig = {
152513
152534
  const makeRunner = createActionRunner(async (options) => {
152514
152535
  const appFolder = options.inputs["input-folder"];
152515
152536
  if (!options.inputs.configuration) throw new Error("Missing electron configuration");
152516
- return await forge("make", appFolder, options, merge(defaultElectronConfig, options.inputs.configuration));
152537
+ await forge("make", appFolder, options, merge(defaultElectronConfig, options.inputs.configuration));
152517
152538
  });
152518
152539
  //#endregion
152519
152540
  //#region src/package.ts
152520
152541
  const packageRunner = createActionRunner(async (options) => {
152521
152542
  const appFolder = options.inputs["input-folder"];
152522
152543
  if (!options.inputs.configuration) throw new Error("Missing electron configuration");
152523
- return await forge("package", appFolder, options, merge(defaultElectronConfig, options.inputs.configuration));
152544
+ await forge("package", appFolder, options, merge(defaultElectronConfig, options.inputs.configuration));
152524
152545
  });
152525
152546
  //#endregion
152526
152547
  //#region src/preview.ts
@@ -152529,6 +152550,7 @@ const previewRunner = createActionRunner(async (options) => {
152529
152550
  if (url === "") throw new Error("URL can't be empty");
152530
152551
  if (!options.inputs.configuration) throw new Error("Missing electron configuration");
152531
152552
  const output = await forge("package", void 0, options, merge(defaultElectronConfig, options.inputs.configuration));
152553
+ if (!output || !output.binary) throw new Error("Failed to build electron package for preview");
152532
152554
  options.log("Opening preview", JSON.stringify(output));
152533
152555
  options.log("Opening url", url);
152534
152556
  await runWithLiveLogs(output.binary, ["--url", url], { cancelSignal: options.abortSignal }, options.log);
@@ -152592,7 +152614,7 @@ const packageV2Runner = createActionRunner(async (options) => {
152592
152614
  serverMode: options.inputs["serverMode"]
152593
152615
  });
152594
152616
  options.log("completeConfiguration", completeConfiguration);
152595
- return await forge("package", appFolder, options, completeConfiguration);
152617
+ await forge("package", appFolder, options, completeConfiguration);
152596
152618
  });
152597
152619
  //#endregion
152598
152620
  //#region src/index.ts