@salty-css/core 0.1.0-alpha.15 → 0.1.0-alpha.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/confirm-install.d.ts +34 -0
- package/bin/context.d.ts +3 -0
- package/bin/integrations/index.d.ts +11 -3
- package/bin/integrations/types.d.ts +14 -2
- package/bin/main.cjs +142 -44
- package/bin/main.js +142 -44
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
/**
|
|
11
|
-
|
|
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,52 @@ 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
|
+
};
|
|
331
|
+
const confirmYesNo = async (question, options = {}) => {
|
|
332
|
+
if (options.yes) return true;
|
|
333
|
+
const input = options.input ?? process.stdin;
|
|
334
|
+
const output = options.output ?? process.stdout;
|
|
335
|
+
const isTTY = options.isTTY ?? process.stdin.isTTY ?? false;
|
|
336
|
+
if (!isTTY) return false;
|
|
337
|
+
const suffix = options.defaultYes ? "(Y/n)" : "(y/N)";
|
|
338
|
+
const rl = promises$1.createInterface({ input, output, terminal: false });
|
|
339
|
+
try {
|
|
340
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
341
|
+
if (answer === "") return !!options.defaultYes;
|
|
342
|
+
return answer === "y" || answer === "yes";
|
|
343
|
+
} finally {
|
|
344
|
+
rl.close();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
299
347
|
const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
|
|
300
348
|
const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
|
|
301
349
|
const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
|
|
@@ -342,17 +390,22 @@ const editAstroConfig = (existing) => {
|
|
|
342
390
|
const astroIntegration = {
|
|
343
391
|
name: "astro",
|
|
344
392
|
detect: (ctx) => findAstroConfig(ctx.projectDir),
|
|
345
|
-
|
|
393
|
+
plan: async (ctx, configPath) => {
|
|
346
394
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
347
|
-
if (existing === void 0) return
|
|
395
|
+
if (existing === void 0) return null;
|
|
348
396
|
const result = editAstroConfig(existing);
|
|
349
397
|
if (result.warning) compiler_saltyCompiler.logger.warn(result.warning);
|
|
350
|
-
if (result.content === null) return
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
398
|
+
if (result.content === null) return null;
|
|
399
|
+
const newContent = result.content;
|
|
400
|
+
return {
|
|
401
|
+
packages: [`-D ${astroPackage(ctx.cliVersion)}`],
|
|
402
|
+
execute: async () => {
|
|
403
|
+
compiler_saltyCompiler.logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
|
|
404
|
+
await promises.writeFile(configPath, newContent);
|
|
405
|
+
await formatWithPrettier(configPath);
|
|
406
|
+
return { changed: true };
|
|
407
|
+
}
|
|
408
|
+
};
|
|
356
409
|
}
|
|
357
410
|
};
|
|
358
411
|
const ESLINT_CONFIG_CANDIDATES = [
|
|
@@ -411,20 +464,25 @@ const eslintIntegration = {
|
|
|
411
464
|
const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
|
|
412
465
|
return candidates.find((p) => fs.existsSync(p)) ?? null;
|
|
413
466
|
},
|
|
414
|
-
|
|
467
|
+
plan: async (ctx, configPath) => {
|
|
415
468
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
416
469
|
if (existing === void 0) {
|
|
417
470
|
compiler_saltyCompiler.logger.error("Could not read ESLint config file.");
|
|
418
|
-
return
|
|
471
|
+
return null;
|
|
419
472
|
}
|
|
420
|
-
if (!ctx.skipInstall) await npmInstall(corePackages.eslintConfigCore(ctx.cliVersion));
|
|
421
473
|
const result = editEslintConfig(existing, configPath.endsWith("js"));
|
|
422
474
|
if (result.warning) compiler_saltyCompiler.logger.warn(result.warning);
|
|
423
|
-
if (result.content === null) return
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
475
|
+
if (result.content === null) return null;
|
|
476
|
+
const newContent = result.content;
|
|
477
|
+
return {
|
|
478
|
+
packages: [corePackages.eslintConfigCore(ctx.cliVersion)],
|
|
479
|
+
execute: async () => {
|
|
480
|
+
compiler_saltyCompiler.logger.info("Edit file: " + configPath);
|
|
481
|
+
await promises.writeFile(configPath, newContent);
|
|
482
|
+
await formatWithPrettier(configPath);
|
|
483
|
+
return { changed: true };
|
|
484
|
+
}
|
|
485
|
+
};
|
|
428
486
|
}
|
|
429
487
|
};
|
|
430
488
|
const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
|
|
@@ -454,16 +512,20 @@ const nextIntegration = {
|
|
|
454
512
|
const found = nextConfigFiles.map((file) => path.join(ctx.projectDir, file)).find((p) => fs.existsSync(p));
|
|
455
513
|
return found ?? null;
|
|
456
514
|
},
|
|
457
|
-
|
|
515
|
+
plan: async (ctx, configPath) => {
|
|
458
516
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
459
|
-
if (existing === void 0) return
|
|
517
|
+
if (existing === void 0) return null;
|
|
460
518
|
const { content } = editNextConfig(existing);
|
|
461
|
-
if (content === null) return
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
519
|
+
if (content === null) return null;
|
|
520
|
+
return {
|
|
521
|
+
packages: [`-D ${nextPackage(ctx.cliVersion)}`],
|
|
522
|
+
execute: async () => {
|
|
523
|
+
compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Next.js config...");
|
|
524
|
+
await promises.writeFile(configPath, content);
|
|
525
|
+
await formatWithPrettier(configPath);
|
|
526
|
+
return { changed: true };
|
|
527
|
+
}
|
|
528
|
+
};
|
|
467
529
|
}
|
|
468
530
|
};
|
|
469
531
|
const vitePackage = (version) => `@salty-css/vite@${version}`;
|
|
@@ -481,27 +543,40 @@ const viteIntegration = {
|
|
|
481
543
|
const path$1 = path.join(ctx.projectDir, "vite.config.ts");
|
|
482
544
|
return fs.existsSync(path$1) ? path$1 : null;
|
|
483
545
|
},
|
|
484
|
-
|
|
546
|
+
plan: async (ctx, configPath) => {
|
|
485
547
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
486
|
-
if (existing === void 0) return
|
|
548
|
+
if (existing === void 0) return null;
|
|
487
549
|
const { content } = editViteConfig(existing);
|
|
488
|
-
if (content === null) return
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
550
|
+
if (content === null) return null;
|
|
551
|
+
return {
|
|
552
|
+
packages: [`-D ${vitePackage(ctx.cliVersion)}`],
|
|
553
|
+
execute: async () => {
|
|
554
|
+
compiler_saltyCompiler.logger.info("Edit file: " + configPath);
|
|
555
|
+
compiler_saltyCompiler.logger.info("Adding Salty-CSS plugin to Vite config...");
|
|
556
|
+
await promises.writeFile(configPath, content);
|
|
557
|
+
await formatWithPrettier(configPath);
|
|
558
|
+
return { changed: true };
|
|
559
|
+
}
|
|
560
|
+
};
|
|
495
561
|
}
|
|
496
562
|
};
|
|
497
563
|
const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
|
|
498
|
-
const
|
|
499
|
-
const
|
|
564
|
+
const planIntegrations = async (ctx) => {
|
|
565
|
+
const planned = [];
|
|
500
566
|
for (const integration of buildIntegrationRegistry) {
|
|
501
567
|
const configPath = await integration.detect(ctx);
|
|
502
568
|
if (!configPath) continue;
|
|
503
|
-
const
|
|
504
|
-
|
|
569
|
+
const plan = await integration.plan(ctx, configPath);
|
|
570
|
+
if (!plan) continue;
|
|
571
|
+
planned.push({ name: integration.name, configPath, plan });
|
|
572
|
+
}
|
|
573
|
+
return planned;
|
|
574
|
+
};
|
|
575
|
+
const applyIntegrationPlans = async (planned) => {
|
|
576
|
+
const results = [];
|
|
577
|
+
for (const { name, configPath, plan } of planned) {
|
|
578
|
+
const result = await plan.execute();
|
|
579
|
+
results.push({ name, configPath, changed: result.changed });
|
|
505
580
|
}
|
|
506
581
|
return results;
|
|
507
582
|
};
|
|
@@ -552,17 +627,24 @@ const wirePrepareScript = async () => {
|
|
|
552
627
|
await updatePackageJson(next);
|
|
553
628
|
};
|
|
554
629
|
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 = ".") {
|
|
630
|
+
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
631
|
try {
|
|
557
632
|
const opts = this.opts();
|
|
558
633
|
const dir = opts.dir ?? _dir;
|
|
559
634
|
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 });
|
|
635
|
+
const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
|
|
561
636
|
compiler_saltyCompiler.logger.info("Initializing a new Salty-CSS project!");
|
|
562
637
|
const framework = await detectFramework(ctx);
|
|
563
638
|
compiler_saltyCompiler.logger.info(`Detected framework: ${framework.name}`);
|
|
639
|
+
const plannedIntegrations = await planIntegrations(ctx);
|
|
564
640
|
if (!ctx.skipInstall) {
|
|
565
|
-
|
|
641
|
+
const packages = [
|
|
642
|
+
corePackages.core(ctx.cliVersion),
|
|
643
|
+
framework.runtimePackage(ctx.cliVersion),
|
|
644
|
+
...plannedIntegrations.flatMap((p) => p.plan.packages)
|
|
645
|
+
];
|
|
646
|
+
await confirmInstall(packages, ctx.yes);
|
|
647
|
+
await npmInstall(...packages);
|
|
566
648
|
}
|
|
567
649
|
const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
|
|
568
650
|
await promises.mkdir(ctx.projectDir, { recursive: true });
|
|
@@ -570,7 +652,7 @@ const registerInitCommand = (program) => {
|
|
|
570
652
|
await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
|
|
571
653
|
await ensureGitignoreSaltygen(ctx.cwd);
|
|
572
654
|
await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
|
|
573
|
-
await
|
|
655
|
+
await applyIntegrationPlans(plannedIntegrations);
|
|
574
656
|
await wirePrepareScript();
|
|
575
657
|
compiler_saltyCompiler.logger.info("Running the build to generate initial CSS...");
|
|
576
658
|
const compiler = new compiler_saltyCompiler.SaltyCompiler(ctx.projectDir);
|
|
@@ -601,8 +683,8 @@ const getSaltyCssPackages = async () => {
|
|
|
601
683
|
return saltyCssPackages;
|
|
602
684
|
};
|
|
603
685
|
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();
|
|
686
|
+
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") {
|
|
687
|
+
const { legacyPeerDeps, version = _version, yes = false, dir } = this.opts();
|
|
606
688
|
const saltyCssPackages = await getSaltyCssPackages();
|
|
607
689
|
if (!saltyCssPackages) return compiler_saltyCompiler.logError("Could not update Salty-CSS packages as any were found in package.json.");
|
|
608
690
|
const cli = await readThisPackageJson();
|
|
@@ -610,6 +692,11 @@ const registerUpdateCommand = (program) => {
|
|
|
610
692
|
if (version === "@") return `${name}@${cli.version}`;
|
|
611
693
|
return `${name}@${version.replace(/^@/, "")}`;
|
|
612
694
|
});
|
|
695
|
+
try {
|
|
696
|
+
await confirmInstall(packagesToUpdate, yes);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
return compiler_saltyCompiler.logError(err instanceof Error ? err.message : String(err));
|
|
699
|
+
}
|
|
613
700
|
if (legacyPeerDeps) {
|
|
614
701
|
compiler_saltyCompiler.logger.warn("Using legacy peer dependencies to update packages.");
|
|
615
702
|
await npmInstall(...packagesToUpdate, "--legacy-peer-deps");
|
|
@@ -632,6 +719,17 @@ const registerUpdateCommand = (program) => {
|
|
|
632
719
|
compiler_saltyCompiler.logger.info(`Updated to ${v.replace(/^\^/, "")}: ${names.join(", ")}`);
|
|
633
720
|
}
|
|
634
721
|
}
|
|
722
|
+
const project = dir ?? await getDefaultProject();
|
|
723
|
+
if (!project) {
|
|
724
|
+
compiler_saltyCompiler.logger.warn("Skipping rebuild: no project directory configured. Run `salty-css build [dir]` manually.");
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const shouldRebuild = await confirmYesNo("Rebuild Salty CSS now?", { yes });
|
|
728
|
+
if (!shouldRebuild) return;
|
|
729
|
+
const projectDir = resolveProjectDir(project);
|
|
730
|
+
compiler_saltyCompiler.logger.info("Rebuilding Salty-CSS project...");
|
|
731
|
+
await new compiler_saltyCompiler.SaltyCompiler(projectDir).generateCss();
|
|
732
|
+
compiler_saltyCompiler.logger.info("Rebuild complete.");
|
|
635
733
|
});
|
|
636
734
|
};
|
|
637
735
|
const registerVersionOption = (program) => {
|
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,52 @@ 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
|
+
};
|
|
328
|
+
const confirmYesNo = async (question, options = {}) => {
|
|
329
|
+
if (options.yes) return true;
|
|
330
|
+
const input = options.input ?? process.stdin;
|
|
331
|
+
const output = options.output ?? process.stdout;
|
|
332
|
+
const isTTY = options.isTTY ?? process.stdin.isTTY ?? false;
|
|
333
|
+
if (!isTTY) return false;
|
|
334
|
+
const suffix = options.defaultYes ? "(Y/n)" : "(y/N)";
|
|
335
|
+
const rl = createInterface({ input, output, terminal: false });
|
|
336
|
+
try {
|
|
337
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
338
|
+
if (answer === "") return !!options.defaultYes;
|
|
339
|
+
return answer === "y" || answer === "yes";
|
|
340
|
+
} finally {
|
|
341
|
+
rl.close();
|
|
342
|
+
}
|
|
343
|
+
};
|
|
296
344
|
const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
|
|
297
345
|
const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
|
|
298
346
|
const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
|
|
@@ -339,17 +387,22 @@ const editAstroConfig = (existing) => {
|
|
|
339
387
|
const astroIntegration = {
|
|
340
388
|
name: "astro",
|
|
341
389
|
detect: (ctx) => findAstroConfig(ctx.projectDir),
|
|
342
|
-
|
|
390
|
+
plan: async (ctx, configPath) => {
|
|
343
391
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
344
|
-
if (existing === void 0) return
|
|
392
|
+
if (existing === void 0) return null;
|
|
345
393
|
const result = editAstroConfig(existing);
|
|
346
394
|
if (result.warning) logger.warn(result.warning);
|
|
347
|
-
if (result.content === null) return
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
395
|
+
if (result.content === null) return null;
|
|
396
|
+
const newContent = result.content;
|
|
397
|
+
return {
|
|
398
|
+
packages: [`-D ${astroPackage(ctx.cliVersion)}`],
|
|
399
|
+
execute: async () => {
|
|
400
|
+
logger.info("Adding Salty-CSS integration to Astro config: " + configPath);
|
|
401
|
+
await writeFile(configPath, newContent);
|
|
402
|
+
await formatWithPrettier(configPath);
|
|
403
|
+
return { changed: true };
|
|
404
|
+
}
|
|
405
|
+
};
|
|
353
406
|
}
|
|
354
407
|
};
|
|
355
408
|
const ESLINT_CONFIG_CANDIDATES = [
|
|
@@ -408,20 +461,25 @@ const eslintIntegration = {
|
|
|
408
461
|
const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
|
|
409
462
|
return candidates.find((p) => existsSync(p)) ?? null;
|
|
410
463
|
},
|
|
411
|
-
|
|
464
|
+
plan: async (ctx, configPath) => {
|
|
412
465
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
413
466
|
if (existing === void 0) {
|
|
414
467
|
logger.error("Could not read ESLint config file.");
|
|
415
|
-
return
|
|
468
|
+
return null;
|
|
416
469
|
}
|
|
417
|
-
if (!ctx.skipInstall) await npmInstall(corePackages.eslintConfigCore(ctx.cliVersion));
|
|
418
470
|
const result = editEslintConfig(existing, configPath.endsWith("js"));
|
|
419
471
|
if (result.warning) logger.warn(result.warning);
|
|
420
|
-
if (result.content === null) return
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
472
|
+
if (result.content === null) return null;
|
|
473
|
+
const newContent = result.content;
|
|
474
|
+
return {
|
|
475
|
+
packages: [corePackages.eslintConfigCore(ctx.cliVersion)],
|
|
476
|
+
execute: async () => {
|
|
477
|
+
logger.info("Edit file: " + configPath);
|
|
478
|
+
await writeFile(configPath, newContent);
|
|
479
|
+
await formatWithPrettier(configPath);
|
|
480
|
+
return { changed: true };
|
|
481
|
+
}
|
|
482
|
+
};
|
|
425
483
|
}
|
|
426
484
|
};
|
|
427
485
|
const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
|
|
@@ -451,16 +509,20 @@ const nextIntegration = {
|
|
|
451
509
|
const found = nextConfigFiles.map((file) => join(ctx.projectDir, file)).find((p) => existsSync(p));
|
|
452
510
|
return found ?? null;
|
|
453
511
|
},
|
|
454
|
-
|
|
512
|
+
plan: async (ctx, configPath) => {
|
|
455
513
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
456
|
-
if (existing === void 0) return
|
|
514
|
+
if (existing === void 0) return null;
|
|
457
515
|
const { content } = editNextConfig(existing);
|
|
458
|
-
if (content === null) return
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
516
|
+
if (content === null) return null;
|
|
517
|
+
return {
|
|
518
|
+
packages: [`-D ${nextPackage(ctx.cliVersion)}`],
|
|
519
|
+
execute: async () => {
|
|
520
|
+
logger.info("Adding Salty-CSS plugin to Next.js config...");
|
|
521
|
+
await writeFile(configPath, content);
|
|
522
|
+
await formatWithPrettier(configPath);
|
|
523
|
+
return { changed: true };
|
|
524
|
+
}
|
|
525
|
+
};
|
|
464
526
|
}
|
|
465
527
|
};
|
|
466
528
|
const vitePackage = (version) => `@salty-css/vite@${version}`;
|
|
@@ -478,27 +540,40 @@ const viteIntegration = {
|
|
|
478
540
|
const path = join(ctx.projectDir, "vite.config.ts");
|
|
479
541
|
return existsSync(path) ? path : null;
|
|
480
542
|
},
|
|
481
|
-
|
|
543
|
+
plan: async (ctx, configPath) => {
|
|
482
544
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
483
|
-
if (existing === void 0) return
|
|
545
|
+
if (existing === void 0) return null;
|
|
484
546
|
const { content } = editViteConfig(existing);
|
|
485
|
-
if (content === null) return
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
547
|
+
if (content === null) return null;
|
|
548
|
+
return {
|
|
549
|
+
packages: [`-D ${vitePackage(ctx.cliVersion)}`],
|
|
550
|
+
execute: async () => {
|
|
551
|
+
logger.info("Edit file: " + configPath);
|
|
552
|
+
logger.info("Adding Salty-CSS plugin to Vite config...");
|
|
553
|
+
await writeFile(configPath, content);
|
|
554
|
+
await formatWithPrettier(configPath);
|
|
555
|
+
return { changed: true };
|
|
556
|
+
}
|
|
557
|
+
};
|
|
492
558
|
}
|
|
493
559
|
};
|
|
494
560
|
const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
|
|
495
|
-
const
|
|
496
|
-
const
|
|
561
|
+
const planIntegrations = async (ctx) => {
|
|
562
|
+
const planned = [];
|
|
497
563
|
for (const integration of buildIntegrationRegistry) {
|
|
498
564
|
const configPath = await integration.detect(ctx);
|
|
499
565
|
if (!configPath) continue;
|
|
500
|
-
const
|
|
501
|
-
|
|
566
|
+
const plan = await integration.plan(ctx, configPath);
|
|
567
|
+
if (!plan) continue;
|
|
568
|
+
planned.push({ name: integration.name, configPath, plan });
|
|
569
|
+
}
|
|
570
|
+
return planned;
|
|
571
|
+
};
|
|
572
|
+
const applyIntegrationPlans = async (planned) => {
|
|
573
|
+
const results = [];
|
|
574
|
+
for (const { name, configPath, plan } of planned) {
|
|
575
|
+
const result = await plan.execute();
|
|
576
|
+
results.push({ name, configPath, changed: result.changed });
|
|
502
577
|
}
|
|
503
578
|
return results;
|
|
504
579
|
};
|
|
@@ -549,17 +624,24 @@ const wirePrepareScript = async () => {
|
|
|
549
624
|
await updatePackageJson(next);
|
|
550
625
|
};
|
|
551
626
|
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 = ".") {
|
|
627
|
+
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
628
|
try {
|
|
554
629
|
const opts = this.opts();
|
|
555
630
|
const dir = opts.dir ?? _dir;
|
|
556
631
|
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 });
|
|
632
|
+
const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
|
|
558
633
|
logger.info("Initializing a new Salty-CSS project!");
|
|
559
634
|
const framework = await detectFramework(ctx);
|
|
560
635
|
logger.info(`Detected framework: ${framework.name}`);
|
|
636
|
+
const plannedIntegrations = await planIntegrations(ctx);
|
|
561
637
|
if (!ctx.skipInstall) {
|
|
562
|
-
|
|
638
|
+
const packages = [
|
|
639
|
+
corePackages.core(ctx.cliVersion),
|
|
640
|
+
framework.runtimePackage(ctx.cliVersion),
|
|
641
|
+
...plannedIntegrations.flatMap((p) => p.plan.packages)
|
|
642
|
+
];
|
|
643
|
+
await confirmInstall(packages, ctx.yes);
|
|
644
|
+
await npmInstall(...packages);
|
|
563
645
|
}
|
|
564
646
|
const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
|
|
565
647
|
await mkdir(ctx.projectDir, { recursive: true });
|
|
@@ -567,7 +649,7 @@ const registerInitCommand = (program) => {
|
|
|
567
649
|
await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
|
|
568
650
|
await ensureGitignoreSaltygen(ctx.cwd);
|
|
569
651
|
await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
|
|
570
|
-
await
|
|
652
|
+
await applyIntegrationPlans(plannedIntegrations);
|
|
571
653
|
await wirePrepareScript();
|
|
572
654
|
logger.info("Running the build to generate initial CSS...");
|
|
573
655
|
const compiler = new SaltyCompiler(ctx.projectDir);
|
|
@@ -598,8 +680,8 @@ const getSaltyCssPackages = async () => {
|
|
|
598
680
|
return saltyCssPackages;
|
|
599
681
|
};
|
|
600
682
|
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();
|
|
683
|
+
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") {
|
|
684
|
+
const { legacyPeerDeps, version = _version, yes = false, dir } = this.opts();
|
|
603
685
|
const saltyCssPackages = await getSaltyCssPackages();
|
|
604
686
|
if (!saltyCssPackages) return logError("Could not update Salty-CSS packages as any were found in package.json.");
|
|
605
687
|
const cli = await readThisPackageJson();
|
|
@@ -607,6 +689,11 @@ const registerUpdateCommand = (program) => {
|
|
|
607
689
|
if (version === "@") return `${name}@${cli.version}`;
|
|
608
690
|
return `${name}@${version.replace(/^@/, "")}`;
|
|
609
691
|
});
|
|
692
|
+
try {
|
|
693
|
+
await confirmInstall(packagesToUpdate, yes);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
return logError(err instanceof Error ? err.message : String(err));
|
|
696
|
+
}
|
|
610
697
|
if (legacyPeerDeps) {
|
|
611
698
|
logger.warn("Using legacy peer dependencies to update packages.");
|
|
612
699
|
await npmInstall(...packagesToUpdate, "--legacy-peer-deps");
|
|
@@ -629,6 +716,17 @@ const registerUpdateCommand = (program) => {
|
|
|
629
716
|
logger.info(`Updated to ${v.replace(/^\^/, "")}: ${names.join(", ")}`);
|
|
630
717
|
}
|
|
631
718
|
}
|
|
719
|
+
const project = dir ?? await getDefaultProject();
|
|
720
|
+
if (!project) {
|
|
721
|
+
logger.warn("Skipping rebuild: no project directory configured. Run `salty-css build [dir]` manually.");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const shouldRebuild = await confirmYesNo("Rebuild Salty CSS now?", { yes });
|
|
725
|
+
if (!shouldRebuild) return;
|
|
726
|
+
const projectDir = resolveProjectDir(project);
|
|
727
|
+
logger.info("Rebuilding Salty-CSS project...");
|
|
728
|
+
await new SaltyCompiler(projectDir).generateCss();
|
|
729
|
+
logger.info("Rebuild complete.");
|
|
632
730
|
});
|
|
633
731
|
};
|
|
634
732
|
const registerVersionOption = (program) => {
|