@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.
- package/bin/confirm-install.d.ts +23 -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 +115 -44
- package/bin/main.js +115 -44
- package/package.json +1 -1
|
@@ -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
|
|
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,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
|
-
|
|
377
|
+
plan: async (ctx, configPath) => {
|
|
346
378
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
347
|
-
if (existing === void 0) return
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
499
|
+
plan: async (ctx, configPath) => {
|
|
458
500
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
459
|
-
if (existing === void 0) return
|
|
501
|
+
if (existing === void 0) return null;
|
|
460
502
|
const { content } = editNextConfig(existing);
|
|
461
|
-
if (content === null) return
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
530
|
+
plan: async (ctx, configPath) => {
|
|
485
531
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
486
|
-
if (existing === void 0) return
|
|
532
|
+
if (existing === void 0) return null;
|
|
487
533
|
const { content } = editViteConfig(existing);
|
|
488
|
-
if (content === null) return
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
499
|
-
const
|
|
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
|
|
504
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
374
|
+
plan: async (ctx, configPath) => {
|
|
343
375
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
344
|
-
if (existing === void 0) return
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
496
|
+
plan: async (ctx, configPath) => {
|
|
455
497
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
456
|
-
if (existing === void 0) return
|
|
498
|
+
if (existing === void 0) return null;
|
|
457
499
|
const { content } = editNextConfig(existing);
|
|
458
|
-
if (content === null) return
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
527
|
+
plan: async (ctx, configPath) => {
|
|
482
528
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
483
|
-
if (existing === void 0) return
|
|
529
|
+
if (existing === void 0) return null;
|
|
484
530
|
const { content } = editViteConfig(existing);
|
|
485
|
-
if (content === null) return
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
496
|
-
const
|
|
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
|
|
501
|
-
|
|
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
|
-
|
|
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
|
|
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");
|