@finggujadhav/compiler 0.9.0
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/cli.js +86 -0
- package/docs/policy.md +40 -0
- package/engine.js +189 -0
- package/package.json +27 -0
- package/scanner.js +49 -0
package/cli.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FingguFlux CLI
|
|
4
|
+
* Phase 6A: Compiler CLI Foundation
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { scanFiles, getProjectFiles } from './scanner.js';
|
|
9
|
+
import { CompilerEngine } from './engine.js';
|
|
10
|
+
|
|
11
|
+
// Basic named argument parser
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const getArg = (flag) => {
|
|
14
|
+
const idx = args.indexOf(flag);
|
|
15
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mode = getArg('--mode') || 'dev';
|
|
19
|
+
const inputDir = getArg('--input') || './';
|
|
20
|
+
const outputDir = getArg('--output') || './dist';
|
|
21
|
+
|
|
22
|
+
async function runBuild() {
|
|
23
|
+
console.log(`\nš FingguFlux Compiler [Mode: ${mode.toUpperCase()}]`);
|
|
24
|
+
|
|
25
|
+
// 1. Scan for used classes
|
|
26
|
+
console.log(`š Scanning files in ${inputDir}...`);
|
|
27
|
+
const files = getProjectFiles(path.resolve(inputDir));
|
|
28
|
+
const usedClasses = scanFiles(files);
|
|
29
|
+
console.log(`ā
Found ${usedClasses.length} used FingguFlux classes.`);
|
|
30
|
+
|
|
31
|
+
// 2. Initialize Engine
|
|
32
|
+
const engine = new CompilerEngine({ mode });
|
|
33
|
+
engine.setUsedClasses(usedClasses);
|
|
34
|
+
|
|
35
|
+
// 3. Collect CSS source files
|
|
36
|
+
let combinedCSS = '';
|
|
37
|
+
const possiblePaths = [
|
|
38
|
+
path.resolve('./node_modules/@finggujadhav/core'),
|
|
39
|
+
path.resolve('./packages/core'),
|
|
40
|
+
path.resolve('../../packages/core')
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
let corePath = possiblePaths.find(p => fs.existsSync(p));
|
|
44
|
+
if (!corePath) throw new Error('Could not find @finggujadhav/core CSS sources.');
|
|
45
|
+
|
|
46
|
+
const collectCSS = (dir) => {
|
|
47
|
+
if (!fs.existsSync(dir)) return;
|
|
48
|
+
const list = fs.readdirSync(dir);
|
|
49
|
+
list.forEach(file => {
|
|
50
|
+
const fullPath = path.join(dir, file);
|
|
51
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
52
|
+
if (file === 'components') collectCSS(fullPath);
|
|
53
|
+
} else if (path.extname(file) === '.css' && file !== 'index.css') {
|
|
54
|
+
combinedCSS += fs.readFileSync(fullPath, 'utf8') + '\n';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
collectCSS(corePath);
|
|
60
|
+
|
|
61
|
+
// 4. Process CSS
|
|
62
|
+
console.log(`š ļø Processing CSS and applying tree-shaking...`);
|
|
63
|
+
const finalCSS = engine.processCSS(combinedCSS);
|
|
64
|
+
const mapping = engine.getMapping();
|
|
65
|
+
|
|
66
|
+
// 5. Output
|
|
67
|
+
if (!fs.existsSync(outputDir)) {
|
|
68
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cssPath = path.join(outputDir, 'finggu.css');
|
|
72
|
+
const mappingPath = path.join(outputDir, 'mapping.json');
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(cssPath, finalCSS);
|
|
75
|
+
fs.writeFileSync(mappingPath, JSON.stringify(mapping, null, 2));
|
|
76
|
+
|
|
77
|
+
console.log(`\nš¦ Build Complete!`);
|
|
78
|
+
console.log(`- CSS: ${cssPath} (${Buffer.byteLength(finalCSS)} bytes)`);
|
|
79
|
+
console.log(`- Mapping: ${mappingPath}`);
|
|
80
|
+
console.log(`- Mode: ${mode}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
runBuild().catch(err => {
|
|
84
|
+
console.error('Build failed:', err);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
package/docs/policy.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# FingguFlux Compiler: Explicit Class Policy
|
|
2
|
+
|
|
3
|
+
To ensure 100% reliable tree-shaking and deterministic hashing, developers must adhere to the following class usage policy.
|
|
4
|
+
|
|
5
|
+
## 1. Explicit Class Strings
|
|
6
|
+
The compiler uses static analysis (regex) to identify used classes. It does **not** execute JavaScript. Therefore, class names must appear as full literal strings in your code.
|
|
7
|
+
|
|
8
|
+
### ā Prohibited: Concatenation
|
|
9
|
+
```javascript
|
|
10
|
+
// This will NOT be detected
|
|
11
|
+
const type = 'primary';
|
|
12
|
+
const className = 'ff-btn-' + type;
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### ā
Recommended: Explicit Literals
|
|
16
|
+
```javascript
|
|
17
|
+
// This will be detected correctly
|
|
18
|
+
const className = isPrimary ? 'ff-btn-primary' : 'ff-btn-secondary';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 2. Template Literals
|
|
22
|
+
Standard template literals are supported as long as the FingguFlux class is a discrete word.
|
|
23
|
+
|
|
24
|
+
### ā
Supported
|
|
25
|
+
```html
|
|
26
|
+
<div class="ff-card ${isActive ? 'ff-shadow-lg' : ''}">
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 3. Escape Hatch: Safelist
|
|
30
|
+
If you must use dynamic classes (e.g., from a CMS or database), add them to the `safelist` in your build configuration (to be implemented in Phase 7).
|
|
31
|
+
|
|
32
|
+
Currently, you can force inclusion by adding invisible comments in your template:
|
|
33
|
+
```html
|
|
34
|
+
<!-- ff-safelist: ff-btn ff-shadow-xl -->
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 4. Determinism
|
|
38
|
+
Hashes are derived from class names.
|
|
39
|
+
- `ff-btn` will **always** hash to the same value across any project or build.
|
|
40
|
+
- Adding new classes does **not** affect existing hashes.
|
package/engine.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FingguFlux Compiler Engine
|
|
3
|
+
* Handles dependency resolution, tree-shaking, and selector mapping.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
export class CompilerEngine {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.mode = options.mode || 'dev'; // dev, opt, ext
|
|
11
|
+
this.mapping = {};
|
|
12
|
+
this.usedClasses = new Set();
|
|
13
|
+
|
|
14
|
+
// Dependency Manifest
|
|
15
|
+
this.dependencies = {
|
|
16
|
+
'ff-btn-': 'ff-btn',
|
|
17
|
+
'ff-tab-': 'ff-tab',
|
|
18
|
+
'ff-dropdown-': 'ff-dropdown',
|
|
19
|
+
'ff-modal-': 'ff-modal',
|
|
20
|
+
'ff-card-': 'ff-card',
|
|
21
|
+
'ff-input-': 'ff-input'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setUsedClasses(classes) {
|
|
26
|
+
this.usedClasses = new Set(classes);
|
|
27
|
+
this.resolveDependencies();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves base class dependencies for variants
|
|
32
|
+
*/
|
|
33
|
+
resolveDependencies() {
|
|
34
|
+
const expanded = new Set(this.usedClasses);
|
|
35
|
+
this.usedClasses.forEach(cls => {
|
|
36
|
+
for (const [prefix, base] of Object.entries(this.dependencies)) {
|
|
37
|
+
if (cls.startsWith(prefix)) {
|
|
38
|
+
expanded.add(base);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
this.usedClasses = expanded;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generates a deterministic short hash for a class name
|
|
47
|
+
* Uses a simple FNV-1a inspired hash for speed and determinism
|
|
48
|
+
*/
|
|
49
|
+
getHash(str) {
|
|
50
|
+
let hash = 0x811c9dc5;
|
|
51
|
+
for (let i = 0; i < str.length; i++) {
|
|
52
|
+
hash ^= str.charCodeAt(i);
|
|
53
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
54
|
+
}
|
|
55
|
+
return (hash >>> 0).toString(36);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
generateMapping(cls) {
|
|
59
|
+
if (this.mapping[cls]) return this.mapping[cls];
|
|
60
|
+
|
|
61
|
+
if (this.mode === 'dev') {
|
|
62
|
+
this.mapping[cls] = cls;
|
|
63
|
+
} else if (this.mode === 'opt') {
|
|
64
|
+
this.mapping[cls] = cls.replace('ff-', '');
|
|
65
|
+
} else if (this.mode === 'ext') {
|
|
66
|
+
const hash = this.getHash(cls);
|
|
67
|
+
this.mapping[cls] = `ff-${hash}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return this.mapping[cls];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
processCSS(cssContent) {
|
|
74
|
+
let processed = cssContent;
|
|
75
|
+
|
|
76
|
+
// 1. Identify all .ff- selectors in the source
|
|
77
|
+
const classRegex = /\.ff-([\w-]+)/g;
|
|
78
|
+
const foundInCSS = new Set();
|
|
79
|
+
let match;
|
|
80
|
+
while ((match = classRegex.exec(cssContent)) !== null) {
|
|
81
|
+
foundInCSS.add(`ff-${match[1]}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. Generate mappings only for USED classes
|
|
85
|
+
foundInCSS.forEach(cls => {
|
|
86
|
+
if (this.usedClasses.has(cls)) {
|
|
87
|
+
this.generateMapping(cls);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 3. Tree-shaking (Block-level pruning) - DO THIS FIRST while names are original
|
|
92
|
+
const blocks = processed.split('}');
|
|
93
|
+
const filteredBlocks = blocks.filter(block => {
|
|
94
|
+
const selectorIndex = block.indexOf('{');
|
|
95
|
+
if (selectorIndex === -1) return block.trim().length > 0;
|
|
96
|
+
|
|
97
|
+
const selector = block.substring(0, selectorIndex).trim();
|
|
98
|
+
|
|
99
|
+
if (selector.includes('.ff-')) {
|
|
100
|
+
const ffClassesInSelector = selector.match(/\.ff-[\w-]+/g);
|
|
101
|
+
if (ffClassesInSelector) {
|
|
102
|
+
// Prune block if ANY ff- class in the selector is unused
|
|
103
|
+
// (Matches our strict tree-shaking policy for components/utilities)
|
|
104
|
+
const hasUnused = ffClassesInSelector.some(cls => {
|
|
105
|
+
const baseCls = cls.substring(1);
|
|
106
|
+
return !this.usedClasses.has(baseCls);
|
|
107
|
+
});
|
|
108
|
+
if (hasUnused) return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
processed = filteredBlocks.join('}') + (filteredBlocks.length > 0 ? '}' : '');
|
|
115
|
+
|
|
116
|
+
// 4. Replace selectors with mapped hashes
|
|
117
|
+
// NOTE: We explicitly DO NOT hash CSS variables starting with --ff-
|
|
118
|
+
// This ensures runtime theme switching remains operational.
|
|
119
|
+
const sortedClasses = Object.keys(this.mapping).sort((a, b) => b.length - a.length);
|
|
120
|
+
sortedClasses.forEach(cls => {
|
|
121
|
+
const mapped = this.mapping[cls];
|
|
122
|
+
if (cls !== mapped) {
|
|
123
|
+
const escapedCls = cls.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
124
|
+
const regex = new RegExp(`\\.${escapedCls}(?![\\w-])`, 'g');
|
|
125
|
+
processed = processed.replace(regex, `.${mapped}`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 5. Keyframe Pruning (Post-pruning)
|
|
130
|
+
return this.pruneKeyframes(processed);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pruneKeyframes(css) {
|
|
134
|
+
// Find all referenced animation names
|
|
135
|
+
const animationRegex = /animation(?:\-name)?\s*:\s*([^;!}]+)/g;
|
|
136
|
+
const usedAnimations = new Set();
|
|
137
|
+
let match;
|
|
138
|
+
while ((match = animationRegex.exec(css)) !== null) {
|
|
139
|
+
// Split by space/comma and filter out durations/easings/etc
|
|
140
|
+
const parts = match[1].split(/[,\s]+/).map(p => p.trim());
|
|
141
|
+
parts.forEach(p => {
|
|
142
|
+
if (p && !/^\d|ms|s|infinite|linear|ease|both|forwards|backwards/.test(p)) {
|
|
143
|
+
usedAnimations.add(p);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Prune unused @keyframes blocks
|
|
149
|
+
const keyframeBlocks = css.split(/@keyframes\s+([\w-]+)\s*\{/);
|
|
150
|
+
if (keyframeBlocks.length <= 1) return css;
|
|
151
|
+
|
|
152
|
+
let finalCSS = keyframeBlocks[0];
|
|
153
|
+
for (let i = 1; i < keyframeBlocks.length; i += 2) {
|
|
154
|
+
const name = keyframeBlocks[i];
|
|
155
|
+
const contentAndRest = keyframeBlocks[i + 1];
|
|
156
|
+
|
|
157
|
+
// Find the end of this @keyframes block (handling nested braces if any)
|
|
158
|
+
let braceCount = 1;
|
|
159
|
+
let endOfBlock = -1;
|
|
160
|
+
for (let j = 0; j < contentAndRest.length; j++) {
|
|
161
|
+
if (contentAndRest[j] === '{') braceCount++;
|
|
162
|
+
if (contentAndRest[j] === '}') braceCount--;
|
|
163
|
+
if (braceCount === 0) {
|
|
164
|
+
endOfBlock = j;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const keyframeContent = contentAndRest.substring(0, endOfBlock + 1);
|
|
170
|
+
const rest = contentAndRest.substring(endOfBlock + 1);
|
|
171
|
+
|
|
172
|
+
if (usedAnimations.has(name)) {
|
|
173
|
+
finalCSS += `@keyframes ${name} {${keyframeContent}`;
|
|
174
|
+
}
|
|
175
|
+
finalCSS += rest;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return finalCSS;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getMapping() {
|
|
182
|
+
// Return sorted mapping for stability
|
|
183
|
+
const sortedMapping = {};
|
|
184
|
+
Object.keys(this.mapping).sort().forEach(key => {
|
|
185
|
+
sortedMapping[key] = this.mapping[key];
|
|
186
|
+
});
|
|
187
|
+
return sortedMapping;
|
|
188
|
+
}
|
|
189
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@finggujadhav/compiler",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "The FingguFlux static analysis and selector hardening engine.",
|
|
5
|
+
"main": "engine.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"finggu": "cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"engine.js",
|
|
11
|
+
"scanner.js",
|
|
12
|
+
"cli.js",
|
|
13
|
+
"docs/policy.md"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=16.0.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"compiler",
|
|
21
|
+
"css-minification",
|
|
22
|
+
"hardening",
|
|
23
|
+
"obfuscation"
|
|
24
|
+
],
|
|
25
|
+
"author": "Finggu Architecture Team",
|
|
26
|
+
"license": "MIT"
|
|
27
|
+
}
|
package/scanner.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FingguFlux Scanner
|
|
3
|
+
* Scans HTML/Template files to extract used ff-* classes.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
export const scanFiles = (filePaths) => {
|
|
9
|
+
const usedClasses = new Set();
|
|
10
|
+
const ffClassRegex = /\bff-[\w-]+\b/g;
|
|
11
|
+
|
|
12
|
+
filePaths.forEach((filePath) => {
|
|
13
|
+
try {
|
|
14
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
15
|
+
const matches = content.match(ffClassRegex);
|
|
16
|
+
if (matches) {
|
|
17
|
+
matches.forEach((cls) => usedClasses.add(cls));
|
|
18
|
+
}
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error(`Error scanning file ${filePath}:`, err.message);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return Array.from(usedClasses);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getProjectFiles = (dir, extensions = ['.html', '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte']) => {
|
|
28
|
+
let results = [];
|
|
29
|
+
const list = fs.readdirSync(dir);
|
|
30
|
+
|
|
31
|
+
list.forEach((file) => {
|
|
32
|
+
file = path.join(dir, file);
|
|
33
|
+
const stat = fs.statSync(file);
|
|
34
|
+
|
|
35
|
+
if (stat && stat.isDirectory()) {
|
|
36
|
+
const base = path.basename(file);
|
|
37
|
+
if (base === 'node_modules' || base.startsWith('.')) return;
|
|
38
|
+
// Recurse into subdirectory
|
|
39
|
+
results = results.concat(getProjectFiles(file, extensions));
|
|
40
|
+
} else {
|
|
41
|
+
// Check extension
|
|
42
|
+
if (extensions.includes(path.extname(file))) {
|
|
43
|
+
results.push(file);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return results;
|
|
49
|
+
};
|