@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.
- 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 +121 -47
- package/bin/main.js +121 -47
- package/compiler/salty-compiler.cjs +3 -1
- package/compiler/salty-compiler.d.ts +6 -1
- package/compiler/salty-compiler.js +3 -1
- 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,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
|
-
|
|
377
|
+
plan: async (ctx, configPath) => {
|
|
343
378
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
344
|
-
if (existing === void 0) return
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
499
|
+
plan: async (ctx, configPath) => {
|
|
455
500
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
456
|
-
if (existing === void 0) return
|
|
501
|
+
if (existing === void 0) return null;
|
|
457
502
|
const { content } = editNextConfig(existing);
|
|
458
|
-
if (content === null) return
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
530
|
+
plan: async (ctx, configPath) => {
|
|
482
531
|
const existing = await promises.readFile(configPath, "utf-8").catch(() => void 0);
|
|
483
|
-
if (existing === void 0) return
|
|
532
|
+
if (existing === void 0) return null;
|
|
484
533
|
const { content } = editViteConfig(existing);
|
|
485
|
-
if (content === null) return
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
496
|
-
const
|
|
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
|
|
501
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
374
|
+
plan: async (ctx, configPath) => {
|
|
340
375
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
341
|
-
if (existing === void 0) return
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
496
|
+
plan: async (ctx, configPath) => {
|
|
452
497
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
453
|
-
if (existing === void 0) return
|
|
498
|
+
if (existing === void 0) return null;
|
|
454
499
|
const { content } = editNextConfig(existing);
|
|
455
|
-
if (content === null) return
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
527
|
+
plan: async (ctx, configPath) => {
|
|
479
528
|
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
480
|
-
if (existing === void 0) return
|
|
529
|
+
if (existing === void 0) return null;
|
|
481
530
|
const { content } = editViteConfig(existing);
|
|
482
|
-
if (content === null) return
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
493
|
-
const
|
|
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
|
|
498
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|