@salty-css/core 0.1.0-alpha.9 → 0.1.0-refactor-add-additional-paths-to-config-cache.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.
Files changed (44) hide show
  1. package/bin/confirm-install.d.ts +34 -0
  2. package/bin/context.d.ts +3 -0
  3. package/bin/integrations/index.d.ts +11 -3
  4. package/bin/integrations/types.d.ts +14 -2
  5. package/bin/main.cjs +197 -95
  6. package/bin/main.js +151 -49
  7. package/cache/resolve-dynamic-config-cache.cjs +64 -16
  8. package/cache/resolve-dynamic-config-cache.d.ts +20 -1
  9. package/cache/resolve-dynamic-config-cache.js +65 -17
  10. package/{class-name-generator-YeSQe_Ik.js → class-name-generator-CMWY5KTJ.js} +1 -1
  11. package/{class-name-generator-B2Pb2obX.cjs → class-name-generator-DB5aQwC_.cjs} +1 -1
  12. package/compiler/copy-config-cache.cjs +39 -0
  13. package/compiler/copy-config-cache.d.ts +17 -0
  14. package/compiler/copy-config-cache.js +39 -0
  15. package/compiler/salty-compiler.cjs +22 -21
  16. package/compiler/salty-compiler.d.ts +11 -1
  17. package/compiler/salty-compiler.js +20 -19
  18. package/css/dynamic-styles.cjs +21 -8
  19. package/css/dynamic-styles.d.ts +39 -0
  20. package/css/dynamic-styles.js +22 -9
  21. package/css/index.cjs +1 -0
  22. package/css/index.js +2 -1
  23. package/css/keyframes.cjs +1 -1
  24. package/css/keyframes.js +1 -1
  25. package/generators/index.cjs +1 -1
  26. package/generators/index.js +2 -2
  27. package/instances/classname-instance.cjs +1 -1
  28. package/instances/classname-instance.js +1 -1
  29. package/logger-7xz0pyAz.cjs +12 -0
  30. package/logger-hHmCwThj.js +13 -0
  31. package/package.json +5 -1
  32. package/{parse-styles-CA3TP5n1.cjs → parse-styles-C54MOrPg.cjs} +106 -7
  33. package/{parse-styles-BTIoYnBr.js → parse-styles-CLMTHo2H.js} +107 -8
  34. package/parsers/index.cjs +2 -1
  35. package/parsers/index.d.ts +1 -0
  36. package/parsers/index.js +4 -3
  37. package/parsers/parser-regexes.d.ts +3 -0
  38. package/parsers/strict.d.ts +2 -0
  39. package/runtime/index.cjs +1 -1
  40. package/runtime/index.js +1 -1
  41. package/{salty.config-cqavVm2t.cjs → salty.config-DogY_sSQ.cjs} +1 -1
  42. package/salty.config-GV37Q-D2.js +4 -0
  43. package/types/config-types.d.ts +9 -0
  44. package/salty.config-DjosWdPw.js +0 -4
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Renders a single package spec for display. Translates the internal
3
+ * `'-D <pkg>@<ver>'` shorthand used by `npmInstall` into a `<pkg>@<ver> (dev)`
4
+ * suffix so the user-facing list reads naturally.
5
+ */
6
+ export declare const formatPackageForDisplay: (spec: string) => string;
7
+ export declare const renderPackageList: (packages: string[]) => string;
8
+ export interface ConfirmInstallOptions {
9
+ /** Streams used for the prompt — defaults to process.stdin/stdout. Allows tests to inject. */
10
+ input?: NodeJS.ReadableStream;
11
+ output?: NodeJS.WritableStream;
12
+ /** Whether the input is a TTY — defaults to process.stdin.isTTY. Allows tests to override. */
13
+ isTTY?: boolean;
14
+ }
15
+ /**
16
+ * Confirm a batched install. Resolves on success, throws on decline.
17
+ *
18
+ * - `yes=true` or empty `packages` → no prompt, resolves immediately.
19
+ * - Non-TTY without `yes` → throws, telling the user to pass `--yes`.
20
+ * - Otherwise prints the list and asks `Proceed? (y/N)`. Accepts y/yes
21
+ * (case-insensitive); anything else throws to abort the command.
22
+ */
23
+ export declare const confirmInstall: (packages: string[], yes: boolean, options?: ConfirmInstallOptions) => Promise<void>;
24
+ export interface ConfirmYesNoOptions extends ConfirmInstallOptions {
25
+ /** When true, resolves true without prompting. */
26
+ yes?: boolean;
27
+ /** When true, an empty answer counts as yes. Defaults to false (empty = no). */
28
+ defaultYes?: boolean;
29
+ }
30
+ /**
31
+ * Generic yes/no prompt. Unlike `confirmInstall`, non-TTY without `yes`
32
+ * resolves `false` instead of throwing — callers can choose policy.
33
+ */
34
+ export declare const confirmYesNo: (question: string, options?: ConfirmYesNoOptions) => Promise<boolean>;
package/bin/context.d.ts CHANGED
@@ -8,11 +8,14 @@ export interface ProjectContext {
8
8
  rcFile: RCFile;
9
9
  cliVersion: string;
10
10
  skipInstall: boolean;
11
+ yes: boolean;
11
12
  }
12
13
  export declare const resolveProjectDir: (dir: string, rootDir?: string) => string;
13
14
  export interface BuildContextOptions {
14
15
  dir: string;
15
16
  skipInstall?: boolean;
17
+ /** Skip the install confirmation prompt and install without asking. */
18
+ yes?: boolean;
16
19
  /** When false, build context even if package.json is missing (used by commands that should not require one). */
17
20
  requirePackageJson?: boolean;
18
21
  }
@@ -1,12 +1,20 @@
1
1
  import { ProjectContext } from '../context';
2
- import { BuildIntegrationAdapter } from './types';
2
+ import { BuildIntegrationAdapter, IntegrationPlan } from './types';
3
3
  export declare const buildIntegrationRegistry: BuildIntegrationAdapter[];
4
- export declare const detectAndApplyIntegrations: (ctx: ProjectContext) => Promise<{
4
+ export interface PlannedIntegration {
5
+ name: string;
6
+ configPath: string;
7
+ plan: IntegrationPlan;
8
+ }
9
+ /** Detect every integration that has work to do and compute its plan. */
10
+ export declare const planIntegrations: (ctx: ProjectContext) => Promise<PlannedIntegration[]>;
11
+ /** Execute each previously-planned integration (writes config files). */
12
+ export declare const applyIntegrationPlans: (planned: PlannedIntegration[]) => Promise<{
5
13
  name: string;
6
14
  configPath: string;
7
15
  changed: boolean;
8
16
  }[]>;
9
- export type { BuildIntegrationAdapter, ConfigEdit } from './types';
17
+ export type { BuildIntegrationAdapter, ConfigEdit, IntegrationPlan } from './types';
10
18
  export { viteIntegration, editViteConfig, vitePackage } from './vite';
11
19
  export { nextIntegration, editNextConfig, nextPackage, nextConfigFiles } from './next';
12
20
  export { astroIntegration, editAstroConfig, astroPackage } from './astro';
@@ -3,12 +3,24 @@ export interface IntegrationApplyResult {
3
3
  /** Whether any file was edited or any install was performed. */
4
4
  changed: boolean;
5
5
  }
6
+ /**
7
+ * Pending integration work — the packages it would like installed and a
8
+ * closure that writes the config edits once the install (if any) has run.
9
+ */
10
+ export interface IntegrationPlan {
11
+ /** Packages to install, using the same `npmInstall` shorthand (e.g. `'-D @salty-css/vite@1.2.3'`). */
12
+ packages: string[];
13
+ execute(): Promise<IntegrationApplyResult>;
14
+ }
6
15
  export interface BuildIntegrationAdapter {
7
16
  name: string;
8
17
  /** Returns the config file path this integration targets, or null when not applicable. */
9
18
  detect(ctx: ProjectContext): Promise<string | null> | string | null;
10
- /** Idempotently wire the integration into the project (edit configs, install dev deps). */
11
- apply(ctx: ProjectContext, configPath: string): Promise<IntegrationApplyResult>;
19
+ /**
20
+ * Idempotently produce the work needed to wire the integration in. Returns
21
+ * null when the integration is already wired (nothing to do).
22
+ */
23
+ plan(ctx: ProjectContext, configPath: string): Promise<IntegrationPlan | null>;
12
24
  }
13
25
  /** Pure transform — returned by an integration when it knows how to rewrite a config file. */
14
26
  export interface ConfigEdit {
package/bin/main.cjs CHANGED
@@ -9,8 +9,10 @@ const path = require("path");
9
9
  const promises = require("fs/promises");
10
10
  const child_process = require("child_process");
11
11
  const ora = require("ora");
12
+ const logger = require("../logger-7xz0pyAz.cjs");
12
13
  const pascalCase = require("../pascal-case-By_l58S-.cjs");
13
14
  const ejs = require("ejs");
15
+ const promises$1 = require("readline/promises");
14
16
  var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
15
17
  const defaultPackageJsonPath = path.join(process.cwd(), "package.json");
16
18
  const readPackageJson = async (filePath = defaultPackageJsonPath) => {
@@ -64,9 +66,9 @@ async function formatWithPrettier(filePath) {
64
66
  const hasPrettier = hasPrettierInstalled();
65
67
  if (!hasPrettier) return;
66
68
  await execAsync(`./node_modules/.bin/prettier --write "${filePath}"`);
67
- compiler_saltyCompiler.logger.info(`Formatted ${filePath} with Prettier`);
69
+ logger.logger.info(`Formatted ${filePath} with Prettier`);
68
70
  } catch (error) {
69
- compiler_saltyCompiler.logger.error(`Error formatting ${filePath} with Prettier:`, error);
71
+ logger.logger.error(`Error formatting ${filePath} with Prettier:`, error);
70
72
  }
71
73
  }
72
74
  const SALTYRC_FILENAME = ".saltyrc.json";
@@ -109,8 +111,8 @@ const writeProjectToRc = async (cwd, relativeProjectPath, framework) => {
109
111
  const existing = await readRawRc(cwd);
110
112
  const { content, changed, created } = upsertProjectInRc(existing, relativeProjectPath, framework);
111
113
  if (!changed) return false;
112
- if (created) compiler_saltyCompiler.logger.info("Creating file: " + path2);
113
- else compiler_saltyCompiler.logger.info("Edit file: " + path2);
114
+ if (created) logger.logger.info("Creating file: " + path2);
115
+ else logger.logger.info("Edit file: " + path2);
114
116
  await promises.writeFile(path2, content);
115
117
  await formatWithPrettier(path2);
116
118
  return true;
@@ -141,20 +143,24 @@ const buildContext = async (opts) => {
141
143
  packageJson,
142
144
  rcFile,
143
145
  cliVersion: cliPackageJson.version || "0.0.0",
144
- skipInstall: !!opts.skipInstall
146
+ skipInstall: !!opts.skipInstall,
147
+ yes: !!opts.yes
145
148
  };
146
149
  };
147
150
  const registerBuildCommand = (program, defaultProject) => {
148
- program.command("build [directory]").alias("b").description("Build the Salty-CSS project.").option("-d, --dir <dir>", "Project directory to build the project in.").option("--watch", "Watch for changes and rebuild the project.").action(async function(_dir = defaultProject) {
149
- compiler_saltyCompiler.logger.info("Building the Salty-CSS project...");
150
- const { dir = _dir, watch } = this.opts();
151
+ program.command("build [directory]").alias("b").description("Build the Salty-CSS project.").option("-d, --dir <dir>", "Project directory to build the project in.").option("--watch", "Watch for changes and rebuild the project.").option("--mode <mode>", 'Build mode: "production" or "development". Defaults to NODE_ENV-based detection.').action(async function(_dir = defaultProject) {
152
+ logger.logger.info("Building the Salty-CSS project...");
153
+ const { dir = _dir, watch, mode } = this.opts();
154
+ if (mode !== void 0 && mode !== "production" && mode !== "development") {
155
+ return logger.logError(`Invalid --mode "${mode}". Expected "production" or "development".`);
156
+ }
151
157
  const resolved = dir ?? await getDefaultProject();
152
- if (!resolved) return compiler_saltyCompiler.logError("Project directory must be provided. Add it as the first argument after build command or use the --dir option.");
158
+ if (!resolved) return logger.logError("Project directory must be provided. Add it as the first argument after build command or use the --dir option.");
153
159
  const projectDir = resolveProjectDir(resolved);
154
- const compiler = new compiler_saltyCompiler.SaltyCompiler(projectDir);
160
+ const compiler = new compiler_saltyCompiler.SaltyCompiler(projectDir, { mode });
155
161
  await compiler.generateCss();
156
162
  if (watch) {
157
- compiler_saltyCompiler.logger.info("Watching for changes in the project directory...");
163
+ logger.logger.info("Watching for changes in the project directory...");
158
164
  fs.watch(projectDir, { recursive: true }, async (_event, filePath) => {
159
165
  const shouldRestart$1 = await shouldRestart.checkShouldRestart(filePath);
160
166
  if (shouldRestart$1) {
@@ -225,7 +231,7 @@ const getFramework = (name) => {
225
231
  return frameworksByName[name];
226
232
  };
227
233
  const templateLoaders = {
228
- "salty.config.ts": () => Promise.resolve().then(() => require("../salty.config-cqavVm2t.cjs")),
234
+ "salty.config.ts": () => Promise.resolve().then(() => require("../salty.config-DogY_sSQ.cjs")),
229
235
  "saltygen/index.css": () => Promise.resolve().then(() => require("../index-ByR0nfaf.cjs")),
230
236
  "react/styled-file.ts": () => Promise.resolve().then(() => require("../styled-file-CPd_rTW2.cjs")),
231
237
  "react/vanilla-file.ts": () => Promise.resolve().then(() => require("../vanilla-file-r0fp2q_m.cjs")),
@@ -240,13 +246,13 @@ const readTemplate = async (key, options) => {
240
246
  const registerGenerateCommand = (program, defaultProject) => {
241
247
  program.command("generate [file] [directory]").alias("g").description("Generate a new component file.").option("-f, --file <file>", "File to generate.").option("-d, --dir <dir>", "Project directory to generate the file in.").option("-t, --tag <tag>", "HTML tag of the component.", "div").option("-n, --name <name>", "Name of the component.").option("-c, --className <className>", "CSS class of the component.").option("-r, --reactComponent", "Generate a wrapper component file alongside the styled definition.").action(async function(_file, _dir = defaultProject) {
242
248
  const { file = _file, dir = _dir, tag, name, className, reactComponent = false } = this.opts();
243
- if (!file) return compiler_saltyCompiler.logError("File to generate must be provided. Add it as the first argument after generate command or use the --file option.");
244
- if (!dir) return compiler_saltyCompiler.logError("Project directory must be provided. Add it as the second argument after generate command or use the --dir option.");
249
+ if (!file) return logger.logError("File to generate must be provided. Add it as the first argument after generate command or use the --file option.");
250
+ if (!dir) return logger.logError("Project directory must be provided. Add it as the second argument after generate command or use the --dir option.");
245
251
  let ctx;
246
252
  try {
247
253
  ctx = await buildContext({ dir, requirePackageJson: false });
248
254
  } catch (err) {
249
- return compiler_saltyCompiler.logError(err instanceof Error ? err.message : String(err));
255
+ return logger.logError(err instanceof Error ? err.message : String(err));
250
256
  }
251
257
  const rcFramework = getFramework(getProjectFramework(ctx.rcFile, ctx.relativeProjectPath));
252
258
  const framework = rcFramework ?? await detectFramework(ctx);
@@ -260,13 +266,13 @@ const registerGenerateCommand = (program, defaultProject) => {
260
266
  const formattedStyledFilePath = path.format(parsedFilePath);
261
267
  const alreadyExists = await promises.readFile(formattedStyledFilePath, "utf-8").catch(() => void 0);
262
268
  if (alreadyExists !== void 0) {
263
- compiler_saltyCompiler.logger.error("File already exists: " + formattedStyledFilePath);
269
+ logger.logger.error("File already exists: " + formattedStyledFilePath);
264
270
  return;
265
271
  }
266
272
  let styledComponentName = pascalCase.pascalCase(name || parsedFilePath.base.replace(/\.css\.\w+$/, ""));
267
273
  if (reactComponent) {
268
274
  if (!framework.templates.component) {
269
- return compiler_saltyCompiler.logError(`--reactComponent is not supported for the ${framework.name} framework.`);
275
+ return logger.logError(`--reactComponent is not supported for the ${framework.name} framework.`);
270
276
  }
271
277
  const componentName = styledComponentName + "Component";
272
278
  styledComponentName = styledComponentName + "Wrapper";
@@ -282,17 +288,63 @@ const registerGenerateCommand = (program, defaultProject) => {
282
288
  parsedFilePath.ext = framework.templates.component.wrapperExt;
283
289
  parsedFilePath.base = parsedFilePath.name + parsedFilePath.ext;
284
290
  const formattedWrapperPath = path.format(parsedFilePath);
285
- compiler_saltyCompiler.logger.info("Generating a new file: " + formattedWrapperPath);
291
+ logger.logger.info("Generating a new file: " + formattedWrapperPath);
286
292
  await promises.writeFile(formattedWrapperPath, wrapperContent);
287
293
  await formatWithPrettier(formattedWrapperPath);
288
294
  }
289
295
  const styledKey = reactComponent && framework.templates.component ? framework.templates.component.styled : framework.templates.styled;
290
296
  const { content } = await readTemplate(styledKey, { tag, name: styledComponentName, className });
291
- compiler_saltyCompiler.logger.info("Generating a new file: " + formattedStyledFilePath);
297
+ logger.logger.info("Generating a new file: " + formattedStyledFilePath);
292
298
  await promises.writeFile(formattedStyledFilePath, content);
293
299
  await formatWithPrettier(formattedStyledFilePath);
294
300
  });
295
301
  };
302
+ const formatPackageForDisplay = (spec) => {
303
+ const trimmed = spec.trim();
304
+ if (trimmed.startsWith("-D ")) return `${trimmed.slice(3).trim()} (dev)`;
305
+ return trimmed;
306
+ };
307
+ const renderPackageList = (packages) => {
308
+ return packages.map((p) => ` + ${formatPackageForDisplay(p)}`).join("\n");
309
+ };
310
+ const confirmInstall = async (packages, yes, options = {}) => {
311
+ if (yes) return;
312
+ if (packages.length === 0) return;
313
+ const input = options.input ?? process.stdin;
314
+ const output = options.output ?? process.stdout;
315
+ const isTTY = options.isTTY ?? process.stdin.isTTY ?? false;
316
+ if (!isTTY) {
317
+ throw new Error("Cannot prompt for install confirmation: stdin is not a TTY. Re-run with --yes to install the listed packages without prompting.");
318
+ }
319
+ output.write(`The following packages will be installed:
320
+ ${renderPackageList(packages)}
321
+ `);
322
+ const rl = promises$1.createInterface({ input, output, terminal: false });
323
+ try {
324
+ const answer = (await rl.question("Proceed? (y/N) ")).trim().toLowerCase();
325
+ if (answer !== "y" && answer !== "yes") {
326
+ throw new Error("Install cancelled by user.");
327
+ }
328
+ } finally {
329
+ rl.close();
330
+ }
331
+ };
332
+ const confirmYesNo = async (question, options = {}) => {
333
+ if (options.yes) return true;
334
+ const input = options.input ?? process.stdin;
335
+ const output = options.output ?? process.stdout;
336
+ const isTTY = options.isTTY ?? process.stdin.isTTY ?? false;
337
+ if (!isTTY) return false;
338
+ const suffix = options.defaultYes ? "(Y/n)" : "(y/N)";
339
+ const rl = promises$1.createInterface({ input, output, terminal: false });
340
+ try {
341
+ const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
342
+ if (answer === "") return !!options.defaultYes;
343
+ return answer === "y" || answer === "yes";
344
+ } finally {
345
+ rl.close();
346
+ }
347
+ };
296
348
  const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
297
349
  const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
298
350
  const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
@@ -339,17 +391,22 @@ const editAstroConfig = (existing) => {
339
391
  const astroIntegration = {
340
392
  name: "astro",
341
393
  detect: (ctx) => findAstroConfig(ctx.projectDir),
342
- apply: async (ctx, configPath) => {
394
+ plan: async (ctx, configPath) => {
343
395
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
344
- if (existing === void 0) return { changed: false };
396
+ if (existing === void 0) return null;
345
397
  const result = editAstroConfig(existing);
346
- if (result.warning) compiler_saltyCompiler.logger.warn(result.warning);
347
- if (result.content === null) return { changed: false };
348
- if (!ctx.skipInstall) await npmInstall(`-D ${astroPackage(ctx.cliVersion)}`);
349
- compiler_saltyCompiler.logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
350
- await promises.writeFile(configPath, result.content);
351
- await formatWithPrettier(configPath);
352
- return { changed: true };
398
+ if (result.warning) logger.logger.warn(result.warning);
399
+ if (result.content === null) return null;
400
+ const newContent = result.content;
401
+ return {
402
+ packages: [`-D ${astroPackage(ctx.cliVersion)}`],
403
+ execute: async () => {
404
+ logger.logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
405
+ await promises.writeFile(configPath, newContent);
406
+ await formatWithPrettier(configPath);
407
+ return { changed: true };
408
+ }
409
+ };
353
410
  }
354
411
  };
355
412
  const ESLINT_CONFIG_CANDIDATES = [
@@ -408,20 +465,25 @@ const eslintIntegration = {
408
465
  const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
409
466
  return candidates.find((p) => fs.existsSync(p)) ?? null;
410
467
  },
411
- apply: async (ctx, configPath) => {
468
+ plan: async (ctx, configPath) => {
412
469
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
413
470
  if (existing === void 0) {
414
- compiler_saltyCompiler.logger.error("Could not read ESLint config file.");
415
- return { changed: false };
471
+ logger.logger.error("Could not read ESLint config file.");
472
+ return null;
416
473
  }
417
- if (!ctx.skipInstall) await npmInstall(corePackages.eslintConfigCore(ctx.cliVersion));
418
474
  const result = editEslintConfig(existing, configPath.endsWith("js"));
419
- if (result.warning) compiler_saltyCompiler.logger.warn(result.warning);
420
- if (result.content === null) return { changed: false };
421
- compiler_saltyCompiler.logger.info("Edit file: " + configPath);
422
- await promises.writeFile(configPath, result.content);
423
- await formatWithPrettier(configPath);
424
- return { changed: true };
475
+ if (result.warning) logger.logger.warn(result.warning);
476
+ if (result.content === null) return null;
477
+ const newContent = result.content;
478
+ return {
479
+ packages: [corePackages.eslintConfigCore(ctx.cliVersion)],
480
+ execute: async () => {
481
+ logger.logger.info("Edit file: " + configPath);
482
+ await promises.writeFile(configPath, newContent);
483
+ await formatWithPrettier(configPath);
484
+ return { changed: true };
485
+ }
486
+ };
425
487
  }
426
488
  };
427
489
  const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
@@ -451,16 +513,20 @@ const nextIntegration = {
451
513
  const found = nextConfigFiles.map((file) => path.join(ctx.projectDir, file)).find((p) => fs.existsSync(p));
452
514
  return found ?? null;
453
515
  },
454
- apply: async (ctx, configPath) => {
516
+ plan: async (ctx, configPath) => {
455
517
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
456
- if (existing === void 0) return { changed: false };
518
+ if (existing === void 0) return null;
457
519
  const { content } = editNextConfig(existing);
458
- if (content === null) return { changed: false };
459
- if (!ctx.skipInstall) await npmInstall(`-D ${nextPackage(ctx.cliVersion)}`);
460
- compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Next.js config...");
461
- await promises.writeFile(configPath, content);
462
- await formatWithPrettier(configPath);
463
- return { changed: true };
520
+ if (content === null) return null;
521
+ return {
522
+ packages: [`-D ${nextPackage(ctx.cliVersion)}`],
523
+ execute: async () => {
524
+ logger.logger.info("Adding Salty-CSS plugin to Next.js config...");
525
+ await promises.writeFile(configPath, content);
526
+ await formatWithPrettier(configPath);
527
+ return { changed: true };
528
+ }
529
+ };
464
530
  }
465
531
  };
466
532
  const vitePackage = (version) => `@salty-css/vite@${version}`;
@@ -478,39 +544,52 @@ const viteIntegration = {
478
544
  const path$1 = path.join(ctx.projectDir, "vite.config.ts");
479
545
  return fs.existsSync(path$1) ? path$1 : null;
480
546
  },
481
- apply: async (ctx, configPath) => {
547
+ plan: async (ctx, configPath) => {
482
548
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
483
- if (existing === void 0) return { changed: false };
549
+ if (existing === void 0) return null;
484
550
  const { content } = editViteConfig(existing);
485
- if (content === null) return { changed: false };
486
- compiler_saltyCompiler.logger.info("Edit file: " + configPath);
487
- if (!ctx.skipInstall) await npmInstall(`-D ${vitePackage(ctx.cliVersion)}`);
488
- compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Vite config...");
489
- await promises.writeFile(configPath, content);
490
- await formatWithPrettier(configPath);
491
- return { changed: true };
551
+ if (content === null) return null;
552
+ return {
553
+ packages: [`-D ${vitePackage(ctx.cliVersion)}`],
554
+ execute: async () => {
555
+ logger.logger.info("Edit file: " + configPath);
556
+ logger.logger.info("Adding Salty-CSS plugin to Vite config...");
557
+ await promises.writeFile(configPath, content);
558
+ await formatWithPrettier(configPath);
559
+ return { changed: true };
560
+ }
561
+ };
492
562
  }
493
563
  };
494
564
  const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
495
- const detectAndApplyIntegrations = async (ctx) => {
496
- const results = [];
565
+ const planIntegrations = async (ctx) => {
566
+ const planned = [];
497
567
  for (const integration of buildIntegrationRegistry) {
498
568
  const configPath = await integration.detect(ctx);
499
569
  if (!configPath) continue;
500
- const result = await integration.apply(ctx, configPath);
501
- results.push({ name: integration.name, configPath, changed: result.changed });
570
+ const plan = await integration.plan(ctx, configPath);
571
+ if (!plan) continue;
572
+ planned.push({ name: integration.name, configPath, plan });
573
+ }
574
+ return planned;
575
+ };
576
+ const applyIntegrationPlans = async (planned) => {
577
+ const results = [];
578
+ for (const { name, configPath, plan } of planned) {
579
+ const result = await plan.execute();
580
+ results.push({ name, configPath, changed: result.changed });
502
581
  }
503
582
  return results;
504
583
  };
505
584
  const writeProjectFile = async (projectDir, fileName, content) => {
506
585
  const filePath = path.join(projectDir, fileName);
507
586
  if (fs.existsSync(filePath)) {
508
- compiler_saltyCompiler.logger.debug("File already exists: " + filePath);
587
+ logger.logger.debug("File already exists: " + filePath);
509
588
  return;
510
589
  }
511
590
  const additionalFolders = fileName.split("/").slice(0, -1).join("/");
512
591
  if (additionalFolders) await promises.mkdir(path.join(projectDir, additionalFolders), { recursive: true });
513
- compiler_saltyCompiler.logger.info("Creating file: " + filePath);
592
+ logger.logger.info("Creating file: " + filePath);
514
593
  await promises.writeFile(filePath, content);
515
594
  await formatWithPrettier(filePath);
516
595
  };
@@ -519,13 +598,13 @@ const ensureGitignoreSaltygen = async (rootDir) => {
519
598
  const existing = await promises.readFile(path$1, "utf-8").catch(() => void 0);
520
599
  if (existing === void 0) return;
521
600
  if (existing.includes("saltygen")) return;
522
- compiler_saltyCompiler.logger.info("Edit file: " + path$1);
601
+ logger.logger.info("Edit file: " + path$1);
523
602
  await promises.writeFile(path$1, existing + "\n\n# Salty-CSS\nsaltygen\n");
524
603
  };
525
604
  const importSaltygenIntoCss = async (projectDir, explicitCssFile) => {
526
605
  const target = explicitCssFile ?? await findGlobalCssFile(projectDir);
527
606
  if (!target) {
528
- compiler_saltyCompiler.logger.warn("Could not find a CSS file to import the generated CSS. Please add it manually.");
607
+ logger.logger.warn("Could not find a CSS file to import the generated CSS. Please add it manually.");
529
608
  return;
530
609
  }
531
610
  const cssFilePath = path.join(projectDir, target);
@@ -534,14 +613,14 @@ const importSaltygenIntoCss = async (projectDir, explicitCssFile) => {
534
613
  if (cssFileContent.includes("saltygen")) return;
535
614
  const cssFileFolder = path.join(cssFilePath, "..");
536
615
  const relPath = path.relative(cssFileFolder, path.join(projectDir, "saltygen/index.css"));
537
- compiler_saltyCompiler.logger.info("Adding global import statement to CSS file: " + cssFilePath);
616
+ logger.logger.info("Adding global import statement to CSS file: " + cssFilePath);
538
617
  await promises.writeFile(cssFilePath, `@import '${relPath}';
539
618
  ` + cssFileContent);
540
619
  await formatWithPrettier(cssFilePath);
541
620
  };
542
621
  const wirePrepareScript = async () => {
543
622
  const pkg = await readPackageJson().catch(() => {
544
- compiler_saltyCompiler.logError("Could not read package.json file.");
623
+ logger.logError("Could not read package.json file.");
545
624
  return void 0;
546
625
  });
547
626
  if (!pkg) return;
@@ -549,17 +628,24 @@ const wirePrepareScript = async () => {
549
628
  await updatePackageJson(next);
550
629
  };
551
630
  const registerInitCommand = (program) => {
552
- program.command("init [directory]").description("Initialize a new Salty-CSS project.").option("-d, --dir <dir>", "Project directory to initialize the project in.").option("--css-file <css-file>", "Existing CSS file where to import the generated CSS. Path must be relative to the given project directory.").option("--skip-install", "Skip installing dependencies.").action(async function(_dir = ".") {
631
+ program.command("init [directory]").description("Initialize a new Salty-CSS project.").option("-d, --dir <dir>", "Project directory to initialize the project in.").option("--css-file <css-file>", "Existing CSS file where to import the generated CSS. Path must be relative to the given project directory.").option("--skip-install", "Skip installing dependencies.").option("-y, --yes", "Skip the install confirmation prompt.").action(async function(_dir = ".") {
553
632
  try {
554
633
  const opts = this.opts();
555
634
  const dir = opts.dir ?? _dir;
556
- if (!dir) return compiler_saltyCompiler.logError("Project directory must be provided. Add it as the first argument after init command or use the --dir option.");
557
- const ctx = await buildContext({ dir, skipInstall: opts.skipInstall });
558
- compiler_saltyCompiler.logger.info("Initializing a new Salty-CSS project!");
635
+ if (!dir) return logger.logError("Project directory must be provided. Add it as the first argument after init command or use the --dir option.");
636
+ const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
637
+ logger.logger.info("Initializing a new Salty-CSS project!");
559
638
  const framework = await detectFramework(ctx);
560
- compiler_saltyCompiler.logger.info(`Detected framework: ${framework.name}`);
639
+ logger.logger.info(`Detected framework: ${framework.name}`);
640
+ const plannedIntegrations = await planIntegrations(ctx);
561
641
  if (!ctx.skipInstall) {
562
- await npmInstall(corePackages.core(ctx.cliVersion), framework.runtimePackage(ctx.cliVersion));
642
+ const packages = [
643
+ corePackages.core(ctx.cliVersion),
644
+ framework.runtimePackage(ctx.cliVersion),
645
+ ...plannedIntegrations.flatMap((p) => p.plan.packages)
646
+ ];
647
+ await confirmInstall(packages, ctx.yes);
648
+ await npmInstall(...packages);
563
649
  }
564
650
  const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
565
651
  await promises.mkdir(ctx.projectDir, { recursive: true });
@@ -567,54 +653,59 @@ const registerInitCommand = (program) => {
567
653
  await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
568
654
  await ensureGitignoreSaltygen(ctx.cwd);
569
655
  await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
570
- await detectAndApplyIntegrations(ctx);
656
+ await applyIntegrationPlans(plannedIntegrations);
571
657
  await wirePrepareScript();
572
- compiler_saltyCompiler.logger.info("Running the build to generate initial CSS...");
658
+ logger.logger.info("Running the build to generate initial CSS...");
573
659
  const compiler = new compiler_saltyCompiler.SaltyCompiler(ctx.projectDir);
574
660
  await compiler.generateCss();
575
- compiler_saltyCompiler.logger.info("🎉 Salty CSS project initialized successfully!");
576
- compiler_saltyCompiler.logger.info("Next steps:");
577
- compiler_saltyCompiler.logger.info("1. Configure variables and templates in `salty.config.ts`");
578
- compiler_saltyCompiler.logger.info("2. Create a new component with `npx salty-css generate [component-name]`");
579
- compiler_saltyCompiler.logger.info("3. Run `npx salty-css build` to generate the CSS");
580
- compiler_saltyCompiler.logger.info("4. Read about the features in the documentation: https://salty-css.dev");
581
- compiler_saltyCompiler.logger.info("5. Star the project on GitHub: https://github.com/margarita-form/salty-css ⭐");
661
+ logger.logger.info("🎉 Salty CSS project initialized successfully!");
662
+ logger.logger.info("Next steps:");
663
+ logger.logger.info("1. Configure variables and templates in `salty.config.ts`");
664
+ logger.logger.info("2. Create a new component with `npx salty-css generate [component-name]`");
665
+ logger.logger.info("3. Run `npx salty-css build` to generate the CSS");
666
+ logger.logger.info("4. Read about the features in the documentation: https://salty-css.dev");
667
+ logger.logger.info("5. Star the project on GitHub: https://github.com/margarita-form/salty-css ⭐");
582
668
  } catch (err) {
583
- return compiler_saltyCompiler.logError(err instanceof Error ? err.message : String(err));
669
+ return logger.logError(err instanceof Error ? err.message : String(err));
584
670
  }
585
671
  });
586
672
  };
587
673
  const getSaltyCssPackages = async () => {
588
674
  const packageJSONPath = path.join(process.cwd(), "package.json");
589
- const packageJson = await readPackageJson(packageJSONPath).catch((err) => compiler_saltyCompiler.logError(err));
590
- if (!packageJson) return compiler_saltyCompiler.logError("Could not read package.json file.");
675
+ const packageJson = await readPackageJson(packageJSONPath).catch((err) => logger.logError(err));
676
+ if (!packageJson) return logger.logError("Could not read package.json file.");
591
677
  const allDependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
592
678
  const saltyCssPackages = Object.entries(allDependencies).filter(([name]) => name === "salty-css" || name.startsWith("@salty-css/"));
593
679
  if (!saltyCssPackages.length) {
594
- return compiler_saltyCompiler.logError(
680
+ return logger.logError(
595
681
  "No Salty-CSS packages found in package.json. Make sure you are running update command in the same directory! Used package.json path: " + packageJSONPath
596
682
  );
597
683
  }
598
684
  return saltyCssPackages;
599
685
  };
600
686
  const registerUpdateCommand = (program) => {
601
- program.command("update [version]").alias("up").description("Update Salty-CSS packages to the latest or specified version.").option("-v, --version <version>", "Version to update to.").option("--legacy-peer-deps <legacyPeerDeps>", "Use legacy peer dependencies (not recommended).", false).action(async function(_version = "latest") {
602
- const { legacyPeerDeps, version = _version } = this.opts();
687
+ program.command("update [version]").alias("up").description("Update Salty-CSS packages to the latest or specified version.").option("-v, --version <version>", "Version to update to.").option("--legacy-peer-deps <legacyPeerDeps>", "Use legacy peer dependencies (not recommended).", false).option("-y, --yes", "Skip confirmation prompts (install and rebuild).").option("-d, --dir <dir>", "Project directory to rebuild after updating.").action(async function(_version = "latest") {
688
+ const { legacyPeerDeps, version = _version, yes = false, dir } = this.opts();
603
689
  const saltyCssPackages = await getSaltyCssPackages();
604
- if (!saltyCssPackages) return compiler_saltyCompiler.logError("Could not update Salty-CSS packages as any were found in package.json.");
690
+ if (!saltyCssPackages) return logger.logError("Could not update Salty-CSS packages as any were found in package.json.");
605
691
  const cli = await readThisPackageJson();
606
692
  const packagesToUpdate = saltyCssPackages.map(([name]) => {
607
693
  if (version === "@") return `${name}@${cli.version}`;
608
694
  return `${name}@${version.replace(/^@/, "")}`;
609
695
  });
696
+ try {
697
+ await confirmInstall(packagesToUpdate, yes);
698
+ } catch (err) {
699
+ return logger.logError(err instanceof Error ? err.message : String(err));
700
+ }
610
701
  if (legacyPeerDeps) {
611
- compiler_saltyCompiler.logger.warn("Using legacy peer dependencies to update packages.");
702
+ logger.logger.warn("Using legacy peer dependencies to update packages.");
612
703
  await npmInstall(...packagesToUpdate, "--legacy-peer-deps");
613
704
  } else {
614
705
  await npmInstall(...packagesToUpdate);
615
706
  }
616
707
  const updatedPackages = await getSaltyCssPackages();
617
- if (!updatedPackages) return compiler_saltyCompiler.logError("Something went wrong while reading the updated packages.");
708
+ if (!updatedPackages) return logger.logError("Something went wrong while reading the updated packages.");
618
709
  const mappedByVersions = updatedPackages.reduce((acc, [name, version2]) => {
619
710
  if (!acc[version2]) acc[version2] = [];
620
711
  acc[version2].push(name);
@@ -623,30 +714,41 @@ const registerUpdateCommand = (program) => {
623
714
  const versionsCount = Object.keys(mappedByVersions).length;
624
715
  if (versionsCount === 1) {
625
716
  const v = Object.keys(mappedByVersions)[0];
626
- compiler_saltyCompiler.logger.info(`Updated to all Salty CSS packages successfully to ${v.replace(/^\^/, "")}`);
717
+ logger.logger.info(`Updated to all Salty CSS packages successfully to ${v.replace(/^\^/, "")}`);
627
718
  } else {
628
719
  for (const [v, names] of Object.entries(mappedByVersions)) {
629
- compiler_saltyCompiler.logger.info(`Updated to ${v.replace(/^\^/, "")}: ${names.join(", ")}`);
720
+ logger.logger.info(`Updated to ${v.replace(/^\^/, "")}: ${names.join(", ")}`);
630
721
  }
631
722
  }
723
+ const project = dir ?? await getDefaultProject();
724
+ if (!project) {
725
+ logger.logger.warn("Skipping rebuild: no project directory configured. Run `salty-css build [dir]` manually.");
726
+ return;
727
+ }
728
+ const shouldRebuild = await confirmYesNo("Rebuild Salty CSS now?", { yes });
729
+ if (!shouldRebuild) return;
730
+ const projectDir = resolveProjectDir(project);
731
+ logger.logger.info("Rebuilding Salty-CSS project...");
732
+ await new compiler_saltyCompiler.SaltyCompiler(projectDir).generateCss();
733
+ logger.logger.info("Rebuild complete.");
632
734
  });
633
735
  };
634
736
  const registerVersionOption = (program) => {
635
737
  program.option("-v, --version", "Show the current version of Salty-CSS.").action(async function() {
636
738
  const cli = await readThisPackageJson();
637
- compiler_saltyCompiler.logger.info("CLI is running: " + cli.version);
739
+ logger.logger.info("CLI is running: " + cli.version);
638
740
  const packageJSONPath = path.join(process.cwd(), "package.json");
639
- const packageJson = await readPackageJson(packageJSONPath).catch((err) => compiler_saltyCompiler.logError(err));
741
+ const packageJson = await readPackageJson(packageJSONPath).catch((err) => logger.logError(err));
640
742
  if (!packageJson) return;
641
743
  const allDependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
642
744
  const saltyCssPackages = Object.keys(allDependencies).filter((dep) => dep === "salty-css" || dep.startsWith("@salty-css/"));
643
745
  if (!saltyCssPackages.length) {
644
- return compiler_saltyCompiler.logError(
746
+ return logger.logError(
645
747
  "No Salty-CSS packages found in package.json. Make sure you are running update command in the same directory! Used package.json path: " + packageJSONPath
646
748
  );
647
749
  }
648
750
  for (const dep of saltyCssPackages) {
649
- compiler_saltyCompiler.logger.info(`${dep}: ${allDependencies[dep]}`);
751
+ logger.logger.info(`${dep}: ${allDependencies[dep]}`);
650
752
  }
651
753
  });
652
754
  };