@salty-css/core 0.1.0-alpha.15 → 0.1.0-alpha.16

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.
@@ -0,0 +1,23 @@
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>;
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
@@ -11,6 +11,7 @@ const child_process = require("child_process");
11
11
  const ora = require("ora");
12
12
  const pascalCase = require("../pascal-case-By_l58S-.cjs");
13
13
  const ejs = require("ejs");
14
+ const promises$1 = require("readline/promises");
14
15
  var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
15
16
  const defaultPackageJsonPath = path.join(process.cwd(), "package.json");
16
17
  const readPackageJson = async (filePath = defaultPackageJsonPath) => {
@@ -141,7 +142,8 @@ const buildContext = async (opts) => {
141
142
  packageJson,
142
143
  rcFile,
143
144
  cliVersion: cliPackageJson.version || "0.0.0",
144
- skipInstall: !!opts.skipInstall
145
+ skipInstall: !!opts.skipInstall,
146
+ yes: !!opts.yes
145
147
  };
146
148
  };
147
149
  const registerBuildCommand = (program, defaultProject) => {
@@ -296,6 +298,36 @@ const registerGenerateCommand = (program, defaultProject) => {
296
298
  await formatWithPrettier(formattedStyledFilePath);
297
299
  });
298
300
  };
301
+ const formatPackageForDisplay = (spec) => {
302
+ const trimmed = spec.trim();
303
+ if (trimmed.startsWith("-D ")) return `${trimmed.slice(3).trim()} (dev)`;
304
+ return trimmed;
305
+ };
306
+ const renderPackageList = (packages) => {
307
+ return packages.map((p) => ` + ${formatPackageForDisplay(p)}`).join("\n");
308
+ };
309
+ const confirmInstall = async (packages, yes, options = {}) => {
310
+ if (yes) return;
311
+ if (packages.length === 0) return;
312
+ const input = options.input ?? process.stdin;
313
+ const output = options.output ?? process.stdout;
314
+ const isTTY = options.isTTY ?? (process.stdin.isTTY ?? false);
315
+ if (!isTTY) {
316
+ throw new Error("Cannot prompt for install confirmation: stdin is not a TTY. Re-run with --yes to install the listed packages without prompting.");
317
+ }
318
+ output.write(`The following packages will be installed:
319
+ ${renderPackageList(packages)}
320
+ `);
321
+ const rl = promises$1.createInterface({ input, output, terminal: false });
322
+ try {
323
+ const answer = (await rl.question("Proceed? (y/N) ")).trim().toLowerCase();
324
+ if (answer !== "y" && answer !== "yes") {
325
+ throw new Error("Install cancelled by user.");
326
+ }
327
+ } finally {
328
+ rl.close();
329
+ }
330
+ };
299
331
  const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
300
332
  const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
301
333
  const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
@@ -342,17 +374,22 @@ const editAstroConfig = (existing) => {
342
374
  const astroIntegration = {
343
375
  name: "astro",
344
376
  detect: (ctx) => findAstroConfig(ctx.projectDir),
345
- apply: async (ctx, configPath) => {
377
+ plan: async (ctx, configPath) => {
346
378
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
347
- if (existing === void 0) return { changed: false };
379
+ if (existing === void 0) return null;
348
380
  const result = editAstroConfig(existing);
349
381
  if (result.warning) compiler_saltyCompiler.logger.warn(result.warning);
350
- if (result.content === null) return { changed: false };
351
- if (!ctx.skipInstall) await npmInstall(`-D ${astroPackage(ctx.cliVersion)}`);
352
- compiler_saltyCompiler.logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
353
- await promises.writeFile(configPath, result.content);
354
- await formatWithPrettier(configPath);
355
- return { changed: true };
382
+ if (result.content === null) return null;
383
+ const newContent = result.content;
384
+ return {
385
+ packages: [`-D ${astroPackage(ctx.cliVersion)}`],
386
+ execute: async () => {
387
+ compiler_saltyCompiler.logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
388
+ await promises.writeFile(configPath, newContent);
389
+ await formatWithPrettier(configPath);
390
+ return { changed: true };
391
+ }
392
+ };
356
393
  }
357
394
  };
358
395
  const ESLINT_CONFIG_CANDIDATES = [
@@ -411,20 +448,25 @@ const eslintIntegration = {
411
448
  const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
412
449
  return candidates.find((p) => fs.existsSync(p)) ?? null;
413
450
  },
414
- apply: async (ctx, configPath) => {
451
+ plan: async (ctx, configPath) => {
415
452
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
416
453
  if (existing === void 0) {
417
454
  compiler_saltyCompiler.logger.error("Could not read ESLint config file.");
418
- return { changed: false };
455
+ return null;
419
456
  }
420
- if (!ctx.skipInstall) await npmInstall(corePackages.eslintConfigCore(ctx.cliVersion));
421
457
  const result = editEslintConfig(existing, configPath.endsWith("js"));
422
458
  if (result.warning) compiler_saltyCompiler.logger.warn(result.warning);
423
- if (result.content === null) return { changed: false };
424
- compiler_saltyCompiler.logger.info("Edit file: " + configPath);
425
- await promises.writeFile(configPath, result.content);
426
- await formatWithPrettier(configPath);
427
- return { changed: true };
459
+ if (result.content === null) return null;
460
+ const newContent = result.content;
461
+ return {
462
+ packages: [corePackages.eslintConfigCore(ctx.cliVersion)],
463
+ execute: async () => {
464
+ compiler_saltyCompiler.logger.info("Edit file: " + configPath);
465
+ await promises.writeFile(configPath, newContent);
466
+ await formatWithPrettier(configPath);
467
+ return { changed: true };
468
+ }
469
+ };
428
470
  }
429
471
  };
430
472
  const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
@@ -454,16 +496,20 @@ const nextIntegration = {
454
496
  const found = nextConfigFiles.map((file) => path.join(ctx.projectDir, file)).find((p) => fs.existsSync(p));
455
497
  return found ?? null;
456
498
  },
457
- apply: async (ctx, configPath) => {
499
+ plan: async (ctx, configPath) => {
458
500
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
459
- if (existing === void 0) return { changed: false };
501
+ if (existing === void 0) return null;
460
502
  const { content } = editNextConfig(existing);
461
- if (content === null) return { changed: false };
462
- if (!ctx.skipInstall) await npmInstall(`-D ${nextPackage(ctx.cliVersion)}`);
463
- compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Next.js config...");
464
- await promises.writeFile(configPath, content);
465
- await formatWithPrettier(configPath);
466
- return { changed: true };
503
+ if (content === null) return null;
504
+ return {
505
+ packages: [`-D ${nextPackage(ctx.cliVersion)}`],
506
+ execute: async () => {
507
+ compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Next.js config...");
508
+ await promises.writeFile(configPath, content);
509
+ await formatWithPrettier(configPath);
510
+ return { changed: true };
511
+ }
512
+ };
467
513
  }
468
514
  };
469
515
  const vitePackage = (version) => `@salty-css/vite@${version}`;
@@ -481,27 +527,40 @@ const viteIntegration = {
481
527
  const path$1 = path.join(ctx.projectDir, "vite.config.ts");
482
528
  return fs.existsSync(path$1) ? path$1 : null;
483
529
  },
484
- apply: async (ctx, configPath) => {
530
+ plan: async (ctx, configPath) => {
485
531
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
486
- if (existing === void 0) return { changed: false };
532
+ if (existing === void 0) return null;
487
533
  const { content } = editViteConfig(existing);
488
- if (content === null) return { changed: false };
489
- compiler_saltyCompiler.logger.info("Edit file: " + configPath);
490
- if (!ctx.skipInstall) await npmInstall(`-D ${vitePackage(ctx.cliVersion)}`);
491
- compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Vite config...");
492
- await promises.writeFile(configPath, content);
493
- await formatWithPrettier(configPath);
494
- return { changed: true };
534
+ if (content === null) return null;
535
+ return {
536
+ packages: [`-D ${vitePackage(ctx.cliVersion)}`],
537
+ execute: async () => {
538
+ compiler_saltyCompiler.logger.info("Edit file: " + configPath);
539
+ compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Vite config...");
540
+ await promises.writeFile(configPath, content);
541
+ await formatWithPrettier(configPath);
542
+ return { changed: true };
543
+ }
544
+ };
495
545
  }
496
546
  };
497
547
  const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
498
- const detectAndApplyIntegrations = async (ctx) => {
499
- const results = [];
548
+ const planIntegrations = async (ctx) => {
549
+ const planned = [];
500
550
  for (const integration of buildIntegrationRegistry) {
501
551
  const configPath = await integration.detect(ctx);
502
552
  if (!configPath) continue;
503
- const result = await integration.apply(ctx, configPath);
504
- results.push({ name: integration.name, configPath, changed: result.changed });
553
+ const plan = await integration.plan(ctx, configPath);
554
+ if (!plan) continue;
555
+ planned.push({ name: integration.name, configPath, plan });
556
+ }
557
+ return planned;
558
+ };
559
+ const applyIntegrationPlans = async (planned) => {
560
+ const results = [];
561
+ for (const { name, configPath, plan } of planned) {
562
+ const result = await plan.execute();
563
+ results.push({ name, configPath, changed: result.changed });
505
564
  }
506
565
  return results;
507
566
  };
@@ -552,17 +611,24 @@ const wirePrepareScript = async () => {
552
611
  await updatePackageJson(next);
553
612
  };
554
613
  const registerInitCommand = (program) => {
555
- 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 = ".") {
614
+ 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 = ".") {
556
615
  try {
557
616
  const opts = this.opts();
558
617
  const dir = opts.dir ?? _dir;
559
618
  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.");
560
- const ctx = await buildContext({ dir, skipInstall: opts.skipInstall });
619
+ const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
561
620
  compiler_saltyCompiler.logger.info("Initializing a new Salty-CSS project!");
562
621
  const framework = await detectFramework(ctx);
563
622
  compiler_saltyCompiler.logger.info(`Detected framework: ${framework.name}`);
623
+ const plannedIntegrations = await planIntegrations(ctx);
564
624
  if (!ctx.skipInstall) {
565
- await npmInstall(corePackages.core(ctx.cliVersion), framework.runtimePackage(ctx.cliVersion));
625
+ const packages = [
626
+ corePackages.core(ctx.cliVersion),
627
+ framework.runtimePackage(ctx.cliVersion),
628
+ ...plannedIntegrations.flatMap((p) => p.plan.packages)
629
+ ];
630
+ await confirmInstall(packages, ctx.yes);
631
+ await npmInstall(...packages);
566
632
  }
567
633
  const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
568
634
  await promises.mkdir(ctx.projectDir, { recursive: true });
@@ -570,7 +636,7 @@ const registerInitCommand = (program) => {
570
636
  await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
571
637
  await ensureGitignoreSaltygen(ctx.cwd);
572
638
  await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
573
- await detectAndApplyIntegrations(ctx);
639
+ await applyIntegrationPlans(plannedIntegrations);
574
640
  await wirePrepareScript();
575
641
  compiler_saltyCompiler.logger.info("Running the build to generate initial CSS...");
576
642
  const compiler = new compiler_saltyCompiler.SaltyCompiler(ctx.projectDir);
@@ -601,8 +667,8 @@ const getSaltyCssPackages = async () => {
601
667
  return saltyCssPackages;
602
668
  };
603
669
  const registerUpdateCommand = (program) => {
604
- 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") {
605
- const { legacyPeerDeps, version = _version } = this.opts();
670
+ 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 the install confirmation prompt.").action(async function(_version = "latest") {
671
+ const { legacyPeerDeps, version = _version, yes = false } = this.opts();
606
672
  const saltyCssPackages = await getSaltyCssPackages();
607
673
  if (!saltyCssPackages) return compiler_saltyCompiler.logError("Could not update Salty-CSS packages as any were found in package.json.");
608
674
  const cli = await readThisPackageJson();
@@ -610,6 +676,11 @@ const registerUpdateCommand = (program) => {
610
676
  if (version === "@") return `${name}@${cli.version}`;
611
677
  return `${name}@${version.replace(/^@/, "")}`;
612
678
  });
679
+ try {
680
+ await confirmInstall(packagesToUpdate, yes);
681
+ } catch (err) {
682
+ return compiler_saltyCompiler.logError(err instanceof Error ? err.message : String(err));
683
+ }
613
684
  if (legacyPeerDeps) {
614
685
  compiler_saltyCompiler.logger.warn("Using legacy peer dependencies to update packages.");
615
686
  await npmInstall(...packagesToUpdate, "--legacy-peer-deps");
package/bin/main.js CHANGED
@@ -9,6 +9,7 @@ import { exec } from "child_process";
9
9
  import ora from "ora";
10
10
  import { p as pascalCase } from "../pascal-case-F3Usi5Wf.js";
11
11
  import ejs from "ejs";
12
+ import { createInterface } from "readline/promises";
12
13
  const defaultPackageJsonPath = join(process.cwd(), "package.json");
13
14
  const readPackageJson = async (filePath = defaultPackageJsonPath) => {
14
15
  const content = await readFile(filePath, "utf-8").then(JSON.parse).catch(() => void 0);
@@ -138,7 +139,8 @@ const buildContext = async (opts) => {
138
139
  packageJson,
139
140
  rcFile,
140
141
  cliVersion: cliPackageJson.version || "0.0.0",
141
- skipInstall: !!opts.skipInstall
142
+ skipInstall: !!opts.skipInstall,
143
+ yes: !!opts.yes
142
144
  };
143
145
  };
144
146
  const registerBuildCommand = (program, defaultProject) => {
@@ -293,6 +295,36 @@ const registerGenerateCommand = (program, defaultProject) => {
293
295
  await formatWithPrettier(formattedStyledFilePath);
294
296
  });
295
297
  };
298
+ const formatPackageForDisplay = (spec) => {
299
+ const trimmed = spec.trim();
300
+ if (trimmed.startsWith("-D ")) return `${trimmed.slice(3).trim()} (dev)`;
301
+ return trimmed;
302
+ };
303
+ const renderPackageList = (packages) => {
304
+ return packages.map((p) => ` + ${formatPackageForDisplay(p)}`).join("\n");
305
+ };
306
+ const confirmInstall = async (packages, yes, options = {}) => {
307
+ if (yes) return;
308
+ if (packages.length === 0) return;
309
+ const input = options.input ?? process.stdin;
310
+ const output = options.output ?? process.stdout;
311
+ const isTTY = options.isTTY ?? (process.stdin.isTTY ?? false);
312
+ if (!isTTY) {
313
+ throw new Error("Cannot prompt for install confirmation: stdin is not a TTY. Re-run with --yes to install the listed packages without prompting.");
314
+ }
315
+ output.write(`The following packages will be installed:
316
+ ${renderPackageList(packages)}
317
+ `);
318
+ const rl = createInterface({ input, output, terminal: false });
319
+ try {
320
+ const answer = (await rl.question("Proceed? (y/N) ")).trim().toLowerCase();
321
+ if (answer !== "y" && answer !== "yes") {
322
+ throw new Error("Install cancelled by user.");
323
+ }
324
+ } finally {
325
+ rl.close();
326
+ }
327
+ };
296
328
  const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
297
329
  const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
298
330
  const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
@@ -339,17 +371,22 @@ const editAstroConfig = (existing) => {
339
371
  const astroIntegration = {
340
372
  name: "astro",
341
373
  detect: (ctx) => findAstroConfig(ctx.projectDir),
342
- apply: async (ctx, configPath) => {
374
+ plan: async (ctx, configPath) => {
343
375
  const existing = await readFile(configPath, "utf-8").catch(() => void 0);
344
- if (existing === void 0) return { changed: false };
376
+ if (existing === void 0) return null;
345
377
  const result = editAstroConfig(existing);
346
378
  if (result.warning) logger.warn(result.warning);
347
- if (result.content === null) return { changed: false };
348
- if (!ctx.skipInstall) await npmInstall(`-D ${astroPackage(ctx.cliVersion)}`);
349
- logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
350
- await writeFile(configPath, result.content);
351
- await formatWithPrettier(configPath);
352
- return { changed: true };
379
+ if (result.content === null) return null;
380
+ const newContent = result.content;
381
+ return {
382
+ packages: [`-D ${astroPackage(ctx.cliVersion)}`],
383
+ execute: async () => {
384
+ logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
385
+ await writeFile(configPath, newContent);
386
+ await formatWithPrettier(configPath);
387
+ return { changed: true };
388
+ }
389
+ };
353
390
  }
354
391
  };
355
392
  const ESLINT_CONFIG_CANDIDATES = [
@@ -408,20 +445,25 @@ const eslintIntegration = {
408
445
  const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
409
446
  return candidates.find((p) => existsSync(p)) ?? null;
410
447
  },
411
- apply: async (ctx, configPath) => {
448
+ plan: async (ctx, configPath) => {
412
449
  const existing = await readFile(configPath, "utf-8").catch(() => void 0);
413
450
  if (existing === void 0) {
414
451
  logger.error("Could not read ESLint config file.");
415
- return { changed: false };
452
+ return null;
416
453
  }
417
- if (!ctx.skipInstall) await npmInstall(corePackages.eslintConfigCore(ctx.cliVersion));
418
454
  const result = editEslintConfig(existing, configPath.endsWith("js"));
419
455
  if (result.warning) logger.warn(result.warning);
420
- if (result.content === null) return { changed: false };
421
- logger.info("Edit file: " + configPath);
422
- await writeFile(configPath, result.content);
423
- await formatWithPrettier(configPath);
424
- return { changed: true };
456
+ if (result.content === null) return null;
457
+ const newContent = result.content;
458
+ return {
459
+ packages: [corePackages.eslintConfigCore(ctx.cliVersion)],
460
+ execute: async () => {
461
+ logger.info("Edit file: " + configPath);
462
+ await writeFile(configPath, newContent);
463
+ await formatWithPrettier(configPath);
464
+ return { changed: true };
465
+ }
466
+ };
425
467
  }
426
468
  };
427
469
  const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
@@ -451,16 +493,20 @@ const nextIntegration = {
451
493
  const found = nextConfigFiles.map((file) => join(ctx.projectDir, file)).find((p) => existsSync(p));
452
494
  return found ?? null;
453
495
  },
454
- apply: async (ctx, configPath) => {
496
+ plan: async (ctx, configPath) => {
455
497
  const existing = await readFile(configPath, "utf-8").catch(() => void 0);
456
- if (existing === void 0) return { changed: false };
498
+ if (existing === void 0) return null;
457
499
  const { content } = editNextConfig(existing);
458
- if (content === null) return { changed: false };
459
- if (!ctx.skipInstall) await npmInstall(`-D ${nextPackage(ctx.cliVersion)}`);
460
- logger.info("Adding Salty-CSS plugin to Next.js config...");
461
- await writeFile(configPath, content);
462
- await formatWithPrettier(configPath);
463
- return { changed: true };
500
+ if (content === null) return null;
501
+ return {
502
+ packages: [`-D ${nextPackage(ctx.cliVersion)}`],
503
+ execute: async () => {
504
+ logger.info("Adding Salty-CSS plugin to Next.js config...");
505
+ await writeFile(configPath, content);
506
+ await formatWithPrettier(configPath);
507
+ return { changed: true };
508
+ }
509
+ };
464
510
  }
465
511
  };
466
512
  const vitePackage = (version) => `@salty-css/vite@${version}`;
@@ -478,27 +524,40 @@ const viteIntegration = {
478
524
  const path = join(ctx.projectDir, "vite.config.ts");
479
525
  return existsSync(path) ? path : null;
480
526
  },
481
- apply: async (ctx, configPath) => {
527
+ plan: async (ctx, configPath) => {
482
528
  const existing = await readFile(configPath, "utf-8").catch(() => void 0);
483
- if (existing === void 0) return { changed: false };
529
+ if (existing === void 0) return null;
484
530
  const { content } = editViteConfig(existing);
485
- if (content === null) return { changed: false };
486
- logger.info("Edit file: " + configPath);
487
- if (!ctx.skipInstall) await npmInstall(`-D ${vitePackage(ctx.cliVersion)}`);
488
- logger.info("Adding Salty-CSS plugin to Vite config...");
489
- await writeFile(configPath, content);
490
- await formatWithPrettier(configPath);
491
- return { changed: true };
531
+ if (content === null) return null;
532
+ return {
533
+ packages: [`-D ${vitePackage(ctx.cliVersion)}`],
534
+ execute: async () => {
535
+ logger.info("Edit file: " + configPath);
536
+ logger.info("Adding Salty-CSS plugin to Vite config...");
537
+ await writeFile(configPath, content);
538
+ await formatWithPrettier(configPath);
539
+ return { changed: true };
540
+ }
541
+ };
492
542
  }
493
543
  };
494
544
  const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
495
- const detectAndApplyIntegrations = async (ctx) => {
496
- const results = [];
545
+ const planIntegrations = async (ctx) => {
546
+ const planned = [];
497
547
  for (const integration of buildIntegrationRegistry) {
498
548
  const configPath = await integration.detect(ctx);
499
549
  if (!configPath) continue;
500
- const result = await integration.apply(ctx, configPath);
501
- results.push({ name: integration.name, configPath, changed: result.changed });
550
+ const plan = await integration.plan(ctx, configPath);
551
+ if (!plan) continue;
552
+ planned.push({ name: integration.name, configPath, plan });
553
+ }
554
+ return planned;
555
+ };
556
+ const applyIntegrationPlans = async (planned) => {
557
+ const results = [];
558
+ for (const { name, configPath, plan } of planned) {
559
+ const result = await plan.execute();
560
+ results.push({ name, configPath, changed: result.changed });
502
561
  }
503
562
  return results;
504
563
  };
@@ -549,17 +608,24 @@ const wirePrepareScript = async () => {
549
608
  await updatePackageJson(next);
550
609
  };
551
610
  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 = ".") {
611
+ 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
612
  try {
554
613
  const opts = this.opts();
555
614
  const dir = opts.dir ?? _dir;
556
615
  if (!dir) return 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 });
616
+ const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
558
617
  logger.info("Initializing a new Salty-CSS project!");
559
618
  const framework = await detectFramework(ctx);
560
619
  logger.info(`Detected framework: ${framework.name}`);
620
+ const plannedIntegrations = await planIntegrations(ctx);
561
621
  if (!ctx.skipInstall) {
562
- await npmInstall(corePackages.core(ctx.cliVersion), framework.runtimePackage(ctx.cliVersion));
622
+ const packages = [
623
+ corePackages.core(ctx.cliVersion),
624
+ framework.runtimePackage(ctx.cliVersion),
625
+ ...plannedIntegrations.flatMap((p) => p.plan.packages)
626
+ ];
627
+ await confirmInstall(packages, ctx.yes);
628
+ await npmInstall(...packages);
563
629
  }
564
630
  const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
565
631
  await mkdir(ctx.projectDir, { recursive: true });
@@ -567,7 +633,7 @@ const registerInitCommand = (program) => {
567
633
  await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
568
634
  await ensureGitignoreSaltygen(ctx.cwd);
569
635
  await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
570
- await detectAndApplyIntegrations(ctx);
636
+ await applyIntegrationPlans(plannedIntegrations);
571
637
  await wirePrepareScript();
572
638
  logger.info("Running the build to generate initial CSS...");
573
639
  const compiler = new SaltyCompiler(ctx.projectDir);
@@ -598,8 +664,8 @@ const getSaltyCssPackages = async () => {
598
664
  return saltyCssPackages;
599
665
  };
600
666
  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();
667
+ 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 the install confirmation prompt.").action(async function(_version = "latest") {
668
+ const { legacyPeerDeps, version = _version, yes = false } = this.opts();
603
669
  const saltyCssPackages = await getSaltyCssPackages();
604
670
  if (!saltyCssPackages) return logError("Could not update Salty-CSS packages as any were found in package.json.");
605
671
  const cli = await readThisPackageJson();
@@ -607,6 +673,11 @@ const registerUpdateCommand = (program) => {
607
673
  if (version === "@") return `${name}@${cli.version}`;
608
674
  return `${name}@${version.replace(/^@/, "")}`;
609
675
  });
676
+ try {
677
+ await confirmInstall(packagesToUpdate, yes);
678
+ } catch (err) {
679
+ return logError(err instanceof Error ? err.message : String(err));
680
+ }
610
681
  if (legacyPeerDeps) {
611
682
  logger.warn("Using legacy peer dependencies to update packages.");
612
683
  await npmInstall(...packagesToUpdate, "--legacy-peer-deps");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salty-css/core",
3
- "version": "0.1.0-alpha.15",
3
+ "version": "0.1.0-alpha.16",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "typings": "./dist/index.d.ts",