emily-css 1.0.7 → 1.0.9
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 +2 -2
- package/bin/emilyui.js +6 -3
- package/package.json +49 -40
- package/src/index.js +86 -26
- package/src/init.js +598 -187
- package/src/purge-cmd.js +2 -2
- package/src/purge.js +1 -1
- package/src/watch.js +193 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A config-driven utility CSS framework. Define your brand once, generate the CSS.
|
|
4
4
|
|
|
5
|
-
**EmilyUI** is the ecosystem name. `emily-css` is the utility layer published on npm
|
|
5
|
+
**EmilyUI** is the ecosystem name. `emily-css` is the utility layer published on npm. It's both the package you install and the CLI you use to generate and purge CSS. More packages coming.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -277,7 +277,7 @@ npx emily-css purge
|
|
|
277
277
|
|
|
278
278
|
## Fonts
|
|
279
279
|
|
|
280
|
-
EmilyUI includes built-in support for **Inter** and **Lexend** via Google Fonts CDN. No font files to download or host
|
|
280
|
+
EmilyUI includes built-in support for **Inter** and **Lexend** via Google Fonts CDN. No font files to download or host. The generated CSS handles the import automatically.
|
|
281
281
|
|
|
282
282
|
Set `fontFamily` as an object to use different fonts for headings and body:
|
|
283
283
|
|
package/bin/emilyui.js
CHANGED
|
@@ -6,16 +6,19 @@ if (command === "init") {
|
|
|
6
6
|
require("../src/init.js");
|
|
7
7
|
} else if (command === "build") {
|
|
8
8
|
const { build } = require("../src/index.js");
|
|
9
|
-
build();
|
|
9
|
+
build({ keepFull: process.argv.includes("--keep-full") });
|
|
10
10
|
} else if (command === "purge") {
|
|
11
11
|
require("../src/purge-cmd.js");
|
|
12
|
+
} else if (command === "watch") {
|
|
13
|
+
require("../src/watch.js");
|
|
12
14
|
} else {
|
|
13
15
|
console.log(`
|
|
14
16
|
emily-css - Config-driven CSS framework generator
|
|
15
17
|
|
|
16
18
|
Usage:
|
|
17
19
|
emily-css init Set up a new project
|
|
18
|
-
emily-css build Generate emily.css
|
|
19
|
-
emily-css
|
|
20
|
+
emily-css build Generate production CSS: dist/emily.min.css
|
|
21
|
+
emily-css watch Dev mode: rebuild full CSS only when needed
|
|
22
|
+
emily-css purge Advanced: manually purge unused utilities
|
|
20
23
|
`);
|
|
21
24
|
}
|
package/package.json
CHANGED
|
@@ -1,40 +1,49 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "emily-css",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A config-driven utility CSS framework. Define your brand once, generate the CSS.",
|
|
5
|
-
"main": "src/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"emily-css": "
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin/",
|
|
11
|
-
"src/",
|
|
12
|
-
"README.md",
|
|
13
|
-
"LICENSE"
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "node src/index.js",
|
|
17
|
-
"dev": "nodemon src/index.js",
|
|
18
|
-
"init": "node src/init.js",
|
|
19
|
-
"test": "node tests/test.js"
|
|
20
|
-
},
|
|
21
|
-
"keywords": [
|
|
22
|
-
"css",
|
|
23
|
-
"design-system",
|
|
24
|
-
"components",
|
|
25
|
-
"config-driven",
|
|
26
|
-
"utility-css",
|
|
27
|
-
"accessibility",
|
|
28
|
-
"drupal",
|
|
29
|
-
"legacy",
|
|
30
|
-
"no-build-step"
|
|
31
|
-
],
|
|
32
|
-
"author": "Andy Terry",
|
|
33
|
-
"license": "MIT",
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=16.0.0"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"nodemon": "^3.0.0"
|
|
39
|
-
}
|
|
40
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "emily-css",
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "A config-driven utility CSS framework. Define your brand once, generate the CSS.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"emily-css": "bin/emilyui.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "node src/index.js",
|
|
17
|
+
"dev": "nodemon src/index.js",
|
|
18
|
+
"init": "node src/init.js",
|
|
19
|
+
"test": "node tests/test.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"css",
|
|
23
|
+
"design-system",
|
|
24
|
+
"components",
|
|
25
|
+
"config-driven",
|
|
26
|
+
"utility-css",
|
|
27
|
+
"accessibility",
|
|
28
|
+
"drupal",
|
|
29
|
+
"legacy",
|
|
30
|
+
"no-build-step"
|
|
31
|
+
],
|
|
32
|
+
"author": "Andy Terry",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"nodemon": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"boxen": "^5.1.2",
|
|
42
|
+
"chalk": "^4.1.2",
|
|
43
|
+
"chokidar": "^5.0.0",
|
|
44
|
+
"cross-spawn": "^7.0.6",
|
|
45
|
+
"emily-css": "^1.0.8",
|
|
46
|
+
"enquirer": "^2.4.1",
|
|
47
|
+
"ora": "^5.4.1"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.js
CHANGED
|
@@ -779,7 +779,7 @@ function addStateVariants(css) {
|
|
|
779
779
|
// BUILD FUNCTION
|
|
780
780
|
// ============================================================================
|
|
781
781
|
|
|
782
|
-
function
|
|
782
|
+
function buildFullFramework() {
|
|
783
783
|
const configPath = path.join(process.cwd(), 'emily.config.json');
|
|
784
784
|
if (!fs.existsSync(configPath)) {
|
|
785
785
|
console.error(`\n emily-css: No config found.\n Expected: ${configPath}\n Run "emily-css init" to create one.\n`);
|
|
@@ -787,24 +787,7 @@ function build(options = {}) {
|
|
|
787
787
|
}
|
|
788
788
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
789
789
|
|
|
790
|
-
|
|
791
|
-
const { purgeCSS } = require('./purge.js');
|
|
792
|
-
const cssPath = path.join(process.cwd(), 'dist/emily.css');
|
|
793
|
-
if (!fs.existsSync(cssPath)) {
|
|
794
|
-
console.error(' emily-css: Run "emily-css build" first.');
|
|
795
|
-
process.exit(1);
|
|
796
|
-
}
|
|
797
|
-
console.log(`Purging unused utilities from ${options.purge}...`);
|
|
798
|
-
const css = fs.readFileSync(cssPath, 'utf8');
|
|
799
|
-
const purged = purgeCSS(css, options.purge);
|
|
800
|
-
const minified = purged.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').replace(/\s?\{/g, '{').replace(/\s?\}/g, '}').replace(/;\s/g, ';').trim();
|
|
801
|
-
fs.writeFileSync(path.join(process.cwd(), 'dist/emily.purged.css'), purged);
|
|
802
|
-
fs.writeFileSync(path.join(process.cwd(), 'dist/emily.purged.min.css'), minified);
|
|
803
|
-
console.log('✓ Purged CSS: dist/emily.purged.css');
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
console.log('Building EmilyUI...');
|
|
790
|
+
console.log('Building EmilyCSS full framework...');
|
|
808
791
|
|
|
809
792
|
// Generate colours
|
|
810
793
|
const colours = generateAllColours(config.colours);
|
|
@@ -926,26 +909,103 @@ ${bodyFont}`;
|
|
|
926
909
|
fs.writeFileSync(outputPath, css);
|
|
927
910
|
console.log(`✓ Generated CSS: ${outputPath}`);
|
|
928
911
|
console.log(`✓ File size: ${(css.length / 1024).toFixed(2)} KB (unminified)`);
|
|
912
|
+
console.log('Full framework build complete');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function minify(css) {
|
|
916
|
+
return css
|
|
917
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
918
|
+
.replace(/\s+/g, ' ')
|
|
919
|
+
.replace(/\s?\{/g, '{')
|
|
920
|
+
.replace(/\s?\}/g, '}')
|
|
921
|
+
.replace(/;\s/g, ';')
|
|
922
|
+
.trim();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function getConfig() {
|
|
926
|
+
const configPath = path.join(process.cwd(), 'emily.config.json');
|
|
929
927
|
|
|
930
|
-
|
|
931
|
-
|
|
928
|
+
if (!fs.existsSync(configPath)) {
|
|
929
|
+
console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
|
|
930
|
+
process.exit(1);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function getSourceDir(config) {
|
|
937
|
+
return config.purge && config.purge.sourceDir ? config.purge.sourceDir : '.';
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function buildProductionCss() {
|
|
941
|
+
const config = getConfig();
|
|
942
|
+
const sourceDir = getSourceDir(config);
|
|
943
|
+
const cssPath = path.join(process.cwd(), 'dist/emily.css');
|
|
932
944
|
const minPath = path.join(process.cwd(), 'dist/emily.min.css');
|
|
945
|
+
|
|
946
|
+
if (!fs.existsSync(cssPath)) {
|
|
947
|
+
buildFullFramework();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const { purgeCSS } = require('./purge.js');
|
|
951
|
+
const css = fs.readFileSync(cssPath, 'utf8');
|
|
952
|
+
const purged = purgeCSS(css, sourceDir, config);
|
|
953
|
+
const minified = minify(purged);
|
|
954
|
+
|
|
933
955
|
fs.writeFileSync(minPath, minified);
|
|
934
|
-
|
|
935
|
-
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
css,
|
|
959
|
+
purged,
|
|
960
|
+
minified,
|
|
961
|
+
originalSize: Buffer.byteLength(css, 'utf8'),
|
|
962
|
+
outputSize: Buffer.byteLength(minified, 'utf8')
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function isFrameworkStale() {
|
|
967
|
+
const configPath = path.join(process.cwd(), 'emily.config.json');
|
|
968
|
+
const cssPath = path.join(process.cwd(), 'dist/emily.css');
|
|
969
|
+
|
|
970
|
+
if (!fs.existsSync(cssPath)) return true;
|
|
971
|
+
if (!fs.existsSync(configPath)) return true;
|
|
972
|
+
|
|
973
|
+
return fs.statSync(configPath).mtimeMs > fs.statSync(cssPath).mtimeMs;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function ensureFullFramework() {
|
|
977
|
+
if (isFrameworkStale()) {
|
|
978
|
+
buildFullFramework();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function build(options = {}) {
|
|
983
|
+
ensureFullFramework();
|
|
984
|
+
|
|
985
|
+
const result = buildProductionCss();
|
|
986
|
+
const cssPath = path.join(process.cwd(), 'dist/emily.css');
|
|
987
|
+
|
|
988
|
+
console.log('✓ Generated production CSS: dist/emily.min.css');
|
|
989
|
+
console.log('✓ File size: ' + (result.outputSize / 1024).toFixed(2) + ' KB');
|
|
990
|
+
|
|
991
|
+
if (!options.keepFull && fs.existsSync(cssPath)) {
|
|
992
|
+
fs.unlinkSync(cssPath);
|
|
993
|
+
console.log('Removed dist/emily.css for production build.');
|
|
994
|
+
}
|
|
936
995
|
|
|
937
996
|
console.log('Build complete');
|
|
938
997
|
}
|
|
939
998
|
|
|
940
999
|
if (require.main === module) {
|
|
941
1000
|
const args = process.argv.slice(2);
|
|
942
|
-
|
|
943
|
-
const purgeDir = purgeIndex !== -1 ? args[purgeIndex + 1] : null;
|
|
944
|
-
build(purgeDir ? { purge: purgeDir } : {});
|
|
1001
|
+
build({ keepFull: args.includes('--keep-full') });
|
|
945
1002
|
}
|
|
946
1003
|
|
|
947
1004
|
module.exports = {
|
|
948
1005
|
build,
|
|
1006
|
+
buildFullFramework,
|
|
1007
|
+
buildProductionCss,
|
|
1008
|
+
ensureFullFramework,
|
|
949
1009
|
hexToOklch,
|
|
950
1010
|
oklchToHex,
|
|
951
1011
|
generateColourScale,
|
package/src/init.js
CHANGED
|
@@ -1,256 +1,667 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const crossSpawn = require("cross-spawn");
|
|
4
|
+
const { Form, Select, Input } = require("enquirer");
|
|
5
|
+
const chalk = require("chalk");
|
|
6
|
+
const ora = require("ora");
|
|
7
|
+
const boxen = require("boxen");
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// CONSTANTS
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PURGE_IGNORE = [
|
|
14
|
+
"node_modules",
|
|
15
|
+
".git",
|
|
16
|
+
".nuxt",
|
|
17
|
+
".next",
|
|
18
|
+
".output",
|
|
19
|
+
"dist",
|
|
20
|
+
"build",
|
|
21
|
+
"coverage",
|
|
22
|
+
".cache",
|
|
23
|
+
".vite",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const DEFAULT_COLOURS = {
|
|
27
|
+
primary: "#DB2777",
|
|
28
|
+
secondary: "#2563EB",
|
|
29
|
+
success: "#017F65",
|
|
30
|
+
warning: "#FFC107",
|
|
31
|
+
error: "#B20000",
|
|
32
|
+
neutral: "#57534E",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const COLOUR_PRESETS = {
|
|
36
|
+
primary: [
|
|
37
|
+
{ value: "#DB2777", label: "Emily Pink" },
|
|
38
|
+
{ value: "#114B5F", label: "Deep Teal" },
|
|
39
|
+
{ value: "#2563EB", label: "Blue" },
|
|
40
|
+
{ value: "#017F65", label: "Green" },
|
|
41
|
+
{ value: "custom", label: "Custom hex" },
|
|
42
|
+
],
|
|
43
|
+
secondary: [
|
|
44
|
+
{ value: "#2563EB", label: "Blue" },
|
|
45
|
+
{ value: "#028090", label: "Teal" },
|
|
46
|
+
{ value: "#7C3AED", label: "Purple" },
|
|
47
|
+
{ value: "#DB2777", label: "Emily Pink" },
|
|
48
|
+
{ value: "custom", label: "Custom hex" },
|
|
49
|
+
],
|
|
50
|
+
success: [
|
|
51
|
+
{ value: "#017F65", label: "Accessible Green" },
|
|
52
|
+
{ value: "#15803D", label: "Forest Green" },
|
|
53
|
+
{ value: "#50C878", label: "Emerald" },
|
|
54
|
+
{ value: "custom", label: "Custom hex" },
|
|
55
|
+
],
|
|
56
|
+
warning: [
|
|
57
|
+
{ value: "#FFC107", label: "Amber" },
|
|
58
|
+
{ value: "#F59E0B", label: "Orange" },
|
|
59
|
+
{ value: "#FFBF00", label: "Yellow" },
|
|
60
|
+
{ value: "custom", label: "Custom hex" },
|
|
61
|
+
],
|
|
62
|
+
error: [
|
|
63
|
+
{ value: "#B20000", label: "Accessible Red" },
|
|
64
|
+
{ value: "#DC2626", label: "Red" },
|
|
65
|
+
{ value: "#F45B69", label: "Coral" },
|
|
66
|
+
{ value: "custom", label: "Custom hex" },
|
|
67
|
+
],
|
|
68
|
+
neutral: [
|
|
69
|
+
{ value: "#57534E", label: "Warm Grey" },
|
|
70
|
+
{ value: "#334155", label: "Slate" },
|
|
71
|
+
{ value: "#111827", label: "Near Black" },
|
|
72
|
+
{ value: "custom", label: "Custom hex" },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const FONT_OPTIONS = [
|
|
77
|
+
{ name: "lexend", message: "Lexend" },
|
|
78
|
+
{ name: "inter", message: "Inter" },
|
|
79
|
+
{ name: "system", message: "System sans" },
|
|
80
|
+
{ name: "georgia", message: "Georgia" },
|
|
81
|
+
{ name: "mono", message: "Monospace" },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const PURGE_EXTENSIONS = [
|
|
85
|
+
".html",
|
|
86
|
+
".htm",
|
|
87
|
+
".twig",
|
|
88
|
+
".njk",
|
|
89
|
+
".liquid",
|
|
90
|
+
".hbs",
|
|
91
|
+
".jsx",
|
|
92
|
+
".tsx",
|
|
93
|
+
".vue",
|
|
94
|
+
".php",
|
|
95
|
+
".astro",
|
|
96
|
+
".svelte",
|
|
97
|
+
".blade.php",
|
|
98
|
+
".jinja",
|
|
99
|
+
".jinja2",
|
|
100
|
+
".j2",
|
|
101
|
+
".md",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// HELPERS
|
|
106
|
+
// ============================================================================
|
|
18
107
|
|
|
19
|
-
// Validate hex colour
|
|
20
108
|
function isValidHex(hex) {
|
|
21
109
|
return /^#[0-9A-F]{6}$/i.test(hex);
|
|
22
110
|
}
|
|
23
111
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
112
|
+
function colourChoice(hex, label) {
|
|
113
|
+
if (hex === "custom") {
|
|
114
|
+
return {
|
|
115
|
+
name: "custom",
|
|
116
|
+
message: "Custom hex",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: hex,
|
|
122
|
+
message: `${chalk.hex(hex)("■")} ${label} ${chalk.gray(hex)}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function askColour(colourName) {
|
|
127
|
+
const choices = COLOUR_PRESETS[colourName].map((option) =>
|
|
128
|
+
colourChoice(option.value, option.label),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const selected = await new Select({
|
|
132
|
+
name: colourName,
|
|
133
|
+
message: `${colourName} colour`,
|
|
134
|
+
choices,
|
|
135
|
+
}).run();
|
|
136
|
+
|
|
137
|
+
if (selected !== "custom") {
|
|
138
|
+
return selected.toUpperCase();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const custom = await new Input({
|
|
142
|
+
name: `${colourName}Custom`,
|
|
143
|
+
message: `Enter custom ${colourName} hex`,
|
|
144
|
+
initial: DEFAULT_COLOURS[colourName],
|
|
145
|
+
validate(value) {
|
|
146
|
+
return isValidHex(value)
|
|
147
|
+
? true
|
|
148
|
+
: "Enter a valid hex colour, for example #0077B6";
|
|
149
|
+
},
|
|
150
|
+
}).run();
|
|
151
|
+
|
|
152
|
+
return custom.toUpperCase();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hasFile(fileName) {
|
|
156
|
+
return fs.existsSync(path.join(process.cwd(), fileName));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readPackageJson() {
|
|
160
|
+
const packagePath = path.join(process.cwd(), "package.json");
|
|
161
|
+
|
|
162
|
+
if (!fs.existsSync(packagePath)) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasDependency(packageJson, dependencyName) {
|
|
174
|
+
if (!packageJson) return false;
|
|
175
|
+
|
|
176
|
+
return Boolean(
|
|
177
|
+
packageJson.dependencies?.[dependencyName] ||
|
|
178
|
+
packageJson.devDependencies?.[dependencyName],
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function addEmilyScriptsToPackageJson() {
|
|
183
|
+
const packagePath = path.join(process.cwd(), "package.json");
|
|
184
|
+
|
|
185
|
+
if (!fs.existsSync(packagePath)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
191
|
+
|
|
192
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
193
|
+
|
|
194
|
+
let changed = false;
|
|
195
|
+
|
|
196
|
+
if (!packageJson.scripts["emily:build"]) {
|
|
197
|
+
packageJson.scripts["emily:build"] = "emily-css build";
|
|
198
|
+
changed = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!packageJson.scripts["emily:watch"]) {
|
|
202
|
+
packageJson.scripts["emily:watch"] = "emily-css watch";
|
|
203
|
+
changed = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (changed) {
|
|
207
|
+
fs.writeFileSync(
|
|
208
|
+
packagePath,
|
|
209
|
+
JSON.stringify(packageJson, null, 2) + "\n",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// PROJECT DETECTION
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
function detectProject() {
|
|
224
|
+
const packageJson = readPackageJson();
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
hasFile("nuxt.config.ts") ||
|
|
228
|
+
hasFile("nuxt.config.js") ||
|
|
229
|
+
hasDependency(packageJson, "nuxt")
|
|
230
|
+
) {
|
|
231
|
+
return {
|
|
232
|
+
name: "Nuxt",
|
|
233
|
+
sourceDir: ".",
|
|
234
|
+
sourceGlobs: [
|
|
235
|
+
"./components/**/*.{vue,js,ts}",
|
|
236
|
+
"./pages/**/*.vue",
|
|
237
|
+
"./layouts/**/*.vue",
|
|
238
|
+
"./app.vue",
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (hasDependency(packageJson, "next")) {
|
|
244
|
+
return {
|
|
245
|
+
name: "Next.js",
|
|
246
|
+
sourceDir: ".",
|
|
247
|
+
sourceGlobs: [
|
|
248
|
+
"./app/**/*.{js,jsx,ts,tsx}",
|
|
249
|
+
"./pages/**/*.{js,jsx,ts,tsx}",
|
|
250
|
+
"./components/**/*.{js,jsx,ts,tsx}",
|
|
251
|
+
"./src/**/*.{js,jsx,ts,tsx}",
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (hasDependency(packageJson, "react")) {
|
|
257
|
+
return {
|
|
258
|
+
name: "React",
|
|
259
|
+
sourceDir: "./src",
|
|
260
|
+
sourceGlobs: [
|
|
261
|
+
"./src/**/*.{js,jsx,ts,tsx}",
|
|
262
|
+
"./components/**/*.{js,jsx,ts,tsx}",
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
hasDependency(packageJson, "vue") ||
|
|
269
|
+
hasFile("vite.config.ts") ||
|
|
270
|
+
hasFile("vite.config.js")
|
|
271
|
+
) {
|
|
272
|
+
return {
|
|
273
|
+
name: "Vue/Vite",
|
|
274
|
+
sourceDir: "./src",
|
|
275
|
+
sourceGlobs: ["./src/**/*.{vue,js,ts}"],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (hasDependency(packageJson, "astro") || hasFile("astro.config.mjs")) {
|
|
280
|
+
return {
|
|
281
|
+
name: "Astro",
|
|
282
|
+
sourceDir: "./src",
|
|
283
|
+
sourceGlobs: ["./src/**/*.{astro,html,js,ts,vue,jsx,tsx,svelte}"],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const rootFiles = fs.readdirSync(process.cwd());
|
|
288
|
+
const hasDrupalInfoFile = rootFiles.some((file) =>
|
|
289
|
+
file.endsWith(".info.yml"),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (
|
|
293
|
+
hasDrupalInfoFile ||
|
|
294
|
+
fs.existsSync(path.join(process.cwd(), "web/core"))
|
|
295
|
+
) {
|
|
296
|
+
return {
|
|
297
|
+
name: "Drupal",
|
|
298
|
+
sourceDir: ".",
|
|
299
|
+
sourceGlobs: [
|
|
300
|
+
"./web/themes/custom/**/*.{twig,js,ts}",
|
|
301
|
+
"./templates/**/*.html.twig",
|
|
302
|
+
"./components/**/*.twig",
|
|
303
|
+
"./**/*.theme",
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
33
307
|
|
|
34
|
-
|
|
35
|
-
|
|
308
|
+
return {
|
|
309
|
+
name: "Static/Generic",
|
|
310
|
+
sourceDir: ".",
|
|
311
|
+
sourceGlobs: [
|
|
312
|
+
"./**/*.{html,htm,twig,njk,liquid,hbs,php,astro,svelte,vue,js,ts}",
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ============================================================================
|
|
318
|
+
// CONFIG BUILDER
|
|
319
|
+
// ============================================================================
|
|
320
|
+
|
|
321
|
+
function createDefaultConfig({
|
|
322
|
+
name,
|
|
323
|
+
colours,
|
|
324
|
+
headingFont,
|
|
325
|
+
bodyFont,
|
|
326
|
+
monoFont,
|
|
327
|
+
baseUnit,
|
|
328
|
+
detectedProject,
|
|
329
|
+
sourceDir,
|
|
330
|
+
}) {
|
|
36
331
|
return {
|
|
37
332
|
name,
|
|
38
333
|
description: `${name} design system`,
|
|
334
|
+
|
|
39
335
|
baseUnit: `${baseUnit}px`,
|
|
40
|
-
baseFontSize:
|
|
41
|
-
|
|
336
|
+
baseFontSize: "16px",
|
|
337
|
+
|
|
338
|
+
fontFamily: {
|
|
339
|
+
heading: headingFont,
|
|
340
|
+
body: bodyFont,
|
|
341
|
+
mono: monoFont,
|
|
342
|
+
},
|
|
343
|
+
|
|
42
344
|
customFonts: [],
|
|
345
|
+
|
|
43
346
|
colours,
|
|
347
|
+
|
|
348
|
+
purge: {
|
|
349
|
+
projectType: detectedProject.name,
|
|
350
|
+
sourceDir,
|
|
351
|
+
sourceGlobs: detectedProject.sourceGlobs,
|
|
352
|
+
ignore: DEFAULT_PURGE_IGNORE,
|
|
353
|
+
extensions: PURGE_EXTENSIONS,
|
|
354
|
+
},
|
|
355
|
+
|
|
44
356
|
breakpoints: {
|
|
45
|
-
sm:
|
|
46
|
-
md:
|
|
47
|
-
lg:
|
|
48
|
-
xl:
|
|
49
|
-
|
|
357
|
+
sm: "640px",
|
|
358
|
+
md: "768px",
|
|
359
|
+
lg: "1024px",
|
|
360
|
+
xl: "1280px",
|
|
361
|
+
"2xl": "1536px",
|
|
50
362
|
},
|
|
363
|
+
|
|
51
364
|
spacing: {
|
|
52
365
|
scale: {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
366
|
+
0: "0px",
|
|
367
|
+
px: "1px",
|
|
368
|
+
0.5: "0.125rem",
|
|
369
|
+
1: "0.25rem",
|
|
370
|
+
1.5: "0.375rem",
|
|
371
|
+
2: "0.5rem",
|
|
372
|
+
2.5: "0.625rem",
|
|
373
|
+
3: "0.75rem",
|
|
374
|
+
3.5: "0.875rem",
|
|
375
|
+
4: "1rem",
|
|
376
|
+
5: "1.25rem",
|
|
377
|
+
6: "1.5rem",
|
|
378
|
+
7: "1.75rem",
|
|
379
|
+
8: "2rem",
|
|
380
|
+
9: "2.25rem",
|
|
381
|
+
10: "2.5rem",
|
|
382
|
+
11: "2.75rem",
|
|
383
|
+
12: "3rem",
|
|
384
|
+
14: "3.5rem",
|
|
385
|
+
16: "4rem",
|
|
386
|
+
20: "5rem",
|
|
387
|
+
24: "6rem",
|
|
388
|
+
28: "7rem",
|
|
389
|
+
32: "8rem",
|
|
390
|
+
36: "9rem",
|
|
391
|
+
40: "10rem",
|
|
392
|
+
44: "11rem",
|
|
393
|
+
48: "12rem",
|
|
394
|
+
52: "13rem",
|
|
395
|
+
56: "14rem",
|
|
396
|
+
60: "15rem",
|
|
397
|
+
64: "16rem",
|
|
398
|
+
72: "18rem",
|
|
399
|
+
80: "20rem",
|
|
400
|
+
96: "24rem",
|
|
88
401
|
},
|
|
402
|
+
|
|
89
403
|
borderWidths: [0, 2, 4, 8],
|
|
404
|
+
|
|
90
405
|
borderRadius: {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
406
|
+
none: "0",
|
|
407
|
+
sm: "4px",
|
|
408
|
+
base: "8px",
|
|
409
|
+
md: "12px",
|
|
410
|
+
lg: "16px",
|
|
411
|
+
full: "9999px",
|
|
412
|
+
},
|
|
98
413
|
},
|
|
414
|
+
|
|
99
415
|
typography: {
|
|
100
416
|
lineHeightRatio: 1.5,
|
|
417
|
+
|
|
101
418
|
fontWeights: {
|
|
102
419
|
light: 300,
|
|
103
420
|
normal: 400,
|
|
104
421
|
medium: 500,
|
|
105
422
|
semibold: 600,
|
|
106
|
-
bold: 700
|
|
423
|
+
bold: 700,
|
|
107
424
|
},
|
|
425
|
+
|
|
108
426
|
fontSizes: [
|
|
109
|
-
{ name:
|
|
110
|
-
{ name:
|
|
111
|
-
{ name:
|
|
112
|
-
{ name:
|
|
113
|
-
{ name:
|
|
114
|
-
{ name:
|
|
115
|
-
{ name:
|
|
116
|
-
{ name:
|
|
117
|
-
]
|
|
427
|
+
{ name: "xs", value: "12px", lineHeight: 1.5 },
|
|
428
|
+
{ name: "sm", value: "14px", lineHeight: 1.5 },
|
|
429
|
+
{ name: "base", value: "16px", lineHeight: 1.6 },
|
|
430
|
+
{ name: "lg", value: "18px", lineHeight: 1.6 },
|
|
431
|
+
{ name: "xl", value: "20px", lineHeight: 1.6 },
|
|
432
|
+
{ name: "2xl", value: "24px", lineHeight: 1.4 },
|
|
433
|
+
{ name: "3xl", value: "30px", lineHeight: 1.4 },
|
|
434
|
+
{ name: "4xl", value: "36px", lineHeight: 1.3 },
|
|
435
|
+
],
|
|
118
436
|
},
|
|
437
|
+
|
|
119
438
|
shadows: {
|
|
120
|
-
sm:
|
|
121
|
-
base:
|
|
122
|
-
md:
|
|
123
|
-
lg:
|
|
124
|
-
none:
|
|
439
|
+
sm: "0 1px 2px rgba(0, 0, 0, 0.05)",
|
|
440
|
+
base: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
|
441
|
+
md: "0 10px 15px rgba(0, 0, 0, 0.1)",
|
|
442
|
+
lg: "0 20px 25px rgba(0, 0, 0, 0.15)",
|
|
443
|
+
none: "none",
|
|
125
444
|
},
|
|
445
|
+
|
|
126
446
|
transitions: {
|
|
127
|
-
fast:
|
|
128
|
-
base:
|
|
129
|
-
slow:
|
|
130
|
-
timing:
|
|
447
|
+
fast: "100ms",
|
|
448
|
+
base: "200ms",
|
|
449
|
+
slow: "300ms",
|
|
450
|
+
timing: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
131
451
|
},
|
|
452
|
+
|
|
132
453
|
zIndex: {
|
|
133
|
-
auto:
|
|
134
|
-
0:
|
|
135
|
-
10:
|
|
136
|
-
20:
|
|
137
|
-
30:
|
|
138
|
-
40:
|
|
139
|
-
50:
|
|
140
|
-
dropdown:
|
|
141
|
-
sticky:
|
|
142
|
-
fixed:
|
|
143
|
-
modal:
|
|
144
|
-
popover:
|
|
145
|
-
tooltip:
|
|
454
|
+
auto: "auto",
|
|
455
|
+
0: "0",
|
|
456
|
+
10: "10",
|
|
457
|
+
20: "20",
|
|
458
|
+
30: "30",
|
|
459
|
+
40: "40",
|
|
460
|
+
50: "50",
|
|
461
|
+
dropdown: "1000",
|
|
462
|
+
sticky: "1020",
|
|
463
|
+
fixed: "1030",
|
|
464
|
+
modal: "1040",
|
|
465
|
+
popover: "1060",
|
|
466
|
+
tooltip: "1070",
|
|
146
467
|
},
|
|
147
|
-
|
|
468
|
+
|
|
469
|
+
opacity: [0, 5, 10, 25, 50, 75, 90, 95, 100],
|
|
148
470
|
};
|
|
149
471
|
}
|
|
150
472
|
|
|
473
|
+
// ============================================================================
|
|
474
|
+
// INIT
|
|
475
|
+
// ============================================================================
|
|
476
|
+
|
|
151
477
|
async function init() {
|
|
152
|
-
console.log(
|
|
153
|
-
|
|
154
|
-
|
|
478
|
+
console.log(
|
|
479
|
+
chalk.bold.magenta("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"),
|
|
480
|
+
);
|
|
481
|
+
console.log(chalk.bold.magenta(" EmilyUI Setup"));
|
|
482
|
+
console.log(
|
|
483
|
+
chalk.bold.magenta("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"),
|
|
484
|
+
);
|
|
155
485
|
|
|
156
486
|
try {
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
487
|
+
const spinner = ora("Analysing project structure...").start();
|
|
488
|
+
const detectedProject = detectProject();
|
|
489
|
+
spinner.succeed(`Detected project: ${chalk.cyan(detectedProject.name)}`);
|
|
490
|
+
|
|
491
|
+
const { projectName } = await new Form({
|
|
492
|
+
name: "project",
|
|
493
|
+
message: "Project details",
|
|
494
|
+
choices: [
|
|
495
|
+
{
|
|
496
|
+
name: "projectName",
|
|
497
|
+
message: "Project name",
|
|
498
|
+
initial: "My Design System",
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
}).run();
|
|
502
|
+
|
|
503
|
+
if (!projectName || !projectName.trim()) {
|
|
504
|
+
console.log(chalk.red("\nProject name is required.\n"));
|
|
505
|
+
process.exit(1);
|
|
163
506
|
}
|
|
164
507
|
|
|
165
|
-
|
|
166
|
-
|
|
508
|
+
console.log(chalk.bold(`\n${chalk.magenta("→")} Brand colours`));
|
|
509
|
+
|
|
510
|
+
const normalisedColours = {};
|
|
511
|
+
|
|
512
|
+
for (const colourName of Object.keys(DEFAULT_COLOURS)) {
|
|
513
|
+
normalisedColours[colourName] = await askColour(colourName);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
console.log(chalk.bold(`\n${chalk.magenta("→")} Typography`));
|
|
517
|
+
|
|
518
|
+
const headingFont = await new Select({
|
|
519
|
+
name: "headingFont",
|
|
520
|
+
message: "Heading font",
|
|
521
|
+
choices: FONT_OPTIONS,
|
|
522
|
+
initial: 0,
|
|
523
|
+
}).run();
|
|
167
524
|
|
|
168
|
-
const
|
|
169
|
-
|
|
525
|
+
const bodyFont = await new Select({
|
|
526
|
+
name: "bodyFont",
|
|
527
|
+
message: "Body font",
|
|
528
|
+
choices: FONT_OPTIONS,
|
|
529
|
+
initial: 1,
|
|
530
|
+
}).run();
|
|
170
531
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
532
|
+
const monoFont = await new Select({
|
|
533
|
+
name: "monoFont",
|
|
534
|
+
message: "Monospace font",
|
|
535
|
+
choices: FONT_OPTIONS,
|
|
536
|
+
initial: 4,
|
|
537
|
+
}).run();
|
|
174
538
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
539
|
+
const { baseUnitInput } = await new Form({
|
|
540
|
+
name: "spacing",
|
|
541
|
+
message: "Spacing",
|
|
542
|
+
choices: [
|
|
543
|
+
{
|
|
544
|
+
name: "baseUnitInput",
|
|
545
|
+
message: "Base spacing unit in px",
|
|
546
|
+
initial: "8",
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
validate(values) {
|
|
550
|
+
const parsed = Number.parseInt(values.baseUnitInput, 10);
|
|
178
551
|
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
valid = true;
|
|
182
|
-
} else {
|
|
183
|
-
console.log(` ❌ Invalid hex colour. Use format: #0077b6`);
|
|
552
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
553
|
+
return "Base spacing unit must be a positive number.";
|
|
184
554
|
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
555
|
|
|
188
|
-
|
|
189
|
-
|
|
556
|
+
return true;
|
|
557
|
+
},
|
|
558
|
+
}).run();
|
|
190
559
|
|
|
191
|
-
const
|
|
192
|
-
sans: await prompt(' Sans-serif font: ') || 'system-ui, -apple-system, sans-serif',
|
|
193
|
-
serif: await prompt(' Serif font: ') || 'Georgia, serif',
|
|
194
|
-
mono: await prompt(' Monospace font: ') || 'Menlo, Monaco, monospace'
|
|
195
|
-
};
|
|
560
|
+
const baseUnit = Number.parseInt(baseUnitInput, 10);
|
|
196
561
|
|
|
197
|
-
|
|
198
|
-
let baseUnit = 8;
|
|
199
|
-
const baseUnitInput = await prompt('\nBase spacing unit (px) [8]: ');
|
|
200
|
-
if (baseUnitInput.trim()) {
|
|
201
|
-
const parsed = parseInt(baseUnitInput);
|
|
202
|
-
if (!isNaN(parsed) && parsed > 0) {
|
|
203
|
-
baseUnit = parsed;
|
|
204
|
-
} else {
|
|
205
|
-
console.log(' ⚠️ Invalid number, using default: 8px');
|
|
206
|
-
}
|
|
207
|
-
}
|
|
562
|
+
console.log(chalk.bold(`\n${chalk.magenta("→")} Purge settings`));
|
|
208
563
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
564
|
+
const { sourceDir } = await new Form({
|
|
565
|
+
name: "paths",
|
|
566
|
+
message: `Detected ${detectedProject.name} project`,
|
|
567
|
+
choices: [
|
|
568
|
+
{
|
|
569
|
+
name: "sourceDir",
|
|
570
|
+
message: "Scan directory",
|
|
571
|
+
initial: detectedProject.sourceDir,
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
}).run();
|
|
213
575
|
|
|
214
|
-
|
|
215
|
-
|
|
576
|
+
const config = createDefaultConfig({
|
|
577
|
+
name: projectName.trim(),
|
|
578
|
+
colours: normalisedColours,
|
|
579
|
+
headingFont,
|
|
580
|
+
bodyFont,
|
|
581
|
+
monoFont,
|
|
582
|
+
baseUnit,
|
|
583
|
+
detectedProject,
|
|
584
|
+
sourceDir: sourceDir.trim() || detectedProject.sourceDir,
|
|
585
|
+
});
|
|
216
586
|
|
|
217
|
-
|
|
218
|
-
const configPath = path.join(process.cwd(), 'emily.config.json');
|
|
587
|
+
const configPath = path.join(process.cwd(), "emily.config.json");
|
|
219
588
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
220
589
|
|
|
221
|
-
console.log(
|
|
222
|
-
|
|
223
|
-
console.log(` Primary colour: ${colours.primary}`);
|
|
224
|
-
console.log(` Base unit: ${baseUnit}px`);
|
|
225
|
-
console.log(`\n📝 Config saved: ${configPath}`);
|
|
590
|
+
console.log("");
|
|
591
|
+
const buildSpinner = ora("Building EmilyUI CSS...").start();
|
|
226
592
|
|
|
227
|
-
|
|
228
|
-
console.log('\n🔨 Building CSS...\n');
|
|
229
|
-
rl.close();
|
|
230
|
-
|
|
231
|
-
// Spawn build process
|
|
232
|
-
const { spawn } = require('child_process');
|
|
233
|
-
const build = spawn('npx', ['emily-css', 'build'], {
|
|
593
|
+
const build = crossSpawn("npx", ["emily-css", "build"], {
|
|
234
594
|
cwd: process.cwd(),
|
|
235
|
-
stdio:
|
|
595
|
+
stdio: "pipe",
|
|
596
|
+
shell: process.platform === "win32",
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
let stderr = "";
|
|
600
|
+
|
|
601
|
+
build.stderr.on("data", (data) => {
|
|
602
|
+
stderr += data.toString();
|
|
236
603
|
});
|
|
237
604
|
|
|
238
|
-
build.on(
|
|
605
|
+
build.on("close", (code) => {
|
|
239
606
|
if (code === 0) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
console.log(
|
|
245
|
-
|
|
607
|
+
buildSpinner.succeed("EmilyUI CSS built successfully.");
|
|
608
|
+
|
|
609
|
+
const scriptsAdded = addEmilyScriptsToPackageJson();
|
|
610
|
+
|
|
611
|
+
console.log(
|
|
612
|
+
"\n" +
|
|
613
|
+
boxen(
|
|
614
|
+
chalk.green.bold("Setup complete") +
|
|
615
|
+
`\n\nConfig: ${chalk.cyan("emily.config.json")}` +
|
|
616
|
+
`\nOutput: ${chalk.cyan("dist/emily.min.css")}` +
|
|
617
|
+
`\nProject: ${chalk.cyan(detectedProject.name)}` +
|
|
618
|
+
`\nScan: ${chalk.cyan(config.purge.sourceDir)}` +
|
|
619
|
+
`\n\nNext: add ${chalk.yellow("dist/emily.min.css")} to your project.` +
|
|
620
|
+
(scriptsAdded
|
|
621
|
+
? `\n\nScripts:\n${chalk.cyan("npm run emily:build")}\n${chalk.cyan("npm run emily:watch")}`
|
|
622
|
+
: ""),
|
|
623
|
+
{
|
|
624
|
+
padding: 1,
|
|
625
|
+
margin: 1,
|
|
626
|
+
borderStyle: "round",
|
|
627
|
+
borderColor: "magenta",
|
|
628
|
+
},
|
|
629
|
+
),
|
|
630
|
+
);
|
|
246
631
|
} else {
|
|
247
|
-
|
|
632
|
+
buildSpinner.fail("Automatic build failed.");
|
|
633
|
+
|
|
634
|
+
console.log("\nYour config was created, but CSS was not built.");
|
|
635
|
+
console.log("\nRun this manually:\n");
|
|
636
|
+
console.log(chalk.cyan(" npx emily-css build"));
|
|
637
|
+
|
|
638
|
+
if (stderr.trim()) {
|
|
639
|
+
console.log(chalk.gray("\nBuild error:\n"));
|
|
640
|
+
console.log(stderr.trim());
|
|
641
|
+
}
|
|
248
642
|
}
|
|
643
|
+
|
|
644
|
+
process.exit(code === 0 ? 0 : 1);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
build.on("error", (error) => {
|
|
648
|
+
buildSpinner.fail("Automatic build failed.");
|
|
649
|
+
|
|
650
|
+
console.log("\nYour config was created, but CSS was not built.");
|
|
651
|
+
console.log(`Reason: ${error.message}`);
|
|
652
|
+
console.log("\nRun this manually:\n");
|
|
653
|
+
console.log(chalk.cyan(" npx emily-css build\n"));
|
|
654
|
+
|
|
655
|
+
process.exit(1);
|
|
249
656
|
});
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.log(chalk.red("\nSetup cancelled or failed."));
|
|
659
|
+
|
|
660
|
+
if (error && error.message) {
|
|
661
|
+
console.log(chalk.gray(error.message));
|
|
662
|
+
}
|
|
250
663
|
|
|
251
|
-
|
|
252
|
-
console.log(`\n❌ Error: ${err.message}`);
|
|
253
|
-
rl.close();
|
|
664
|
+
process.exit(1);
|
|
254
665
|
}
|
|
255
666
|
}
|
|
256
667
|
|
package/src/purge-cmd.js
CHANGED
|
@@ -30,7 +30,7 @@ function runPurge() {
|
|
|
30
30
|
console.log('\nPurging unused utilities from ' + sourceDir + '...');
|
|
31
31
|
|
|
32
32
|
const css = fs.readFileSync(cssPath, 'utf8');
|
|
33
|
-
const purged = purgeCSS(css, sourceDir);
|
|
33
|
+
const purged = purgeCSS(css, sourceDir, config);
|
|
34
34
|
const minified = purged
|
|
35
35
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
36
36
|
.replace(/\s+/g, ' ')
|
|
@@ -52,4 +52,4 @@ function runPurge() {
|
|
|
52
52
|
console.log('\n ' + Math.round(original / 1024) + 'KB -> ' + Math.round(purgedSize / 1024) + 'KB (' + reduction + '% reduction)\n');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
runPurge();
|
|
55
|
+
runPurge();
|
package/src/purge.js
CHANGED
|
@@ -119,7 +119,7 @@ function purgeBlock(block, usedClasses) {
|
|
|
119
119
|
if (!selector.includes('.')) return true;
|
|
120
120
|
|
|
121
121
|
for (const used of usedClasses) {
|
|
122
|
-
const escapedUsed = used.replace(
|
|
122
|
+
const escapedUsed = used.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/:/g, '\\\\:');
|
|
123
123
|
const boundaryRegex = new RegExp(`\\.${escapedUsed}(?::[\\w\\-]+|[\\s,>+~]|$)`);
|
|
124
124
|
if (boundaryRegex.test(selector)) return true;
|
|
125
125
|
}
|
package/src/watch.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chokidar = require('chokidar');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { buildFullFramework, buildProductionCss, ensureFullFramework } = require('./index.js');
|
|
6
|
+
const { getAllFiles, extractClassNames } = require('./purge.js');
|
|
7
|
+
|
|
8
|
+
let isRunning = false;
|
|
9
|
+
let pendingRun = false;
|
|
10
|
+
let previousClasses = new Set();
|
|
11
|
+
let hasRunOnce = false;
|
|
12
|
+
|
|
13
|
+
function readConfig() {
|
|
14
|
+
const configPath = path.join(process.cwd(), 'emily.config.json');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(configPath)) {
|
|
17
|
+
console.error('\n emily-css: No config found. Run "emily-css init" first.\n');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shouldIgnore(filePath) {
|
|
25
|
+
const normalised = filePath.replace(/\\/g, '/');
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
'node_modules/',
|
|
29
|
+
'.git/',
|
|
30
|
+
'.nuxt/',
|
|
31
|
+
'.next/',
|
|
32
|
+
'.output/',
|
|
33
|
+
'dist/',
|
|
34
|
+
'build/',
|
|
35
|
+
'coverage/',
|
|
36
|
+
'.cache/',
|
|
37
|
+
'.vite/'
|
|
38
|
+
].some(part => normalised.includes('/' + part) || normalised.startsWith(part));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runQuietly(fn) {
|
|
42
|
+
const originalLog = console.log;
|
|
43
|
+
const originalWarn = console.warn;
|
|
44
|
+
|
|
45
|
+
console.log = () => {};
|
|
46
|
+
console.warn = () => {};
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return fn();
|
|
50
|
+
} finally {
|
|
51
|
+
console.log = originalLog;
|
|
52
|
+
console.warn = originalWarn;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function collectUsedClasses(sourceDir, config) {
|
|
57
|
+
const files = getAllFiles(sourceDir, config.purge?.extensions);
|
|
58
|
+
const usedClasses = new Set();
|
|
59
|
+
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
if (shouldIgnore(file)) continue;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
65
|
+
extractClassNames(content).forEach(cls => usedClasses.add(cls));
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return usedClasses;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getClassDiff(currentClasses) {
|
|
73
|
+
const added = [...currentClasses].filter(cls => !previousClasses.has(cls));
|
|
74
|
+
const removed = [...previousClasses].filter(cls => !currentClasses.has(cls));
|
|
75
|
+
|
|
76
|
+
previousClasses = new Set(currentClasses);
|
|
77
|
+
|
|
78
|
+
return { added, removed };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatClassList(classes) {
|
|
82
|
+
if (classes.length === 0) return '';
|
|
83
|
+
|
|
84
|
+
const shown = classes.slice(0, 8).join(', ');
|
|
85
|
+
const extra = classes.length > 8 ? ' +' + (classes.length - 8) + ' more' : '';
|
|
86
|
+
|
|
87
|
+
return shown + extra;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printSummary({ currentClasses, result, added, removed }) {
|
|
91
|
+
const reduction = (((result.originalSize - result.outputSize) / result.originalSize) * 100).toFixed(1);
|
|
92
|
+
const sizeKb = (result.outputSize / 1024).toFixed(1);
|
|
93
|
+
const time = new Date().toLocaleTimeString();
|
|
94
|
+
|
|
95
|
+
console.log(
|
|
96
|
+
chalk.green('✓ ' + time + ' updated') +
|
|
97
|
+
chalk.gray(' | ' + currentClasses.size + ' classes | ' + sizeKb + ' KB | ' + reduction + '% reduced')
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (!hasRunOnce) return;
|
|
101
|
+
|
|
102
|
+
if (removed.length > 0) {
|
|
103
|
+
console.log(chalk.red('− removed ' + removed.length + ' class' + (removed.length === 1 ? '' : 'es')) + chalk.gray(' (' + formatClassList(removed) + ')'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (added.length > 0) {
|
|
107
|
+
console.log(chalk.green('+ added ' + added.length + ' class' + (added.length === 1 ? '' : 'es')) + chalk.gray(' (' + formatClassList(added) + ')'));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runProductionUpdate(filePath) {
|
|
112
|
+
if (isRunning) {
|
|
113
|
+
pendingRun = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
isRunning = true;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const config = readConfig();
|
|
121
|
+
const sourceDir = config.purge?.sourceDir || '.';
|
|
122
|
+
const isConfigChange = filePath && filePath.replace(/\\/g, '/').endsWith('emily.config.json');
|
|
123
|
+
const cssPath = path.join(process.cwd(), 'dist/emily.css');
|
|
124
|
+
|
|
125
|
+
if (isConfigChange) {
|
|
126
|
+
runQuietly(() => buildFullFramework());
|
|
127
|
+
} else {
|
|
128
|
+
runQuietly(() => ensureFullFramework());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const result = runQuietly(() => buildProductionCss());
|
|
132
|
+
const currentClasses = collectUsedClasses(sourceDir, config);
|
|
133
|
+
const { added, removed } = getClassDiff(currentClasses);
|
|
134
|
+
|
|
135
|
+
printSummary({ currentClasses, result, added, removed });
|
|
136
|
+
|
|
137
|
+
hasRunOnce = true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('\n❌ EmilyUI watch failed');
|
|
140
|
+
console.error(error.message);
|
|
141
|
+
} finally {
|
|
142
|
+
isRunning = false;
|
|
143
|
+
|
|
144
|
+
if (pendingRun) {
|
|
145
|
+
pendingRun = false;
|
|
146
|
+
runProductionUpdate();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getWatchPaths(config) {
|
|
152
|
+
return [
|
|
153
|
+
config.purge?.sourceDir || '.',
|
|
154
|
+
'emily.config.json'
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function queueUpdate(filePath) {
|
|
159
|
+
if (filePath && shouldIgnore(filePath)) return;
|
|
160
|
+
runProductionUpdate(filePath);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function runWatch() {
|
|
164
|
+
const config = readConfig();
|
|
165
|
+
const watchPaths = getWatchPaths(config);
|
|
166
|
+
|
|
167
|
+
console.log('\n👀 EmilyUI is watching...');
|
|
168
|
+
console.log(chalk.gray(' Watching:'));
|
|
169
|
+
watchPaths.forEach(item => console.log(chalk.gray(' - ' + item)));
|
|
170
|
+
|
|
171
|
+
runQuietly(() => ensureFullFramework());
|
|
172
|
+
runProductionUpdate();
|
|
173
|
+
|
|
174
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
175
|
+
ignored: shouldIgnore,
|
|
176
|
+
ignoreInitial: true,
|
|
177
|
+
awaitWriteFinish: {
|
|
178
|
+
stabilityThreshold: 500,
|
|
179
|
+
pollInterval: 100
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
watcher.on('change', queueUpdate);
|
|
184
|
+
watcher.on('add', queueUpdate);
|
|
185
|
+
watcher.on('unlink', queueUpdate);
|
|
186
|
+
|
|
187
|
+
watcher.on('error', error => {
|
|
188
|
+
console.error('\n❌ EmilyUI watcher error');
|
|
189
|
+
console.error(error.message);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
runWatch();
|