@nuee/cli 0.4.1 → 0.5.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/dist/add.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export type AddOptions = {
2
- defaults?: boolean;
3
- "dry-run"?: boolean;
1
+ export { getMissingDependencies } from "./dependencies.js";
2
+ import type { CliOptions } from "./arguments.js";
3
+ export type AddOptions = CliOptions & {
4
+ /** @deprecated Use skip-dependencies. */
4
5
  skipDependencyInstall?: boolean;
5
- "skip-dependencies"?: boolean;
6
- } & Record<string, boolean | string | undefined>;
6
+ };
7
7
  export declare function add(projectDirectory: string, componentNames: string | readonly string[], options?: AddOptions): Promise<void>;
package/dist/add.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, relative, resolve } from "node:path";
2
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
- import { defaultConfig, hasConfig, readConfig, resolveConfigAlias } from "./config.js";
5
- import { installDependencies } from "./dependencies.js";
6
- import { init } from "./init.js";
4
+ import { defaultConfig, getDefaultAliases, hasConfig, readConfig, resolveConfigAlias, } from "./config.js";
5
+ export { getMissingDependencies } from "./dependencies.js";
6
+ import { getMissingDependencies, installDependencies } from "./dependencies.js";
7
+ import { applyInitialization, prepareInitialization } from "./init.js";
7
8
  import { resolveComponent } from "./registry.js";
9
+ import { parseSource, removeReducedMotionStyles } from "./source.js";
8
10
  function isNotFoundError(error) {
9
11
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
10
12
  }
@@ -23,13 +25,11 @@ async function askYesNo(question, defaultValue) {
23
25
  readline.close();
24
26
  }
25
27
  }
26
- async function writeSource(source, targetPath, confirmOverwrite) {
28
+ async function writeSource(source, targetPath) {
27
29
  try {
28
30
  const currentSource = await readFile(targetPath, "utf8");
29
31
  if (currentSource === source)
30
- return "unchanged";
31
- if (!(await confirmOverwrite()))
32
- throw new Error("Component installation canceled.");
32
+ return;
33
33
  }
34
34
  catch (error) {
35
35
  if (!isNotFoundError(error))
@@ -37,7 +37,6 @@ async function writeSource(source, targetPath, confirmOverwrite) {
37
37
  }
38
38
  await mkdir(dirname(targetPath), { recursive: true });
39
39
  await writeFile(targetPath, source, "utf8");
40
- return "written";
41
40
  }
42
41
  function resolveTargetPath(uiDirectory, filePath) {
43
42
  if (isAbsolute(filePath))
@@ -52,35 +51,6 @@ function resolveTargetPath(uiDirectory, filePath) {
52
51
  function replaceTokenImport(source, stylesAlias) {
53
52
  return source.replaceAll("@nuee/tokens/semantic.stylex", `${stylesAlias}/semantic.stylex`);
54
53
  }
55
- function removeReducedMotionStyles(source) {
56
- const mediaQuery = '"@media (prefers-reduced-motion: reduce)":';
57
- let transformedSource = source;
58
- let mediaQueryIndex = transformedSource.indexOf(mediaQuery);
59
- while (mediaQueryIndex !== -1) {
60
- const propertyStart = transformedSource.lastIndexOf("\n", mediaQueryIndex) + 1;
61
- const openingBraceIndex = transformedSource.indexOf("{", mediaQueryIndex + mediaQuery.length);
62
- let depth = 0;
63
- let propertyEnd = openingBraceIndex;
64
- for (let index = openingBraceIndex; index < transformedSource.length; index += 1) {
65
- if (transformedSource[index] === "{")
66
- depth += 1;
67
- if (transformedSource[index] === "}")
68
- depth -= 1;
69
- if (depth !== 0)
70
- continue;
71
- propertyEnd = index + 1;
72
- if (transformedSource[propertyEnd] === ",")
73
- propertyEnd += 1;
74
- if (transformedSource[propertyEnd] === "\n")
75
- propertyEnd += 1;
76
- break;
77
- }
78
- transformedSource =
79
- transformedSource.slice(0, propertyStart) + transformedSource.slice(propertyEnd);
80
- mediaQueryIndex = transformedSource.indexOf(mediaQuery);
81
- }
82
- return transformedSource;
83
- }
84
54
  export async function add(projectDirectory, componentNames, options = {}) {
85
55
  const componentNameList = [
86
56
  ...new Set(typeof componentNames === "string" ? [componentNames] : componentNames),
@@ -89,43 +59,75 @@ export async function add(projectDirectory, componentNames, options = {}) {
89
59
  throw new Error("Enter at least one component name to add.");
90
60
  }
91
61
  const shouldInitialize = !(await hasConfig(projectDirectory));
92
- if (shouldInitialize && !options["dry-run"]) {
93
- await init(projectDirectory, {
94
- ...options,
95
- "skip-dependencies": options.skipDependencyInstall || options["skip-dependencies"],
96
- }, false);
62
+ let config;
63
+ if (shouldInitialize) {
64
+ const defaultAliases = await getDefaultAliases(projectDirectory);
65
+ config = {
66
+ ...defaultConfig,
67
+ aliases: {
68
+ ui: options.ui ?? defaultAliases.ui,
69
+ styles: options.styles ?? options.tokens ?? defaultAliases.styles,
70
+ },
71
+ };
72
+ }
73
+ else {
74
+ config = await readConfig(projectDirectory);
97
75
  }
98
- const config = options["dry-run"] && shouldInitialize ? defaultConfig : await readConfig(projectDirectory);
99
76
  const resolvedList = await Promise.all(componentNameList.map(resolveComponent));
77
+ await resolveConfigAlias(projectDirectory, config.aliases.styles, "aliases.styles");
100
78
  const uiDirectory = await resolveConfigAlias(projectDirectory, config.aliases.ui, "aliases.ui");
101
- let isOverwriteConfirmed = false;
102
- async function confirmOverwrite() {
103
- if (isOverwriteConfirmed)
104
- return true;
105
- isOverwriteConfirmed = await askYesNo("Files with the same names already exist. Overwrite all?", false);
106
- return isOverwriteConfirmed;
107
- }
79
+ const sources = [];
108
80
  for (const resolved of resolvedList) {
109
81
  for (const file of resolved.files) {
110
- const targetPath = resolveTargetPath(uiDirectory, file.path);
111
- if (options["dry-run"])
112
- continue;
113
- await writeSource(config.accessibility.respectReducedMotion
114
- ? replaceTokenImport(file.content, config.aliases.styles)
115
- : removeReducedMotionStyles(replaceTokenImport(file.content, config.aliases.styles)), targetPath, confirmOverwrite);
82
+ const isScript = /\.[cm]?[jt]sx?$/.test(file.path);
83
+ let source = replaceTokenImport(file.content, config.aliases.styles);
84
+ if (isScript && !config.accessibility.respectReducedMotion) {
85
+ source = removeReducedMotionStyles(source);
86
+ }
87
+ if (isScript)
88
+ parseSource(source);
89
+ sources.push({ source, targetPath: resolveTargetPath(uiDirectory, file.path) });
116
90
  }
117
91
  }
118
- const externalDependencySet = new Set();
119
- for (const resolved of resolvedList) {
120
- for (const dependency of resolved.externalDependencies)
121
- externalDependencySet.add(dependency);
92
+ const externalDependencies = await getMissingDependencies(projectDirectory, [
93
+ ...new Set(resolvedList.flatMap((resolved) => resolved.externalDependencies)),
94
+ ]);
95
+ const skipDependencies = options.skipDependencyInstall || options["skip-dependencies"];
96
+ const initialization = shouldInitialize
97
+ ? await prepareInitialization(projectDirectory, {
98
+ ...options,
99
+ defaults: true,
100
+ ui: config.aliases.ui,
101
+ styles: config.aliases.styles,
102
+ "skip-dependencies": skipDependencies,
103
+ })
104
+ : undefined;
105
+ const overwriteFileNames = [];
106
+ for (const { source, targetPath } of sources) {
107
+ try {
108
+ if ((await readFile(targetPath, "utf8")) !== source)
109
+ overwriteFileNames.push(basename(targetPath));
110
+ }
111
+ catch (error) {
112
+ if (!isNotFoundError(error))
113
+ throw error;
114
+ }
115
+ }
116
+ if (overwriteFileNames.length > 0 && !options["dry-run"]) {
117
+ const fileLabel = overwriteFileNames.join(", ");
118
+ const shouldOverwrite = await askYesNo(`Overwrite ${overwriteFileNames.length} existing file${overwriteFileNames.length === 1 ? "" : "s"} (${fileLabel})?`, false);
119
+ if (!shouldOverwrite)
120
+ throw new Error("Component installation canceled.");
121
+ }
122
+ if (initialization && !options["dry-run"]) {
123
+ await applyInitialization(projectDirectory, initialization);
124
+ }
125
+ for (const { source, targetPath } of sources) {
126
+ if (options["dry-run"])
127
+ continue;
128
+ await writeSource(source, targetPath);
122
129
  }
123
- const externalDependencies = [...externalDependencySet];
124
- const shouldInstallDependencies = externalDependencies.length > 0 &&
125
- !options.skipDependencyInstall &&
126
- !options["skip-dependencies"] &&
127
- !options["dry-run"] &&
128
- (await askYesNo(`Install external dependencies (${externalDependencies.join(", ")})?`, true));
130
+ const shouldInstallDependencies = externalDependencies.length > 0 && !skipDependencies && !options["dry-run"];
129
131
  if (shouldInstallDependencies) {
130
132
  await installDependencies(projectDirectory, externalDependencies);
131
133
  }
@@ -1,16 +1,26 @@
1
- export type CliOptions = Record<string, boolean | string | undefined> & {
1
+ export type CliOptions = {
2
2
  cwd?: string;
3
3
  defaults?: boolean;
4
- framework?: string;
5
4
  "dry-run"?: boolean;
6
5
  force?: boolean;
7
6
  "skip-dependencies"?: boolean;
8
7
  styles?: string;
9
8
  tokens?: string;
10
9
  ui?: string;
10
+ vite?: boolean;
11
11
  };
12
12
  export declare function parseArguments(arguments_: readonly string[]): {
13
13
  command: string;
14
- options: CliOptions;
14
+ options: {
15
+ cwd?: string | undefined;
16
+ defaults?: boolean | undefined;
17
+ "dry-run"?: boolean | undefined;
18
+ force?: boolean | undefined;
19
+ "skip-dependencies"?: boolean | undefined;
20
+ styles?: string | undefined;
21
+ tokens?: string | undefined;
22
+ ui?: string | undefined;
23
+ vite?: boolean | undefined;
24
+ };
15
25
  positionals: string[];
16
26
  };
package/dist/arguments.js CHANGED
@@ -1,21 +1,20 @@
1
+ import { parseArgs } from "node:util";
1
2
  export function parseArguments(arguments_) {
2
- const [command, ...rest] = arguments_;
3
- const options = {};
4
- const positionals = [];
5
- for (let index = 0; index < rest.length; index += 1) {
6
- const argument = rest[index];
7
- if (!argument.startsWith("--")) {
8
- positionals.push(argument);
9
- continue;
10
- }
11
- const name = argument.slice(2);
12
- const nextArgument = rest[index + 1];
13
- if (!nextArgument || nextArgument.startsWith("--")) {
14
- options[name] = true;
15
- continue;
16
- }
17
- options[name] = nextArgument;
18
- index += 1;
19
- }
3
+ const [command, ...args] = arguments_;
4
+ const { values: options, positionals } = parseArgs({
5
+ args,
6
+ allowPositionals: true,
7
+ options: {
8
+ cwd: { type: "string" },
9
+ defaults: { type: "boolean" },
10
+ "dry-run": { type: "boolean" },
11
+ force: { type: "boolean" },
12
+ "skip-dependencies": { type: "boolean" },
13
+ styles: { type: "string" },
14
+ tokens: { type: "string" },
15
+ ui: { type: "string" },
16
+ vite: { type: "boolean" },
17
+ },
18
+ });
20
19
  return { command, options, positionals };
21
20
  }
package/dist/config.d.ts CHANGED
@@ -14,4 +14,8 @@ export declare function validateConfig(config: unknown): NueeConfig;
14
14
  export declare function readConfig(projectDirectory: string): Promise<NueeConfig>;
15
15
  export declare function writeConfig(projectDirectory: string, config: NueeConfig): Promise<string>;
16
16
  export declare function resolveConfigPath(projectDirectory: string, path: string, name: string): string;
17
+ export declare function getDefaultAliases(projectDirectory: string): Promise<{
18
+ ui: string;
19
+ styles: string;
20
+ }>;
17
21
  export declare function resolveConfigAlias(projectDirectory: string, alias: string, name: string): Promise<string>;
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { access, readFile, writeFile } from "node:fs/promises";
2
2
  import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { createPathsMatcher, parseTsconfig } from "get-tsconfig";
3
4
  import { parse } from "jsonc-parser";
4
5
  export const configFileName = "nuee.json";
5
6
  export const defaultConfig = {
@@ -11,12 +12,26 @@ export const defaultConfig = {
11
12
  styles: "@/styles",
12
13
  },
13
14
  };
15
+ function getAliasPrefix(paths) {
16
+ if (!paths)
17
+ return undefined;
18
+ for (const [pattern, targetList] of Object.entries(paths)) {
19
+ if (!pattern.endsWith("*"))
20
+ continue;
21
+ const hasProjectRootTarget = targetList.some((target) => ["*", "./*", "src/*", "./src/*"].includes(target.replaceAll("\\", "/")));
22
+ if (hasProjectRootTarget)
23
+ return pattern.slice(0, -1);
24
+ }
25
+ return undefined;
26
+ }
14
27
  export async function hasConfig(projectDirectory) {
15
28
  try {
16
29
  await access(resolve(projectDirectory, configFileName));
17
30
  return true;
18
31
  }
19
- catch {
32
+ catch (error) {
33
+ if (!isNotFoundError(error))
34
+ throw error;
20
35
  return false;
21
36
  }
22
37
  }
@@ -28,6 +43,9 @@ function ensureRelativePath(projectDirectory, path, name) {
28
43
  }
29
44
  }
30
45
  export function validateConfig(config) {
46
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
47
+ throw new Error("Configure nuee.json as an object.");
48
+ }
31
49
  const candidate = config;
32
50
  if (candidate.paths || candidate.tokens) {
33
51
  throw new Error("This nuee.json uses an older format. Run `nuee init --force` to create alias-based configuration.");
@@ -43,7 +61,8 @@ export function validateConfig(config) {
43
61
  throw new Error("Configure aliases.styles.");
44
62
  }
45
63
  if (candidate.accessibility !== undefined &&
46
- (typeof candidate.accessibility !== "object" ||
64
+ (candidate.accessibility === null ||
65
+ typeof candidate.accessibility !== "object" ||
47
66
  typeof candidate.accessibility.respectReducedMotion !== "boolean")) {
48
67
  throw new Error("Configure accessibility.respectReducedMotion as a boolean.");
49
68
  }
@@ -88,44 +107,51 @@ export function resolveConfigPath(projectDirectory, path, name) {
88
107
  }
89
108
  async function readTsConfig(projectDirectory) {
90
109
  for (const fileName of ["tsconfig.json", "jsconfig.json"]) {
110
+ const path = resolve(projectDirectory, fileName);
91
111
  try {
92
- return parse(await readFile(resolve(projectDirectory, fileName), "utf8"));
112
+ const source = await readFile(path, "utf8");
113
+ const errors = [];
114
+ const value = parse(source, errors, { allowTrailingComma: true });
115
+ if (errors.length || !value || typeof value !== "object" || Array.isArray(value)) {
116
+ throw new Error(`Invalid configuration: ${fileName}`);
117
+ }
118
+ return { path, config: parseTsconfig(path) };
93
119
  }
94
120
  catch (error) {
95
- if (!isNotFoundError(error)) {
96
- continue;
97
- }
121
+ if (!isNotFoundError(error))
122
+ throw error;
98
123
  }
99
124
  }
100
125
  return undefined;
101
126
  }
102
- function resolvePathAlias(alias, paths) {
103
- for (const [pattern, targets] of Object.entries(paths)) {
104
- const wildcardIndex = pattern.indexOf("*");
105
- if (wildcardIndex === -1) {
106
- if (pattern === alias)
107
- return targets[0];
108
- continue;
109
- }
110
- const prefix = pattern.slice(0, wildcardIndex);
111
- const suffix = pattern.slice(wildcardIndex + 1);
112
- if (!alias.startsWith(prefix) || !alias.endsWith(suffix))
113
- continue;
114
- const wildcard = alias.slice(prefix.length, alias.length - suffix.length);
115
- return targets[0]?.replace("*", wildcard);
116
- }
117
- return undefined;
127
+ export async function getDefaultAliases(projectDirectory) {
128
+ const tsConfig = await readTsConfig(projectDirectory);
129
+ const aliasPrefix = getAliasPrefix(tsConfig?.config.compilerOptions?.paths);
130
+ if (!aliasPrefix)
131
+ return defaultConfig.aliases;
132
+ return {
133
+ styles: `${aliasPrefix}styles`,
134
+ ui: `${aliasPrefix}components/ui`,
135
+ };
118
136
  }
119
137
  export async function resolveConfigAlias(projectDirectory, alias, name) {
120
- if (alias.startsWith("@/")) {
121
- return resolveConfigPath(projectDirectory, join("src", alias.slice(2)), name);
122
- }
123
138
  const tsConfig = await readTsConfig(projectDirectory);
124
- const target = tsConfig?.compilerOptions?.paths
125
- ? resolvePathAlias(alias, tsConfig.compilerOptions.paths)
126
- : undefined;
127
- if (!target) {
128
- throw new Error(`Could not resolve ${name} (${alias}). Add it to tsconfig.json or jsconfig.json compilerOptions.paths.`);
139
+ const paths = tsConfig?.config.compilerOptions?.paths;
140
+ // Only explicit paths mappings override the conventional @/src fallback.
141
+ const hasMatchingPath = paths &&
142
+ Object.keys(paths).some((pattern) => {
143
+ const wildcard = pattern.indexOf("*");
144
+ if (wildcard === -1)
145
+ return pattern === alias;
146
+ return (alias.startsWith(pattern.slice(0, wildcard)) && alias.endsWith(pattern.slice(wildcard + 1)));
147
+ });
148
+ if (tsConfig && hasMatchingPath) {
149
+ const targets = createPathsMatcher(tsConfig)?.(alias);
150
+ if (targets?.[0])
151
+ return resolveConfigPath(projectDirectory, targets[0], name);
152
+ }
153
+ else if (alias.startsWith("@/")) {
154
+ return resolveConfigPath(projectDirectory, join("src", alias.slice(2)), name);
129
155
  }
130
- return resolveConfigPath(projectDirectory, join(tsConfig?.compilerOptions?.baseUrl ?? ".", target), name);
156
+ throw new Error(`Could not resolve ${name} (${alias}). Add it to tsconfig.json or jsconfig.json compilerOptions.paths.`);
131
157
  }
@@ -1 +1,2 @@
1
1
  export declare function installDependencies(projectDirectory: string, dependencies: readonly string[], isDevelopmentDependency?: boolean): Promise<void>;
2
+ export declare function getMissingDependencies(projectDirectory: string, dependencies: readonly string[]): Promise<readonly string[]>;
@@ -1,4 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
2
4
  function detectPackageManager() {
3
5
  const userAgent = process.env.npm_config_user_agent ?? "";
4
6
  if (userAgent.startsWith("pnpm"))
@@ -13,10 +15,10 @@ export function installDependencies(projectDirectory, dependencies, isDevelopmen
13
15
  if (dependencies.length === 0)
14
16
  return Promise.resolve();
15
17
  const packageManager = detectPackageManager();
16
- const developmentFlag = isDevelopmentDependency ? "-D" : undefined;
17
- const arguments_ = packageManager === "npm"
18
- ? ["install", developmentFlag, ...dependencies].filter((argument) => Boolean(argument))
19
- : ["add", developmentFlag, ...dependencies].filter((argument) => Boolean(argument));
18
+ const arguments_ = [packageManager === "npm" ? "install" : "add"];
19
+ if (isDevelopmentDependency)
20
+ arguments_.push("-D");
21
+ arguments_.push(...dependencies);
20
22
  return new Promise((resolvePromise, reject) => {
21
23
  const child = spawn(packageManager, arguments_, { cwd: projectDirectory, stdio: "ignore" });
22
24
  child.on("error", reject);
@@ -28,3 +30,22 @@ export function installDependencies(projectDirectory, dependencies, isDevelopmen
28
30
  });
29
31
  });
30
32
  }
33
+ function getPackageName(dependency) {
34
+ const versionStart = dependency.lastIndexOf("@");
35
+ return versionStart > 0 ? dependency.slice(0, versionStart) : dependency;
36
+ }
37
+ export async function getMissingDependencies(projectDirectory, dependencies) {
38
+ try {
39
+ const packageJson = JSON.parse(await readFile(resolve(projectDirectory, "package.json"), "utf8"));
40
+ const installedDependencies = new Set([
41
+ ...Object.keys(packageJson.dependencies ?? {}),
42
+ ...Object.keys(packageJson.devDependencies ?? {}),
43
+ ]);
44
+ return dependencies.filter((dependency) => !installedDependencies.has(getPackageName(dependency)));
45
+ }
46
+ catch (error) {
47
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
48
+ return dependencies;
49
+ throw error;
50
+ }
51
+ }