@salty-css/core 0.1.0-alpha.3 → 0.1.0-alpha.30
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/README.md +209 -0
- package/astro-component-5hrNTCJ5.js +4 -0
- package/astro-component-Dj3enX6K.cjs +4 -0
- package/bin/commands/build.d.ts +2 -0
- package/bin/commands/generate.d.ts +2 -0
- package/bin/commands/init.d.ts +2 -0
- package/bin/commands/update.d.ts +2 -0
- package/bin/commands/version.d.ts +2 -0
- package/bin/confirm-install.d.ts +34 -0
- package/bin/context.d.ts +22 -0
- package/bin/detection/css-file.d.ts +5 -0
- package/bin/frameworks/astro.d.ts +4 -0
- package/bin/frameworks/index.d.ts +13 -0
- package/bin/frameworks/react.d.ts +2 -0
- package/bin/frameworks/types.d.ts +27 -0
- package/bin/integrations/astro.d.ts +11 -0
- package/bin/integrations/eslint.d.ts +6 -0
- package/bin/integrations/index.d.ts +21 -0
- package/bin/integrations/next.d.ts +9 -0
- package/bin/integrations/types.d.ts +29 -0
- package/bin/integrations/vite.d.ts +8 -0
- package/bin/main.cjs +653 -336
- package/bin/main.d.ts +8 -0
- package/bin/main.js +653 -336
- package/bin/package-json.d.ts +21 -0
- package/bin/saltyrc.d.ts +31 -0
- package/bin/templates.d.ts +14 -0
- package/{class-name-generator-YeSQe_Ik.js → class-name-generator-B0WkxoIg.js} +17 -2
- package/{class-name-generator-B2Pb2obX.cjs → class-name-generator-BEOEMEKX.cjs} +17 -2
- package/compiler/resolve-import.d.ts +17 -0
- package/compiler/salty-compiler.cjs +131 -30
- package/compiler/salty-compiler.d.ts +8 -1
- package/compiler/salty-compiler.js +133 -31
- package/config/index.cjs +4 -0
- package/config/index.js +5 -1
- package/css/dynamic-styles.cjs +15 -0
- package/css/dynamic-styles.d.ts +10 -0
- package/css/dynamic-styles.js +15 -0
- package/css/index.cjs +3 -0
- package/css/index.d.ts +1 -0
- package/css/index.js +3 -0
- package/css/keyframes.cjs +1 -1
- package/css/keyframes.js +1 -1
- package/factories/define-font.d.ts +28 -0
- package/factories/define-import.d.ts +14 -0
- package/factories/index.cjs +141 -0
- package/factories/index.d.ts +2 -0
- package/factories/index.js +141 -0
- package/generators/index.cjs +1 -1
- package/generators/index.js +2 -2
- package/instances/classname-instance.cjs +1 -1
- package/instances/classname-instance.js +1 -1
- package/package.json +5 -1
- package/parse-styles-BBJ3PWyV.js +514 -0
- package/parse-styles-lOMGe_c5.cjs +513 -0
- package/parsers/index.cjs +93 -3
- package/parsers/index.d.ts +1 -0
- package/parsers/index.js +97 -7
- package/parsers/parse-templates.d.ts +10 -0
- package/parsers/parser-regexes.d.ts +3 -0
- package/parsers/resolve-template-variants.d.ts +21 -0
- package/parsers/strict.d.ts +2 -0
- package/runtime/index.cjs +1 -1
- package/runtime/index.js +1 -1
- package/{salty.config-cqavVm2t.cjs → salty.config-DogY_sSQ.cjs} +1 -1
- package/salty.config-GV37Q-D2.js +4 -0
- package/styled-file-BzmB9_Ez.cjs +12 -0
- package/{react-styled-file-U02jek-B.cjs → styled-file-CPd_rTW2.cjs} +2 -2
- package/{react-styled-file-B99mwk0w.js → styled-file-Cda3EeR6.js} +2 -2
- package/styled-file-DLcgYmGN.js +12 -0
- package/types/config-types.d.ts +42 -2
- package/types/font-types.d.ts +53 -0
- package/{react-vanilla-file-D9px70iK.js → vanilla-file-1kOqbCIM.js} +2 -2
- package/{react-vanilla-file-Bj6XC8GS.cjs → vanilla-file-r0fp2q_m.cjs} +2 -2
- package/parse-styles-BTIoYnBr.js +0 -232
- package/parse-styles-CA3TP5n1.cjs +0 -231
- package/salty.config-DjosWdPw.js +0 -4
package/bin/main.js
CHANGED
|
@@ -1,14 +1,44 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { existsSync, watch } from "fs";
|
|
3
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4
|
-
import { join, relative, parse, format } from "path";
|
|
5
|
-
import ejs from "ejs";
|
|
6
|
-
import { p as pascalCase } from "../pascal-case-F3Usi5Wf.js";
|
|
7
3
|
import { l as logger, a as logError, SaltyCompiler } from "../compiler/salty-compiler.js";
|
|
4
|
+
import { isSaltyFile } from "../compiler/helpers.js";
|
|
5
|
+
import { c as checkShouldRestart } from "../should-restart-CXIO0jxY.js";
|
|
6
|
+
import { join, relative, parse, format } from "path";
|
|
7
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
8
8
|
import { exec } from "child_process";
|
|
9
9
|
import ora from "ora";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
10
|
+
import { p as pascalCase } from "../pascal-case-F3Usi5Wf.js";
|
|
11
|
+
import ejs from "ejs";
|
|
12
|
+
import { createInterface } from "readline/promises";
|
|
13
|
+
const defaultPackageJsonPath = join(process.cwd(), "package.json");
|
|
14
|
+
const readPackageJson = async (filePath = defaultPackageJsonPath) => {
|
|
15
|
+
const content = await readFile(filePath, "utf-8").then(JSON.parse).catch(() => void 0);
|
|
16
|
+
if (!content) throw "Could not read package.json file!";
|
|
17
|
+
return content;
|
|
18
|
+
};
|
|
19
|
+
const updatePackageJson = async (content, filePath = defaultPackageJsonPath) => {
|
|
20
|
+
if (typeof content === "object") content = JSON.stringify(content, null, 2);
|
|
21
|
+
await writeFile(filePath, content);
|
|
22
|
+
};
|
|
23
|
+
const readThisPackageJson = async () => {
|
|
24
|
+
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
25
|
+
return readPackageJson(packageJsonPath);
|
|
26
|
+
};
|
|
27
|
+
const corePackages = {
|
|
28
|
+
core: (version) => `@salty-css/core@${version}`,
|
|
29
|
+
eslintConfigCore: (version) => `@salty-css/eslint-config-core@${version}`
|
|
30
|
+
};
|
|
31
|
+
const addPrepareScript = (pkg) => {
|
|
32
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
33
|
+
const current = pkg.scripts["prepare"];
|
|
34
|
+
if (current) {
|
|
35
|
+
if (current.includes("salty-css")) return { changed: false, pkg };
|
|
36
|
+
pkg.scripts["prepare"] = current + " && npx salty-css build";
|
|
37
|
+
} else {
|
|
38
|
+
pkg.scripts["prepare"] = "npx salty-css build";
|
|
39
|
+
}
|
|
40
|
+
return { changed: true, pkg };
|
|
41
|
+
};
|
|
12
42
|
const execAsync = (command) => {
|
|
13
43
|
return new Promise((resolve, reject) => {
|
|
14
44
|
exec(command, (error) => {
|
|
@@ -37,367 +67,633 @@ async function formatWithPrettier(filePath) {
|
|
|
37
67
|
logger.error(`Error formatting ${filePath} with Prettier:`, error);
|
|
38
68
|
}
|
|
39
69
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const packageJsonContent = await readFile(filePath, "utf-8").then(JSON.parse).catch(() => void 0);
|
|
64
|
-
if (!packageJsonContent) throw "Could not read package.json file!";
|
|
65
|
-
return packageJsonContent;
|
|
66
|
-
};
|
|
67
|
-
const updatePackageJson = async (content, filePath = defaultPackageJsonPath) => {
|
|
68
|
-
if (typeof content === "object") content = JSON.stringify(content, null, 2);
|
|
69
|
-
await writeFile(filePath, content);
|
|
70
|
-
};
|
|
71
|
-
const readThisPackageJson = async () => {
|
|
72
|
-
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
73
|
-
return readPackageJson(packageJsonPath);
|
|
74
|
-
};
|
|
75
|
-
const getDefaultProject = async () => {
|
|
76
|
-
const rcContent = await readRCFile();
|
|
77
|
-
return rcContent.defaultProject;
|
|
78
|
-
};
|
|
79
|
-
const defaultProject = await getDefaultProject();
|
|
80
|
-
const currentPackageJson = await readThisPackageJson();
|
|
81
|
-
const packages = {
|
|
82
|
-
core: `@salty-css/core@${currentPackageJson.version}`,
|
|
83
|
-
react: `@salty-css/react@${currentPackageJson.version}`,
|
|
84
|
-
eslintConfigCore: `@salty-css/eslint-config-core@${currentPackageJson.version}`,
|
|
85
|
-
vite: `@salty-css/vite@${currentPackageJson.version}`,
|
|
86
|
-
next: `@salty-css/next@${currentPackageJson.version}`
|
|
87
|
-
};
|
|
88
|
-
const resolveProjectDir = (dir) => {
|
|
89
|
-
const dirName = dir === "." ? "" : dir;
|
|
90
|
-
const rootDir = process.cwd();
|
|
91
|
-
const projectDir = join(rootDir, dirName);
|
|
92
|
-
return projectDir;
|
|
93
|
-
};
|
|
94
|
-
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 = ".") {
|
|
95
|
-
const packageJson = await readPackageJson().catch(() => void 0);
|
|
96
|
-
if (!packageJson) return logError("Salty CSS project must be initialized in a directory with a package.json file.");
|
|
97
|
-
logger.info("Initializing a new Salty-CSS project!");
|
|
98
|
-
const { dir = _dir, cssFile, skipInstall } = this.opts();
|
|
99
|
-
if (!dir) return logError("Project directory must be provided. Add it as the first argument after init command or use the --dir option.");
|
|
100
|
-
if (!skipInstall) await npmInstall(packages.core, packages.react);
|
|
101
|
-
const rootDir = process.cwd();
|
|
102
|
-
const projectDir = resolveProjectDir(dir);
|
|
103
|
-
const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
|
|
104
|
-
const saltyCompiler = new SaltyCompiler(projectDir);
|
|
105
|
-
await mkdir(projectDir, { recursive: true });
|
|
106
|
-
const writeFiles = projectFiles.map(async ({ fileName, content }) => {
|
|
107
|
-
const filePath = join(projectDir, fileName);
|
|
108
|
-
const existingContent = await readFile(filePath, "utf-8").catch(() => void 0);
|
|
109
|
-
if (existingContent !== void 0) {
|
|
110
|
-
logger.debug("File already exists: " + filePath);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const additionalFolders = fileName.split("/").slice(0, -1).join("/");
|
|
114
|
-
if (additionalFolders) await mkdir(join(projectDir, additionalFolders), { recursive: true });
|
|
115
|
-
logger.info("Creating file: " + filePath);
|
|
116
|
-
await writeFile(filePath, content);
|
|
117
|
-
await formatWithPrettier(filePath);
|
|
118
|
-
});
|
|
119
|
-
await Promise.all(writeFiles);
|
|
120
|
-
const relativeProjectPath = relative(rootDir, projectDir) || ".";
|
|
121
|
-
const saltyrcPath = join(rootDir, ".saltyrc.json");
|
|
122
|
-
const existingSaltyrc = await readFile(saltyrcPath, "utf-8").catch(() => void 0);
|
|
123
|
-
if (existingSaltyrc === void 0) {
|
|
124
|
-
logger.info("Creating file: " + saltyrcPath);
|
|
125
|
-
const rcContent = {
|
|
126
|
-
$schema: "./node_modules/@salty-css/core/.saltyrc.schema.json",
|
|
127
|
-
info: "This file is used to define projects and their configurations for Salty CSS cli. Do not delete, modify or add this file to .gitignore.",
|
|
128
|
-
defaultProject: relativeProjectPath,
|
|
129
|
-
projects: [
|
|
130
|
-
{
|
|
131
|
-
dir: relativeProjectPath,
|
|
132
|
-
framework: "react"
|
|
133
|
-
}
|
|
134
|
-
]
|
|
135
|
-
};
|
|
136
|
-
const content = JSON.stringify(rcContent, null, 2);
|
|
137
|
-
await writeFile(saltyrcPath, content);
|
|
138
|
-
await formatWithPrettier(saltyrcPath);
|
|
139
|
-
} else {
|
|
140
|
-
const rcContent = JSON.parse(existingSaltyrc);
|
|
141
|
-
const projects = (rcContent == null ? void 0 : rcContent.projects) || [];
|
|
142
|
-
const projectIndex = projects.findIndex((p) => p.dir === relativeProjectPath);
|
|
143
|
-
if (projectIndex === -1) {
|
|
144
|
-
projects.push({ dir: relativeProjectPath, framework: "react" });
|
|
145
|
-
rcContent.projects = [...projects];
|
|
146
|
-
const content = JSON.stringify(rcContent, null, 2);
|
|
147
|
-
if (content !== existingSaltyrc) {
|
|
148
|
-
logger.info("Edit file: " + saltyrcPath);
|
|
149
|
-
await writeFile(saltyrcPath, content);
|
|
150
|
-
await formatWithPrettier(saltyrcPath);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const gitIgnorePath = join(rootDir, ".gitignore");
|
|
155
|
-
const gitIgnoreContent = await readFile(gitIgnorePath, "utf-8").catch(() => void 0);
|
|
156
|
-
if (gitIgnoreContent !== void 0) {
|
|
157
|
-
const alreadyIgnoresSaltygen = gitIgnoreContent.includes("saltygen");
|
|
158
|
-
if (!alreadyIgnoresSaltygen) {
|
|
159
|
-
logger.info("Edit file: " + gitIgnorePath);
|
|
160
|
-
await writeFile(gitIgnorePath, gitIgnoreContent + "\n\n# Salty-CSS\nsaltygen\n");
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const cssFileFoldersToLookFor = ["src", "public", "assets", "styles", "css", "app"];
|
|
164
|
-
const secondLevelFolders = ["styles", "css", "app", "pages"];
|
|
165
|
-
const cssFilesToLookFor = ["index", "styles", "main", "app", "global", "globals"];
|
|
166
|
-
const cssFileExtensions = [".css", ".scss", ".sass"];
|
|
167
|
-
const getTargetCssFile = async () => {
|
|
168
|
-
if (cssFile) return cssFile;
|
|
169
|
-
for (const folder of cssFileFoldersToLookFor) {
|
|
170
|
-
for (const file of cssFilesToLookFor) {
|
|
171
|
-
for (const ext of cssFileExtensions) {
|
|
172
|
-
const filePath = join(projectDir, folder, file + ext);
|
|
173
|
-
const fileContent = await readFile(filePath, "utf-8").catch(() => void 0);
|
|
174
|
-
if (fileContent !== void 0) return relative(projectDir, filePath);
|
|
175
|
-
for (const secondLevelFolder of secondLevelFolders) {
|
|
176
|
-
const filePath2 = join(projectDir, folder, secondLevelFolder, file + ext);
|
|
177
|
-
const fileContent2 = await readFile(filePath2, "utf-8").catch(() => void 0);
|
|
178
|
-
if (fileContent2 !== void 0) return relative(projectDir, filePath2);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return void 0;
|
|
184
|
-
};
|
|
185
|
-
const targetCSSFile = await getTargetCssFile();
|
|
186
|
-
if (targetCSSFile) {
|
|
187
|
-
const cssFilePath = join(projectDir, targetCSSFile);
|
|
188
|
-
const cssFileContent = await readFile(cssFilePath, "utf-8").catch(() => void 0);
|
|
189
|
-
if (cssFileContent !== void 0) {
|
|
190
|
-
const alreadyImportsSaltygen = cssFileContent.includes("saltygen");
|
|
191
|
-
if (!alreadyImportsSaltygen) {
|
|
192
|
-
const cssFileFolder = join(cssFilePath, "..");
|
|
193
|
-
const relativePath = relative(cssFileFolder, join(projectDir, "saltygen/index.css"));
|
|
194
|
-
const importStatement = `@import '${relativePath}';`;
|
|
195
|
-
logger.info("Adding global import statement to CSS file: " + cssFilePath);
|
|
196
|
-
await writeFile(cssFilePath, importStatement + "\n" + cssFileContent);
|
|
197
|
-
await formatWithPrettier(cssFilePath);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
logger.warn("Could not find a CSS file to import the generated CSS. Please add it manually.");
|
|
202
|
-
}
|
|
203
|
-
const eslintConfigs = {
|
|
204
|
-
projectJs: join(projectDir, "eslint.config.js"),
|
|
205
|
-
rootJs: join(rootDir, "eslint.config.js"),
|
|
206
|
-
projectMjs: join(projectDir, "eslint.config.mjs"),
|
|
207
|
-
rootMjs: join(rootDir, "eslint.config.mjs"),
|
|
208
|
-
projectJson: join(projectDir, ".eslintrc.json"),
|
|
209
|
-
rootJson: join(rootDir, ".eslintrc.json")
|
|
70
|
+
const SALTYRC_FILENAME = ".saltyrc.json";
|
|
71
|
+
const SALTYRC_SCHEMA = "./node_modules/@salty-css/core/.saltyrc.schema.json";
|
|
72
|
+
const SALTYRC_INFO = "This file is used to define projects and their configurations for Salty CSS cli. Do not delete, modify or add this file to .gitignore.";
|
|
73
|
+
const saltyrcPath = (rootDir = process.cwd()) => join(rootDir, SALTYRC_FILENAME);
|
|
74
|
+
const readRc = async (rootDir = process.cwd()) => {
|
|
75
|
+
const content = await readFile(saltyrcPath(rootDir), "utf-8").then(JSON.parse).catch(() => ({}));
|
|
76
|
+
return content;
|
|
77
|
+
};
|
|
78
|
+
const readRawRc = async (rootDir = process.cwd()) => {
|
|
79
|
+
return readFile(saltyrcPath(rootDir), "utf-8").catch(() => void 0);
|
|
80
|
+
};
|
|
81
|
+
const getDefaultProject = async (rootDir = process.cwd()) => {
|
|
82
|
+
const rc = await readRc(rootDir);
|
|
83
|
+
return rc.defaultProject;
|
|
84
|
+
};
|
|
85
|
+
const upsertProjectInRc = (existingRaw, relativeProjectPath, framework) => {
|
|
86
|
+
const projectPath = join(relativeProjectPath, framework.srcDirectory);
|
|
87
|
+
if (existingRaw === void 0) {
|
|
88
|
+
const fresh = {
|
|
89
|
+
$schema: SALTYRC_SCHEMA,
|
|
90
|
+
info: SALTYRC_INFO,
|
|
91
|
+
defaultProject: projectPath,
|
|
92
|
+
projects: [{ dir: projectPath, framework: framework.name }]
|
|
210
93
|
};
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const alreadyHasPlugin = nextConfigContent.includes("withSaltyCss");
|
|
266
|
-
if (!alreadyHasPlugin) {
|
|
267
|
-
let saltyCssAppended = false;
|
|
268
|
-
const hasPluginsArray = /\splugins([^=]*)=[^[]\[/.test(nextConfigContent);
|
|
269
|
-
if (hasPluginsArray && !saltyCssAppended) {
|
|
270
|
-
nextConfigContent = nextConfigContent.replace(/\splugins([^=]*)=[^[]\[/, (_, config) => {
|
|
271
|
-
return ` plugins${config}= [withSaltyCss,`;
|
|
272
|
-
});
|
|
273
|
-
saltyCssAppended = true;
|
|
274
|
-
}
|
|
275
|
-
const useRequire = nextConfigContent.includes("module.exports");
|
|
276
|
-
const pluginImport = useRequire ? "const { withSaltyCss } = require('@salty-css/next');\n" : "import { withSaltyCss } from '@salty-css/next';\n";
|
|
277
|
-
if (useRequire && !saltyCssAppended) {
|
|
278
|
-
nextConfigContent = nextConfigContent.replace(/module.exports = ([^;]+)/, (_, config) => {
|
|
279
|
-
return `module.exports = withSaltyCss(${config})`;
|
|
280
|
-
});
|
|
281
|
-
saltyCssAppended = true;
|
|
282
|
-
} else if (!saltyCssAppended) {
|
|
283
|
-
nextConfigContent = nextConfigContent.replace(/export default ([^;]+)/, (_, config) => {
|
|
284
|
-
return `export default withSaltyCss(${config})`;
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
if (!skipInstall) await npmInstall(`-D ${packages.next}`);
|
|
288
|
-
logger.info("Adding Salty-CSS plugin to Next.js config...");
|
|
289
|
-
await writeFile(nextConfigPath, pluginImport + nextConfigContent);
|
|
290
|
-
await formatWithPrettier(nextConfigPath);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
const packageJsonContent = await readPackageJson().catch(() => logError("Could not read package.json file.")).then((content) => {
|
|
295
|
-
if (!content.scripts) content.scripts = {};
|
|
296
|
-
if (content.scripts.prepare) {
|
|
297
|
-
const alreadyHasSaltyCss = content.scripts.prepare.includes("salty-css");
|
|
298
|
-
if (!alreadyHasSaltyCss) {
|
|
299
|
-
logger.info("Edit file: " + defaultPackageJsonPath);
|
|
300
|
-
content.scripts.prepare = content.scripts.prepare + " && npx salty-css build";
|
|
301
|
-
}
|
|
302
|
-
} else {
|
|
303
|
-
logger.info("Edit file: " + defaultPackageJsonPath);
|
|
304
|
-
content.scripts.prepare = "npx salty-css build";
|
|
305
|
-
}
|
|
306
|
-
return content;
|
|
307
|
-
});
|
|
308
|
-
await updatePackageJson(packageJsonContent);
|
|
309
|
-
logger.info("Running the build to generate initial CSS...");
|
|
310
|
-
await saltyCompiler.generateCss();
|
|
311
|
-
logger.info("🎉 Salty CSS project initialized successfully!");
|
|
312
|
-
logger.info("Next steps:");
|
|
313
|
-
logger.info("1. Configure variables and templates in `salty.config.ts`");
|
|
314
|
-
logger.info("2. Create a new component with `npx salty-css generate [component-name]`");
|
|
315
|
-
logger.info("3. Run `npx salty-css build` to generate the CSS");
|
|
316
|
-
logger.info("4. Read about the features in the documentation: https://salty-css.dev");
|
|
317
|
-
logger.info("5. Star the project on GitHub: https://github.com/margarita-form/salty-css ⭐");
|
|
318
|
-
});
|
|
319
|
-
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) {
|
|
94
|
+
return { content: JSON.stringify(fresh, null, 2), changed: true, created: true };
|
|
95
|
+
}
|
|
96
|
+
const rc = JSON.parse(existingRaw);
|
|
97
|
+
const projects = rc.projects || [];
|
|
98
|
+
const exists = projects.some((p) => p.dir === projectPath);
|
|
99
|
+
if (exists) return { content: existingRaw, changed: false, created: false };
|
|
100
|
+
projects.push({ dir: projectPath, framework: framework.name });
|
|
101
|
+
rc.projects = [...projects];
|
|
102
|
+
const next = JSON.stringify(rc, null, 2);
|
|
103
|
+
return { content: next, changed: next !== existingRaw, created: false };
|
|
104
|
+
};
|
|
105
|
+
const writeProjectToRc = async (cwd, relativeProjectPath, framework) => {
|
|
106
|
+
const path = saltyrcPath(cwd);
|
|
107
|
+
const existing = await readRawRc(cwd);
|
|
108
|
+
const { content, changed, created } = upsertProjectInRc(existing, relativeProjectPath, framework);
|
|
109
|
+
if (!changed) return false;
|
|
110
|
+
if (created) logger.info("Creating file: " + path);
|
|
111
|
+
else logger.info("Edit file: " + path);
|
|
112
|
+
await writeFile(path, content);
|
|
113
|
+
await formatWithPrettier(path);
|
|
114
|
+
return true;
|
|
115
|
+
};
|
|
116
|
+
const getProjectFramework = (rc, relativeProjectPath) => {
|
|
117
|
+
const projects = rc.projects || [];
|
|
118
|
+
const entry = projects.find((p) => p.dir === relativeProjectPath);
|
|
119
|
+
return entry == null ? void 0 : entry.framework;
|
|
120
|
+
};
|
|
121
|
+
const resolveProjectDir = (dir, rootDir = process.cwd()) => {
|
|
122
|
+
const dirName = dir === "." ? "" : dir;
|
|
123
|
+
return join(rootDir, dirName);
|
|
124
|
+
};
|
|
125
|
+
const buildContext = async (opts) => {
|
|
126
|
+
const cwd = process.cwd();
|
|
127
|
+
const projectDir = resolveProjectDir(opts.dir, cwd);
|
|
128
|
+
const relativeProjectPath = relative(cwd, projectDir) || ".";
|
|
129
|
+
const packageJson = await readPackageJson().catch(() => void 0);
|
|
130
|
+
if (opts.requirePackageJson !== false && !packageJson) {
|
|
131
|
+
throw new Error("Salty CSS project must be initialized in a directory with a package.json file.");
|
|
132
|
+
}
|
|
133
|
+
const rcFile = await readRc(cwd);
|
|
134
|
+
const cliPackageJson = await readThisPackageJson();
|
|
135
|
+
return {
|
|
136
|
+
cwd,
|
|
137
|
+
projectDir,
|
|
138
|
+
relativeProjectPath,
|
|
139
|
+
packageJson,
|
|
140
|
+
rcFile,
|
|
141
|
+
cliVersion: cliPackageJson.version || "0.0.0",
|
|
142
|
+
skipInstall: !!opts.skipInstall,
|
|
143
|
+
yes: !!opts.yes
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const registerBuildCommand = (program, 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) {
|
|
320
148
|
logger.info("Building the Salty-CSS project...");
|
|
321
|
-
const { dir = _dir, watch: watch$1 } = this.opts();
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
await
|
|
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
|
+
}
|
|
153
|
+
const resolved = dir ?? await getDefaultProject();
|
|
154
|
+
if (!resolved) return logError("Project directory must be provided. Add it as the first argument after build command or use the --dir option.");
|
|
155
|
+
const projectDir = resolveProjectDir(resolved);
|
|
156
|
+
const compiler = new SaltyCompiler(projectDir, { mode });
|
|
157
|
+
await compiler.generateCss();
|
|
326
158
|
if (watch$1) {
|
|
327
159
|
logger.info("Watching for changes in the project directory...");
|
|
328
|
-
watch(projectDir, { recursive: true }, async (
|
|
160
|
+
watch(projectDir, { recursive: true }, async (_event, filePath) => {
|
|
329
161
|
const shouldRestart = await checkShouldRestart(filePath);
|
|
330
162
|
if (shouldRestart) {
|
|
331
|
-
await
|
|
332
|
-
} else {
|
|
333
|
-
|
|
334
|
-
if (saltyFile) await saltyCompiler.generateFile(filePath);
|
|
163
|
+
await compiler.generateCss(false);
|
|
164
|
+
} else if (isSaltyFile(filePath)) {
|
|
165
|
+
await compiler.generateFile(filePath);
|
|
335
166
|
}
|
|
336
167
|
});
|
|
337
168
|
}
|
|
338
169
|
});
|
|
339
|
-
|
|
170
|
+
};
|
|
171
|
+
const astroConfigFiles = ["astro.config.mjs", "astro.config.ts", "astro.config.js", "astro.config.cjs"];
|
|
172
|
+
const findAstroConfig = (projectDir) => {
|
|
173
|
+
for (const name of astroConfigFiles) {
|
|
174
|
+
const path = join(projectDir, name);
|
|
175
|
+
if (existsSync(path)) return path;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
};
|
|
179
|
+
const hasAstroDependency = (ctx) => {
|
|
180
|
+
const pkg = ctx.packageJson;
|
|
181
|
+
if (!pkg) return false;
|
|
182
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
183
|
+
return Object.prototype.hasOwnProperty.call(all, "astro");
|
|
184
|
+
};
|
|
185
|
+
const astroFramework = {
|
|
186
|
+
name: "astro",
|
|
187
|
+
srcDirectory: "src",
|
|
188
|
+
detect: (ctx) => Boolean(findAstroConfig(ctx.projectDir)) || hasAstroDependency(ctx),
|
|
189
|
+
runtimePackage: (version) => `@salty-css/astro@${version}`,
|
|
190
|
+
templates: {
|
|
191
|
+
styled: "astro/styled-file.ts",
|
|
192
|
+
component: {
|
|
193
|
+
styled: "astro/styled-file.ts",
|
|
194
|
+
wrapper: "astro/component.astro",
|
|
195
|
+
wrapperExt: ".astro"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const reactFramework = {
|
|
200
|
+
name: "react",
|
|
201
|
+
srcDirectory: "",
|
|
202
|
+
detect: () => true,
|
|
203
|
+
// default fallback — evaluated last in the registry
|
|
204
|
+
runtimePackage: (version) => `@salty-css/react@${version}`,
|
|
205
|
+
templates: {
|
|
206
|
+
styled: "react/styled-file.ts",
|
|
207
|
+
component: {
|
|
208
|
+
styled: "react/styled-file.ts",
|
|
209
|
+
wrapper: "react/vanilla-file.ts",
|
|
210
|
+
wrapperExt: ".tsx"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const frameworkRegistry = [astroFramework, reactFramework];
|
|
215
|
+
const frameworksByName = {
|
|
216
|
+
astro: astroFramework,
|
|
217
|
+
react: reactFramework
|
|
218
|
+
};
|
|
219
|
+
const detectFramework = async (ctx) => {
|
|
220
|
+
for (const adapter of frameworkRegistry) {
|
|
221
|
+
if (await adapter.detect(ctx)) return adapter;
|
|
222
|
+
}
|
|
223
|
+
return reactFramework;
|
|
224
|
+
};
|
|
225
|
+
const getFramework = (name) => {
|
|
226
|
+
if (!name) return void 0;
|
|
227
|
+
return frameworksByName[name];
|
|
228
|
+
};
|
|
229
|
+
const templateLoaders = {
|
|
230
|
+
"salty.config.ts": () => import("../salty.config-GV37Q-D2.js"),
|
|
231
|
+
"saltygen/index.css": () => import("../index-DKz1QXqs.js"),
|
|
232
|
+
"react/styled-file.ts": () => import("../styled-file-Cda3EeR6.js"),
|
|
233
|
+
"react/vanilla-file.ts": () => import("../vanilla-file-1kOqbCIM.js"),
|
|
234
|
+
"astro/styled-file.ts": () => import("../styled-file-DLcgYmGN.js"),
|
|
235
|
+
"astro/component.astro": () => import("../astro-component-5hrNTCJ5.js")
|
|
236
|
+
};
|
|
237
|
+
const readTemplate = async (key, options) => {
|
|
238
|
+
const { default: file } = await templateLoaders[key]();
|
|
239
|
+
const content = ejs.render(file, options);
|
|
240
|
+
return { fileName: key, content };
|
|
241
|
+
};
|
|
242
|
+
const registerGenerateCommand = (program, defaultProject) => {
|
|
243
|
+
program.command("generate [file] [directory]").alias("g").description("Generate a new component file.").option("-f, --file <file>", "File to generate.").option("-d, --dir <dir>", "Project directory to generate the file in.").option("-t, --tag <tag>", "HTML tag of the component.", "div").option("-n, --name <name>", "Name of the component.").option("-c, --className <className>", "CSS class of the component.").option("-r, --reactComponent", "Generate a wrapper component file alongside the styled definition.").action(async function(_file, _dir = defaultProject) {
|
|
340
244
|
const { file = _file, dir = _dir, tag, name, className, reactComponent = false } = this.opts();
|
|
341
245
|
if (!file) return logError("File to generate must be provided. Add it as the first argument after generate command or use the --file option.");
|
|
342
246
|
if (!dir) return logError("Project directory must be provided. Add it as the second argument after generate command or use the --dir option.");
|
|
343
|
-
|
|
247
|
+
let ctx;
|
|
248
|
+
try {
|
|
249
|
+
ctx = await buildContext({ dir, requirePackageJson: false });
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return logError(err instanceof Error ? err.message : String(err));
|
|
252
|
+
}
|
|
253
|
+
const rcFramework = getFramework(getProjectFramework(ctx.rcFile, ctx.relativeProjectPath));
|
|
254
|
+
const framework = rcFramework ?? await detectFramework(ctx);
|
|
344
255
|
const additionalFolders = file.split("/").slice(0, -1).join("/");
|
|
345
|
-
if (additionalFolders) await mkdir(join(projectDir, additionalFolders), { recursive: true });
|
|
346
|
-
const filePath = join(projectDir, file);
|
|
256
|
+
if (additionalFolders) await mkdir(join(ctx.projectDir, additionalFolders), { recursive: true });
|
|
257
|
+
const filePath = join(ctx.projectDir, file);
|
|
347
258
|
const parsedFilePath = parse(filePath);
|
|
348
|
-
if (!parsedFilePath.ext)
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
if (!parsedFilePath.name.endsWith(".css")) {
|
|
352
|
-
parsedFilePath.name = parsedFilePath.name + ".css";
|
|
353
|
-
}
|
|
259
|
+
if (!parsedFilePath.ext) parsedFilePath.ext = ".ts";
|
|
260
|
+
if (!parsedFilePath.name.endsWith(".css")) parsedFilePath.name = parsedFilePath.name + ".css";
|
|
354
261
|
parsedFilePath.base = parsedFilePath.name + parsedFilePath.ext;
|
|
355
262
|
const formattedStyledFilePath = format(parsedFilePath);
|
|
356
263
|
const alreadyExists = await readFile(formattedStyledFilePath, "utf-8").catch(() => void 0);
|
|
357
264
|
if (alreadyExists !== void 0) {
|
|
358
|
-
logger.error("File already exists:"
|
|
265
|
+
logger.error("File already exists: " + formattedStyledFilePath);
|
|
359
266
|
return;
|
|
360
267
|
}
|
|
361
268
|
let styledComponentName = pascalCase(name || parsedFilePath.base.replace(/\.css\.\w+$/, ""));
|
|
362
269
|
if (reactComponent) {
|
|
270
|
+
if (!framework.templates.component) {
|
|
271
|
+
return logError(`--reactComponent is not supported for the ${framework.name} framework.`);
|
|
272
|
+
}
|
|
363
273
|
const componentName = styledComponentName + "Component";
|
|
364
274
|
styledComponentName = styledComponentName + "Wrapper";
|
|
365
275
|
const fileName = parsedFilePath.base.replace(/\.css\.\w+$/, "");
|
|
366
|
-
const { content:
|
|
276
|
+
const { content: wrapperContent } = await readTemplate(framework.templates.component.wrapper, {
|
|
277
|
+
tag,
|
|
278
|
+
componentName,
|
|
279
|
+
styledComponentName,
|
|
280
|
+
className,
|
|
281
|
+
fileName
|
|
282
|
+
});
|
|
367
283
|
parsedFilePath.name = fileName.replace(/\.css$/, "");
|
|
368
|
-
parsedFilePath.ext =
|
|
284
|
+
parsedFilePath.ext = framework.templates.component.wrapperExt;
|
|
369
285
|
parsedFilePath.base = parsedFilePath.name + parsedFilePath.ext;
|
|
370
|
-
const
|
|
371
|
-
logger.info("Generating a new file: " +
|
|
372
|
-
await writeFile(
|
|
373
|
-
await formatWithPrettier(
|
|
286
|
+
const formattedWrapperPath = format(parsedFilePath);
|
|
287
|
+
logger.info("Generating a new file: " + formattedWrapperPath);
|
|
288
|
+
await writeFile(formattedWrapperPath, wrapperContent);
|
|
289
|
+
await formatWithPrettier(formattedWrapperPath);
|
|
374
290
|
}
|
|
375
|
-
const
|
|
291
|
+
const styledKey = reactComponent && framework.templates.component ? framework.templates.component.styled : framework.templates.styled;
|
|
292
|
+
const { content } = await readTemplate(styledKey, { tag, name: styledComponentName, className });
|
|
376
293
|
logger.info("Generating a new file: " + formattedStyledFilePath);
|
|
377
294
|
await writeFile(formattedStyledFilePath, content);
|
|
378
295
|
await formatWithPrettier(formattedStyledFilePath);
|
|
379
296
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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.");
|
|
390
323
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
};
|
|
344
|
+
const CSS_FILE_FOLDERS = ["src", "public", "assets", "styles", "css", "app"];
|
|
345
|
+
const CSS_SECOND_LEVEL_FOLDERS = ["styles", "css", "app", "pages"];
|
|
346
|
+
const CSS_FILE_NAMES = ["index", "styles", "main", "app", "global", "globals"];
|
|
347
|
+
const CSS_FILE_EXTENSIONS = [".css", ".scss", ".sass"];
|
|
348
|
+
const findGlobalCssFile = async (projectDir) => {
|
|
349
|
+
for (const folder of CSS_FILE_FOLDERS) {
|
|
350
|
+
for (const file of CSS_FILE_NAMES) {
|
|
351
|
+
for (const ext of CSS_FILE_EXTENSIONS) {
|
|
352
|
+
const filePath = join(projectDir, folder, file + ext);
|
|
353
|
+
const fileContent = await readFile(filePath, "utf-8").catch(() => void 0);
|
|
354
|
+
if (fileContent !== void 0) return relative(projectDir, filePath);
|
|
355
|
+
for (const second of CSS_SECOND_LEVEL_FOLDERS) {
|
|
356
|
+
const nestedPath = join(projectDir, folder, second, file + ext);
|
|
357
|
+
const nestedContent = await readFile(nestedPath, "utf-8").catch(() => void 0);
|
|
358
|
+
if (nestedContent !== void 0) return relative(projectDir, nestedPath);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return void 0;
|
|
364
|
+
};
|
|
365
|
+
const astroPackage = (version) => `@salty-css/astro@${version}`;
|
|
366
|
+
const SALTY_ASTRO_IMPORT = "import saltyIntegration from '@salty-css/astro/integration';\n";
|
|
367
|
+
const editAstroConfig = (existing) => {
|
|
368
|
+
if (existing.includes("@salty-css/astro")) return { content: null };
|
|
369
|
+
let next = existing;
|
|
370
|
+
let inserted = false;
|
|
371
|
+
if (/integrations\s*:\s*\[/.test(next)) {
|
|
372
|
+
next = next.replace(/integrations\s*:\s*\[/, (m) => `${m}saltyIntegration(),`);
|
|
373
|
+
inserted = true;
|
|
374
|
+
} else if (/defineConfig\s*\(\s*\{/.test(next)) {
|
|
375
|
+
next = next.replace(/defineConfig\s*\(\s*\{/, (m) => `${m}
|
|
376
|
+
integrations: [saltyIntegration()],`);
|
|
377
|
+
inserted = true;
|
|
378
|
+
}
|
|
379
|
+
if (!inserted) {
|
|
380
|
+
return {
|
|
381
|
+
content: null,
|
|
382
|
+
warning: "Could not find a place to add saltyIntegration() in the Astro config. Please add it manually."
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return { content: SALTY_ASTRO_IMPORT + next };
|
|
386
|
+
};
|
|
387
|
+
const astroIntegration = {
|
|
388
|
+
name: "astro",
|
|
389
|
+
detect: (ctx) => findAstroConfig(ctx.projectDir),
|
|
390
|
+
plan: async (ctx, configPath) => {
|
|
391
|
+
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
392
|
+
if (existing === void 0) return null;
|
|
393
|
+
const result = editAstroConfig(existing);
|
|
394
|
+
if (result.warning) logger.warn(result.warning);
|
|
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
|
+
};
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const ESLINT_CONFIG_CANDIDATES = [
|
|
409
|
+
// Project-local candidates first, then root-level.
|
|
410
|
+
["projectJs", "eslint.config.js"],
|
|
411
|
+
["rootJs", "eslint.config.js"],
|
|
412
|
+
["projectMjs", "eslint.config.mjs"],
|
|
413
|
+
["rootMjs", "eslint.config.mjs"],
|
|
414
|
+
["projectJson", ".eslintrc.json"],
|
|
415
|
+
["rootJson", ".eslintrc.json"]
|
|
416
|
+
];
|
|
417
|
+
const eslintConfigCandidates = (projectDir, rootDir) => {
|
|
418
|
+
return ESLINT_CONFIG_CANDIDATES.map(([scope, name]) => {
|
|
419
|
+
const base = scope.startsWith("root") ? rootDir : projectDir;
|
|
420
|
+
return join(base, name);
|
|
421
|
+
});
|
|
422
|
+
};
|
|
423
|
+
const editEslintConfig = (existing, isJsFlat) => {
|
|
424
|
+
if (existing.includes("salty-css")) return { content: null };
|
|
425
|
+
if (isJsFlat) {
|
|
426
|
+
const importStatement = 'import saltyCss from "@salty-css/eslint-config-core/flat";';
|
|
427
|
+
let newContent = `${importStatement}
|
|
428
|
+
${existing}`;
|
|
429
|
+
const isTsEslint = existing.includes("typescript-eslint");
|
|
430
|
+
if (isTsEslint) {
|
|
431
|
+
if (newContent.includes(".config(")) {
|
|
432
|
+
newContent = newContent.replace(".config(", ".config(saltyCss,");
|
|
433
|
+
} else {
|
|
434
|
+
return {
|
|
435
|
+
content: null,
|
|
436
|
+
warning: "Could not find the correct place to add the Salty-CSS config for ESLint. Please add it manually."
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
if (newContent.includes("export default [")) {
|
|
441
|
+
newContent = newContent.replace("export default [", "export default [ saltyCss,");
|
|
442
|
+
} else if (newContent.includes("eslintConfig = [")) {
|
|
443
|
+
newContent = newContent.replace("eslintConfig = [", "eslintConfig = [ saltyCss,");
|
|
444
|
+
} else {
|
|
445
|
+
return {
|
|
446
|
+
content: null,
|
|
447
|
+
warning: "Could not find the correct place to add the Salty-CSS config for ESLint. Please add it manually."
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return { content: newContent };
|
|
452
|
+
}
|
|
453
|
+
const json = JSON.parse(existing);
|
|
454
|
+
if (!json.extends) json.extends = [];
|
|
455
|
+
if (!json.extends.includes("@salty-css/core")) json.extends.push("@salty-css/core");
|
|
456
|
+
return { content: JSON.stringify(json, null, 2) };
|
|
457
|
+
};
|
|
458
|
+
const eslintIntegration = {
|
|
459
|
+
name: "eslint",
|
|
460
|
+
detect: (ctx) => {
|
|
461
|
+
const candidates = eslintConfigCandidates(ctx.projectDir, ctx.cwd);
|
|
462
|
+
return candidates.find((p) => existsSync(p)) ?? null;
|
|
463
|
+
},
|
|
464
|
+
plan: async (ctx, configPath) => {
|
|
465
|
+
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
466
|
+
if (existing === void 0) {
|
|
467
|
+
logger.error("Could not read ESLint config file.");
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
const result = editEslintConfig(existing, configPath.endsWith("js"));
|
|
471
|
+
if (result.warning) logger.warn(result.warning);
|
|
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
|
+
};
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
const nextConfigFiles = ["next.config.js", "next.config.cjs", "next.config.ts", "next.config.mjs"];
|
|
486
|
+
const nextPackage = (version) => `@salty-css/next@${version}`;
|
|
487
|
+
const editNextConfig = (existing) => {
|
|
488
|
+
if (existing.includes("withSaltyCss")) return { content: null };
|
|
489
|
+
let next = existing;
|
|
490
|
+
let saltyCssAppended = false;
|
|
491
|
+
const hasPluginsArray = /\splugins([^=]*)=[^[]\[/.test(next);
|
|
492
|
+
if (hasPluginsArray) {
|
|
493
|
+
next = next.replace(/\splugins([^=]*)=[^[]\[/, (_, config) => ` plugins${config}= [withSaltyCss,`);
|
|
494
|
+
saltyCssAppended = true;
|
|
495
|
+
}
|
|
496
|
+
const useRequire = next.includes("module.exports");
|
|
497
|
+
const pluginImport = useRequire ? "const { withSaltyCss } = require('@salty-css/next');\n" : "import { withSaltyCss } from '@salty-css/next';\n";
|
|
498
|
+
if (useRequire && !saltyCssAppended) {
|
|
499
|
+
next = next.replace(/module.exports = ([^;]+)/, (_, config) => `module.exports = withSaltyCss(${config})`);
|
|
500
|
+
saltyCssAppended = true;
|
|
501
|
+
} else if (!saltyCssAppended) {
|
|
502
|
+
next = next.replace(/export default ([^;]+)/, (_, config) => `export default withSaltyCss(${config})`);
|
|
503
|
+
}
|
|
504
|
+
return { content: pluginImport + next };
|
|
505
|
+
};
|
|
506
|
+
const nextIntegration = {
|
|
507
|
+
name: "next",
|
|
508
|
+
detect: (ctx) => {
|
|
509
|
+
const found = nextConfigFiles.map((file) => join(ctx.projectDir, file)).find((p) => existsSync(p));
|
|
510
|
+
return found ?? null;
|
|
511
|
+
},
|
|
512
|
+
plan: async (ctx, configPath) => {
|
|
513
|
+
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
514
|
+
if (existing === void 0) return null;
|
|
515
|
+
const { content } = editNextConfig(existing);
|
|
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
|
+
};
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const vitePackage = (version) => `@salty-css/vite@${version}`;
|
|
529
|
+
const editViteConfig = (existing) => {
|
|
530
|
+
if (existing.includes("saltyPlugin")) return { content: null };
|
|
531
|
+
const pluginImport = "import { saltyPlugin } from '@salty-css/vite';\n";
|
|
532
|
+
const pluginConfig = "saltyPlugin(__dirname),";
|
|
533
|
+
const withPlugin = existing.replace(/(plugins: \[)/, `$1
|
|
534
|
+
${pluginConfig}`);
|
|
535
|
+
return { content: pluginImport + withPlugin };
|
|
536
|
+
};
|
|
537
|
+
const viteIntegration = {
|
|
538
|
+
name: "vite",
|
|
539
|
+
detect: (ctx) => {
|
|
540
|
+
const path = join(ctx.projectDir, "vite.config.ts");
|
|
541
|
+
return existsSync(path) ? path : null;
|
|
542
|
+
},
|
|
543
|
+
plan: async (ctx, configPath) => {
|
|
544
|
+
const existing = await readFile(configPath, "utf-8").catch(() => void 0);
|
|
545
|
+
if (existing === void 0) return null;
|
|
546
|
+
const { content } = editViteConfig(existing);
|
|
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
|
+
};
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
const buildIntegrationRegistry = [eslintIntegration, viteIntegration, nextIntegration, astroIntegration];
|
|
561
|
+
const planIntegrations = async (ctx) => {
|
|
562
|
+
const planned = [];
|
|
563
|
+
for (const integration of buildIntegrationRegistry) {
|
|
564
|
+
const configPath = await integration.detect(ctx);
|
|
565
|
+
if (!configPath) continue;
|
|
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 });
|
|
577
|
+
}
|
|
578
|
+
return results;
|
|
579
|
+
};
|
|
580
|
+
const writeProjectFile = async (projectDir, fileName, content) => {
|
|
581
|
+
const filePath = join(projectDir, fileName);
|
|
582
|
+
if (existsSync(filePath)) {
|
|
583
|
+
logger.debug("File already exists: " + filePath);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const additionalFolders = fileName.split("/").slice(0, -1).join("/");
|
|
587
|
+
if (additionalFolders) await mkdir(join(projectDir, additionalFolders), { recursive: true });
|
|
588
|
+
logger.info("Creating file: " + filePath);
|
|
589
|
+
await writeFile(filePath, content);
|
|
590
|
+
await formatWithPrettier(filePath);
|
|
591
|
+
};
|
|
592
|
+
const ensureGitignoreSaltygen = async (rootDir) => {
|
|
593
|
+
const path = join(rootDir, ".gitignore");
|
|
594
|
+
const existing = await readFile(path, "utf-8").catch(() => void 0);
|
|
595
|
+
if (existing === void 0) return;
|
|
596
|
+
if (existing.includes("saltygen")) return;
|
|
597
|
+
logger.info("Edit file: " + path);
|
|
598
|
+
await writeFile(path, existing + "\n\n# Salty-CSS\nsaltygen\n");
|
|
599
|
+
};
|
|
600
|
+
const importSaltygenIntoCss = async (projectDir, explicitCssFile) => {
|
|
601
|
+
const target = explicitCssFile ?? await findGlobalCssFile(projectDir);
|
|
602
|
+
if (!target) {
|
|
603
|
+
logger.warn("Could not find a CSS file to import the generated CSS. Please add it manually.");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const cssFilePath = join(projectDir, target);
|
|
607
|
+
const cssFileContent = await readFile(cssFilePath, "utf-8").catch(() => void 0);
|
|
608
|
+
if (cssFileContent === void 0) return;
|
|
609
|
+
if (cssFileContent.includes("saltygen")) return;
|
|
610
|
+
const cssFileFolder = join(cssFilePath, "..");
|
|
611
|
+
const relPath = relative(cssFileFolder, join(projectDir, "saltygen/index.css"));
|
|
612
|
+
logger.info("Adding global import statement to CSS file: " + cssFilePath);
|
|
613
|
+
await writeFile(cssFilePath, `@import '${relPath}';
|
|
614
|
+
` + cssFileContent);
|
|
615
|
+
await formatWithPrettier(cssFilePath);
|
|
616
|
+
};
|
|
617
|
+
const wirePrepareScript = async () => {
|
|
618
|
+
const pkg = await readPackageJson().catch(() => {
|
|
619
|
+
logError("Could not read package.json file.");
|
|
620
|
+
return void 0;
|
|
621
|
+
});
|
|
622
|
+
if (!pkg) return;
|
|
623
|
+
const { pkg: next } = addPrepareScript(pkg);
|
|
624
|
+
await updatePackageJson(next);
|
|
625
|
+
};
|
|
626
|
+
const registerInitCommand = (program) => {
|
|
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 = ".") {
|
|
628
|
+
try {
|
|
629
|
+
const opts = this.opts();
|
|
630
|
+
const dir = opts.dir ?? _dir;
|
|
631
|
+
if (!dir) return logError("Project directory must be provided. Add it as the first argument after init command or use the --dir option.");
|
|
632
|
+
const ctx = await buildContext({ dir, skipInstall: opts.skipInstall, yes: opts.yes });
|
|
633
|
+
logger.info("Initializing a new Salty-CSS project!");
|
|
634
|
+
const framework = await detectFramework(ctx);
|
|
635
|
+
logger.info(`Detected framework: ${framework.name}`);
|
|
636
|
+
const plannedIntegrations = await planIntegrations(ctx);
|
|
637
|
+
if (!ctx.skipInstall) {
|
|
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);
|
|
645
|
+
}
|
|
646
|
+
const projectFiles = await Promise.all([readTemplate("salty.config.ts"), readTemplate("saltygen/index.css")]);
|
|
647
|
+
await mkdir(ctx.projectDir, { recursive: true });
|
|
648
|
+
await Promise.all(projectFiles.map(({ fileName, content }) => writeProjectFile(ctx.projectDir, fileName, content)));
|
|
649
|
+
await writeProjectToRc(ctx.cwd, ctx.relativeProjectPath, framework);
|
|
650
|
+
await ensureGitignoreSaltygen(ctx.cwd);
|
|
651
|
+
await importSaltygenIntoCss(ctx.projectDir, opts.cssFile);
|
|
652
|
+
await applyIntegrationPlans(plannedIntegrations);
|
|
653
|
+
await wirePrepareScript();
|
|
654
|
+
logger.info("Running the build to generate initial CSS...");
|
|
655
|
+
const compiler = new SaltyCompiler(ctx.projectDir);
|
|
656
|
+
await compiler.generateCss();
|
|
657
|
+
logger.info("🎉 Salty CSS project initialized successfully!");
|
|
658
|
+
logger.info("Next steps:");
|
|
659
|
+
logger.info("1. Configure variables and templates in `salty.config.ts`");
|
|
660
|
+
logger.info("2. Create a new component with `npx salty-css generate [component-name]`");
|
|
661
|
+
logger.info("3. Run `npx salty-css build` to generate the CSS");
|
|
662
|
+
logger.info("4. Read about the features in the documentation: https://salty-css.dev");
|
|
663
|
+
logger.info("5. Star the project on GitHub: https://github.com/margarita-form/salty-css ⭐");
|
|
664
|
+
} catch (err) {
|
|
665
|
+
return logError(err instanceof Error ? err.message : String(err));
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
};
|
|
669
|
+
const getSaltyCssPackages = async () => {
|
|
670
|
+
const packageJSONPath = join(process.cwd(), "package.json");
|
|
671
|
+
const packageJson = await readPackageJson(packageJSONPath).catch((err) => logError(err));
|
|
672
|
+
if (!packageJson) return logError("Could not read package.json file.");
|
|
673
|
+
const allDependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
674
|
+
const saltyCssPackages = Object.entries(allDependencies).filter(([name]) => name === "salty-css" || name.startsWith("@salty-css/"));
|
|
675
|
+
if (!saltyCssPackages.length) {
|
|
676
|
+
return logError(
|
|
677
|
+
"No Salty-CSS packages found in package.json. Make sure you are running update command in the same directory! Used package.json path: " + packageJSONPath
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return saltyCssPackages;
|
|
681
|
+
};
|
|
682
|
+
const registerUpdateCommand = (program) => {
|
|
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();
|
|
395
685
|
const saltyCssPackages = await getSaltyCssPackages();
|
|
396
686
|
if (!saltyCssPackages) return logError("Could not update Salty-CSS packages as any were found in package.json.");
|
|
687
|
+
const cli = await readThisPackageJson();
|
|
397
688
|
const packagesToUpdate = saltyCssPackages.map(([name]) => {
|
|
398
|
-
if (version === "@") return `${name}@${
|
|
689
|
+
if (version === "@") return `${name}@${cli.version}`;
|
|
399
690
|
return `${name}@${version.replace(/^@/, "")}`;
|
|
400
691
|
});
|
|
692
|
+
try {
|
|
693
|
+
await confirmInstall(packagesToUpdate, yes);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
return logError(err instanceof Error ? err.message : String(err));
|
|
696
|
+
}
|
|
401
697
|
if (legacyPeerDeps) {
|
|
402
698
|
logger.warn("Using legacy peer dependencies to update packages.");
|
|
403
699
|
await npmInstall(...packagesToUpdate, "--legacy-peer-deps");
|
|
@@ -413,19 +709,30 @@ ${eslintConfigContent}`;
|
|
|
413
709
|
}, {});
|
|
414
710
|
const versionsCount = Object.keys(mappedByVersions).length;
|
|
415
711
|
if (versionsCount === 1) {
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
logger.info(`Updated to all Salty CSS packages successfully to ${versionString}`);
|
|
712
|
+
const v = Object.keys(mappedByVersions)[0];
|
|
713
|
+
logger.info(`Updated to all Salty CSS packages successfully to ${v.replace(/^\^/, "")}`);
|
|
419
714
|
} else {
|
|
420
|
-
for (const [
|
|
421
|
-
|
|
422
|
-
logger.info(`Updated to ${versionString}: ${names.join(", ")}`);
|
|
715
|
+
for (const [v, names] of Object.entries(mappedByVersions)) {
|
|
716
|
+
logger.info(`Updated to ${v.replace(/^\^/, "")}: ${names.join(", ")}`);
|
|
423
717
|
}
|
|
424
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.");
|
|
425
730
|
});
|
|
731
|
+
};
|
|
732
|
+
const registerVersionOption = (program) => {
|
|
426
733
|
program.option("-v, --version", "Show the current version of Salty-CSS.").action(async function() {
|
|
427
|
-
const
|
|
428
|
-
logger.info("CLI is running: " +
|
|
734
|
+
const cli = await readThisPackageJson();
|
|
735
|
+
logger.info("CLI is running: " + cli.version);
|
|
429
736
|
const packageJSONPath = join(process.cwd(), "package.json");
|
|
430
737
|
const packageJson = await readPackageJson(packageJSONPath).catch((err) => logError(err));
|
|
431
738
|
if (!packageJson) return;
|
|
@@ -440,6 +747,16 @@ ${eslintConfigContent}`;
|
|
|
440
747
|
logger.info(`${dep}: ${allDependencies[dep]}`);
|
|
441
748
|
}
|
|
442
749
|
});
|
|
750
|
+
};
|
|
751
|
+
async function main() {
|
|
752
|
+
const program = new Command();
|
|
753
|
+
program.name("salty-css").description("Salty-CSS CLI tool to help with annoying configuration tasks.");
|
|
754
|
+
const defaultProject = await getDefaultProject();
|
|
755
|
+
registerInitCommand(program);
|
|
756
|
+
registerBuildCommand(program, defaultProject);
|
|
757
|
+
registerGenerateCommand(program, defaultProject);
|
|
758
|
+
registerUpdateCommand(program);
|
|
759
|
+
registerVersionOption(program);
|
|
443
760
|
program.parseAsync(process.argv);
|
|
444
761
|
}
|
|
445
762
|
export {
|