@salty-css/core 0.1.0-alpha.14 → 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,17 +142,21 @@ 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) => {
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) {
150
+ 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) {
149
151
  compiler_saltyCompiler.logger.info("Building the Salty-CSS project...");
150
- const { dir = _dir, watch } = this.opts();
152
+ const { dir = _dir, watch, mode } = this.opts();
153
+ if (mode !== void 0 && mode !== "production" && mode !== "development") {
154
+ return compiler_saltyCompiler.logError(`Invalid --mode "${mode}". Expected "production" or "development".`);
155
+ }
151
156
  const resolved = dir ?? await getDefaultProject();
152
157
  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.");
153
158
  const projectDir = resolveProjectDir(resolved);
154
- const compiler = new compiler_saltyCompiler.SaltyCompiler(projectDir);
159
+ const compiler = new compiler_saltyCompiler.SaltyCompiler(projectDir, { mode });
155
160
  await compiler.generateCss();
156
161
  if (watch) {
157
162
  compiler_saltyCompiler.logger.info("Watching for changes in the project directory...");
@@ -293,6 +298,36 @@ const registerGenerateCommand = (program, defaultProject) => {
293
298
  await formatWithPrettier(formattedStyledFilePath);
294
299
  });
295
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
+ };
296
331
  const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
297
332
  const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
298
333
  const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
@@ -339,17 +374,22 @@ const editAstroConfig = (existing) => {
339
374
  const astroIntegration = {
340
375
  name: "astro",
341
376
  detect: (ctx) => findAstroConfig(ctx.projectDir),
342
- apply: async (ctx, configPath) => {
377
+ plan: async (ctx, configPath) => {
343
378
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
344
- if (existing === void 0) return { changed: false };
379
+ if (existing === void 0) return null;
345
380
  const result = editAstroConfig(existing);
346
381
  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 };
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
+ };
353
393
  }
354
394
  };
355
395
  const ESLINT_CONFIG_CANDIDATES = [
@@ -408,20 +448,25 @@ const eslintIntegration = {
408
448
  const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
409
449
  return candidates.find((p) => fs.existsSync(p)) ?? null;
410
450
  },
411
- apply: async (ctx, configPath) => {
451
+ plan: async (ctx, configPath) => {
412
452
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
413
453
  if (existing === void 0) {
414
454
  compiler_saltyCompiler.logger.error("Could not read ESLint config file.");
415
- return { changed: false };
455
+ return null;
416
456
  }
417
- if (!ctx.skipInstall) await npmInstall(corePackages.eslintConfigCore(ctx.cliVersion));
418
457
  const result = editEslintConfig(existing, configPath.endsWith("js"));
419
458
  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 };
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
+ };
425
470
  }
426
471
  };
427
472
  const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
@@ -451,16 +496,20 @@ const nextIntegration = {
451
496
  const found = nextConfigFiles.map((file) => path.join(ctx.projectDir, file)).find((p) => fs.existsSync(p));
452
497
  return found ?? null;
453
498
  },
454
- apply: async (ctx, configPath) => {
499
+ plan: async (ctx, configPath) => {
455
500
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
456
- if (existing === void 0) return { changed: false };
501
+ if (existing === void 0) return null;
457
502
  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 };
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
+ };
464
513
  }
465
514
  };
466
515
  const vitePackage = (version) => `@salty-css/vite@${version}`;
@@ -478,27 +527,40 @@ const viteIntegration = {
478
527
  const path$1 = path.join(ctx.projectDir, "vite.config.ts");
479
528
  return fs.existsSync(path$1) ? path$1 : null;
480
529
  },
481
- apply: async (ctx, configPath) => {
530
+ plan: async (ctx, configPath) => {
482
531
  const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
483
- if (existing === void 0) return { changed: false };
532
+ if (existing === void 0) return null;
484
533
  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 };
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
+ };
492
545
  }
493
546
  };
494
547
  const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
495
- const detectAndApplyIntegrations = async (ctx) => {
496
- const results = [];
548
+ const planIntegrations = async (ctx) => {
549
+ const planned = [];
497
550
  for (const integration of buildIntegrationRegistry) {
498
551
  const configPath = await integration.detect(ctx);
499
552
  if (!configPath) continue;
500
- const result = await integration.apply(ctx, configPath);
501
- 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 });
502
564
  }
503
565
  return results;
504
566
  };
@@ -549,17 +611,24 @@ const wirePrepareScript = async () => {
549
611
  await updatePackageJson(next);
550
612
  };
551
613
  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 = ".") {
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 = ".") {
553
615
  try {
554
616
  const opts = this.opts();
555
617
  const dir = opts.dir ?? _dir;
556
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.");
557
- const ctx = await buildContext({ dir, skipInstall: opts.skipInstall });
619
+ const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
558
620
  compiler_saltyCompiler.logger.info("Initializing a new Salty-CSS project!");
559
621
  const framework = await detectFramework(ctx);
560
622
  compiler_saltyCompiler.logger.info(`Detected framework: ${framework.name}`);
623
+ const plannedIntegrations = await planIntegrations(ctx);
561
624
  if (!ctx.skipInstall) {
562
- 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);
563
632
  }
564
633
  const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
565
634
  await promises.mkdir(ctx.projectDir, { recursive: true });
@@ -567,7 +636,7 @@ const registerInitCommand = (program) => {
567
636
  await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
568
637
  await ensureGitignoreSaltygen(ctx.cwd);
569
638
  await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
570
- await detectAndApplyIntegrations(ctx);
639
+ await applyIntegrationPlans(plannedIntegrations);
571
640
  await wirePrepareScript();
572
641
  compiler_saltyCompiler.logger.info("Running the build to generate initial CSS...");
573
642
  const compiler = new compiler_saltyCompiler.SaltyCompiler(ctx.projectDir);
@@ -598,8 +667,8 @@ const getSaltyCssPackages = async () => {
598
667
  return saltyCssPackages;
599
668
  };
600
669
  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();
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();
603
672
  const saltyCssPackages = await getSaltyCssPackages();
604
673
  if (!saltyCssPackages) return compiler_saltyCompiler.logError("Could not update Salty-CSS packages as any were found in package.json.");
605
674
  const cli = await readThisPackageJson();
@@ -607,6 +676,11 @@ const registerUpdateCommand = (program) => {
607
676
  if (version === "@") return `${name}@${cli.version}`;
608
677
  return `${name}@${version.replace(/^@/, "")}`;
609
678
  });
679
+ try {
680
+ await confirmInstall(packagesToUpdate, yes);
681
+ } catch (err) {
682
+ return compiler_saltyCompiler.logError(err instanceof Error ? err.message : String(err));
683
+ }
610
684
  if (legacyPeerDeps) {
611
685
  compiler_saltyCompiler.logger.warn("Using legacy peer dependencies to update packages.");
612
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,17 +139,21 @@ 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) => {
145
- 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) {
147
+ 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) {
146
148
  logger.info("Building the Salty-CSS project...");
147
- const { dir = _dir, watch: watch$1 } = this.opts();
149
+ const { dir = _dir, watch: watch$1, mode } = this.opts();
150
+ if (mode !== void 0 && mode !== "production" && mode !== "development") {
151
+ return logError(`Invalid --mode "${mode}". Expected "production" or "development".`);
152
+ }
148
153
  const resolved = dir ?? await getDefaultProject();
149
154
  if (!resolved) return logError("Project directory must be provided. Add it as the first argument after build command or use the --dir option.");
150
155
  const projectDir = resolveProjectDir(resolved);
151
- const compiler = new SaltyCompiler(projectDir);
156
+ const compiler = new SaltyCompiler(projectDir, { mode });
152
157
  await compiler.generateCss();
153
158
  if (watch$1) {
154
159
  logger.info("Watching for changes in the project directory...");
@@ -290,6 +295,36 @@ const registerGenerateCommand = (program, defaultProject) => {
290
295
  await formatWithPrettier(formattedStyledFilePath);
291
296
  });
292
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
+ };
293
328
  const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
294
329
  const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
295
330
  const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
@@ -336,17 +371,22 @@ const editAstroConfig = (existing) => {
336
371
  const astroIntegration = {
337
372
  name: "astro",
338
373
  detect: (ctx) => findAstroConfig(ctx.projectDir),
339
- apply: async (ctx, configPath) => {
374
+ plan: async (ctx, configPath) => {
340
375
  const existing = await readFile(configPath, "utf-8").catch(() => void 0);
341
- if (existing === void 0) return { changed: false };
376
+ if (existing === void 0) return null;
342
377
  const result = editAstroConfig(existing);
343
378
  if (result.warning) logger.warn(result.warning);
344
- if (result.content === null) return { changed: false };
345
- if (!ctx.skipInstall) await npmInstall(`-D ${astroPackage(ctx.cliVersion)}`);
346
- logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
347
- await writeFile(configPath, result.content);
348
- await formatWithPrettier(configPath);
349
- 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
+ };
350
390
  }
351
391
  };
352
392
  const ESLINT_CONFIG_CANDIDATES = [
@@ -405,20 +445,25 @@ const eslintIntegration = {
405
445
  const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
406
446
  return candidates.find((p) => existsSync(p)) ?? null;
407
447
  },
408
- apply: async (ctx, configPath) => {
448
+ plan: async (ctx, configPath) => {
409
449
  const existing = await readFile(configPath, "utf-8").catch(() => void 0);
410
450
  if (existing === void 0) {
411
451
  logger.error("Could not read ESLint config file.");
412
- return { changed: false };
452
+ return null;
413
453
  }
414
- if (!ctx.skipInstall) await npmInstall(corePackages.eslintConfigCore(ctx.cliVersion));
415
454
  const result = editEslintConfig(existing, configPath.endsWith("js"));
416
455
  if (result.warning) logger.warn(result.warning);
417
- if (result.content === null) return { changed: false };
418
- logger.info("Edit file: " + configPath);
419
- await writeFile(configPath, result.content);
420
- await formatWithPrettier(configPath);
421
- 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
+ };
422
467
  }
423
468
  };
424
469
  const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
@@ -448,16 +493,20 @@ const nextIntegration = {
448
493
  const found = nextConfigFiles.map((file) => join(ctx.projectDir, file)).find((p) => existsSync(p));
449
494
  return found ?? null;
450
495
  },
451
- apply: async (ctx, configPath) => {
496
+ plan: async (ctx, configPath) => {
452
497
  const existing = await readFile(configPath, "utf-8").catch(() => void 0);
453
- if (existing === void 0) return { changed: false };
498
+ if (existing === void 0) return null;
454
499
  const { content } = editNextConfig(existing);
455
- if (content === null) return { changed: false };
456
- if (!ctx.skipInstall) await npmInstall(`-D ${nextPackage(ctx.cliVersion)}`);
457
- logger.info("Adding Salty-CSS plugin to Next.js config...");
458
- await writeFile(configPath, content);
459
- await formatWithPrettier(configPath);
460
- 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
+ };
461
510
  }
462
511
  };
463
512
  const vitePackage = (version) => `@salty-css/vite@${version}`;
@@ -475,27 +524,40 @@ const viteIntegration = {
475
524
  const path = join(ctx.projectDir, "vite.config.ts");
476
525
  return existsSync(path) ? path : null;
477
526
  },
478
- apply: async (ctx, configPath) => {
527
+ plan: async (ctx, configPath) => {
479
528
  const existing = await readFile(configPath, "utf-8").catch(() => void 0);
480
- if (existing === void 0) return { changed: false };
529
+ if (existing === void 0) return null;
481
530
  const { content } = editViteConfig(existing);
482
- if (content === null) return { changed: false };
483
- logger.info("Edit file: " + configPath);
484
- if (!ctx.skipInstall) await npmInstall(`-D ${vitePackage(ctx.cliVersion)}`);
485
- logger.info("Adding Salty-CSS plugin to Vite config...");
486
- await writeFile(configPath, content);
487
- await formatWithPrettier(configPath);
488
- 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
+ };
489
542
  }
490
543
  };
491
544
  const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
492
- const detectAndApplyIntegrations = async (ctx) => {
493
- const results = [];
545
+ const planIntegrations = async (ctx) => {
546
+ const planned = [];
494
547
  for (const integration of buildIntegrationRegistry) {
495
548
  const configPath = await integration.detect(ctx);
496
549
  if (!configPath) continue;
497
- const result = await integration.apply(ctx, configPath);
498
- 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 });
499
561
  }
500
562
  return results;
501
563
  };
@@ -546,17 +608,24 @@ const wirePrepareScript = async () => {
546
608
  await updatePackageJson(next);
547
609
  };
548
610
  const registerInitCommand = (program) => {
549
- 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 = ".") {
550
612
  try {
551
613
  const opts = this.opts();
552
614
  const dir = opts.dir ?? _dir;
553
615
  if (!dir) return logError("Project directory must be provided. Add it as the first argument after init command or use the --dir option.");
554
- const ctx = await buildContext({ dir, skipInstall: opts.skipInstall });
616
+ const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
555
617
  logger.info("Initializing a new Salty-CSS project!");
556
618
  const framework = await detectFramework(ctx);
557
619
  logger.info(`Detected framework: ${framework.name}`);
620
+ const plannedIntegrations = await planIntegrations(ctx);
558
621
  if (!ctx.skipInstall) {
559
- 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);
560
629
  }
561
630
  const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
562
631
  await mkdir(ctx.projectDir, { recursive: true });
@@ -564,7 +633,7 @@ const registerInitCommand = (program) => {
564
633
  await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
565
634
  await ensureGitignoreSaltygen(ctx.cwd);
566
635
  await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
567
- await detectAndApplyIntegrations(ctx);
636
+ await applyIntegrationPlans(plannedIntegrations);
568
637
  await wirePrepareScript();
569
638
  logger.info("Running the build to generate initial CSS...");
570
639
  const compiler = new SaltyCompiler(ctx.projectDir);
@@ -595,8 +664,8 @@ const getSaltyCssPackages = async () => {
595
664
  return saltyCssPackages;
596
665
  };
597
666
  const registerUpdateCommand = (program) => {
598
- 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") {
599
- 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();
600
669
  const saltyCssPackages = await getSaltyCssPackages();
601
670
  if (!saltyCssPackages) return logError("Could not update Salty-CSS packages as any were found in package.json.");
602
671
  const cli = await readThisPackageJson();
@@ -604,6 +673,11 @@ const registerUpdateCommand = (program) => {
604
673
  if (version === "@") return `${name}@${cli.version}`;
605
674
  return `${name}@${version.replace(/^@/, "")}`;
606
675
  });
676
+ try {
677
+ await confirmInstall(packagesToUpdate, yes);
678
+ } catch (err) {
679
+ return logError(err instanceof Error ? err.message : String(err));
680
+ }
607
681
  if (legacyPeerDeps) {
608
682
  logger.warn("Using legacy peer dependencies to update packages.");
609
683
  await npmInstall(...packagesToUpdate, "--legacy-peer-deps");
@@ -111,7 +111,7 @@ const saltyReset = {
111
111
  }
112
112
  };
113
113
  class SaltyCompiler {
114
- constructor(projectRootDir) {
114
+ constructor(projectRootDir, options = {}) {
115
115
  __publicField(this, "importFile", (path2) => {
116
116
  const now = Date.now();
117
117
  return import(
@@ -691,11 +691,13 @@ ${newContent}
691
691
  });
692
692
  });
693
693
  this.projectRootDir = projectRootDir;
694
+ this.options = options;
694
695
  if (typeof process === "undefined") {
695
696
  throw new Error("SaltyServer can only be used in a Node.js environment.");
696
697
  }
697
698
  }
698
699
  get isProduction() {
700
+ if (this.options.mode) return this.options.mode === "production";
699
701
  try {
700
702
  return process.env["NODE_ENV"] !== "development";
701
703
  } catch {
@@ -1,9 +1,14 @@
1
1
  import { CachedConfig, SaltyConfig } from '../config';
2
+ export type SaltyCompilerMode = 'production' | 'development';
3
+ export interface SaltyCompilerOptions {
4
+ mode?: SaltyCompilerMode;
5
+ }
2
6
  export declare class SaltyCompiler {
3
7
  projectRootDir: string;
8
+ private options;
4
9
  importFile: (path: string) => Promise<any>;
5
10
  private cache;
6
- constructor(projectRootDir: string);
11
+ constructor(projectRootDir: string, options?: SaltyCompilerOptions);
7
12
  get isProduction(): boolean;
8
13
  /**
9
14
  * Locate and read the .saltyrc.json file starting from the current directory and moving up the directory tree.
@@ -91,7 +91,7 @@ const saltyReset = {
91
91
  }
92
92
  };
93
93
  class SaltyCompiler {
94
- constructor(projectRootDir) {
94
+ constructor(projectRootDir, options = {}) {
95
95
  __publicField(this, "importFile", (path) => {
96
96
  const now = Date.now();
97
97
  return import(
@@ -671,11 +671,13 @@ ${newContent}
671
671
  });
672
672
  });
673
673
  this.projectRootDir = projectRootDir;
674
+ this.options = options;
674
675
  if (typeof process === "undefined") {
675
676
  throw new Error("SaltyServer can only be used in a Node.js environment.");
676
677
  }
677
678
  }
678
679
  get isProduction() {
680
+ if (this.options.mode) return this.options.mode === "production";
679
681
  try {
680
682
  return process.env["NODE_ENV"] !== "development";
681
683
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salty-css/core",
3
- "version": "0.1.0-alpha.14",
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",