@yamada-ui/cli 2.0.0-dev-20250821011016 → 2.0.0-dev-20250823155214

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.
Files changed (2) hide show
  1. package/dist/index.js +661 -188
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import path from "node:path";
10
10
  import { format, promisify } from "node:util";
11
11
  import os from "node:os";
12
12
  import fs, { existsSync, realpathSync, statSync } from "fs";
13
- import os$1 from "os";
13
+ import os$1, { tmpdir } from "os";
14
14
  import path$1 from "path";
15
15
  import fs$1 from "node:fs";
16
16
  import tty from "node:tty";
@@ -18,12 +18,12 @@ import semverDiff from "semver/functions/diff.js";
18
18
  import semverGt from "semver/functions/gt.js";
19
19
  import semver from "semver";
20
20
  import boxen from "boxen";
21
- import { getObject, isArray, isObject, isString, isUndefined, merge, omitObject } from "@yamada-ui/utils";
22
- import { mkdir, readFile, readdir, writeFile } from "fs/promises";
21
+ import { getObject, isArray, isObject, isString, isUndefined, merge, omitObject, toArray } from "@yamada-ui/utils";
22
+ import { mkdir, mkdtemp, readFile, readdir, writeFile } from "fs/promises";
23
23
  import { Listr } from "listr2";
24
24
  import ora from "ora";
25
25
  import prompts from "prompts";
26
- import { execa } from "execa";
26
+ import { ExecaError, execa } from "execa";
27
27
  import "validate-npm-package-name";
28
28
  import YAML from "yamljs";
29
29
  import { glob } from "glob";
@@ -34,8 +34,8 @@ import nodeEval from "node-eval";
34
34
  import { Script } from "vm";
35
35
  import { HttpsProxyAgent } from "https-proxy-agent";
36
36
  import fetch from "node-fetch";
37
- import { diffLines } from "diff";
38
37
  import { rimraf } from "rimraf";
38
+ import { diffLines } from "diff";
39
39
 
40
40
  //#region rolldown:runtime
41
41
  var __create = Object.create;
@@ -5283,7 +5283,7 @@ var package_default = {
5283
5283
 
5284
5284
  //#endregion
5285
5285
  //#region src/utils/lint.ts
5286
- async function lint(content, { cwd: cwd$3, enabled = true, filePath } = {}) {
5286
+ async function lintText(content, { cwd: cwd$3, enabled = true, filePath } = {}) {
5287
5287
  if (!enabled) return content;
5288
5288
  try {
5289
5289
  const eslint = new ESLint({
@@ -5297,10 +5297,21 @@ async function lint(content, { cwd: cwd$3, enabled = true, filePath } = {}) {
5297
5297
  return content;
5298
5298
  }
5299
5299
  }
5300
+ async function lintFiles(patterns, { cwd: cwd$3, enabled = true } = {}) {
5301
+ if (!enabled) return;
5302
+ try {
5303
+ const eslint = new ESLint({
5304
+ cwd: cwd$3,
5305
+ fix: true
5306
+ });
5307
+ const results = await eslint.lintFiles(patterns);
5308
+ await ESLint.outputFixes(results);
5309
+ } catch {}
5310
+ }
5300
5311
 
5301
5312
  //#endregion
5302
5313
  //#region src/utils/prettier.ts
5303
- async function format$2(content, { configPath, enabled = true,...options } = {}) {
5314
+ async function formatText(content, { configPath, enabled = true,...options } = {}) {
5304
5315
  if (!enabled) return content;
5305
5316
  try {
5306
5317
  configPath ??= await resolveConfigFile();
@@ -5314,6 +5325,15 @@ async function format$2(content, { configPath, enabled = true,...options } = {})
5314
5325
  return content;
5315
5326
  }
5316
5327
  }
5328
+ async function formatFiles(patterns, { enabled = true,...options } = {}) {
5329
+ if (!enabled) return;
5330
+ patterns = toArray(patterns);
5331
+ await Promise.all(patterns.map(async (pattern) => {
5332
+ let content = await readFile(pattern, "utf-8");
5333
+ content = await formatText(content, options);
5334
+ await writeFile(pattern, content, "utf-8");
5335
+ }));
5336
+ }
5317
5337
 
5318
5338
  //#endregion
5319
5339
  //#region src/utils/fs.ts
@@ -5326,13 +5346,13 @@ async function isWriteable(directory) {
5326
5346
  return false;
5327
5347
  }
5328
5348
  }
5329
- async function writeFile$1(path$9, content, { format: formatConfig, lint: lintConfig,...rest } = {}) {
5330
- content = await lint(content, {
5331
- cwd: rest.cwd ?? cwd,
5332
- ...lintConfig
5349
+ async function writeFile$1(path$9, content, options = {}) {
5350
+ await writeFile(path$9, content, options.encoding ?? "utf-8");
5351
+ await lintFiles(path$9, {
5352
+ cwd: options.cwd ?? cwd,
5353
+ ...options.lint
5333
5354
  });
5334
- content = await format$2(content, formatConfig);
5335
- await writeFile(path$9, content, rest.encoding ?? "utf-8");
5355
+ await formatFiles(path$9, options.format);
5336
5356
  }
5337
5357
  async function writeFileSafe(path$9, content, options) {
5338
5358
  if (path$9.includes("/")) {
@@ -5343,9 +5363,9 @@ async function writeFileSafe(path$9, content, options) {
5343
5363
  }
5344
5364
  async function validateDir(path$9) {
5345
5365
  const writeable = await isWriteable(path$9);
5346
- if (!writeable) throw new Error(`The path ${path$9} does not writeable. Please check the permissions.`);
5347
- if (!existsSync(path$9)) throw new Error(`The path ${path$9} does not exist. Please try again.`);
5348
- if (!statSync(path$9).isDirectory()) throw new Error(`The path ${path$9} is not a directory. Please try again.`);
5366
+ if (!writeable) throw new Error(`The path ${c.yellow(path$9)} does not writeable. Please check the permissions.`);
5367
+ if (!existsSync(path$9)) throw new Error(`The path ${c.yellow(path$9)} does not exist. Please try again.`);
5368
+ if (!statSync(path$9).isDirectory()) throw new Error(`The path ${c.yellow(path$9)} is not a directory. Please try again.`);
5349
5369
  return true;
5350
5370
  }
5351
5371
  function timer() {
@@ -5361,10 +5381,9 @@ function timer() {
5361
5381
  start
5362
5382
  };
5363
5383
  }
5364
- async function getComponentFiles(componentName, { srcPath }) {
5384
+ async function getFiles(pattern) {
5365
5385
  const files$1 = {};
5366
- const [dirPath] = await glob(path$1.join(srcPath, "**", componentName));
5367
- if (!dirPath) return files$1;
5386
+ const [dirPath] = await glob(pattern);
5368
5387
  const dirents = await readdir(dirPath, { withFileTypes: true });
5369
5388
  await Promise.all(dirents.map(async (dirent) => {
5370
5389
  const name$1 = dirent.name;
@@ -5376,13 +5395,16 @@ async function getComponentFiles(componentName, { srcPath }) {
5376
5395
  const data$1 = await readFile(targetPath, "utf-8");
5377
5396
  files$1[`${name$1}/${dirent$1.name}`] = data$1;
5378
5397
  }));
5379
- } else {
5398
+ } else if (!name$1.endsWith(".json")) {
5380
5399
  const targetPath = path$1.join(dirent.parentPath, dirent.name);
5381
5400
  const data = await readFile(targetPath, "utf-8");
5382
5401
  files$1[name$1] = data;
5383
5402
  }
5384
5403
  }));
5385
- return files$1;
5404
+ return {
5405
+ dirPath,
5406
+ files: files$1
5407
+ };
5386
5408
  }
5387
5409
 
5388
5410
  //#endregion
@@ -5480,6 +5502,16 @@ async function installDependencies(dependencies$1, { cwd: cwd$3, dev, exact = tr
5480
5502
  stdout: "ignore"
5481
5503
  });
5482
5504
  }
5505
+ async function uninstallDependencies(dependencies$1, { cwd: cwd$3 } = {}) {
5506
+ const packageManager = getPackageManager();
5507
+ if (dependencies$1.length) {
5508
+ const command = packageManager === "npm" ? "uninstall" : "remove";
5509
+ await execa(packageManager, [command, ...dependencies$1], {
5510
+ cwd: cwd$3,
5511
+ stdout: "ignore"
5512
+ });
5513
+ }
5514
+ }
5483
5515
  async function addWorkspace(cwd$3, workspacePath, config$1) {
5484
5516
  const packageManager = getPackageManager();
5485
5517
  switch (packageManager) {
@@ -5515,12 +5547,12 @@ async function addWorkspace(cwd$3, workspacePath, config$1) {
5515
5547
 
5516
5548
  //#endregion
5517
5549
  //#region src/utils/config.ts
5518
- async function getConfig(cwd$3, configPath, { format: format$3, lint: lint$1 } = {}) {
5550
+ async function getConfig(cwd$3, configPath, { format: format$2, lint } = {}) {
5519
5551
  try {
5520
- const data = await readFile(path$1.resolve(cwd$3, configPath), "utf8");
5552
+ const data = await readFile(path$1.resolve(cwd$3, configPath), "utf-8");
5521
5553
  const userConfig = JSON.parse(data);
5522
- if (!isUndefined(format$3)) userConfig.format = { enabled: format$3 };
5523
- if (!isUndefined(lint$1)) userConfig.lint = { enabled: lint$1 };
5554
+ if (!isUndefined(format$2)) userConfig.format = { enabled: format$2 };
5555
+ if (!isUndefined(lint)) userConfig.lint = { enabled: lint };
5524
5556
  const rootPath = path$1.resolve(cwd$3, userConfig.path ?? (userConfig.monorepo ? DEFAULT_PATH.monorepo : DEFAULT_PATH.polyrepo));
5525
5557
  const src = existsSync(path$1.resolve(rootPath, "src"));
5526
5558
  const srcPath = src ? path$1.resolve(rootPath, "src") : rootPath;
@@ -5529,8 +5561,12 @@ async function getConfig(cwd$3, configPath, { format: format$3, lint: lint$1 } =
5529
5561
  const replacedSection = path$9.replace(/(\.\.\/|\.\/)/g, "").replace(/(^\/|\/$)/g, "");
5530
5562
  return [section, replacedSection];
5531
5563
  }));
5532
- userConfig.lint ??= {};
5533
- userConfig.lint.filePath ??= path$1.resolve(srcPath, "index.ts");
5564
+ const indexPath = path$1.resolve(srcPath, "index.ts");
5565
+ const registryPath = path$1.resolve(srcPath, REGISTRY_FILE_NAME);
5566
+ if (userConfig.theme?.path) userConfig.theme.path = path$1.resolve(cwd$3, userConfig.theme.path);
5567
+ function isSection(section) {
5568
+ return SECTION_NAMES.includes(section);
5569
+ }
5534
5570
  function getSectionAbsolutePath(section) {
5535
5571
  return path$1.resolve(srcPath, userConfig[section]?.path ?? DEFAULT_PATH[section]);
5536
5572
  }
@@ -5569,6 +5605,9 @@ async function getConfig(cwd$3, configPath, { format: format$3, lint: lint$1 } =
5569
5605
  getSection,
5570
5606
  getSectionAbsolutePath,
5571
5607
  getSectionPath,
5608
+ indexPath,
5609
+ isSection,
5610
+ registryPath,
5572
5611
  rootPath,
5573
5612
  srcPath
5574
5613
  };
@@ -5576,7 +5615,7 @@ async function getConfig(cwd$3, configPath, { format: format$3, lint: lint$1 } =
5576
5615
  const packageManager = getPackageManager();
5577
5616
  const { args, command } = packageExecuteCommands(packageManager);
5578
5617
  const prefix = `${command}${args.length ? ` ${args.join(" ")}` : ""}`;
5579
- throw new Error(`No config found. Please run ${c.cyan(`${prefix} ${package_default.name}@latest init`)}.`);
5618
+ throw new Error(`No ${c.yellow("config")} found. Please run ${c.cyan(`${prefix} ${package_default.name}@latest init`)}.`);
5580
5619
  }
5581
5620
  }
5582
5621
 
@@ -5671,7 +5710,7 @@ async function fetchRegistries(names, config$1, { cache: cache$1 = true, depende
5671
5710
  results[name$1] = await results[name$1];
5672
5711
  const { dependencies: dependencies$1, dependents, section } = results[name$1];
5673
5712
  const target = [];
5674
- if (section === "root" || section === "theme") return;
5713
+ if (!config$1.isSection(section)) return;
5675
5714
  withDependencies = withDependencies && (config$1[section]?.dependents ?? true);
5676
5715
  withDependents = withDependents && (config$1[section]?.dependencies ?? false);
5677
5716
  if (withDependencies && dependents) {
@@ -5691,6 +5730,9 @@ async function fetchRegistries(names, config$1, { cache: cache$1 = true, depende
5691
5730
  await fetch$1(names);
5692
5731
  return results;
5693
5732
  }
5733
+ async function fetchLocaleRegistry(path$9) {
5734
+ return JSON.parse(await readFile(path$9, "utf-8"));
5735
+ }
5694
5736
  async function getGeneratedNameMap(config$1) {
5695
5737
  const results = await Promise.all(SECTION_NAMES.map(async (section) => {
5696
5738
  try {
@@ -5746,6 +5788,17 @@ function transformContent(targetSection, content, { getSection }, generatedNames
5746
5788
  });
5747
5789
  return content;
5748
5790
  }
5791
+ async function transformContentWithFormatAndLint(filePath, section, content, config$1, generatedNames) {
5792
+ const { cwd: cwd$3, format: format$2, lint } = config$1;
5793
+ content = transformContent(section, content, config$1, generatedNames);
5794
+ content = await lintText(content, {
5795
+ ...lint,
5796
+ cwd: cwd$3,
5797
+ filePath
5798
+ });
5799
+ content = await formatText(content, format$2);
5800
+ return content;
5801
+ }
5749
5802
  function transformTemplateContent(template, data) {
5750
5803
  let content = template;
5751
5804
  Object.entries(data).forEach(([key, value]) => {
@@ -5753,20 +5806,7 @@ function transformTemplateContent(template, data) {
5753
5806
  });
5754
5807
  return content;
5755
5808
  }
5756
- async function generateSources(dirPath, { section, sources }, config$1, generatedNames = []) {
5757
- await Promise.all(sources.map(async ({ name: fileName, content, data, template }) => {
5758
- const targetPath = path$1.resolve(dirPath, fileName);
5759
- if (content) {
5760
- content = transformContent(section, content, config$1, generatedNames);
5761
- await writeFileSafe(targetPath, content, config$1);
5762
- } else if (template && data) await Promise.all(data.map(async ({ name: fileName$1,...rest }) => {
5763
- content = transformTemplateContent(template, rest);
5764
- content = transformContent(section, content, config$1, generatedNames);
5765
- await writeFileSafe(path$1.resolve(targetPath, fileName$1), content, config$1);
5766
- }));
5767
- }));
5768
- }
5769
- function replaceIndex(generatedNames, content, { getSection }) {
5809
+ function transformIndex(generatedNames, content, { getSection }) {
5770
5810
  const matches = content.matchAll(/from\s+["']([^"']+)["']/g);
5771
5811
  matches.forEach((match) => {
5772
5812
  const [searchValue, value] = match;
@@ -5791,10 +5831,36 @@ function replaceIndex(generatedNames, content, { getSection }) {
5791
5831
  });
5792
5832
  return content;
5793
5833
  }
5834
+ async function transformIndexWithFormatAndLint(content, config$1, generatedNames) {
5835
+ const { cwd: cwd$3, format: format$2, lint } = config$1;
5836
+ content = transformIndex(generatedNames, content, config$1);
5837
+ content = await lintText(content, {
5838
+ ...lint,
5839
+ cwd: cwd$3,
5840
+ filePath: config$1.indexPath
5841
+ });
5842
+ content = await formatText(content, format$2);
5843
+ return content;
5844
+ }
5845
+ async function generateSource(dirPath, section, { name: fileName, content, data, template }, config$1, generatedNames = []) {
5846
+ const targetPath = path$1.resolve(dirPath, fileName);
5847
+ if (content) {
5848
+ content = transformContent(section, content, config$1, generatedNames);
5849
+ await writeFileSafe(targetPath, content, config$1);
5850
+ } else if (template && data) await Promise.all(data.map(async ({ name: fileName$1,...rest }) => {
5851
+ const content$1 = transformContent(section, transformTemplateContent(template, rest), config$1, generatedNames);
5852
+ await writeFileSafe(path$1.resolve(targetPath, fileName$1), content$1, config$1);
5853
+ }));
5854
+ }
5855
+ async function generateSources(dirPath, registry, config$1, generatedNames = []) {
5856
+ await Promise.all([...registry.sources.map((source) => generateSource(dirPath, registry.section, source, config$1, generatedNames)), writeFileSafe(path$1.resolve(dirPath, REGISTRY_FILE_NAME), JSON.stringify(registry), merge(config$1, { format: { parser: "json" } }))]);
5857
+ }
5794
5858
 
5795
5859
  //#endregion
5796
5860
  //#region src/constant.ts
5797
5861
  const CONFIG_FILE_NAME = "ui.json";
5862
+ const REGISTRY_FILE_NAME = "registry.json";
5863
+ const THEME_PATH = "./theme";
5798
5864
  const DEFAULT_PACKAGE_NAME = "@workspaces/ui";
5799
5865
  const REGISTRY_URL = "https://v2.yamada-ui.com/registry/v2";
5800
5866
  const DEFAULT_PATH = {
@@ -5863,23 +5929,22 @@ const TSCONFIG_JSON = {
5863
5929
 
5864
5930
  //#endregion
5865
5931
  //#region src/commands/add/index.ts
5866
- const add = new Command("add").description("Add a component to your project").argument("[components...]", "Components to add").option("--cwd <path>", "Current working directory", cwd).option("-c, --config <path>", "Path to the config file", CONFIG_FILE_NAME).option("-o, --overwrite", "overwrite existing files.", false).option("-i, --install", "Install dependencies", false).option("-f, --format", "Format the output files.", false).option("-l, --lint", "Lint the output files.", false).action(async function(componentNames, { config: configPath, cwd: cwd$3, format: format$3, install, lint: lint$1, overwrite }) {
5932
+ const add = new Command("add").description("add a component to your project").argument("[components...]", "components to add").option("--cwd <path>", "current working directory", cwd).option("-c, --config <path>", "path to the config file", CONFIG_FILE_NAME).option("-o, --overwrite", "overwrite existing files.", false).option("-i, --install", "install dependencies", false).option("-s, --sequential", "run tasks sequentially.", false).option("-f, --format", "format the output files.").option("-l, --lint", "lint the output files.").action(async function(componentNames, { config: configPath, cwd: cwd$3, format: format$2, install, lint, overwrite, sequential }) {
5867
5933
  const spinner = ora();
5868
5934
  try {
5869
5935
  const { end } = timer();
5870
- const all = !componentNames.length;
5871
5936
  spinner.start("Validating directory");
5872
5937
  await validateDir(cwd$3);
5873
5938
  spinner.succeed("Validated directory");
5874
5939
  spinner.start("Fetching config");
5875
5940
  const config$1 = await getConfig(cwd$3, configPath, {
5876
- format: format$3,
5877
- lint: lint$1
5941
+ format: format$2,
5942
+ lint
5878
5943
  });
5879
5944
  spinner.succeed("Fetched config");
5880
5945
  let generatedNameMap;
5881
5946
  const omittedGeneratedNames = [];
5882
- if (all) {
5947
+ if (!componentNames.length) {
5883
5948
  const { proceed } = await prompts({
5884
5949
  type: "confirm",
5885
5950
  name: "proceed",
@@ -5919,8 +5984,8 @@ const add = new Command("add").description("Add a component to your project").ar
5919
5984
  }
5920
5985
  spinner.start("Fetching registries");
5921
5986
  const registries = await fetchRegistries(componentNames, config$1, {
5922
- dependencies: !all,
5923
- dependents: !all,
5987
+ dependencies: !!componentNames.length,
5988
+ dependents: !!componentNames.length,
5924
5989
  omit: omittedGeneratedNames
5925
5990
  });
5926
5991
  const registryNames = Object.keys(registries);
@@ -5931,9 +5996,19 @@ const add = new Command("add").description("Add a component to your project").ar
5931
5996
  ...dependents?.providers ?? []
5932
5997
  ]).flat().filter((dependent) => omittedGeneratedNames.includes(dependent)))];
5933
5998
  spinner.succeed("Fetched registries");
5999
+ if (componentNames.length !== registryNames.length) {
6000
+ const colorizedNames = registryNames.map((name$1) => c.yellow(name$1));
6001
+ const { proceed } = await prompts({
6002
+ type: "confirm",
6003
+ name: "proceed",
6004
+ initial: true,
6005
+ message: c.reset(`The following components will be added: ${colorizedNames.join(", ")}. Do you want to add them?`)
6006
+ });
6007
+ if (!proceed) process.exit(0);
6008
+ }
5934
6009
  const targetNames = [...new Set([...omittedGeneratedNames, ...registryNames])];
5935
6010
  const tasks = new Listr(Object.entries(registries).map(([name$1, registry]) => {
5936
- if (registry.section === "root" || registry.section === "theme") return;
6011
+ if (!config$1.isSection(registry.section)) return;
5937
6012
  const sectionPath = config$1.getSectionAbsolutePath(registry.section);
5938
6013
  const dirPath = path$1.join(sectionPath, name$1);
5939
6014
  return {
@@ -5943,36 +6018,17 @@ const add = new Command("add").description("Add a component to your project").ar
5943
6018
  },
5944
6019
  title: `Generating ${c.cyan(name$1)}`
5945
6020
  };
5946
- }).filter((task) => !isUndefined(task)), { concurrent: true });
5947
- if (existsSync(path$1.resolve(config$1.srcPath, "index.ts"))) tasks.add({
5948
- task: async (_, task) => {
5949
- const targetPath = path$1.resolve(config$1.srcPath, "index.ts");
5950
- const content = replaceIndex(targetNames, await readFile(targetPath, "utf-8"), config$1);
5951
- await writeFileSafe(targetPath, content, config$1);
5952
- task.title = `Updated ${c.cyan("index.ts")}`;
5953
- },
5954
- title: `Updating ${c.cyan("index.ts")}`
5955
- });
5956
- else tasks.add({
5957
- task: async (_, task) => {
5958
- const { sources } = await fetchRegistry("index");
5959
- const targetPath = path$1.resolve(config$1.srcPath, "index.ts");
5960
- const content = replaceIndex(targetNames, sources[0].content, config$1);
5961
- await writeFileSafe(targetPath, content, config$1);
5962
- task.title = `Generated ${c.cyan("index.ts")}`;
5963
- },
5964
- title: `Generating ${c.cyan("index.ts")}`
5965
- });
6021
+ }).filter((task) => !isUndefined(task)), { concurrent: !sequential });
5966
6022
  if (affectedNames.length && generatedNameMap) {
5967
6023
  if (!overwrite) {
5968
6024
  const colorizedNames = affectedNames.map((name$1) => c.yellow(name$1));
5969
- const { update } = await prompts({
6025
+ const { update: update$1 } = await prompts({
5970
6026
  type: "confirm",
5971
6027
  name: "update",
5972
6028
  initial: true,
5973
6029
  message: c.reset([`The following generated files will be updated: ${colorizedNames.join(", ")}.`, "Do you want to update them?"].join(" "))
5974
6030
  });
5975
- if (update) overwrite = update;
6031
+ if (update$1) overwrite = update$1;
5976
6032
  }
5977
6033
  if (overwrite) Object.entries(generatedNameMap).forEach(([section, names]) => {
5978
6034
  names.forEach((name$1) => {
@@ -5995,6 +6051,23 @@ const add = new Command("add").description("Add a component to your project").ar
5995
6051
  });
5996
6052
  });
5997
6053
  }
6054
+ if (existsSync(config$1.indexPath)) tasks.add({
6055
+ task: async (_, task) => {
6056
+ const content = transformIndex(targetNames, await readFile(config$1.indexPath, "utf-8"), config$1);
6057
+ await writeFileSafe(config$1.indexPath, content, config$1);
6058
+ task.title = `Updated ${c.cyan("index.ts")}`;
6059
+ },
6060
+ title: `Updating ${c.cyan("index.ts")}`
6061
+ });
6062
+ else tasks.add({
6063
+ task: async (_, task) => {
6064
+ const { sources: [source] } = await fetchRegistry("index");
6065
+ const content = transformIndex(targetNames, source.content, config$1);
6066
+ await writeFileSafe(config$1.indexPath, content, config$1);
6067
+ task.title = `Generated ${c.cyan("index.ts")}`;
6068
+ },
6069
+ title: `Generating ${c.cyan("index.ts")}`
6070
+ });
5998
6071
  if (dependencies$1.length) {
5999
6072
  const targetPath = config$1.monorepo ? config$1.rootPath : cwd$3;
6000
6073
  spinner.start(`Checking ${c.cyan("package.json")} dependencies`);
@@ -6028,125 +6101,386 @@ const add = new Command("add").description("Add a component to your project").ar
6028
6101
  });
6029
6102
 
6030
6103
  //#endregion
6031
- //#region src/commands/diff/index.ts
6032
- async function getData(componentNames, config$1) {
6033
- const data = {};
6034
- const registries = {};
6035
- const tasks = new Listr([...componentNames.map((componentName) => ({
6104
+ //#region src/commands/update/print-conflicts.ts
6105
+ function printConflicts(conflictMap, config$1) {
6106
+ Object.entries(conflictMap).forEach(([name$1, files$1]) => {
6107
+ if (name$1 === "index") console.log(`- ${c.yellow("index.ts")}: ${config$1.indexPath.replace(`${config$1.cwd}/`, "")}`);
6108
+ else {
6109
+ console.log(`- ${name$1}`);
6110
+ Object.entries(files$1).forEach(([fileName, path$9]) => {
6111
+ console.log(` - ${c.yellow(fileName)}: ${path$9.replace(`${config$1.cwd}/`, "")}`);
6112
+ });
6113
+ }
6114
+ });
6115
+ }
6116
+
6117
+ //#endregion
6118
+ //#region src/commands/diff/get-diff.ts
6119
+ function getFilePath(section, name$1, fileName, config$1) {
6120
+ return config$1.isSection(section) ? path$1.join(config$1.getSectionAbsolutePath(section), name$1, fileName) : path$1.join(config$1.srcPath, section === "theme" ? name$1 : "", fileName);
6121
+ }
6122
+ async function getDiff(generatedNames, { locale, remote }, config$1, concurrent = true) {
6123
+ const changes = {};
6124
+ const tasks = new Listr(Object.entries(remote).map(([componentName, { section, sources }]) => ({
6125
+ task: async (_, task) => {
6126
+ const localeRegistry = locale[componentName];
6127
+ if (componentName === "index") {
6128
+ const [source] = sources;
6129
+ const fileName = source.name;
6130
+ const [remoteContent, localeContent] = await Promise.all([transformIndexWithFormatAndLint(source.content, config$1, generatedNames), transformIndexWithFormatAndLint(localeRegistry.sources[0].content, config$1, generatedNames)]);
6131
+ const diff$1 = diffLines(localeContent, remoteContent);
6132
+ if (diff$1.length < 2) return;
6133
+ changes[componentName] ??= {};
6134
+ changes[componentName][fileName] = diff$1;
6135
+ } else await Promise.all(sources.map(async ({ name: name$1, content, data, template }) => {
6136
+ const filePath = getFilePath(section, componentName, name$1, config$1);
6137
+ const source = localeRegistry.sources.find((source$1) => source$1.name === name$1);
6138
+ if (content) if (source) {
6139
+ const [remoteContent, localeContent] = await Promise.all([transformContentWithFormatAndLint(filePath, section, content, config$1, generatedNames), transformContentWithFormatAndLint(filePath, section, source.content, config$1, generatedNames)]);
6140
+ const diff$1 = diffLines(localeContent, remoteContent);
6141
+ if (diff$1.length < 2) return;
6142
+ changes[componentName] ??= {};
6143
+ changes[componentName][name$1] = diff$1;
6144
+ } else {
6145
+ const remoteContent = transformContent(section, content, config$1, generatedNames);
6146
+ changes[componentName] ??= {};
6147
+ changes[componentName][name$1] = [{
6148
+ added: true,
6149
+ count: remoteContent.length,
6150
+ removed: false,
6151
+ value: remoteContent
6152
+ }];
6153
+ }
6154
+ else if (template && data) await Promise.all(data.map(async ({ name: fileName,...remoteRest }) => {
6155
+ const localeData = source?.data?.find(({ name: name$2 }) => name$2 === fileName);
6156
+ if (localeData) {
6157
+ if (template === source?.template) return;
6158
+ const { name: _name,...localeRest } = localeData;
6159
+ const [remoteContent, localeContent] = await Promise.all([transformContentWithFormatAndLint(path$1.join(filePath, fileName), section, transformTemplateContent(template, remoteRest), config$1, generatedNames), transformContentWithFormatAndLint(path$1.join(filePath, fileName), section, transformTemplateContent(source.template, localeRest), config$1, generatedNames)]);
6160
+ const diff$1 = diffLines(localeContent, remoteContent);
6161
+ if (diff$1.length < 2) return;
6162
+ changes[componentName] ??= {};
6163
+ changes[componentName][`${name$1}/${fileName}`] = diff$1;
6164
+ } else {
6165
+ const remoteContent = transformContent(section, transformTemplateContent(template, remoteRest), config$1, generatedNames);
6166
+ changes[componentName] ??= {};
6167
+ changes[componentName][`${name$1}/${fileName}`] = [{
6168
+ added: true,
6169
+ count: remoteContent.length,
6170
+ removed: false,
6171
+ value: remoteContent
6172
+ }];
6173
+ }
6174
+ }));
6175
+ }));
6176
+ task.title = `Checked ${c.cyan(componentName)}`;
6177
+ },
6178
+ title: `Checking ${c.cyan(componentName)}`
6179
+ })), { concurrent });
6180
+ await tasks.run();
6181
+ return changes;
6182
+ }
6183
+
6184
+ //#endregion
6185
+ //#region src/commands/update/update-files.ts
6186
+ async function mergeContent(remotePath, localePath, currentPath, fallback) {
6187
+ let content = "";
6188
+ let conflict = false;
6189
+ try {
6190
+ const { stdout } = await execa("diff3", [
6191
+ "-m",
6192
+ remotePath,
6193
+ localePath,
6194
+ currentPath
6195
+ ]);
6196
+ content = stdout;
6197
+ } catch (e) {
6198
+ if (e instanceof ExecaError) if (e.stdout) {
6199
+ conflict = true;
6200
+ content = e.stdout;
6201
+ } else content = fallback;
6202
+ }
6203
+ content = content.replaceAll(remotePath, "remote");
6204
+ content = content.replaceAll(localePath, "locale");
6205
+ content = content.replaceAll(currentPath, "current");
6206
+ content = content.replace(/\|\|\|\|\|\|\|[\s\S]*?=======/g, "=======");
6207
+ return {
6208
+ conflict,
6209
+ content
6210
+ };
6211
+ }
6212
+ async function updateFiles(generatedNames, { locale, remote }, config$1, { concurrent = true, install = false } = {}) {
6213
+ const conflictMap = {};
6214
+ const notInstalledDependencies = [];
6215
+ const shouldUninstallDependencies = [];
6216
+ const disabledFormatAndLint = {
6217
+ format: { enabled: false },
6218
+ lint: { enabled: false }
6219
+ };
6220
+ const tasks = new Listr(Object.entries(remote).map(([componentName, { dependencies: dependencies$1, section, sources }]) => ({
6221
+ task: async (_, task) => {
6222
+ const tempDirPath = await mkdtemp(path$1.join(tmpdir(), `yamada-ui-${componentName}-`));
6223
+ const localeRegistry = locale[componentName];
6224
+ if (dependencies$1 || localeRegistry.dependencies) {
6225
+ const add$1 = dependencies$1?.externals.filter((name$1) => !localeRegistry.dependencies?.externals.includes(name$1)) ?? [];
6226
+ const remove = localeRegistry.dependencies?.externals.filter((name$1) => !dependencies$1?.externals.includes(name$1)) ?? [];
6227
+ notInstalledDependencies.push(...add$1);
6228
+ shouldUninstallDependencies.push(...remove);
6229
+ }
6230
+ try {
6231
+ if (componentName === "index") {
6232
+ const [source] = sources;
6233
+ const fileName = source.name;
6234
+ const remotePath = path$1.join(tempDirPath, `remote-${fileName}`);
6235
+ const localePath = path$1.join(tempDirPath, `locale-${fileName}`);
6236
+ const [remoteContent, localeContent] = await Promise.all([transformIndexWithFormatAndLint(sources[0].content, config$1, generatedNames), transformIndexWithFormatAndLint(localeRegistry.sources[0].content, config$1, generatedNames)]);
6237
+ await Promise.all([writeFileSafe(remotePath, remoteContent, disabledFormatAndLint), writeFileSafe(localePath, localeContent, disabledFormatAndLint)]);
6238
+ const { conflict, content: mergedContent } = await mergeContent(remotePath, localePath, config$1.indexPath, remoteContent);
6239
+ await writeFileSafe(config$1.indexPath, mergedContent, conflict ? merge(config$1, disabledFormatAndLint) : config$1);
6240
+ if (conflict) {
6241
+ conflictMap[componentName] ??= {};
6242
+ conflictMap[componentName][fileName] = config$1.indexPath;
6243
+ }
6244
+ } else await Promise.all(sources.map(async ({ name: name$1, content, data, template }) => {
6245
+ const currentPath = getFilePath(section, componentName, name$1, config$1);
6246
+ const source = localeRegistry.sources.find((source$1) => source$1.name === name$1);
6247
+ if (content) if (source) {
6248
+ const remotePath = path$1.join(tempDirPath, `remote-${name$1}`);
6249
+ const localePath = path$1.join(tempDirPath, `locale-${name$1}`);
6250
+ const [remoteContent, localeContent] = await Promise.all([transformContentWithFormatAndLint(currentPath, section, content, config$1, generatedNames), transformContentWithFormatAndLint(currentPath, section, source.content, config$1, generatedNames)]);
6251
+ await Promise.all([writeFileSafe(remotePath, remoteContent, disabledFormatAndLint), writeFileSafe(localePath, localeContent, disabledFormatAndLint)]);
6252
+ const { conflict, content: mergedContent } = await mergeContent(remotePath, localePath, currentPath, remoteContent);
6253
+ await writeFileSafe(currentPath, mergedContent, conflict ? merge(config$1, disabledFormatAndLint) : config$1);
6254
+ if (conflict) {
6255
+ conflictMap[componentName] ??= {};
6256
+ conflictMap[componentName][name$1] = currentPath;
6257
+ }
6258
+ } else {
6259
+ const remoteContent = transformContent(section, content, config$1, generatedNames);
6260
+ await writeFileSafe(currentPath, remoteContent, config$1);
6261
+ }
6262
+ else if (template && data) await Promise.all(data.map(async ({ name: fileName,...remoteRest }) => {
6263
+ const currentFilePath = path$1.join(currentPath, fileName);
6264
+ const localeData = source?.data?.find(({ name: name$2 }) => name$2 === fileName);
6265
+ if (localeData) {
6266
+ if (template === source.template) return;
6267
+ const { name: _name,...localeRest } = localeData;
6268
+ const remotePath = path$1.join(tempDirPath, `remote-${name$1}-${fileName}`);
6269
+ const localePath = path$1.join(tempDirPath, `locale-${name$1}-${fileName}`);
6270
+ const [remoteContent, localeContent] = await Promise.all([transformContentWithFormatAndLint(currentFilePath, section, transformTemplateContent(template, remoteRest), config$1, generatedNames), transformContentWithFormatAndLint(currentFilePath, section, transformTemplateContent(source.template, localeRest), config$1, generatedNames)]);
6271
+ await Promise.all([writeFileSafe(remotePath, remoteContent, disabledFormatAndLint), writeFileSafe(localePath, localeContent, disabledFormatAndLint)]);
6272
+ const { conflict, content: mergedContent } = await mergeContent(remotePath, localePath, currentFilePath, remoteContent);
6273
+ await writeFileSafe(currentFilePath, mergedContent, conflict ? merge(config$1, disabledFormatAndLint) : config$1);
6274
+ if (conflict) {
6275
+ conflictMap[componentName] ??= {};
6276
+ conflictMap[componentName][`${name$1}/${fileName}`] = currentFilePath;
6277
+ }
6278
+ } else {
6279
+ const remoteContent = transformContent(section, transformTemplateContent(template, remoteRest), config$1, generatedNames);
6280
+ await writeFileSafe(currentFilePath, remoteContent, config$1);
6281
+ }
6282
+ }));
6283
+ }));
6284
+ } catch {} finally {
6285
+ await rimraf(tempDirPath);
6286
+ }
6287
+ task.title = `Changed ${c.cyan(componentName)}`;
6288
+ },
6289
+ title: `Changing ${c.cyan(componentName)}`
6290
+ })), { concurrent });
6291
+ await tasks.run();
6292
+ if (!install && (notInstalledDependencies.length || shouldUninstallDependencies.length)) {
6293
+ const { install: install$1 } = await prompts({
6294
+ type: "confirm",
6295
+ name: "install",
6296
+ initial: true,
6297
+ message: c.reset("There are dependency updates. Do you want to install them?")
6298
+ });
6299
+ if (!install$1) return conflictMap;
6300
+ }
6301
+ const cwd$3 = config$1.monorepo ? config$1.rootPath : config$1.cwd;
6302
+ if (shouldUninstallDependencies.length) await uninstallDependencies(shouldUninstallDependencies.map((value) => splitVersion(value)[0]), { cwd: cwd$3 });
6303
+ if (notInstalledDependencies.length) await installDependencies(notInstalledDependencies, { cwd: cwd$3 });
6304
+ return conflictMap;
6305
+ }
6306
+
6307
+ //#endregion
6308
+ //#region src/commands/diff/get-registries-and-files.ts
6309
+ async function getRegistriesAndFiles(componentNames, config$1, { concurrent = true, index = false, theme: theme$1 = false } = {}) {
6310
+ const fileMap = {};
6311
+ const registries = {
6312
+ locale: {},
6313
+ remote: {}
6314
+ };
6315
+ const tasks = new Listr([], { concurrent });
6316
+ if (index) tasks.add([{
6317
+ task: async (_, task) => {
6318
+ fileMap.index = { "index.ts": await readFile(config$1.indexPath, "utf-8") };
6319
+ registries.locale.index = await fetchLocaleRegistry(config$1.registryPath);
6320
+ task.title = `Got ${c.cyan("index")} file`;
6321
+ },
6322
+ title: `Getting ${c.cyan("index")} file`
6323
+ }, {
6324
+ task: async (_, task) => {
6325
+ registries.remote.index = await fetchRegistry("index");
6326
+ task.title = `Fetched ${c.cyan("index")} registry`;
6327
+ },
6328
+ title: `Fetching ${c.cyan("index")} registry`
6329
+ }]);
6330
+ if (theme$1) tasks.add([{
6036
6331
  task: async (_, task) => {
6037
- data[componentName] = await getComponentFiles(componentName, config$1);
6332
+ if (!config$1.theme?.path) return;
6333
+ const { dirPath, files: files$1 } = await getFiles(config$1.theme.path);
6334
+ fileMap.theme = files$1;
6335
+ registries.locale.theme = await fetchLocaleRegistry(path$1.join(dirPath, REGISTRY_FILE_NAME));
6336
+ task.title = `Got ${c.cyan("theme")} files`;
6337
+ },
6338
+ title: `Getting ${c.cyan("theme")} files`
6339
+ }, {
6340
+ task: async (_, task) => {
6341
+ registries.remote.theme = await fetchRegistry("theme");
6342
+ task.title = `Fetched ${c.cyan("theme")} registry`;
6343
+ },
6344
+ title: `Fetching ${c.cyan("theme")} registry`
6345
+ }]);
6346
+ tasks.add(componentNames.map((componentName) => [{
6347
+ task: async (_, task) => {
6348
+ const { dirPath, files: files$1 } = await getFiles(path$1.join(config$1.srcPath, "**", componentName));
6349
+ fileMap[componentName] = files$1;
6350
+ registries.locale[componentName] = await fetchLocaleRegistry(path$1.join(dirPath, REGISTRY_FILE_NAME));
6038
6351
  task.title = `Got ${c.cyan(componentName)} files`;
6039
6352
  },
6040
6353
  title: `Getting ${c.cyan(componentName)} files`
6041
- })), ...componentNames.map((componentName) => ({
6354
+ }, {
6042
6355
  task: async (_, task) => {
6043
- registries[componentName] = await fetchRegistry(componentName);
6356
+ registries.remote[componentName] = await fetchRegistry(componentName);
6044
6357
  task.title = `Fetched ${c.cyan(componentName)} registry`;
6045
6358
  },
6046
6359
  title: `Fetching ${c.cyan(componentName)} registry`
6047
- }))], { concurrent: true });
6360
+ }]).flat());
6048
6361
  await tasks.run();
6049
6362
  return {
6050
- data,
6363
+ fileMap,
6051
6364
  registries
6052
6365
  };
6053
6366
  }
6054
- async function getDiff(generatedNames, data, registries, config$1) {
6055
- const changes = {};
6056
- await Promise.all(Object.entries(data).map(async ([name$1, files$1]) => {
6057
- const registry = registries[name$1];
6058
- if (!registry) return;
6059
- await Promise.all(Object.entries(files$1).map(async ([fileName, file$1]) => {
6060
- const registryFile = registry.sources.find(({ name: name$2 }) => name$2 === fileName);
6061
- if (!registryFile?.content) return;
6062
- const content = await format$2(transformContent(registry.section, registryFile.content, config$1, generatedNames));
6063
- const diff$1 = diffLines(file$1, content);
6064
- if (diff$1.length < 2) return;
6065
- changes[name$1] ??= {};
6066
- changes[name$1][fileName] = diff$1;
6067
- }));
6068
- }));
6069
- return changes;
6367
+
6368
+ //#endregion
6369
+ //#region src/commands/diff/print-diff.ts
6370
+ function printDiff(diff$1, detail = false) {
6371
+ if (!diff$1) return;
6372
+ Object.entries(diff$1).forEach(([fileName, diff$2]) => {
6373
+ console.log(`- ${c.cyan(fileName)}`);
6374
+ console.log("");
6375
+ diff$2.forEach(({ added, removed, value }) => {
6376
+ if (added) return process.stdout.write(c.green(value));
6377
+ else if (removed) return process.stdout.write(c.red(value));
6378
+ else if (detail) return process.stdout.write(value);
6379
+ });
6380
+ });
6381
+ }
6382
+ function printDiffFiles(name$1, diff$1) {
6383
+ if (!diff$1) return;
6384
+ console.log(`- ${name$1}`);
6385
+ Object.entries(diff$1).forEach(([fileName, changes]) => {
6386
+ printDiffFile(fileName, changes, " ");
6387
+ });
6070
6388
  }
6071
- const diff = new Command("diff").description("Check for updates against the registry").argument("[component]", "Component to check").option("--cwd <path>", "Current working directory", cwd).option("-c, --config <path>", "Path to the config file", CONFIG_FILE_NAME).option("-d, --detail", "Show detailed changes", false).action(async function(componentName, { config: configPath, cwd: cwd$3, detail = false }) {
6389
+ function printDiffFile(name$1, changes = [], space = "") {
6390
+ const added = changes.reduce((prev, { added: added$1, count }) => {
6391
+ if (added$1) return prev + count;
6392
+ return prev;
6393
+ }, 0);
6394
+ const removed = changes.reduce((prev, { count, removed: removed$1 }) => {
6395
+ if (removed$1) return prev + count;
6396
+ return prev;
6397
+ }, 0);
6398
+ console.log(`${space}- ${c.cyan(name$1)} ${c.green(added)} insertions ${c.red(removed)} deletions`);
6399
+ }
6400
+
6401
+ //#endregion
6402
+ //#region src/commands/diff/index.ts
6403
+ const diff = new Command("diff").description("check for updates against the registry").argument("[component]", "component to check").option("--cwd <path>", "current working directory", cwd).option("-c, --config <path>", "path to the config file", CONFIG_FILE_NAME).option("-s, --sequential", "run tasks sequentially.", false).option("-d, --detail", "show detailed changes", false).action(async function(targetName, { config: configPath, cwd: cwd$3, detail, sequential }) {
6072
6404
  const spinner = ora();
6073
6405
  try {
6074
6406
  const { end } = timer();
6075
6407
  const packageManager = getPackageManager();
6076
6408
  const { args, command } = packageExecuteCommands(packageManager);
6077
6409
  const prefix = `${command}${args.length ? ` ${args.join(" ")}` : ""}`;
6078
- const addCommand = c.cyan(`${prefix} ${package_default.name}@latest add`);
6079
- const diffCommand = c.cyan(`${prefix} ${package_default.name}@latest diff`);
6410
+ const getCommand = (command$1) => c.cyan(`${prefix} ${package_default.name}@latest ${command$1}`);
6080
6411
  spinner.start("Validating directory");
6081
6412
  await validateDir(cwd$3);
6082
6413
  spinner.succeed("Validated directory");
6083
6414
  spinner.start("Fetching config");
6084
6415
  const config$1 = await getConfig(cwd$3, configPath);
6085
6416
  spinner.succeed("Fetched config");
6086
- const componentNames = [];
6087
6417
  spinner.start("Getting generated components");
6418
+ const componentNames = [];
6088
6419
  const generatedNameMap = await getGeneratedNameMap(config$1);
6089
6420
  const generatedNames = Object.values(generatedNameMap).flat();
6421
+ const existsTheme = !!config$1.theme?.path && existsSync(config$1.theme.path);
6422
+ let index = targetName === "index";
6423
+ let theme$1 = targetName === "theme";
6090
6424
  spinner.succeed("Got generated components");
6091
- if (componentName) if (generatedNames.includes(componentName)) componentNames.push(componentName);
6425
+ if (theme$1 && !existsTheme) throw new Error([
6426
+ `No ${c.yellow("theme")} found.`,
6427
+ `Please run ${getCommand("theme")}`,
6428
+ "to generate it."
6429
+ ].join(" "));
6430
+ if (!index && !theme$1) if (targetName) if (generatedNames.includes(targetName)) componentNames.push(targetName);
6092
6431
  else throw new Error([
6093
- `No ${c.yellow(componentName)} found in generated components.`,
6094
- `Please run ${addCommand} ${c.green(componentName)}`,
6432
+ `No ${c.yellow(targetName)} found in generated components.`,
6433
+ `Please run ${getCommand("add")} ${c.green(targetName)}`,
6095
6434
  "to add it."
6096
6435
  ].join(" "));
6097
- else componentNames.push(...generatedNames);
6098
- if (!componentNames.length) throw new Error([
6436
+ else {
6437
+ componentNames.push(...generatedNames);
6438
+ index = true;
6439
+ if (existsTheme) theme$1 = true;
6440
+ }
6441
+ if (!index && !theme$1 && !componentNames.length) throw new Error([
6099
6442
  "No components found.",
6100
- `Please run ${addCommand} ${c.green("<component>")}`,
6443
+ `Please run ${getCommand("add")} ${c.green("<component>")}`,
6101
6444
  "to add components."
6102
6445
  ].join(" "));
6103
- const { data, registries } = await getData(componentNames, config$1);
6104
- const changes = await getDiff(generatedNames, data, registries, config$1);
6446
+ const { registries } = await getRegistriesAndFiles(componentNames, config$1, {
6447
+ concurrent: !sequential,
6448
+ index,
6449
+ theme: theme$1
6450
+ });
6451
+ const changes = await getDiff(generatedNames, registries, config$1, !sequential);
6105
6452
  const hasChanges = Object.keys(changes).length;
6106
6453
  console.log("---------------------------------");
6107
6454
  if (!hasChanges) console.log(c.cyan("No updates found."));
6108
- else if (componentName) {
6109
- const diff$1 = changes[componentName];
6110
- if (!diff$1) return;
6111
- Object.entries(diff$1).forEach(([fileName, diff$2], index) => {
6112
- if (!!index) console.log("");
6113
- console.log(`- ${c.cyan(fileName)}`);
6114
- console.log("");
6115
- diff$2.forEach(({ added, removed, value }) => {
6116
- if (added) return process.stdout.write(c.green(value));
6117
- else if (removed) return process.stdout.write(c.red(value));
6118
- else if (detail) return process.stdout.write(value);
6119
- });
6120
- });
6121
- } else {
6122
- componentNames.forEach((name$1) => {
6123
- const diff$1 = changes[name$1];
6124
- if (!diff$1) return;
6125
- console.log(`- ${name$1}`);
6126
- Object.entries(diff$1).forEach(([fileName, diff$2]) => {
6127
- const added = diff$2.reduce((prev, { added: added$1, count }) => {
6128
- if (added$1) return prev + count;
6129
- return prev;
6130
- }, 0);
6131
- const removed = diff$2.reduce((prev, { count, removed: removed$1 }) => {
6132
- if (removed$1) return prev + count;
6133
- return prev;
6134
- }, 0);
6135
- console.log(` - ${c.cyan(fileName)} ${c.green(added)} insertions ${c.red(removed)} deletions`);
6455
+ else {
6456
+ if (targetName) printDiff(changes[targetName], detail);
6457
+ else {
6458
+ if (index) printDiffFile("index.ts", changes.index?.["index.ts"]);
6459
+ if (theme$1) printDiffFiles("theme", changes.theme);
6460
+ componentNames.forEach((name$1) => {
6461
+ printDiffFiles(name$1, changes[name$1]);
6136
6462
  });
6463
+ }
6464
+ console.log("---------------------------------");
6465
+ const { update: update$1 } = await prompts({
6466
+ type: "confirm",
6467
+ name: "update",
6468
+ initial: true,
6469
+ message: c.reset("Do you want to update the files?")
6137
6470
  });
6138
- console.log("");
6139
- console.log(boxen([
6140
- "Run",
6141
- c.cyan(diffCommand),
6142
- c.green("<component>"),
6143
- "to see the changes."
6144
- ].join(" "), {
6145
- borderColor: "yellow",
6146
- borderStyle: "round",
6147
- padding: 1,
6148
- textAlignment: "center"
6149
- }));
6471
+ if (update$1) {
6472
+ const changeNames = Object.keys(changes);
6473
+ const omittedRegistries = {
6474
+ locale: Object.fromEntries(Object.entries(registries.locale).filter(([name$1]) => changeNames.includes(name$1))),
6475
+ remote: Object.fromEntries(Object.entries(registries.remote).filter(([name$1]) => changeNames.includes(name$1)))
6476
+ };
6477
+ const conflictMap = await updateFiles(generatedNames, omittedRegistries, config$1, { concurrent: !sequential });
6478
+ if (Object.keys(conflictMap).length) {
6479
+ console.log("---------------------------------");
6480
+ spinner.warn("There are conflicts. Please check the following files:");
6481
+ printConflicts(conflictMap, config$1);
6482
+ }
6483
+ }
6150
6484
  }
6151
6485
  end();
6152
6486
  } catch (e) {
@@ -6157,7 +6491,7 @@ const diff = new Command("diff").description("Check for updates against the regi
6157
6491
 
6158
6492
  //#endregion
6159
6493
  //#region src/commands/init/index.ts
6160
- const init = new Command("init").description("Initialize your project and install dependencies").option("--cwd <path>", "Current working directory", cwd).option("-c, --config <path>", "Path to the config file", CONFIG_FILE_NAME).option("-o, --overwrite", "Overwrite existing files.", false).action(async function({ config: configPath, cwd: cwd$3, overwrite }) {
6494
+ const init = new Command("init").description("initialize your project and install dependencies").option("--cwd <path>", "current working directory", cwd).option("-c, --config <path>", "path to the config file", CONFIG_FILE_NAME).option("-o, --overwrite", "overwrite existing files.", false).action(async function({ config: configPath, cwd: cwd$3, overwrite }) {
6161
6495
  const spinner = ora();
6162
6496
  try {
6163
6497
  const { end } = timer();
@@ -6167,7 +6501,7 @@ const init = new Command("init").description("Initialize your project and instal
6167
6501
  configPath = path$1.resolve(cwd$3, configPath);
6168
6502
  let dependencies$1;
6169
6503
  let devDependencies$1;
6170
- let { src = true, format: format$3 = true, lint: lint$1 = true, monorepo = true, outdir = "", packageName = "" } = await prompts([
6504
+ let { src = true, format: format$2 = true, lint = true, monorepo = true, outdir = "", packageName = "" } = await prompts([
6171
6505
  {
6172
6506
  type: "toggle",
6173
6507
  name: "monorepo",
@@ -6219,8 +6553,8 @@ const init = new Command("init").description("Initialize your project and instal
6219
6553
  packageName ||= DEFAULT_PACKAGE_NAME;
6220
6554
  config$1.monorepo = monorepo;
6221
6555
  config$1.path = outdir;
6222
- config$1.format = { enabled: format$3 };
6223
- config$1.lint = { enabled: lint$1 };
6556
+ config$1.format = { enabled: format$2 };
6557
+ config$1.lint = { enabled: lint };
6224
6558
  const { generate } = await prompts({
6225
6559
  type: "confirm",
6226
6560
  name: "generate",
@@ -6285,10 +6619,10 @@ const init = new Command("init").description("Initialize your project and instal
6285
6619
  },
6286
6620
  {
6287
6621
  task: async (_, task) => {
6288
- const targetPath = path$1.resolve(outdirPath, src ? "src" : "", "index.ts");
6289
- const { sources } = await fetchRegistry("index");
6290
- const content = sources[0].content;
6291
- await writeFileSafe(targetPath, content, config$1);
6622
+ const targetPath = path$1.resolve(outdirPath, src ? "src" : "");
6623
+ const registry = await fetchRegistry("index");
6624
+ const content = registry.sources[0].content;
6625
+ await Promise.all([writeFileSafe(path$1.join(targetPath, "index.ts"), content, config$1), writeFileSafe(path$1.join(targetPath, REGISTRY_FILE_NAME), JSON.stringify(registry), merge(config$1, { format: { parser: "json" } }))]);
6292
6626
  task.title = `Generated ${c.cyan("index.ts")}`;
6293
6627
  },
6294
6628
  title: `Generating ${c.cyan("index.ts")}`
@@ -6325,10 +6659,9 @@ const init = new Command("init").description("Initialize your project and instal
6325
6659
  title: `Checking ${c.cyan("package.json")} dependencies`
6326
6660
  }, {
6327
6661
  task: async (_, task) => {
6328
- const { sources } = await fetchRegistry("index");
6329
- const targetPath = path$1.resolve(outdirPath, "index.ts");
6330
- const content = sources[0].content;
6331
- await writeFileSafe(targetPath, content, config$1);
6662
+ const registry = await fetchRegistry("index");
6663
+ const content = registry.sources[0].content;
6664
+ await Promise.all([writeFileSafe(path$1.resolve(outdirPath, "index.ts"), content, config$1), writeFileSafe(path$1.resolve(outdirPath, REGISTRY_FILE_NAME), JSON.stringify(registry), merge(config$1, { format: { parser: "json" } }))]);
6332
6665
  task.title = `Generated ${c.cyan("index.ts")}`;
6333
6666
  },
6334
6667
  title: `Generating ${c.cyan("index.ts")}`
@@ -6380,6 +6713,67 @@ const init = new Command("init").description("Initialize your project and instal
6380
6713
  }
6381
6714
  });
6382
6715
 
6716
+ //#endregion
6717
+ //#region src/commands/theme/index.ts
6718
+ const theme = new Command("theme").description("generate theme to your project").argument("[path]", "path to the theme directory").option("--cwd <path>", "current working directory", cwd).option("-c, --config <path>", "path to the config file", CONFIG_FILE_NAME).option("-o, --overwrite", "overwrite existing directory.", false).option("-f, --format", "format the output files.").option("-l, --lint", "lint the output files.").action(async function(themePath, { config: configPath, cwd: cwd$3, format: format$2, lint, overwrite }) {
6719
+ const spinner = ora();
6720
+ try {
6721
+ const { end } = timer();
6722
+ spinner.start("Validating directory");
6723
+ await validateDir(cwd$3);
6724
+ spinner.succeed("Validated directory");
6725
+ spinner.start("Fetching config");
6726
+ const config$1 = await getConfig(cwd$3, configPath, {
6727
+ format: format$2,
6728
+ lint
6729
+ });
6730
+ const themeAbsolutePath = themePath ? path$1.resolve(cwd$3, themePath) : config$1.theme?.path ?? path$1.resolve(cwd$3, THEME_PATH);
6731
+ spinner.succeed("Fetched config");
6732
+ if (!overwrite && existsSync(themeAbsolutePath)) {
6733
+ const { overwrite: overwrite$1 } = await prompts({
6734
+ type: "confirm",
6735
+ name: "overwrite",
6736
+ initial: false,
6737
+ message: c.reset(`The directory already exists. Do you want to overwrite it?`)
6738
+ });
6739
+ if (!overwrite$1) process.exit(0);
6740
+ spinner.start("Clearing directory");
6741
+ await rimraf(themeAbsolutePath);
6742
+ spinner.succeed("Cleared directory");
6743
+ }
6744
+ spinner.start("Fetching registry");
6745
+ const registry = await fetchRegistry("theme");
6746
+ spinner.succeed("Fetched registry");
6747
+ const tasks = new Listr([{
6748
+ task: async (_, task) => {
6749
+ await Promise.all([...registry.sources.map(async ({ name: name$1, content }) => {
6750
+ if (!content) return;
6751
+ const targetPath = path$1.resolve(themeAbsolutePath, name$1);
6752
+ await writeFileSafe(targetPath, content, config$1);
6753
+ }), writeFileSafe(path$1.resolve(themeAbsolutePath, REGISTRY_FILE_NAME), JSON.stringify(registry), merge(config$1, { format: { parser: "json" } }))]);
6754
+ task.title = `Generated theme`;
6755
+ },
6756
+ title: `Generating theme`
6757
+ }, {
6758
+ task: async (_, task) => {
6759
+ const targetPath = path$1.resolve(cwd$3, configPath);
6760
+ const data = await readFile(targetPath, "utf-8");
6761
+ const userConfig = JSON.parse(data);
6762
+ userConfig.theme ??= {};
6763
+ userConfig.theme.path ??= themePath ?? THEME_PATH;
6764
+ await writeFileSafe(targetPath, JSON.stringify(userConfig), merge(config$1, { format: { parser: "json" } }));
6765
+ task.title = `Updated config`;
6766
+ },
6767
+ title: `Updating config`
6768
+ }], { concurrent: true });
6769
+ await tasks.run();
6770
+ end();
6771
+ } catch (e) {
6772
+ if (e instanceof Error) spinner.fail(e.message);
6773
+ else spinner.fail("An unknown error occurred");
6774
+ }
6775
+ });
6776
+
6383
6777
  //#endregion
6384
6778
  //#region src/commands/tokens/config.ts
6385
6779
  const config = [
@@ -6441,9 +6835,9 @@ function isTone(value) {
6441
6835
  function print(unions) {
6442
6836
  return Object.entries(unions).sort(([a], [b]) => a.localeCompare(b)).map(([key, union]) => `${key}: ${!union.length ? "never" : union.map((value) => `"${value}"`).join(" | ")};`).join("\n");
6443
6837
  }
6444
- function extractColorSchemes(theme, colorTokens = []) {
6838
+ function extractColorSchemes(theme$1, colorTokens = []) {
6445
6839
  colorTokens.push("colorScheme.contrast", "colorScheme.fg", "colorScheme.subtle", "colorScheme.muted", "colorScheme.emphasized", "colorScheme.ghost", "colorScheme.solid", "colorScheme.outline");
6446
- const { colors, semanticTokens } = theme;
6840
+ const { colors, semanticTokens } = theme$1;
6447
6841
  const colorSchemes = [];
6448
6842
  Object.entries(colors ?? {}).forEach(([key, value]) => {
6449
6843
  if (!isTone(value)) return;
@@ -6467,8 +6861,8 @@ function extractColorSchemes(theme, colorTokens = []) {
6467
6861
  });
6468
6862
  return { colorSchemes };
6469
6863
  }
6470
- function extractThemeSchemes(theme) {
6471
- const { themeSchemes } = theme;
6864
+ function extractThemeSchemes(theme$1) {
6865
+ const { themeSchemes } = theme$1;
6472
6866
  return Object.keys(themeSchemes ?? {});
6473
6867
  }
6474
6868
  function extractPaths(target, { maxDepth = Infinity, replaceKey = (key) => key, shouldProcess = () => true } = {}) {
@@ -6488,10 +6882,10 @@ function extractKeys(obj, key) {
6488
6882
  if (!isObject(property)) return [];
6489
6883
  return Object.keys(property);
6490
6884
  }
6491
- function generateThemeTokens(theme, { internal = false, theme: { responsive = false } = {} }) {
6885
+ function generateThemeTokens(theme$1, { internal = false, theme: { responsive = false } = {} }) {
6492
6886
  let shouldProcess = () => true;
6493
- if (responsive && isObject(theme.breakpoints)) {
6494
- const keys = ["base", ...Object.keys(theme.breakpoints)];
6887
+ if (responsive && isObject(theme$1.breakpoints)) {
6888
+ const keys = ["base", ...Object.keys(theme$1.breakpoints)];
6495
6889
  const isResponsive = (obj) => {
6496
6890
  const providedKeys = Object.keys(obj);
6497
6891
  if (!providedKeys.length) return false;
@@ -6501,15 +6895,15 @@ function generateThemeTokens(theme, { internal = false, theme: { responsive = fa
6501
6895
  shouldProcess = (obj) => !isResponsive(obj);
6502
6896
  }
6503
6897
  const tokens$1 = config.reduce((prev, { key, flatMap = (value) => value, maxDepth, replaceKey, shouldProcess: additionalShouldProcess = () => true }) => {
6504
- const target = getObject(theme, key);
6898
+ const target = getObject(theme$1, key);
6505
6899
  prev[key] = [];
6506
6900
  if (isObject(target) || isArray(target)) prev[key] = extractPaths(target, {
6507
6901
  maxDepth,
6508
6902
  replaceKey,
6509
6903
  shouldProcess: (obj) => shouldProcess(obj) && additionalShouldProcess(obj)
6510
6904
  }).flatMap(flatMap);
6511
- if (isObject(theme.semanticTokens)) {
6512
- const target$1 = getObject(theme.semanticTokens, key);
6905
+ if (isObject(theme$1.semanticTokens)) {
6906
+ const target$1 = getObject(theme$1.semanticTokens, key);
6513
6907
  const semanticKeys = extractPaths(target$1, {
6514
6908
  maxDepth,
6515
6909
  replaceKey,
@@ -6519,16 +6913,16 @@ function generateThemeTokens(theme, { internal = false, theme: { responsive = fa
6519
6913
  }
6520
6914
  return prev;
6521
6915
  }, {});
6522
- const textStyles = extractKeys(theme, "styles.textStyles");
6523
- const layerStyles = extractKeys(theme, "styles.layerStyles");
6524
- const apply = extractPaths(omitObject(theme.styles ?? {}, [
6916
+ const textStyles = extractKeys(theme$1, "styles.textStyles");
6917
+ const layerStyles = extractKeys(theme$1, "styles.layerStyles");
6918
+ const apply = extractPaths(omitObject(theme$1.styles ?? {}, [
6525
6919
  "globalStyle",
6526
6920
  "layerStyles",
6527
6921
  "resetStyle",
6528
6922
  "textStyles"
6529
6923
  ]), { maxDepth: 2 });
6530
- const { colorSchemes } = extractColorSchemes(theme, tokens$1.colors);
6531
- const themeSchemes = extractThemeSchemes(theme);
6924
+ const { colorSchemes } = extractColorSchemes(theme$1, tokens$1.colors);
6925
+ const themeSchemes = extractThemeSchemes(theme$1);
6532
6926
  if (internal) return [
6533
6927
  `import type { UsageThemeTokens } from "./system"`,
6534
6928
  ``,
@@ -6562,15 +6956,15 @@ function generateThemeTokens(theme, { internal = false, theme: { responsive = fa
6562
6956
  }
6563
6957
  async function getTheme(path$9, cwd$3) {
6564
6958
  const { dependencies: dependencies$1, mod } = await getModule(path$9, cwd$3);
6565
- const theme = mod?.default ?? mod?.theme ?? mod?.customTheme ?? mod?.defaultTheme ?? {};
6959
+ const theme$1 = mod?.default ?? mod?.theme ?? mod?.customTheme ?? mod?.defaultTheme ?? {};
6566
6960
  const config$1 = mod?.config ?? mod?.customConfig ?? mod?.defaultConfig ?? {};
6567
6961
  return {
6568
6962
  config: config$1,
6569
6963
  dependencies: dependencies$1,
6570
- theme
6964
+ theme: theme$1
6571
6965
  };
6572
6966
  }
6573
- const tokens = new Command("tokens").description("Generate theme typings").argument("<path>", "Path to the theme file").option("--cwd <path>", "Current working directory", cwd).option("-c, --config <path>", "Path to the config file", CONFIG_FILE_NAME).option("-o, --out <path>", `Output path`).option("-f, --format", "Format the output file").option("-l, --lint", "Lint the output file").option("--internal", "Generate internal tokens", false).action(async function(inputPath, { config: configPath, cwd: cwd$3, format: format$3, internal, lint: lint$1, out: outPath }) {
6967
+ const tokens = new Command("tokens").description("generate theme typings").argument("[path]", "path to the theme file").option("--cwd <path>", "current working directory", cwd).option("-c, --config <path>", "path to the config file", CONFIG_FILE_NAME).option("-o, --out <path>", `output path`).option("-f, --format", "format the output file").option("-l, --lint", "lint the output file").option("--internal", "generate internal tokens", false).action(async function(inputPath, { config: configPath, cwd: cwd$3, format: format$2, internal, lint, out: outPath }) {
6574
6968
  const spinner = ora();
6575
6969
  try {
6576
6970
  const { end } = timer();
@@ -6581,11 +6975,13 @@ const tokens = new Command("tokens").description("Generate theme typings").argum
6581
6975
  if (!internal) {
6582
6976
  spinner.start("Fetching config");
6583
6977
  config$1 = await getConfig(cwd$3, configPath, {
6584
- format: format$3,
6585
- lint: lint$1
6978
+ format: format$2,
6979
+ lint
6586
6980
  });
6981
+ if (config$1.theme?.path) inputPath ??= path$1.resolve(cwd$3, config$1.theme.path, "index.ts");
6587
6982
  spinner.succeed("Fetched config");
6588
6983
  }
6984
+ if (!inputPath) throw new Error("No input path provided");
6589
6985
  spinner.start(`Getting theme`);
6590
6986
  cwd$3 = path$1.resolve(cwd$3);
6591
6987
  inputPath = path$1.resolve(cwd$3, inputPath);
@@ -6594,18 +6990,18 @@ const tokens = new Command("tokens").description("Generate theme typings").argum
6594
6990
  const dirPath = inputPath.split("/").slice(0, -1).join("/");
6595
6991
  outPath = path$1.join(dirPath, "index.types.ts");
6596
6992
  } else outPath = path$1.join(cwd$3, "index.types.ts");
6597
- const { config: themeConfig, theme } = await getTheme(inputPath, cwd$3);
6993
+ const { config: themeConfig, theme: theme$1 } = await getTheme(inputPath, cwd$3);
6598
6994
  spinner.succeed(`Got theme`);
6599
6995
  spinner.start(`Generating theme typings`);
6600
- const content = generateThemeTokens(theme, {
6996
+ const content = generateThemeTokens(theme$1, {
6601
6997
  ...themeConfig,
6602
6998
  internal
6603
6999
  });
6604
7000
  await writeFileSafe(outPath, content, config$1 ? merge(config$1, { lint: { filePath: inputPath } }) : {
6605
7001
  cwd: cwd$3,
6606
- format: { enabled: format$3 },
7002
+ format: { enabled: format$2 },
6607
7003
  lint: {
6608
- enabled: lint$1,
7004
+ enabled: lint,
6609
7005
  filePath: inputPath
6610
7006
  }
6611
7007
  });
@@ -6617,6 +7013,81 @@ const tokens = new Command("tokens").description("Generate theme typings").argum
6617
7013
  }
6618
7014
  });
6619
7015
 
7016
+ //#endregion
7017
+ //#region src/commands/update/index.ts
7018
+ const update = new Command("update").description("update components in your project").argument("[components...]", "components to update").option("--cwd <path>", "current working directory", cwd).option("-c, --config <path>", "path to the config file", CONFIG_FILE_NAME).option("-i, --install", "install dependencies", false).option("-s, --sequential", "run tasks sequentially.", false).option("-f, --format", "format the output files.").option("-l, --lint", "lint the output files.").action(async function(targetNames, { config: configPath, cwd: cwd$3, format: format$2, install, lint, sequential }) {
7019
+ const spinner = ora();
7020
+ try {
7021
+ const { end } = timer();
7022
+ const packageManager = getPackageManager();
7023
+ const { args, command } = packageExecuteCommands(packageManager);
7024
+ const prefix = `${command}${args.length ? ` ${args.join(" ")}` : ""}`;
7025
+ const getCommand = (command$1) => c.cyan(`${prefix} ${package_default.name}@latest ${command$1}`);
7026
+ spinner.start("Validating directory");
7027
+ await validateDir(cwd$3);
7028
+ spinner.succeed("Validated directory");
7029
+ spinner.start("Fetching config");
7030
+ const config$1 = await getConfig(cwd$3, configPath, {
7031
+ format: format$2,
7032
+ lint
7033
+ });
7034
+ spinner.succeed("Fetched config");
7035
+ spinner.start("Getting generated components");
7036
+ const all = !targetNames.length;
7037
+ const index = all || targetNames.includes("index");
7038
+ const theme$1 = all || targetNames.includes("theme");
7039
+ const existsTheme = !!config$1.theme?.path && existsSync(config$1.theme.path);
7040
+ if (theme$1 && !existsTheme) throw new Error([
7041
+ `No ${c.yellow("theme")} found.`,
7042
+ `Please run ${getCommand("theme")}`,
7043
+ "to generate it."
7044
+ ].join(" "));
7045
+ const omittedTargetNames = targetNames.filter((name$1) => !["index", "theme"].includes(name$1));
7046
+ const componentNames = [];
7047
+ const generatedNameMap = await getGeneratedNameMap(config$1);
7048
+ const generatedNames = Object.values(generatedNameMap).flat();
7049
+ spinner.succeed("Got generated components");
7050
+ if (omittedTargetNames.length) {
7051
+ const notFoundNames = omittedTargetNames.filter((name$1) => !generatedNames.includes(name$1));
7052
+ if (notFoundNames.length) throw new Error([
7053
+ `No ${notFoundNames.map((name$1) => c.yellow(name$1)).join(", ")} found in generated components.`,
7054
+ `Please run ${getCommand("add")} ${notFoundNames.map((name$1) => c.green(name$1)).join(", ")}`,
7055
+ "to add it."
7056
+ ].join(" "));
7057
+ else componentNames.push(...omittedTargetNames);
7058
+ } else if (all || !index && !theme$1) componentNames.push(...generatedNames);
7059
+ const { registries } = await getRegistriesAndFiles(componentNames, config$1, {
7060
+ concurrent: !sequential,
7061
+ index,
7062
+ theme: theme$1
7063
+ });
7064
+ const changes = await getDiff(generatedNames, registries, config$1, !sequential);
7065
+ const hasChanges = Object.keys(changes).length;
7066
+ console.log("---------------------------------");
7067
+ if (!hasChanges) console.log(c.cyan("No updates found."));
7068
+ else {
7069
+ const changeNames = Object.keys(changes);
7070
+ const omittedRegistries = {
7071
+ locale: Object.fromEntries(Object.entries(registries.locale).filter(([name$1]) => changeNames.includes(name$1))),
7072
+ remote: Object.fromEntries(Object.entries(registries.remote).filter(([name$1]) => changeNames.includes(name$1)))
7073
+ };
7074
+ const conflictMap = await updateFiles(generatedNames, omittedRegistries, config$1, {
7075
+ concurrent: !sequential,
7076
+ install
7077
+ });
7078
+ if (Object.keys(conflictMap).length) {
7079
+ console.log("---------------------------------");
7080
+ spinner.warn("There are conflicts. Please check the following files:");
7081
+ printConflicts(conflictMap, config$1);
7082
+ }
7083
+ }
7084
+ end();
7085
+ } catch (e) {
7086
+ if (e instanceof Error) spinner.fail(e.message);
7087
+ else spinner.fail("An unknown error occurred");
7088
+ }
7089
+ });
7090
+
6620
7091
  //#endregion
6621
7092
  //#region src/index.ts
6622
7093
  async function run() {
@@ -6629,10 +7100,12 @@ async function run() {
6629
7100
  }).notify({ isGlobal: true });
6630
7101
  console.log(`\n${c.bold(c.green("Yamada UI CLI"))} v${package_default.version} ${c.dim("by Yamada UI")}`);
6631
7102
  console.log(`${c.dim(package_default.description)}\n`);
6632
- const program = new Command("Yamada UI CLI").version(package_default.version, "-v, --version", "Display the version number").usage(`${c.green("<command>")} [options]`);
7103
+ const program = new Command("Yamada UI CLI").version(package_default.version, "-v, --version", "display the version number").usage(`${c.green("<command>")} [options]`);
6633
7104
  program.addCommand(init);
6634
7105
  program.addCommand(add);
7106
+ program.addCommand(update);
6635
7107
  program.addCommand(diff);
7108
+ program.addCommand(theme);
6636
7109
  program.addCommand(tokens);
6637
7110
  program.parse();
6638
7111
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yamada-ui/cli",
3
3
  "type": "module",
4
- "version": "2.0.0-dev-20250821011016",
4
+ "version": "2.0.0-dev-20250823155214",
5
5
  "description": "The official CLI for Yamada UI projects",
6
6
  "keywords": [
7
7
  "theme",