juxscript 1.0.129 → 1.0.131
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/index.js +1 -1
- package/lib/componentsv2/base/BaseSkin.d.ts +2 -3
- package/lib/componentsv2/base/BaseSkin.js +2 -3
- package/package.json +1 -1
- package/lib/componentsv2/base/GlobalBus.d.ts +0 -22
- package/lib/componentsv2/base/GlobalBus.js +0 -56
- package/lib/componentsv2/tools/CreateSkin.js +0 -62
- package/lib/componentsv2/tools/DocSpam.js +0 -134
- package/lib/componentsv2/tools/FluencyAudit.js +0 -141
- package/lib/componentsv2/tools/OptionsAudit.js +0 -177
- package/lib/componentsv2/tools/Scaffold.js +0 -124
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { List } from './lib/componentsv2/list/List.js';
|
|
|
16
16
|
export { BaseEngine } from './lib/componentsv2/base/BaseEngine.js';
|
|
17
17
|
export { BaseSkin } from './lib/componentsv2/base/BaseSkin.js';
|
|
18
18
|
export { State } from './lib/componentsv2/base/State.js';
|
|
19
|
-
export {
|
|
19
|
+
export { Neighborhood } from './lib/componentsv2/base/Neighborhood.js';
|
|
20
20
|
|
|
21
21
|
// Utilities
|
|
22
22
|
export { validateOptions } from './lib/componentsv2/base/OptionsContract.js';
|
|
@@ -36,15 +36,14 @@ export declare abstract class BaseSkin<TState extends BaseState, TEngine extends
|
|
|
36
36
|
*
|
|
37
37
|
* Use this for internal signals like "loginSuccess", "error", or "animationStart".
|
|
38
38
|
*/
|
|
39
|
-
protected
|
|
39
|
+
protected onSelf(event: string, callback: (data: any) => void): void;
|
|
40
40
|
/**
|
|
41
41
|
* Utility: Subscribe to the Global Bus (The Neighborhood).
|
|
42
42
|
* "Neighbors" refers to other components or global system events.
|
|
43
43
|
*
|
|
44
|
-
* ⚠️ USE SPARINGLY. Ideally, data flows down from the Engine via State.
|
|
45
44
|
* Use this for purely visual coordination like "theme:switched" or "layout:collapse-all".
|
|
46
45
|
*/
|
|
47
|
-
protected
|
|
46
|
+
protected onNeighbor(channel: string, callback: (data: any) => void): void;
|
|
48
47
|
/**
|
|
49
48
|
* Component Teardown.
|
|
50
49
|
* Removes the root element and cleans up all event listeners.
|
|
@@ -40,7 +40,7 @@ export class BaseSkin {
|
|
|
40
40
|
*
|
|
41
41
|
* Use this for internal signals like "loginSuccess", "error", or "animationStart".
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
onSelf(event, callback) {
|
|
44
44
|
this.engine.on(event, callback);
|
|
45
45
|
this._cleanupTasks.push(() => this.engine.off(event, callback));
|
|
46
46
|
}
|
|
@@ -48,10 +48,9 @@ export class BaseSkin {
|
|
|
48
48
|
* Utility: Subscribe to the Global Bus (The Neighborhood).
|
|
49
49
|
* "Neighbors" refers to other components or global system events.
|
|
50
50
|
*
|
|
51
|
-
* ⚠️ USE SPARINGLY. Ideally, data flows down from the Engine via State.
|
|
52
51
|
* Use this for purely visual coordination like "theme:switched" or "layout:collapse-all".
|
|
53
52
|
*/
|
|
54
|
-
|
|
53
|
+
onNeighbor(channel, callback) {
|
|
55
54
|
Neighborhood.on(channel, callback);
|
|
56
55
|
this._cleanupTasks.push(() => Neighborhood.off(channel, callback));
|
|
57
56
|
}
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
type GlobalListener = (data: any) => void;
|
|
2
|
-
/**
|
|
3
|
-
* THE NEIGHBORHOOD (Global Event Mediator)
|
|
4
|
-
*
|
|
5
|
-
* Acts as the certal nervous system where all components post updates.
|
|
6
|
-
* Other components can tune in to specific frequencies (channels) here.
|
|
7
|
-
*/
|
|
8
|
-
export declare class JuxGlobalBus {
|
|
9
|
-
private listeners;
|
|
10
|
-
on(channel: string, callback: GlobalListener): void;
|
|
11
|
-
off(channel: string, callback: GlobalListener): void;
|
|
12
|
-
emit(channel: string, data: any): void;
|
|
13
|
-
/**
|
|
14
|
-
* DEBUG: Read-only Snapshot of active channels and listener identities.
|
|
15
|
-
* usage: juxV2.events.registry
|
|
16
|
-
* Returns: { "channelName": ["functionName", "(anonymous)"] }
|
|
17
|
-
*/
|
|
18
|
-
get registry(): Record<string, string[]>;
|
|
19
|
-
}
|
|
20
|
-
export declare const GlobalBus: JuxGlobalBus;
|
|
21
|
-
export {};
|
|
22
|
-
//# sourceMappingURL=GlobalBus.d.ts.map
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* THE NEIGHBORHOOD (Global Event Mediator)
|
|
3
|
-
*
|
|
4
|
-
* Acts as the certal nervous system where all components post updates.
|
|
5
|
-
* Other components can tune in to specific frequencies (channels) here.
|
|
6
|
-
*/
|
|
7
|
-
export class JuxGlobalBus {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.listeners = new Map();
|
|
10
|
-
}
|
|
11
|
-
on(channel, callback) {
|
|
12
|
-
if (!this.listeners.has(channel)) {
|
|
13
|
-
this.listeners.set(channel, new Set());
|
|
14
|
-
}
|
|
15
|
-
this.listeners.get(channel).add(callback);
|
|
16
|
-
}
|
|
17
|
-
off(channel, callback) {
|
|
18
|
-
const set = this.listeners.get(channel);
|
|
19
|
-
if (set) {
|
|
20
|
-
set.delete(callback);
|
|
21
|
-
if (set.size === 0) {
|
|
22
|
-
this.listeners.delete(channel);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
emit(channel, data) {
|
|
27
|
-
const set = this.listeners.get(channel);
|
|
28
|
-
if (set) {
|
|
29
|
-
set.forEach(callback => {
|
|
30
|
-
try {
|
|
31
|
-
callback(data);
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
console.error(`Jux GlobalBus Error [${channel}]:`, e);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
// Optional: We could have a wildcard '*' listener here for debuggers/loggers
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* DEBUG: Read-only Snapshot of active channels and listener identities.
|
|
42
|
-
* usage: juxV2.events.registry
|
|
43
|
-
* Returns: { "channelName": ["functionName", "(anonymous)"] }
|
|
44
|
-
*/
|
|
45
|
-
get registry() {
|
|
46
|
-
const snapshot = {};
|
|
47
|
-
this.listeners.forEach((set, channel) => {
|
|
48
|
-
// Map the Set of functions to their names for easier debugging
|
|
49
|
-
snapshot[channel] = Array.from(set).map(fn => fn.name || '(anonymous)');
|
|
50
|
-
});
|
|
51
|
-
return snapshot;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// Singleton Instance
|
|
55
|
-
export const GlobalBus = new JuxGlobalBus();
|
|
56
|
-
//# sourceMappingURL=GlobalBus.js.map
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
const COMPONENTS_DIR = path.resolve(__dirname, '..');
|
|
8
|
-
|
|
9
|
-
const componentName = process.argv[2];
|
|
10
|
-
const skinName = process.argv[3];
|
|
11
|
-
|
|
12
|
-
if (!componentName || !skinName) {
|
|
13
|
-
console.error('❌ Usage: npm run create-skin <component-name> <skin-name>');
|
|
14
|
-
console.error(' Example: npm run create-skin list dark-mode');
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const targetDir = path.join(COMPONENTS_DIR, componentName, 'skins');
|
|
19
|
-
|
|
20
|
-
if (!fs.existsSync(targetDir)) {
|
|
21
|
-
console.error(`❌ Component "${componentName}" does not exist or has no skins folder.`);
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const fileName = `${skinName}.css`;
|
|
26
|
-
const filePath = path.join(targetDir, fileName);
|
|
27
|
-
|
|
28
|
-
const cssContent = `/**
|
|
29
|
-
* 🎨 Skin: ${skinName}
|
|
30
|
-
* Component: ${componentName}
|
|
31
|
-
*
|
|
32
|
-
* Usage:
|
|
33
|
-
* import theme from './skins/${fileName}';
|
|
34
|
-
* component.skin.setTheme(theme);
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/* Override Base Variables for this Skin */
|
|
38
|
-
.jux-${componentName} {
|
|
39
|
-
--color-background: #000;
|
|
40
|
-
--color-text-primary: #fff;
|
|
41
|
-
--radius-sm: 0px; /* Square Look */
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/* Component Specific Styles */
|
|
45
|
-
.jux-${componentName} {
|
|
46
|
-
border: 2px solid var(--color-brand);
|
|
47
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/* Interactive States */
|
|
51
|
-
.jux-${componentName}:hover {
|
|
52
|
-
border-color: var(--color-brand-hover);
|
|
53
|
-
}
|
|
54
|
-
`;
|
|
55
|
-
|
|
56
|
-
fs.writeFileSync(filePath, cssContent);
|
|
57
|
-
|
|
58
|
-
console.log(`\n✨ New Skin Created:`);
|
|
59
|
-
console.log(` 📂 ${filePath}`);
|
|
60
|
-
console.log(`\nTo use this skin in your code:`);
|
|
61
|
-
console.log(` 1. Import the CSS file (as string using a loader or inlining)`);
|
|
62
|
-
console.log(` 2. Call component.skin.setTheme(myNewCssString)`);
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
// --- Configuration ---
|
|
6
|
-
const EMOJI_REGEX = /\p{Extended_Pictographic}/u;
|
|
7
|
-
const IGNORE_DIRS = ['node_modules', '.git', 'dist', '.jux-dist'];
|
|
8
|
-
const EXTENSIONS = ['.ts', '.js', '.jux', '.md'];
|
|
9
|
-
|
|
10
|
-
// --- Patterns ---
|
|
11
|
-
// 1. Console Spam: console.log("✨...")
|
|
12
|
-
const PATTERN_CONSOLE = /console\.(log|warn|error|info|group|groupCollapsed)\((['"`])(.*?)\2/g;
|
|
13
|
-
|
|
14
|
-
// 2. Line Comments: // ✨ ...
|
|
15
|
-
const PATTERN_LINE_COMMENT = /\/\/(.*)$/gm;
|
|
16
|
-
|
|
17
|
-
// 3. Block Comments: /* ✨ ... */
|
|
18
|
-
const PATTERN_BLOCK_COMMENT = /\/\*([\s\S]*?)\*\//gm;
|
|
19
|
-
|
|
20
|
-
function scanFile(filePath) {
|
|
21
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
22
|
-
const ext = path.extname(filePath);
|
|
23
|
-
const findings = [];
|
|
24
|
-
|
|
25
|
-
// 1. Check Console Logs (JS/TS Files)
|
|
26
|
-
if (['.js', '.ts', '.jux'].includes(ext)) {
|
|
27
|
-
let match;
|
|
28
|
-
while ((match = PATTERN_CONSOLE.exec(content)) !== null) {
|
|
29
|
-
const logContent = match[3]; // The text inside quotes
|
|
30
|
-
if (EMOJI_REGEX.test(logContent)) {
|
|
31
|
-
findings.push({
|
|
32
|
-
type: 'Console',
|
|
33
|
-
line: getLineNumber(content, match.index),
|
|
34
|
-
match: match[0].trim()
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 2. Check Line Comments
|
|
41
|
-
if (['.js', '.ts', '.jux'].includes(ext)) {
|
|
42
|
-
let match;
|
|
43
|
-
while ((match = PATTERN_LINE_COMMENT.exec(content)) !== null) {
|
|
44
|
-
const commentText = match[1];
|
|
45
|
-
if (EMOJI_REGEX.test(commentText)) {
|
|
46
|
-
findings.push({
|
|
47
|
-
type: 'Line Comment',
|
|
48
|
-
line: getLineNumber(content, match.index),
|
|
49
|
-
match: match[0].trim()
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 3. Check Block Comments
|
|
56
|
-
if (['.js', '.ts', '.jux'].includes(ext)) {
|
|
57
|
-
let match;
|
|
58
|
-
while ((match = PATTERN_BLOCK_COMMENT.exec(content)) !== null) {
|
|
59
|
-
const commentText = match[1];
|
|
60
|
-
if (EMOJI_REGEX.test(commentText)) {
|
|
61
|
-
findings.push({
|
|
62
|
-
type: 'Block Comment',
|
|
63
|
-
line: getLineNumber(content, match.index),
|
|
64
|
-
match: match[0].replace(/\n/g, ' ').substring(0, 50) + '...'
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 4. Markdown (Treat almost everything as "Content" but flag emojis)
|
|
71
|
-
if (ext === '.md') {
|
|
72
|
-
const lines = content.split('\n');
|
|
73
|
-
lines.forEach((line, i) => {
|
|
74
|
-
if (EMOJI_REGEX.test(line)) {
|
|
75
|
-
findings.push({
|
|
76
|
-
type: 'Markdown Content',
|
|
77
|
-
line: i + 1,
|
|
78
|
-
match: line.trim().substring(0, 60)
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return findings;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getLineNumber(content, index) {
|
|
88
|
-
return content.substring(0, index).split('\n').length;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function walkDir(dir) {
|
|
92
|
-
const results = {};
|
|
93
|
-
const list = fs.readdirSync(dir);
|
|
94
|
-
|
|
95
|
-
list.forEach(file => {
|
|
96
|
-
if (IGNORE_DIRS.includes(file)) return;
|
|
97
|
-
|
|
98
|
-
const filePath = path.join(dir, file);
|
|
99
|
-
const stat = fs.statSync(filePath);
|
|
100
|
-
|
|
101
|
-
if (stat && stat.isDirectory()) {
|
|
102
|
-
Object.assign(results, walkDir(filePath));
|
|
103
|
-
} else {
|
|
104
|
-
if (EXTENSIONS.includes(path.extname(filePath))) {
|
|
105
|
-
const findings = scanFile(filePath);
|
|
106
|
-
if (findings.length > 0) {
|
|
107
|
-
results[filePath] = findings;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
return results;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// --- Main Execution ---
|
|
116
|
-
const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
|
|
117
|
-
|
|
118
|
-
console.log(`\n🔎 Scanning for Emoji Spam in: ${targetDir}\n`);
|
|
119
|
-
const report = walkDir(targetDir);
|
|
120
|
-
const fileCount = Object.keys(report).length;
|
|
121
|
-
|
|
122
|
-
if (fileCount === 0) {
|
|
123
|
-
console.log("✅ No emoji spam found! Clean codebase.");
|
|
124
|
-
} else {
|
|
125
|
-
Object.entries(report).forEach(([file, items]) => {
|
|
126
|
-
const relativePath = path.relative(process.cwd(), file);
|
|
127
|
-
console.log(`\n📄 \x1b[1m${relativePath}\x1b[0m`);
|
|
128
|
-
items.forEach(item => {
|
|
129
|
-
const typeColor = item.type === 'Console' ? '\x1b[31m' : '\x1b[33m'; // Red for console, Yellow for comments
|
|
130
|
-
console.log(` L${item.line.toString().padEnd(4)} ${typeColor}[${item.type}]\x1b[0m ${item.match}`);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
console.log(`\n🚨 Found emoji usage in ${fileCount} files.`);
|
|
134
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import ts from 'typescript';
|
|
4
|
-
|
|
5
|
-
const TARGET_DIR = path.resolve('./lib/componentsv2');
|
|
6
|
-
const ENGINE_SUFFIX = 'Engine.ts';
|
|
7
|
-
|
|
8
|
-
// Methods to ignore (Lifecycle, Private, Getters)
|
|
9
|
-
const IGNORE_LIST = [
|
|
10
|
-
'constructor', 'prepareState', 'subscribe', 'debug', 'dispose',
|
|
11
|
-
'on', 'off', 'listenTo', 'addPlugin', 'removePlugin',
|
|
12
|
-
'get eventRegistry', 'get stateHistory', 'get emitHistory'
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* AST Traversal to analyze method behaviors
|
|
17
|
-
*/
|
|
18
|
-
function analyzeMethod(node) {
|
|
19
|
-
let returnsThis = false;
|
|
20
|
-
let updatesState = false;
|
|
21
|
-
let emitsEvent = false;
|
|
22
|
-
|
|
23
|
-
// Recursive visitor to inspect code inside the method body
|
|
24
|
-
function visit(n) {
|
|
25
|
-
// Check 1: Return This ("return this;")
|
|
26
|
-
if (ts.isReturnStatement(n) && n.expression && n.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
27
|
-
returnsThis = true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Check 2 & 3: Method Calls ("this.updateState(...)", "this.emit(...)")
|
|
31
|
-
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
32
|
-
const propAccess = n.expression;
|
|
33
|
-
// Ensure caller is 'this'
|
|
34
|
-
if (propAccess.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
35
|
-
const methodName = propAccess.name.text;
|
|
36
|
-
if (methodName === 'updateState') updatesState = true;
|
|
37
|
-
if (methodName === 'emit') emitsEvent = true;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
ts.forEachChild(n, visit);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (node.body) {
|
|
45
|
-
visit(node.body);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { returnsThis, updatesState, emitsEvent };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function gradeFile(filePath) {
|
|
52
|
-
const code = fs.readFileSync(filePath, 'utf-8');
|
|
53
|
-
|
|
54
|
-
// Create AST from TS Source
|
|
55
|
-
const sourceFile = ts.createSourceFile(
|
|
56
|
-
filePath,
|
|
57
|
-
code,
|
|
58
|
-
ts.ScriptTarget.Latest,
|
|
59
|
-
true
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
let score = 0;
|
|
63
|
-
let totalMutators = 0;
|
|
64
|
-
const report = [];
|
|
65
|
-
|
|
66
|
-
function visitNode(node) {
|
|
67
|
-
// Look for Classes
|
|
68
|
-
if (ts.isClassDeclaration(node)) {
|
|
69
|
-
node.members.forEach(member => {
|
|
70
|
-
// Look for Methods
|
|
71
|
-
if (ts.isMethodDeclaration(member)) {
|
|
72
|
-
const name = member.name.getText(sourceFile);
|
|
73
|
-
|
|
74
|
-
// Skip ignored, private (#), or static methods
|
|
75
|
-
if (
|
|
76
|
-
IGNORE_LIST.includes(name) ||
|
|
77
|
-
name.startsWith('#') ||
|
|
78
|
-
(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static)
|
|
79
|
-
) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Analyze
|
|
84
|
-
const analysis = analyzeMethod(member);
|
|
85
|
-
|
|
86
|
-
// Logic: If it mutates state, it MUST emit and rely on the fluent chain.
|
|
87
|
-
// We only grade methods that exhibit at least one of these traits to avoid grading helpers.
|
|
88
|
-
if (analysis.updatesState || analysis.emitsEvent || analysis.returnsThis) {
|
|
89
|
-
totalMutators++;
|
|
90
|
-
|
|
91
|
-
const issues = [];
|
|
92
|
-
if (!analysis.returnsThis) issues.push('❌ Return `this`');
|
|
93
|
-
if (!analysis.updatesState) issues.push('⚠️ No State Update');
|
|
94
|
-
if (!analysis.emitsEvent) issues.push('❌ No Emission');
|
|
95
|
-
|
|
96
|
-
if (issues.length === 0) {
|
|
97
|
-
score++;
|
|
98
|
-
} else {
|
|
99
|
-
report.push(` METHOD: ${name}`);
|
|
100
|
-
issues.forEach(i => report.push(` ${i}`));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
ts.forEachChild(node, visitNode);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
visitNode(sourceFile);
|
|
110
|
-
|
|
111
|
-
return { total: totalMutators, passed: score, report };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function walk(dir) {
|
|
115
|
-
const files = fs.readdirSync(dir);
|
|
116
|
-
files.forEach(f => {
|
|
117
|
-
const full = path.join(dir, f);
|
|
118
|
-
if (fs.statSync(full).isDirectory()) {
|
|
119
|
-
walk(full);
|
|
120
|
-
} else if (full.endsWith(ENGINE_SUFFIX) && !full.endsWith('BaseEngine.ts')) {
|
|
121
|
-
const relative = path.relative(process.cwd(), full);
|
|
122
|
-
const { total, passed, report } = gradeFile(full);
|
|
123
|
-
|
|
124
|
-
if (total > 0) {
|
|
125
|
-
const percent = Math.round((passed / total) * 100);
|
|
126
|
-
const color = percent === 100 ? '\x1b[32m' : (percent > 80 ? '\x1b[33m' : '\x1b[31m');
|
|
127
|
-
|
|
128
|
-
console.log(`\n📄 ${relative}`);
|
|
129
|
-
console.log(` Fluency Score: ${color}${percent}% (${passed}/${total})\x1b[0m`);
|
|
130
|
-
if (report.length > 0) {
|
|
131
|
-
console.log(report.join('\n'));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
console.log('\n🕵️ JUX FLUENCY AUDIT (AST Powered)');
|
|
139
|
-
console.log(' Checking for: 1. return this, 2. updateState(), 3. emit()\n');
|
|
140
|
-
walk(TARGET_DIR);
|
|
141
|
-
console.log('\n');
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import ts from 'typescript';
|
|
4
|
-
|
|
5
|
-
const TARGET_DIR = path.resolve('./lib/componentsv2');
|
|
6
|
-
const ENGINE_SUFFIX = 'Engine.ts'; // Only scan engines
|
|
7
|
-
|
|
8
|
-
// BaseState properties that EVERY engine must initialize
|
|
9
|
-
const BASE_STATE_KEYS = ['id', 'classes', 'visible', 'disabled', 'loading', 'attributes'];
|
|
10
|
-
|
|
11
|
-
function getInterfaceProperties(node, sourceFile) {
|
|
12
|
-
const props = [];
|
|
13
|
-
node.members.forEach(member => {
|
|
14
|
-
if (ts.isPropertySignature(member)) {
|
|
15
|
-
props.push(member.name.getText(sourceFile));
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
return props;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function analyzePrepareState(node, sourceFile, stateKeys, optionsKeys) {
|
|
22
|
-
const report = [];
|
|
23
|
-
let passed = true;
|
|
24
|
-
|
|
25
|
-
// 1. Capture Return Object Keys
|
|
26
|
-
let returnedKeys = [];
|
|
27
|
-
let foundReturn = false;
|
|
28
|
-
|
|
29
|
-
function visitReturn(n) {
|
|
30
|
-
// ✅ STOP recursion if entering a nested function (like .map(() => ...))
|
|
31
|
-
// We only care about the return statement of `prepareState` itself.
|
|
32
|
-
if (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (ts.isReturnStatement(n)) {
|
|
37
|
-
foundReturn = true;
|
|
38
|
-
if (n.expression && ts.isObjectLiteralExpression(n.expression)) {
|
|
39
|
-
n.expression.properties.forEach(prop => {
|
|
40
|
-
// Start Update: Handle both Assignment types
|
|
41
|
-
if (ts.isPropertyAssignment(prop)) {
|
|
42
|
-
returnedKeys.push(prop.name.getText(sourceFile));
|
|
43
|
-
}
|
|
44
|
-
// ✅ ADDED: Handle shorthand assignments (e.g. { id })
|
|
45
|
-
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
46
|
-
returnedKeys.push(prop.name.getText(sourceFile));
|
|
47
|
-
}
|
|
48
|
-
// End Update
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
ts.forEachChild(n, visitReturn);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Start visiting from the function body
|
|
56
|
-
if (node.body) {
|
|
57
|
-
ts.forEachChild(node.body, visitReturn);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!foundReturn) {
|
|
61
|
-
return { passed: false, report: ['❌ prepareState does not return an object literal directly.'] };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 2. Check State Coverage (Expected = BaseState + ComponentState)
|
|
65
|
-
const expectedKeys = new Set([...BASE_STATE_KEYS, ...stateKeys]);
|
|
66
|
-
const actualKeys = new Set(returnedKeys);
|
|
67
|
-
|
|
68
|
-
// Missing Keys
|
|
69
|
-
const missing = [...expectedKeys].filter(k => !actualKeys.has(k));
|
|
70
|
-
if (missing.length > 0) {
|
|
71
|
-
report.push(`❌ Missing Initialization for: ${missing.join(', ')}`);
|
|
72
|
-
passed = false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Extra Keys (Pollution / Typos)
|
|
76
|
-
const extra = [...actualKeys].filter(k => !expectedKeys.has(k));
|
|
77
|
-
if (extra.length > 0) {
|
|
78
|
-
report.push(`⚠️ Unknown properties in State: ${extra.join(', ')}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 3. Check Options Usage
|
|
82
|
-
// We scan for `options.KEY` usage in the function body
|
|
83
|
-
const usedOptions = new Set();
|
|
84
|
-
|
|
85
|
-
function visitOptionsUsage(n) {
|
|
86
|
-
if (ts.isPropertyAccessExpression(n)) {
|
|
87
|
-
if (n.expression.getText(sourceFile) === 'options') {
|
|
88
|
-
usedOptions.add(n.name.getText(sourceFile));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
ts.forEachChild(n, visitOptionsUsage);
|
|
92
|
-
}
|
|
93
|
-
if (node.body) {
|
|
94
|
-
visitOptionsUsage(node.body);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const unusedOptions = optionsKeys.filter(k => !usedOptions.has(k));
|
|
98
|
-
if (unusedOptions.length > 0) {
|
|
99
|
-
report.push(`⚠️ Unused Options: ${unusedOptions.join(', ')}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { passed, report };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function auditFile(filePath) {
|
|
106
|
-
const code = fs.readFileSync(filePath, 'utf-8');
|
|
107
|
-
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
108
|
-
|
|
109
|
-
let stateInterfaceProps = [];
|
|
110
|
-
let optionsInterfaceProps = [];
|
|
111
|
-
let prepareStateMethod = null;
|
|
112
|
-
|
|
113
|
-
function visit(node) {
|
|
114
|
-
// Find Interfaces
|
|
115
|
-
if (ts.isInterfaceDeclaration(node)) {
|
|
116
|
-
const name = node.name.text;
|
|
117
|
-
if (name.endsWith('State') && name !== 'BaseState') {
|
|
118
|
-
stateInterfaceProps = getInterfaceProperties(node, sourceFile);
|
|
119
|
-
} else if (name.endsWith('Options')) {
|
|
120
|
-
optionsInterfaceProps = getInterfaceProperties(node, sourceFile);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Find Class & prepareState
|
|
125
|
-
if (ts.isClassDeclaration(node)) {
|
|
126
|
-
node.members.forEach(member => {
|
|
127
|
-
if (ts.isMethodDeclaration(member) && member.name.getText(sourceFile) === 'prepareState') {
|
|
128
|
-
prepareStateMethod = member;
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
ts.forEachChild(node, visit);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
visit(sourceFile);
|
|
136
|
-
|
|
137
|
-
// If no prepareState found, it might be abstract or a utility class; skip nicely
|
|
138
|
-
if (!prepareStateMethod) return null;
|
|
139
|
-
|
|
140
|
-
const result = analyzePrepareState(prepareStateMethod, sourceFile, stateInterfaceProps, optionsInterfaceProps);
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
file: path.relative(process.cwd(), filePath),
|
|
144
|
-
...result
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function printResult(res) {
|
|
149
|
-
if (!res) return;
|
|
150
|
-
const color = res.passed ? '\x1b[32m' : '\x1b[31m'; // Green / Red
|
|
151
|
-
|
|
152
|
-
console.log(`📄 ${res.file}`);
|
|
153
|
-
if (res.passed && res.report.length === 0) {
|
|
154
|
-
console.log(` ${color}100% Match\x1b[0m`);
|
|
155
|
-
} else {
|
|
156
|
-
res.report.forEach(msg => console.log(` ${msg}`));
|
|
157
|
-
if (!res.passed) console.log(` ${color}Validation Failed\x1b[0m`);
|
|
158
|
-
}
|
|
159
|
-
console.log('');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function walk(dir) {
|
|
163
|
-
const files = fs.readdirSync(dir);
|
|
164
|
-
files.forEach(f => {
|
|
165
|
-
const full = path.join(dir, f);
|
|
166
|
-
if (fs.statSync(full).isDirectory()) {
|
|
167
|
-
walk(full);
|
|
168
|
-
} else if (full.endsWith(ENGINE_SUFFIX) && !full.endsWith('BaseEngine.ts')) {
|
|
169
|
-
const res = auditFile(full);
|
|
170
|
-
printResult(res);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
console.log(`\n📋 JUX OPTIONS AUDIT`);
|
|
176
|
-
console.log(` Verifying prepareState() maps all Options -> State correctly.\n`);
|
|
177
|
-
walk(TARGET_DIR);
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { execSync } from 'child_process';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import readline from 'readline';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
|
-
const COMPONENTS_DIR = path.resolve(__dirname, '..');
|
|
10
|
-
const STUBS_DIR = path.join(COMPONENTS_DIR, 'stubs');
|
|
11
|
-
const INDEX_FILE = path.join(COMPONENTS_DIR, 'index.ts');
|
|
12
|
-
const RESERVED = ['base', 'bases', 'plugin', 'plugins', 'tool', 'tools', 'stub', 'stubs', 'skin', 'skins', 'structure', 'structures', 'engine', 'engines', 'component', 'components'];
|
|
13
|
-
|
|
14
|
-
const toPascalCase = (str) => str.replace(/(^\w|-\w)/g, (clear) => clear.replace('-', '').toUpperCase());
|
|
15
|
-
const log = (msg, color = '\x1b[36m') => console.log(`${color}${msg}\x1b[0m`);
|
|
16
|
-
const error = (msg) => { console.error(`\x1b[31m❌ ${msg}\x1b[0m`); process.exit(1); };
|
|
17
|
-
|
|
18
|
-
const componentName = process.argv[2];
|
|
19
|
-
|
|
20
|
-
if (!componentName) error('Usage: npm run scaffold -- <component-name>');
|
|
21
|
-
if (!/^[a-z0-9-]+$/.test(componentName)) error('Name must be kebab-case (e.g. task-list)');
|
|
22
|
-
if (RESERVED.includes(componentName)) error(`"${componentName}" is a reserved system name.`);
|
|
23
|
-
|
|
24
|
-
const ComponentName = toPascalCase(componentName);
|
|
25
|
-
const targetDir = path.join(COMPONENTS_DIR, componentName);
|
|
26
|
-
|
|
27
|
-
if (fs.existsSync(targetDir)) error(`Directory already exists: ${targetDir}`);
|
|
28
|
-
|
|
29
|
-
log(`🚀 Scaffolding Jux v2 Component: ${ComponentName} (${componentName})...`);
|
|
30
|
-
|
|
31
|
-
const indexBackup = fs.readFileSync(INDEX_FILE, 'utf-8');
|
|
32
|
-
|
|
33
|
-
(async function main() {
|
|
34
|
-
try {
|
|
35
|
-
// 1. Create Directory
|
|
36
|
-
fs.mkdirSync(targetDir);
|
|
37
|
-
|
|
38
|
-
// 2. Generate Files from Stubs
|
|
39
|
-
const stubs = {
|
|
40
|
-
'ComponentComposition.ts.stub': `component.ts`,
|
|
41
|
-
'ComponentEngine.ts.stub': `engine.ts`,
|
|
42
|
-
'ComponentSkin.ts.stub': `skin.ts`,
|
|
43
|
-
'ComponentStructure.css.stub': `structure.css`
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
Object.entries(stubs).forEach(([stub, file]) => {
|
|
47
|
-
let content = fs.readFileSync(path.join(STUBS_DIR, stub), 'utf-8');
|
|
48
|
-
|
|
49
|
-
content = content.replace(/\[%\s*ComponentName\s*%\]/g, ComponentName)
|
|
50
|
-
.replace(/\[%\s*component-name\s*%\]/g, componentName);
|
|
51
|
-
|
|
52
|
-
fs.writeFileSync(path.join(targetDir, file), content);
|
|
53
|
-
log(` + Created ${file}`);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// 3. Update Index
|
|
57
|
-
log(` ... Registering in ${path.basename(INDEX_FILE)}`);
|
|
58
|
-
let indexContent = indexBackup;
|
|
59
|
-
|
|
60
|
-
const importLine = `import { ${ComponentName} } from './${componentName}/component.js';`; // Updated import path
|
|
61
|
-
if (!indexContent.includes(importLine) && !indexContent.includes(`import { ${ComponentName} }`)) {
|
|
62
|
-
if (indexContent.includes(`// Export individually`)) {
|
|
63
|
-
indexContent = indexContent.replace(`// Export individually`, `${importLine}\n\n// Export individually`);
|
|
64
|
-
} else {
|
|
65
|
-
indexContent = `${importLine}\n${indexContent}`;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const exportRegex = /export\s*\{([^}]+)\};/;
|
|
70
|
-
if (exportRegex.test(indexContent)) {
|
|
71
|
-
indexContent = indexContent.replace(exportRegex, (match, content) => {
|
|
72
|
-
const parts = content.split(',').map(s => s.trim()).filter(Boolean);
|
|
73
|
-
const unique = new Set(parts);
|
|
74
|
-
unique.add(ComponentName);
|
|
75
|
-
return `export { ${Array.from(unique).join(', ')} };`;
|
|
76
|
-
});
|
|
77
|
-
} else {
|
|
78
|
-
indexContent += `\nexport { ${ComponentName} };`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
fs.writeFileSync(INDEX_FILE, indexContent);
|
|
82
|
-
|
|
83
|
-
console.log(`\n\x1b[33m⚠️ MANUAL ACTION REQUIRED:\x1b[0m`);
|
|
84
|
-
console.log(` Please register \x1b[1m${ComponentName}\x1b[0m in the \x1b[1mjuxV2\x1b[0m namespace object.`);
|
|
85
|
-
console.log(` File: \x1b[36m${INDEX_FILE}\x1b[0m`);
|
|
86
|
-
console.log(` add: \x1b[32m${ComponentName},\x1b[0m\n`);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// 5. Verification
|
|
90
|
-
log(`\n🕵️ Verifying Integrity (Targeted TypeScript Check)...`);
|
|
91
|
-
try {
|
|
92
|
-
const filesToCheck = Object.values(stubs)
|
|
93
|
-
.filter(f => f.endsWith('.ts'))
|
|
94
|
-
.map(f => path.join(targetDir, f))
|
|
95
|
-
.join(' ');
|
|
96
|
-
|
|
97
|
-
execSync(`npx tsc --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext --esModuleInterop --skipLibCheck ${filesToCheck}`, { stdio: 'inherit' });
|
|
98
|
-
|
|
99
|
-
log(`\n✅ Verification Passed! Component Ready.`);
|
|
100
|
-
} catch (e) {
|
|
101
|
-
throw new Error("Type check failed on new component.");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
} catch (e) {
|
|
105
|
-
console.error(`\n\x1b[31m❌ ERROR: ${e.message}\x1b[0m`);
|
|
106
|
-
// Rollback...
|
|
107
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
108
|
-
const answer = await new Promise(resolve => {
|
|
109
|
-
rl.question(`\n⚠️ An error occurred. Rollback changes (delete created files)? [y/n]: `, (ans) => {
|
|
110
|
-
resolve(ans.trim().toLowerCase());
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
rl.close();
|
|
114
|
-
|
|
115
|
-
if (answer === 'y' || answer === 'yes') {
|
|
116
|
-
log(`⏪ Rolling back changes...`, '\x1b[33m');
|
|
117
|
-
fs.writeFileSync(INDEX_FILE, indexBackup);
|
|
118
|
-
if (fs.existsSync(targetDir)) {
|
|
119
|
-
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
})();
|