@iviva/uxp-lint 0.0.1
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 +273 -0
- package/bin/uxp-lint.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +100 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/eslint/config.d.ts +2 -0
- package/dist/eslint/config.js +225 -0
- package/dist/eslint/config.js.map +1 -0
- package/dist/eslint/rules/event-name-format.d.ts +5 -0
- package/dist/eslint/rules/event-name-format.js +59 -0
- package/dist/eslint/rules/event-name-format.js.map +1 -0
- package/dist/eslint/rules/no-bad-hook-deps.d.ts +5 -0
- package/dist/eslint/rules/no-bad-hook-deps.js +57 -0
- package/dist/eslint/rules/no-bad-hook-deps.js.map +1 -0
- package/dist/eslint/rules/no-fa-prefix.d.ts +5 -0
- package/dist/eslint/rules/no-fa-prefix.js +59 -0
- package/dist/eslint/rules/no-fa-prefix.js.map +1 -0
- package/dist/eslint/rules/no-hardcoded-jsx-text.d.ts +5 -0
- package/dist/eslint/rules/no-hardcoded-jsx-text.js +55 -0
- package/dist/eslint/rules/no-hardcoded-jsx-text.js.map +1 -0
- package/dist/eslint/rules/no-inline-styles.d.ts +8 -0
- package/dist/eslint/rules/no-inline-styles.js +41 -0
- package/dist/eslint/rules/no-inline-styles.js.map +1 -0
- package/dist/eslint/rules/no-native-html-interactive.d.ts +5 -0
- package/dist/eslint/rules/no-native-html-interactive.js +81 -0
- package/dist/eslint/rules/no-native-html-interactive.js.map +1 -0
- package/dist/eslint/rules/require-memo.d.ts +9 -0
- package/dist/eslint/rules/require-memo.js +70 -0
- package/dist/eslint/rules/require-memo.js.map +1 -0
- package/dist/eslint/rules/service-config-shape.d.ts +5 -0
- package/dist/eslint/rules/service-config-shape.js +80 -0
- package/dist/eslint/rules/service-config-shape.js.map +1 -0
- package/dist/eslint/rules/url-params-form-state.d.ts +5 -0
- package/dist/eslint/rules/url-params-form-state.js +75 -0
- package/dist/eslint/rules/url-params-form-state.js.map +1 -0
- package/dist/reporter.d.ts +22 -0
- package/dist/reporter.js +260 -0
- package/dist/reporter.js.map +1 -0
- package/dist/rule-registry.d.ts +2 -0
- package/dist/rule-registry.js +46 -0
- package/dist/rule-registry.js.map +1 -0
- package/dist/rules-reader.d.ts +4 -0
- package/dist/rules-reader.js +16 -0
- package/dist/rules-reader.js.map +1 -0
- package/dist/runner.d.ts +7 -0
- package/dist/runner.js +62 -0
- package/dist/runner.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +80 -0
- package/dist/setup.js.map +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +39 -0
- package/dist/types.js.map +1 -0
- package/dist/validators/ai.d.ts +2 -0
- package/dist/validators/ai.js +115 -0
- package/dist/validators/ai.js.map +1 -0
- package/dist/validators/bulk-imports.d.ts +3 -0
- package/dist/validators/bulk-imports.js +94 -0
- package/dist/validators/bulk-imports.js.map +1 -0
- package/dist/validators/bundle-json.d.ts +3 -0
- package/dist/validators/bundle-json.js +130 -0
- package/dist/validators/bundle-json.js.map +1 -0
- package/dist/validators/bundle-size.d.ts +3 -0
- package/dist/validators/bundle-size.js +51 -0
- package/dist/validators/bundle-size.js.map +1 -0
- package/dist/validators/config-yml.d.ts +3 -0
- package/dist/validators/config-yml.js +148 -0
- package/dist/validators/config-yml.js.map +1 -0
- package/dist/validators/duplication.d.ts +3 -0
- package/dist/validators/duplication.js +65 -0
- package/dist/validators/duplication.js.map +1 -0
- package/dist/validators/folder-structure.d.ts +3 -0
- package/dist/validators/folder-structure.js +123 -0
- package/dist/validators/folder-structure.js.map +1 -0
- package/dist/validators/formatting.d.ts +3 -0
- package/dist/validators/formatting.js +97 -0
- package/dist/validators/formatting.js.map +1 -0
- package/dist/validators/scss-rules.d.ts +3 -0
- package/dist/validators/scss-rules.js +156 -0
- package/dist/validators/scss-rules.js.map +1 -0
- package/package.json +58 -0
- package/templates/prettier.config.js +11 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.docs = void 0;
|
|
7
|
+
exports.validateConfigYml = validateConfigYml;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
exports.docs = [
|
|
12
|
+
{
|
|
13
|
+
id: 'pageId-format',
|
|
14
|
+
severity: 'error',
|
|
15
|
+
category: 'configuration',
|
|
16
|
+
description: 'pageId values in navigationLinks must be in the format "ui/<id>" or "widget/<id>".',
|
|
17
|
+
rationale: 'The UXP shell uses this prefix to route to the correct component type.',
|
|
18
|
+
fix: 'Set pageId to "ui/my-component" or "widget/my-widget".',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'nav-group-no-pageid',
|
|
22
|
+
severity: 'error',
|
|
23
|
+
category: 'configuration',
|
|
24
|
+
description: 'Nav group parents (items with children but no link) must not set a pageId.',
|
|
25
|
+
rationale: 'Group nodes are not rendered as pages — pageId is ignored and causes confusion.',
|
|
26
|
+
fix: 'Remove pageId from nav groups, or set it to null.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'nav-parent-redirect',
|
|
30
|
+
severity: 'warn',
|
|
31
|
+
category: 'configuration',
|
|
32
|
+
description: 'Nav items with children and their own link should have an otherRoutes redirect to a default child.',
|
|
33
|
+
rationale: 'Without a redirect, navigating to the parent link renders nothing.',
|
|
34
|
+
fix: 'Add an otherRoutes entry: `"/parent": { redirectTo: "/parent/default-child" }`.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'unique-nav-links',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
category: 'configuration',
|
|
40
|
+
description: 'Navigation links must be unique — no two top-level items can share the same link value.',
|
|
41
|
+
rationale: 'Duplicate links cause the router to match the wrong page.',
|
|
42
|
+
fix: 'Ensure each navigationLink has a unique link path.',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
function collectNavLinks(links, out = []) {
|
|
46
|
+
var _a;
|
|
47
|
+
for (const link of links) {
|
|
48
|
+
out.push(link);
|
|
49
|
+
if ((_a = link.children) === null || _a === void 0 ? void 0 : _a.length)
|
|
50
|
+
collectNavLinks(link.children, out);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
async function validateConfigYml(config, cwd) {
|
|
55
|
+
var _a, _b, _c, _d, _e;
|
|
56
|
+
const errors = [];
|
|
57
|
+
const warnings = [];
|
|
58
|
+
const filePath = path_1.default.resolve(cwd, config.configYml);
|
|
59
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
60
|
+
return {
|
|
61
|
+
group: 'Configuration.yml',
|
|
62
|
+
passed: false,
|
|
63
|
+
errors: [{ message: `File not found: ${config.configYml}` }],
|
|
64
|
+
warnings: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
const raw = fs_1.default.readFileSync(filePath, 'utf8');
|
|
70
|
+
parsed = js_yaml_1.default.load(raw);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return {
|
|
74
|
+
group: 'Configuration.yml',
|
|
75
|
+
passed: false,
|
|
76
|
+
errors: [{ message: `Failed to parse YAML: ${e}` }],
|
|
77
|
+
warnings: [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const file = config.configYml;
|
|
81
|
+
// Required fields
|
|
82
|
+
if (!parsed.appId)
|
|
83
|
+
errors.push({ file, message: 'Missing required field: appId' });
|
|
84
|
+
if (!parsed.bundleId)
|
|
85
|
+
errors.push({ file, message: 'Missing required field: bundleId' });
|
|
86
|
+
if (!parsed.baseRoute)
|
|
87
|
+
errors.push({ file, message: 'Missing required field: baseRoute' });
|
|
88
|
+
if (!Array.isArray(parsed.scripts) || parsed.scripts.length === 0) {
|
|
89
|
+
errors.push({ file, message: 'Missing or empty scripts array' });
|
|
90
|
+
}
|
|
91
|
+
const navLinks = (_a = parsed.navigationLinks) !== null && _a !== void 0 ? _a : [];
|
|
92
|
+
const otherRoutes = (_b = parsed.otherRoutes) !== null && _b !== void 0 ? _b : {};
|
|
93
|
+
const allLinks = collectNavLinks(navLinks);
|
|
94
|
+
for (const link of allLinks) {
|
|
95
|
+
// pageId format check (if present and not null)
|
|
96
|
+
if (link.pageId !== undefined && link.pageId !== null) {
|
|
97
|
+
if (!/^(ui|widget)\/.+/.test(link.pageId)) {
|
|
98
|
+
errors.push({
|
|
99
|
+
file,
|
|
100
|
+
message: `Nav link "${(_c = link.label) !== null && _c !== void 0 ? _c : link.id}" has invalid pageId "${link.pageId}" — must be "ui/<id>" or "widget/<id>"`,
|
|
101
|
+
rule: 'pageId-format',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Nav groups (no link, has children) must have no pageId and must have an otherRoutes redirect
|
|
106
|
+
if (((_d = link.children) === null || _d === void 0 ? void 0 : _d.length) && !link.link) {
|
|
107
|
+
if (link.pageId) {
|
|
108
|
+
errors.push({
|
|
109
|
+
file,
|
|
110
|
+
message: `Nav group "${link.label}" has children and no link but sets pageId — group parents should have pageId: null`,
|
|
111
|
+
rule: 'nav-group-no-pageid',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Parents that have children and a link should have an otherRoutes redirect to one of their children
|
|
116
|
+
if (((_e = link.children) === null || _e === void 0 ? void 0 : _e.length) && link.link) {
|
|
117
|
+
const fullLink = `${parsed.baseRoute}${link.link}`.replace(/\/+/g, '/');
|
|
118
|
+
const hasRedirect = Object.entries(otherRoutes).some(([route, val]) => {
|
|
119
|
+
const fullRoute = `${parsed.baseRoute}${route}`.replace(/\/+/g, '/');
|
|
120
|
+
return (fullRoute === fullLink || route === link.link) && !!val.redirectTo;
|
|
121
|
+
});
|
|
122
|
+
if (!hasRedirect) {
|
|
123
|
+
warnings.push({
|
|
124
|
+
file,
|
|
125
|
+
message: `Nav item "${link.label}" has children — add an otherRoutes redirect for "${link.link}" to its default child`,
|
|
126
|
+
rule: 'nav-parent-redirect',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Check for duplicate links (same link value appearing more than once at top level)
|
|
132
|
+
const topLevelLinks = navLinks.filter((l) => l.link).map((l) => l.link);
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
for (const link of topLevelLinks) {
|
|
135
|
+
if (link && seen.has(link)) {
|
|
136
|
+
errors.push({ file, message: `Duplicate navigation link: "${link}"`, rule: 'unique-nav-links' });
|
|
137
|
+
}
|
|
138
|
+
if (link)
|
|
139
|
+
seen.add(link);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
group: 'Configuration.yml',
|
|
143
|
+
passed: errors.length === 0,
|
|
144
|
+
errors,
|
|
145
|
+
warnings,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=config-yml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-yml.js","sourceRoot":"","sources":["../../src/validators/config-yml.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.doc = void 0;
|
|
7
|
+
exports.validateDuplication = validateDuplication;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
exports.doc = {
|
|
11
|
+
id: 'no-duplication',
|
|
12
|
+
severity: 'warn',
|
|
13
|
+
category: 'standards',
|
|
14
|
+
description: 'Significant blocks of duplicated code (default: ≥10 lines) should be extracted to shared utilities.',
|
|
15
|
+
rationale: 'Duplicated logic drifts — the same bug gets fixed in one copy but not the other.',
|
|
16
|
+
fix: 'Extract repeated logic into src/shared/ or a custom hook. Run `npx jscpd src/` to see details.',
|
|
17
|
+
};
|
|
18
|
+
async function validateDuplication(config, cwd) {
|
|
19
|
+
const srcDir = path_1.default.resolve(cwd, config.viewsSrc);
|
|
20
|
+
const minLines = config.duplication.minLines;
|
|
21
|
+
const severity = config.duplication.severity;
|
|
22
|
+
// Run jscpd as a subprocess — most reliable way to handle its output
|
|
23
|
+
let output;
|
|
24
|
+
try {
|
|
25
|
+
output = (0, child_process_1.execSync)(`npx jscpd "${srcDir}" --min-lines ${minLines} --reporters json --silent 2>/dev/null || true`, { cwd, encoding: 'utf8', timeout: 30000 });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {
|
|
29
|
+
group: 'Duplicate Code',
|
|
30
|
+
passed: true,
|
|
31
|
+
errors: [],
|
|
32
|
+
warnings: [],
|
|
33
|
+
skipped: true,
|
|
34
|
+
skipReason: 'jscpd not available',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// jscpd outputs JSON to stdout with --reporters json
|
|
38
|
+
let clones = [];
|
|
39
|
+
try {
|
|
40
|
+
// jscpd outputs to a report file; extract clone count from summary line if present
|
|
41
|
+
const match = output.match(/Found\s+(\d+)\s+clone/i);
|
|
42
|
+
if (match) {
|
|
43
|
+
const count = parseInt(match[1], 10);
|
|
44
|
+
if (count > 0) {
|
|
45
|
+
const msg = {
|
|
46
|
+
message: `Found ${count} code clone${count !== 1 ? 's' : ''} (min ${minLines} lines). Run jscpd manually for details.`,
|
|
47
|
+
rule: 'no-duplication',
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
group: 'Duplicate Code',
|
|
51
|
+
passed: severity !== 'error',
|
|
52
|
+
errors: severity === 'error' ? [msg] : [],
|
|
53
|
+
warnings: severity === 'warn' ? [msg] : [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// No clones found
|
|
58
|
+
void clones;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore parse errors
|
|
62
|
+
}
|
|
63
|
+
return { group: 'Duplicate Code', passed: true, errors: [], warnings: [] };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=duplication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplication.js","sourceRoot":"","sources":["../../src/validators/duplication.ts"],"names":[],"mappings":";;;;;;AAaA,kDAkDC;AA/DD,gDAAwB;AACxB,iDAAyC;AAG5B,QAAA,GAAG,GAAY;IAC1B,EAAE,EAAE,gBAAgB;IACpB,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,WAAW;IACrB,WAAW,EAAE,qGAAqG;IAClH,SAAS,EAAE,kFAAkF;IAC7F,GAAG,EAAE,gGAAgG;CACtG,CAAC;AAEK,KAAK,UAAU,mBAAmB,CAAC,MAAkB,EAAE,GAAW;IACvE,MAAM,MAAM,GAAG,cAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC;IAC7C,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC;IAE7C,qEAAqE;IACrE,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAA,wBAAQ,EACf,cAAc,MAAM,iBAAiB,QAAQ,gDAAgD,EAC7F,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAC1C,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,KAAK,EAAE,gBAAgB;YACvB,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,EAAE;YACV,QAAQ,EAAE,EAAE;YACZ,OAAO,EAAE,IAAI;YACb,UAAU,EAAE,qBAAqB;SAClC,CAAC;IACJ,CAAC;IAED,qDAAqD;IACrD,IAAI,MAAM,GAAuG,EAAE,CAAC;IACpH,IAAI,CAAC;QACH,mFAAmF;QACnF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;QACrD,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACrC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACd,MAAM,GAAG,GAAG;oBACV,OAAO,EAAE,SAAS,KAAK,cAAc,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,SAAS,QAAQ,0CAA0C;oBACtH,IAAI,EAAE,gBAAgB;iBACvB,CAAC;gBACF,OAAO;oBACL,KAAK,EAAE,gBAAgB;oBACvB,MAAM,EAAE,QAAQ,KAAK,OAAO;oBAC5B,MAAM,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE;oBACzC,QAAQ,EAAE,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE;iBAC3C,CAAC;YACJ,CAAC;QACH,CAAC;QACD,kBAAkB;QAClB,KAAK,MAAM,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,sBAAsB;IACxB,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.doc = void 0;
|
|
7
|
+
exports.validateFolderStructure = validateFolderStructure;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
exports.doc = {
|
|
11
|
+
id: 'folder-structure',
|
|
12
|
+
severity: 'warn',
|
|
13
|
+
category: 'patterns',
|
|
14
|
+
description: 'Each views/ subfolder must contain exactly one .tsx file; forms belong in forms/; services.ts required.',
|
|
15
|
+
rationale: 'Consistent structure makes navigation predictable and keeps views from growing into component dumps.',
|
|
16
|
+
fix: 'Move extra .tsx files to components/, form components to forms/, ensure src/index.tsx and src/services.ts exist.',
|
|
17
|
+
};
|
|
18
|
+
async function validateFolderStructure(config, cwd) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
const warnings = [];
|
|
21
|
+
const srcDir = path_1.default.resolve(cwd, config.viewsSrc);
|
|
22
|
+
if (!fs_1.default.existsSync(srcDir)) {
|
|
23
|
+
return {
|
|
24
|
+
group: 'Folder Structure',
|
|
25
|
+
passed: false,
|
|
26
|
+
errors: [{ message: `src/ directory not found: ${config.viewsSrc}` }],
|
|
27
|
+
warnings: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// ── Required files ──────────────────────────────────────────────────────
|
|
31
|
+
const indexTsx = path_1.default.join(srcDir, 'index.tsx');
|
|
32
|
+
if (!fs_1.default.existsSync(indexTsx)) {
|
|
33
|
+
errors.push({ file: `${config.viewsSrc}/index.tsx`, message: 'Missing src/index.tsx' });
|
|
34
|
+
}
|
|
35
|
+
const servicesTsOptions = ['services.ts', 'services.tsx'];
|
|
36
|
+
const hasServices = servicesTsOptions.some((f) => fs_1.default.existsSync(path_1.default.join(srcDir, f)));
|
|
37
|
+
if (!hasServices) {
|
|
38
|
+
errors.push({
|
|
39
|
+
file: `${config.viewsSrc}/services.ts`,
|
|
40
|
+
message: 'Missing src/services.ts — all service configs must be centralized here',
|
|
41
|
+
rule: 'folder-structure',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// ── views/ structure ────────────────────────────────────────────────────
|
|
45
|
+
const viewsDir = path_1.default.join(srcDir, 'views');
|
|
46
|
+
if (fs_1.default.existsSync(viewsDir)) {
|
|
47
|
+
const viewSubdirs = fs_1.default.readdirSync(viewsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
48
|
+
for (const subdir of viewSubdirs) {
|
|
49
|
+
const subdirPath = path_1.default.join(viewsDir, subdir.name);
|
|
50
|
+
const entries = fs_1.default.readdirSync(subdirPath, { withFileTypes: true });
|
|
51
|
+
const tsxFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.tsx'));
|
|
52
|
+
// Each view subfolder should have exactly one .tsx file
|
|
53
|
+
if (tsxFiles.length === 0) {
|
|
54
|
+
warnings.push({
|
|
55
|
+
file: `${config.viewsSrc}/views/${subdir.name}/`,
|
|
56
|
+
message: `View folder "${subdir.name}" has no .tsx file — add a view component`,
|
|
57
|
+
rule: 'folder-structure',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else if (tsxFiles.length > 1) {
|
|
61
|
+
warnings.push({
|
|
62
|
+
file: `${config.viewsSrc}/views/${subdir.name}/`,
|
|
63
|
+
message: `View folder "${subdir.name}" has ${tsxFiles.length} .tsx files. Views should contain exactly one view component — move extras to components/`,
|
|
64
|
+
rule: 'folder-structure',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Form files don't belong in views/ subfolders
|
|
68
|
+
const formFiles = tsxFiles.filter((f) => f.name.toLowerCase().includes('form') || f.name.toLowerCase().includes('dialog'));
|
|
69
|
+
for (const f of formFiles) {
|
|
70
|
+
warnings.push({
|
|
71
|
+
file: `${config.viewsSrc}/views/${subdir.name}/${f.name}`,
|
|
72
|
+
message: `Form/dialog "${f.name}" should be in src/forms/, not src/views/`,
|
|
73
|
+
rule: 'folder-structure',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Also check top-level .tsx files directly in views/ (not in a subfolder)
|
|
78
|
+
const topLevelTsx = fs_1.default.readdirSync(viewsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith('.tsx'));
|
|
79
|
+
for (const f of topLevelTsx) {
|
|
80
|
+
warnings.push({
|
|
81
|
+
file: `${config.viewsSrc}/views/${f.name}`,
|
|
82
|
+
message: `View "${f.name}" should be in its own subfolder: views/${f.name.replace('.tsx', '')}/`,
|
|
83
|
+
rule: 'folder-structure',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ── forms/ check ────────────────────────────────────────────────────────
|
|
88
|
+
// Warn if there are form-named files outside of forms/ directory
|
|
89
|
+
const componentsDir = path_1.default.join(srcDir, 'components');
|
|
90
|
+
if (fs_1.default.existsSync(componentsDir)) {
|
|
91
|
+
const formFilesInComponents = findFormFiles(componentsDir);
|
|
92
|
+
for (const f of formFilesInComponents) {
|
|
93
|
+
const rel = path_1.default.relative(srcDir, f);
|
|
94
|
+
warnings.push({
|
|
95
|
+
file: `${config.viewsSrc}/${rel}`,
|
|
96
|
+
message: `Form component found in components/ — forms belong in src/forms/`,
|
|
97
|
+
rule: 'folder-structure',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
group: 'Folder Structure',
|
|
103
|
+
passed: errors.length === 0,
|
|
104
|
+
errors,
|
|
105
|
+
warnings,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function findFormFiles(dir) {
|
|
109
|
+
const results = [];
|
|
110
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
111
|
+
const full = path_1.default.join(dir, entry.name);
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
results.push(...findFormFiles(full));
|
|
114
|
+
}
|
|
115
|
+
else if (entry.isFile() &&
|
|
116
|
+
entry.name.endsWith('.tsx') &&
|
|
117
|
+
(entry.name.toLowerCase().includes('form') || entry.name.toLowerCase().includes('dialog'))) {
|
|
118
|
+
results.push(full);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=folder-structure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-structure.js","sourceRoot":"","sources":["../../src/validators/folder-structure.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.doc = void 0;
|
|
7
|
+
exports.validateFormatting = validateFormatting;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
exports.doc = {
|
|
11
|
+
id: 'prettier',
|
|
12
|
+
severity: 'warn',
|
|
13
|
+
category: 'standards',
|
|
14
|
+
description: 'All source files must be formatted according to the project Prettier config.',
|
|
15
|
+
rationale: 'Consistent formatting eliminates noise in code reviews and prevents merge conflicts.',
|
|
16
|
+
fix: 'Run `uxp-lint --fix` to auto-format, or `npx prettier --write .`',
|
|
17
|
+
};
|
|
18
|
+
function collectSourceFiles(dir) {
|
|
19
|
+
if (!fs_1.default.existsSync(dir))
|
|
20
|
+
return [];
|
|
21
|
+
const results = [];
|
|
22
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const full = path_1.default.join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') {
|
|
25
|
+
results.push(...collectSourceFiles(full));
|
|
26
|
+
}
|
|
27
|
+
else if (entry.isFile() &&
|
|
28
|
+
(entry.name.endsWith('.ts') ||
|
|
29
|
+
entry.name.endsWith('.tsx') ||
|
|
30
|
+
entry.name.endsWith('.scss'))) {
|
|
31
|
+
results.push(full);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
async function validateFormatting(config, cwd, fix) {
|
|
37
|
+
var _a;
|
|
38
|
+
const errors = [];
|
|
39
|
+
const warnings = [];
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
let prettier;
|
|
42
|
+
try {
|
|
43
|
+
prettier = require('prettier');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {
|
|
47
|
+
group: 'Formatting (Prettier)',
|
|
48
|
+
passed: true,
|
|
49
|
+
errors: [],
|
|
50
|
+
warnings: [],
|
|
51
|
+
skipped: true,
|
|
52
|
+
skipReason: 'prettier not installed',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const srcDir = path_1.default.resolve(cwd, config.viewsSrc);
|
|
56
|
+
const files = collectSourceFiles(srcDir);
|
|
57
|
+
// Use project's own Prettier config if it exists, otherwise fall back to defaults
|
|
58
|
+
const resolvedConfig = (_a = (await prettier.resolveConfig(cwd))) !== null && _a !== void 0 ? _a : {
|
|
59
|
+
singleQuote: true,
|
|
60
|
+
trailingComma: 'es5',
|
|
61
|
+
printWidth: 120,
|
|
62
|
+
tabWidth: 4,
|
|
63
|
+
semi: true,
|
|
64
|
+
};
|
|
65
|
+
for (const filePath of files) {
|
|
66
|
+
const source = fs_1.default.readFileSync(filePath, 'utf8');
|
|
67
|
+
const relPath = path_1.default.relative(cwd, filePath);
|
|
68
|
+
const parser = filePath.endsWith('.scss') ? 'scss' : 'typescript';
|
|
69
|
+
let formatted;
|
|
70
|
+
try {
|
|
71
|
+
formatted = await prettier.format(source, { ...resolvedConfig, parser });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Skip files that can't be parsed by Prettier
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (formatted !== source) {
|
|
78
|
+
if (fix) {
|
|
79
|
+
fs_1.default.writeFileSync(filePath, formatted, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
warnings.push({
|
|
83
|
+
file: relPath,
|
|
84
|
+
message: 'File is not formatted. Run uxp-lint --fix to auto-format.',
|
|
85
|
+
rule: 'prettier',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
group: 'Formatting (Prettier)',
|
|
92
|
+
passed: true, // formatting issues are always warnings
|
|
93
|
+
errors,
|
|
94
|
+
warnings,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=formatting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatting.js","sourceRoot":"","sources":["../../src/validators/formatting.ts"],"names":[],"mappings":";;;;;;AAgCA,gDA+DC;AA/FD,4CAAoB;AACpB,gDAAwB;AAGX,QAAA,GAAG,GAAY;IAC1B,EAAE,EAAE,UAAU;IACd,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,WAAW;IACrB,WAAW,EAAE,8EAA8E;IAC3F,SAAS,EAAE,sFAAsF;IACjG,GAAG,EAAE,kEAAkE;CACxE,CAAC;AAEF,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,KAAK,IAAI,YAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,KAAK,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAClF,OAAO,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5C,CAAC;aAAM,IACL,KAAK,CAAC,MAAM,EAAE;YACd,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;gBACzB,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC3B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAC/B,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAEM,KAAK,UAAU,kBAAkB,CAAC,MAAkB,EAAE,GAAW,EAAE,GAAY;;IACpF,MAAM,MAAM,GAA0B,EAAE,CAAC;IACzC,MAAM,QAAQ,GAA4B,EAAE,CAAC;IAE7C,iEAAiE;IACjE,IAAI,QAAmC,CAAC;IACxC,IAAI,CAAC;QACH,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,KAAK,EAAE,uBAAuB;YAC9B,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,EAAE;YACV,QAAQ,EAAE,EAAE;YACZ,OAAO,EAAE,IAAI;YACb,UAAU,EAAE,wBAAwB;SACrC,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,cAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAEzC,kFAAkF;IAClF,MAAM,cAAc,GAAG,MAAA,CAAC,MAAM,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,mCAAI;QAC5D,WAAW,EAAE,IAAI;QACjB,aAAa,EAAE,KAAc;QAC7B,UAAU,EAAE,GAAG;QACf,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,IAAI;KACX,CAAC;IAEF,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,cAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC;QAElE,IAAI,SAAiB,CAAC;QACtB,IAAI,CAAC;YACH,SAAS,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,GAAG,cAAc,EAAE,MAAM,EAAE,CAAC,CAAC;QAC3E,CAAC;QAAC,MAAM,CAAC;YACP,8CAA8C;YAC9C,SAAS;QACX,CAAC;QAED,IAAI,SAAS,KAAK,MAAM,EAAE,CAAC;YACzB,IAAI,GAAG,EAAE,CAAC;gBACR,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;YAChD,CAAC;iBAAM,CAAC;gBACN,QAAQ,CAAC,IAAI,CAAC;oBACZ,IAAI,EAAE,OAAO;oBACb,OAAO,EAAE,2DAA2D;oBACpE,IAAI,EAAE,UAAU;iBACjB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,EAAE,uBAAuB;QAC9B,MAAM,EAAE,IAAI,EAAE,wCAAwC;QACtD,MAAM;QACN,QAAQ;KACT,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.docs = void 0;
|
|
7
|
+
exports.validateScssRules = validateScssRules;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
exports.docs = [
|
|
11
|
+
{
|
|
12
|
+
id: 'scss-app-prefix',
|
|
13
|
+
severity: 'warn',
|
|
14
|
+
category: 'patterns',
|
|
15
|
+
description: 'Top-level SCSS class selectors must start with the app-specific prefix (e.g. ilocapp_).',
|
|
16
|
+
rationale: 'App-prefixed classes prevent style collisions between apps sharing the same global stylesheet.',
|
|
17
|
+
fix: 'Rename .my-class to .ilocapp_myclass. Set appPrefix in .uxplintrc.json or auto-detect from global.scss.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'no-direct-uxp-override',
|
|
21
|
+
severity: 'error',
|
|
22
|
+
category: 'patterns',
|
|
23
|
+
description: 'Direct top-level overrides of .uxp-* or .uxpcore_* classes are not allowed.',
|
|
24
|
+
rationale: 'Top-level UXP overrides break other apps sharing the stylesheet and couple code to UXP internals.',
|
|
25
|
+
fix: 'Nest UXP class overrides inside your app-prefixed class: `.ilocapp_myview { .uxp-button { ... } }`',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
async function validateScssRules(config, cwd) {
|
|
29
|
+
var _a, _b, _c;
|
|
30
|
+
const errors = [];
|
|
31
|
+
const warnings = [];
|
|
32
|
+
const srcDir = path_1.default.resolve(cwd, config.viewsSrc);
|
|
33
|
+
if (!fs_1.default.existsSync(srcDir)) {
|
|
34
|
+
return { group: 'SCSS Rules', passed: true, errors: [], warnings: [], skipped: true, skipReason: `src/ not found` };
|
|
35
|
+
}
|
|
36
|
+
const scssFiles = collectScssFiles(srcDir);
|
|
37
|
+
if (scssFiles.length === 0) {
|
|
38
|
+
return { group: 'SCSS Rules', passed: true, errors: [], warnings: [], skipped: true, skipReason: 'No .scss files found' };
|
|
39
|
+
}
|
|
40
|
+
// Derive app prefix: from config, or auto-detect from global.scss
|
|
41
|
+
const appPrefix = (_a = config.appPrefix) !== null && _a !== void 0 ? _a : detectAppPrefix(srcDir);
|
|
42
|
+
for (const filePath of scssFiles) {
|
|
43
|
+
const relPath = path_1.default.relative(cwd, filePath);
|
|
44
|
+
const lines = fs_1.default.readFileSync(filePath, 'utf8').split('\n');
|
|
45
|
+
let depth = 0;
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i];
|
|
48
|
+
const lineNum = i + 1;
|
|
49
|
+
// Track brace depth — count opens and closes on this line
|
|
50
|
+
const opens = ((_b = line.match(/\{/g)) !== null && _b !== void 0 ? _b : []).length;
|
|
51
|
+
const closes = ((_c = line.match(/\}/g)) !== null && _c !== void 0 ? _c : []).length;
|
|
52
|
+
// Check selectors only at depth 0 (before counting opens on this line)
|
|
53
|
+
if (depth === 0) {
|
|
54
|
+
const selector = extractSelector(line);
|
|
55
|
+
if (selector) {
|
|
56
|
+
if (isUxpSelector(selector)) {
|
|
57
|
+
errors.push({
|
|
58
|
+
file: relPath,
|
|
59
|
+
line: lineNum,
|
|
60
|
+
message: `Top-level UXP override "${selector}" — nest this inside a custom app-prefixed class`,
|
|
61
|
+
rule: 'no-direct-uxp-override',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else if (appPrefix && !selector.startsWith(`.${appPrefix}`) && !selector.startsWith('%') && !selector.startsWith('@')) {
|
|
65
|
+
// Only warn on class selectors that don't match the app prefix
|
|
66
|
+
if (selector.startsWith('.') && !selector.startsWith('.uxp')) {
|
|
67
|
+
warnings.push({
|
|
68
|
+
file: relPath,
|
|
69
|
+
line: lineNum,
|
|
70
|
+
message: `Top-level selector "${selector}" should start with app prefix ".${appPrefix}"`,
|
|
71
|
+
rule: 'scss-app-prefix',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
depth += opens - closes;
|
|
78
|
+
if (depth < 0)
|
|
79
|
+
depth = 0; // guard against malformed SCSS
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
group: 'SCSS Rules',
|
|
84
|
+
passed: errors.length === 0,
|
|
85
|
+
errors,
|
|
86
|
+
warnings,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
90
|
+
function collectScssFiles(dir) {
|
|
91
|
+
const results = [];
|
|
92
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
const full = path_1.default.join(dir, entry.name);
|
|
94
|
+
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') {
|
|
95
|
+
results.push(...collectScssFiles(full));
|
|
96
|
+
}
|
|
97
|
+
else if (entry.isFile() && entry.name.endsWith('.scss')) {
|
|
98
|
+
results.push(full);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Extract the primary selector from a line of SCSS, or null if the line is not a selector.
|
|
105
|
+
* Handles: `.class {`, `.class,`, `#id {`, `&:hover {`, `@mixin name {`
|
|
106
|
+
*/
|
|
107
|
+
function extractSelector(line) {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (!trimmed)
|
|
110
|
+
return null;
|
|
111
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
|
|
112
|
+
return null;
|
|
113
|
+
// Lines with property: value patterns are not selectors
|
|
114
|
+
if (/^[\w-]+\s*:/.test(trimmed) && !trimmed.includes('{'))
|
|
115
|
+
return null;
|
|
116
|
+
// Must end with { or , (selector line), or be a pure selector without property
|
|
117
|
+
if (!trimmed.includes('{') && !trimmed.endsWith(','))
|
|
118
|
+
return null;
|
|
119
|
+
// Pull first token up to whitespace, comma, colon, or brace
|
|
120
|
+
const match = trimmed.match(/^([.#%@&\w][\w-]*(?:\.[.\w-]+)*)/);
|
|
121
|
+
if (!match)
|
|
122
|
+
return null;
|
|
123
|
+
// Return the first "word" — the primary selector token
|
|
124
|
+
const sel = trimmed.split(/[\s,{]/)[0].trim();
|
|
125
|
+
return sel || null;
|
|
126
|
+
}
|
|
127
|
+
function isUxpSelector(selector) {
|
|
128
|
+
return selector.startsWith('.uxp-') || selector.startsWith('.uxpcore_') || selector.startsWith('.uxp_');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Auto-detect the app prefix by scanning for the first top-level class in global.scss
|
|
132
|
+
* that ends with an underscore and looks like an app prefix (e.g. "isystemapp_").
|
|
133
|
+
*/
|
|
134
|
+
function detectAppPrefix(srcDir) {
|
|
135
|
+
const candidates = [
|
|
136
|
+
path_1.default.join(srcDir, 'assets', 'styles', 'global.scss'),
|
|
137
|
+
path_1.default.join(srcDir, 'assets', 'scss', 'global.scss'),
|
|
138
|
+
path_1.default.join(srcDir, 'styles', 'global.scss'),
|
|
139
|
+
path_1.default.join(srcDir, 'global.scss'),
|
|
140
|
+
];
|
|
141
|
+
for (const candidate of candidates) {
|
|
142
|
+
if (!fs_1.default.existsSync(candidate))
|
|
143
|
+
continue;
|
|
144
|
+
const lines = fs_1.default.readFileSync(candidate, 'utf8').split('\n');
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
if (!trimmed.startsWith('.'))
|
|
148
|
+
continue;
|
|
149
|
+
const m = trimmed.match(/^\.(([a-z]+app)_[\w-]*)\s*[{,]/);
|
|
150
|
+
if (m)
|
|
151
|
+
return `${m[2]}_`; // e.g. "isystemapp_"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=scss-rules.js.map
|