@birdapi/velinstyle 1.2.1 → 1.3.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/README.de.md +148 -11
- package/README.md +174 -16
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/01-login/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/04-pricing/index.html +5 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.css +1 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/app.js +2 -0
- package/cli/__fixtures__/atelier-library/showcases/36-calendar/index.html +5 -0
- package/cli/atelier-catalog.json +1267 -0
- package/cli/atelier-formats.js +159 -0
- package/cli/atelier.js +382 -0
- package/cli/blueprints/empty-state.html +3 -5
- package/cli/cli-manifest.json +441 -161
- package/cli/docgen/extract-cli.js +2 -1
- package/cli/docs-generate.js +9 -0
- package/cli/experience.js +173 -0
- package/cli/index.js +1384 -361
- package/cli/motion.js +738 -0
- package/cli/production/component-graph.json +166 -0
- package/cli/production/explain.js +52 -0
- package/cli/production/extract.js +278 -0
- package/cli/production/graph.js +102 -0
- package/cli/production/report.js +168 -0
- package/cli/production/run.js +355 -0
- package/cli/production/trim-css.js +177 -0
- package/cli/production/trim-fonts.js +23 -0
- package/cli/production/trim-icons.js +38 -0
- package/cli/production/trim-js.js +48 -0
- package/cli/production/trim-motion.js +22 -0
- package/cli/production/trim-themes.js +50 -0
- package/cli/production/watch.js +38 -0
- package/cli/review.js +48 -1
- package/cli/scripts/write-atelier-fixtures.mjs +33 -0
- package/cli/security.js +190 -0
- package/cli/skills.js +53 -10
- package/cli/transparency.js +140 -41
- package/cli/workflow.js +32 -17
- package/components/index.js +3 -0
- package/components/runtime/component-loaders.js +3 -0
- package/components/velin-drawer.js +43 -4
- package/components/velin-empty-state.js +83 -0
- package/components/velin-modal.js +76 -15
- package/components/velin-otp-input.js +220 -0
- package/components/velin-password-strength.js +147 -0
- package/components/velin-sheet.js +49 -4
- package/components/velin-theme-toggle.js +15 -1
- package/core/a11y/component-contracts.json +391 -255
- package/core/attributes/registry.js +1 -1
- package/core/meta/knowledge/components.json +75 -3
- package/core/motion/analyze.js +38 -0
- package/core/motion/blueprint.js +51 -0
- package/core/motion/composer.js +114 -0
- package/core/motion/data/registry.json +352 -0
- package/core/motion/effects.js +17 -1
- package/core/motion/index.js +112 -9
- package/core/motion/optimize.js +44 -0
- package/core/motion/policy.js +77 -0
- package/core/motion/registry.js +131 -0
- package/core/motion/review.js +59 -0
- package/core/motion/scan.js +150 -0
- package/core/motion/timeline.js +68 -0
- package/core/motion/transitions.js +49 -0
- package/core/motion/triggers.js +168 -0
- package/core/security/checks/fs.js +92 -0
- package/core/security/detect.js +35 -0
- package/core/security/engine.js +116 -0
- package/core/security/index.js +18 -0
- package/core/security/registry.js +85 -0
- package/core/security/report/report.js +113 -0
- package/core/security/rules/ci/index.js +146 -0
- package/core/security/rules/consumer/index.js +174 -0
- package/core/security/rules/deps/index.js +182 -0
- package/core/security/rules/index.js +24 -0
- package/core/security/rules/publish/index.js +230 -0
- package/core/security/rules/repo/index.js +143 -0
- package/core/security/rules/secrets/index.js +133 -0
- package/core/security/score/dependency-health.js +53 -0
- package/core/security/score/release-health.js +66 -0
- package/core/security/score/score.js +102 -0
- package/core/security/timeline/timeline.js +76 -0
- package/dist/chunks/attributes-2ORR27KA.js +404 -0
- package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
- package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
- package/dist/chunks/chunk-MYKUI364.js +116 -0
- package/dist/chunks/chunk-NEVBPD5T.js +116 -0
- package/dist/chunks/chunk-Y6HN7HWX.js +116 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/velin-drawer-A7Q7EBOS.js +162 -0
- package/dist/chunks/velin-empty-state-3NTUH2RF.js +81 -0
- package/dist/chunks/velin-modal-3MV4FYBK.js +231 -0
- package/dist/chunks/velin-otp-input-PSK42MM6.js +207 -0
- package/dist/chunks/velin-password-strength-TAXMLSED.js +136 -0
- package/dist/chunks/velin-sheet-N2VVYXFP.js +157 -0
- package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
- package/dist/llms.txt +4 -4
- package/dist/search-index.json +232 -5
- package/dist/velin-agent.json +433 -46
- package/dist/velinstyle-components.iife.js +3695 -2272
- package/dist/velinstyle-components.js +3698 -2271
- package/dist/velinstyle-components.min.js +262 -120
- package/dist/velinstyle.css +96 -13
- package/dist/velinstyle.d.ts +3 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +19 -4
- package/packages/velinstyle-cli-core/package.json +11 -0
- package/packages/velinstyle-cli-core/src/config.js +84 -0
- package/packages/velinstyle-cli-core/src/contract.js +56 -0
- package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
- package/packages/velinstyle-cli-core/src/index.js +18 -0
- package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
- package/packages/velinstyle-cli-core/src/runner.js +129 -0
- package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
- package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
- package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
- package/packages/velinstyle-cli-registry/package.json +13 -0
- package/packages/velinstyle-cli-registry/src/index.js +88 -0
- package/packages/velinstyle-cli-renderer/package.json +8 -0
- package/packages/velinstyle-cli-renderer/src/index.js +286 -0
- package/packages/velinstyle-cli-theme/package.json +8 -0
- package/packages/velinstyle-cli-theme/src/index.js +84 -0
- package/packages/velinstyle-cli-ui/package.json +8 -0
- package/packages/velinstyle-cli-ui/src/index.js +34 -0
- package/packages/velinstyle-motion/README.md +81 -0
- package/packages/velinstyle-motion/package.json +37 -0
- package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
- package/packages/velinstyle-motion/src/ai/provider.js +59 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
- package/packages/velinstyle-motion/src/config.js +257 -0
- package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
- package/packages/velinstyle-motion/src/index.js +304 -0
- package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
- package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
- package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
- package/packages/velinstyle-motion/src/local/effects.js +139 -0
- package/packages/velinstyle-motion/src/local/engine.js +96 -0
- package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
- package/packages/velinstyle-motion/src/registry/presets.js +25 -0
- package/packages/velinstyle-motion/src/registry/presets.json +85 -0
- package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
- package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
- package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
- package/packages/velinstyle-motion/src/validate-image.js +91 -0
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/packages/velinstyle-skills/registry.json +1 -142
- package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
- package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
- package/src/components/data-table.css +19 -0
- package/src/components/empty-auth.css +33 -0
- package/src/components/table.css +16 -6
- package/src/tokens/motion.css +7 -0
- package/src/utilities/scroll-animation.css +66 -0
- package/src/velinstyle.css +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font utility trim — keep only used velin-font-* / weight utilities in text.css slice.
|
|
3
|
+
* (Full tokens/typography.css stays in alwaysCss; this filters utility text layer extras.)
|
|
4
|
+
*/
|
|
5
|
+
import { filterCssByUsedClasses } from './trim-css.js';
|
|
6
|
+
|
|
7
|
+
export function filterFontUtilities(cssText, usedClasses) {
|
|
8
|
+
const used = usedClasses instanceof Set ? usedClasses : new Set(usedClasses);
|
|
9
|
+
// Always keep base text utilities that are structural if any font class used
|
|
10
|
+
const keep = new Set(used);
|
|
11
|
+
if ([...used].some((c) => c.startsWith('velin-font-') || c.startsWith('velin-text-'))) {
|
|
12
|
+
keep.add('velin-font-sans');
|
|
13
|
+
keep.add('velin-font-normal');
|
|
14
|
+
}
|
|
15
|
+
return filterCssByUsedClasses(cssText, keep, { keepAllWithoutVelinClass: false });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function summarizeFontUsage(extracted) {
|
|
19
|
+
const classes = [...(extracted.fontWeights || extracted.classes || [])]
|
|
20
|
+
.filter((c) => String(c).startsWith('velin-font-') || String(c).startsWith('velin-text-'))
|
|
21
|
+
.sort();
|
|
22
|
+
return { classes, count: classes.length };
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon sprite trim — subset of icons/svg → production sprite.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join, basename } from 'path';
|
|
6
|
+
|
|
7
|
+
export function buildIconSpriteSubset({ pkgRoot, outDir, iconNames }) {
|
|
8
|
+
const svgDir = join(pkgRoot, 'icons', 'svg');
|
|
9
|
+
const names = new Set([...(iconNames || [])].map((n) => String(n).toLowerCase()));
|
|
10
|
+
const available = existsSync(svgDir)
|
|
11
|
+
? readdirSync(svgDir).filter((f) => f.endsWith('.svg')).map((f) => basename(f, '.svg'))
|
|
12
|
+
: [];
|
|
13
|
+
|
|
14
|
+
const selected = available.filter((id) => names.has(id.toLowerCase()));
|
|
15
|
+
// If icons were requested but none match, still emit empty sprite shell when WC icon used
|
|
16
|
+
const includeAllIfEmptyScan = names.size === 0;
|
|
17
|
+
const ids = includeAllIfEmptyScan ? [] : selected;
|
|
18
|
+
const skipped = available.filter((id) => !ids.includes(id));
|
|
19
|
+
|
|
20
|
+
const symbols = ids.map((id) => {
|
|
21
|
+
let content = readFileSync(join(svgDir, `${id}.svg`), 'utf-8');
|
|
22
|
+
content = content.replace(/<svg[^>]*>/, '').replace(/<\/svg>/, '').trim();
|
|
23
|
+
return ` <symbol id="${id}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n ${content}\n </symbol>`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">\n${symbols.join('\n')}\n</svg>\n`;
|
|
27
|
+
mkdirSync(outDir, { recursive: true });
|
|
28
|
+
const outFile = join(outDir, 'velin-icons.svg');
|
|
29
|
+
writeFileSync(outFile, sprite, 'utf-8');
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
outFile,
|
|
33
|
+
included: ids,
|
|
34
|
+
skipped,
|
|
35
|
+
availableCount: available.length,
|
|
36
|
+
bytes: Buffer.byteLength(sprite, 'utf-8'),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit a slim runtime stub that registers only detected Web Components.
|
|
3
|
+
*/
|
|
4
|
+
export function buildProductionJs({ tags = [], features = {} } = {}) {
|
|
5
|
+
const unique = [...new Set(tags.map((t) => String(t).toLowerCase()))].sort();
|
|
6
|
+
const list = JSON.stringify(unique, null, 2);
|
|
7
|
+
const wantAttributes = Boolean(features.attributes);
|
|
8
|
+
const wantHighlight = Boolean(features.highlight);
|
|
9
|
+
const wantMotion = Boolean(features.motion);
|
|
10
|
+
|
|
11
|
+
return `/* VelinStyle Production Runtime — generated */
|
|
12
|
+
/* Registers only Web Components detected in your project. */
|
|
13
|
+
|
|
14
|
+
const TAGS = ${list};
|
|
15
|
+
|
|
16
|
+
export async function bootProduction(options = {}) {
|
|
17
|
+
const { register, bootFromDOM } = await import('@birdapi/velinstyle/runtime');
|
|
18
|
+
if (options.root || ${wantAttributes} || ${wantHighlight} || ${wantMotion}) {
|
|
19
|
+
return bootFromDOM(options.root || (typeof document !== 'undefined' ? document : undefined), {
|
|
20
|
+
tags: TAGS,
|
|
21
|
+
attributes: options.attributes ?? ${wantAttributes},
|
|
22
|
+
highlight: options.highlight ?? ${wantHighlight},
|
|
23
|
+
haptic: options.haptic ?? false,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return register(TAGS);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { TAGS };
|
|
30
|
+
|
|
31
|
+
if (typeof document !== 'undefined' && !globalThis.__VELIN_PRODUCTION_NO_AUTOBOOT__) {
|
|
32
|
+
bootProduction().catch((err) => {
|
|
33
|
+
console.warn('[velinstyle production] boot failed:', err);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function detectRuntimeFeatures(extracted) {
|
|
40
|
+
const attrs = extracted.attrs || new Set();
|
|
41
|
+
const classes = extracted.classes || new Set();
|
|
42
|
+
const motion = extracted.motion || new Set();
|
|
43
|
+
return {
|
|
44
|
+
attributes: [...attrs].some((a) => a.startsWith('data-velin-') && a !== 'data-velin-theme' && a !== 'data-velin-component'),
|
|
45
|
+
highlight: [...classes].some((c) => c.includes('highlight') || c.includes('code')),
|
|
46
|
+
motion: motion.size > 0 || [...classes].some((c) => c.includes('animate')),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion trim helpers — decide whether motion CSS files participate + explain removals.
|
|
3
|
+
*/
|
|
4
|
+
export function shouldIncludeMotion(extracted, { force = null } = {}) {
|
|
5
|
+
if (force === true) return true;
|
|
6
|
+
if (force === false) return false;
|
|
7
|
+
const motion = extracted.motion || new Set();
|
|
8
|
+
const classes = extracted.classes || new Set();
|
|
9
|
+
if (motion.size > 0) return true;
|
|
10
|
+
for (const c of classes) {
|
|
11
|
+
if (/animate|motion|transition|scroll-fade|scroll-reveal/i.test(c)) return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function motionClassSet(extracted) {
|
|
17
|
+
const set = new Set(extracted.motion || []);
|
|
18
|
+
for (const c of extracted.classes || []) {
|
|
19
|
+
if (/animate|motion|scroll-/i.test(c)) set.add(c);
|
|
20
|
+
}
|
|
21
|
+
return set;
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme trim — copy only used theme CSS into production output.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, copyFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { listKnownThemeNames } from './graph.js';
|
|
7
|
+
|
|
8
|
+
export function resolveUsedThemes(extracted, { configThemes = 'auto', pkgRoot, defaultThemes = ['dark'] } = {}) {
|
|
9
|
+
const known = new Set(listKnownThemeNames(pkgRoot));
|
|
10
|
+
const used = new Set();
|
|
11
|
+
|
|
12
|
+
if (Array.isArray(configThemes)) {
|
|
13
|
+
for (const t of configThemes) used.add(String(t).toLowerCase());
|
|
14
|
+
} else {
|
|
15
|
+
for (const t of extracted.themes || []) used.add(String(t).toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Always allow dark / light token paths if referenced via contrast toggles
|
|
19
|
+
for (const d of defaultThemes) {
|
|
20
|
+
if (known.has(d)) used.add(d);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If nothing detected and auto — keep no optional themes (tokens cover default)
|
|
24
|
+
const selected = [...used].filter((t) => known.has(t)).sort();
|
|
25
|
+
const skipped = [...known].filter((t) => !used.has(t)).sort();
|
|
26
|
+
return { selected, skipped, known: [...known].sort() };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeThemes({ pkgRoot, outDir, themes, minifyPrefer = true }) {
|
|
30
|
+
const themeOut = join(outDir, 'themes');
|
|
31
|
+
mkdirSync(themeOut, { recursive: true });
|
|
32
|
+
const written = [];
|
|
33
|
+
const missing = [];
|
|
34
|
+
|
|
35
|
+
for (const name of themes) {
|
|
36
|
+
const distMin = join(pkgRoot, 'dist', 'themes', `${name}.min.css`);
|
|
37
|
+
const src = join(pkgRoot, 'src', 'themes', `${name}.css`);
|
|
38
|
+
const dest = join(themeOut, minifyPrefer && existsSync(distMin) ? `${name}.min.css` : `${name}.css`);
|
|
39
|
+
if (minifyPrefer && existsSync(distMin)) {
|
|
40
|
+
copyFileSync(distMin, dest);
|
|
41
|
+
written.push({ name, file: dest, source: 'dist' });
|
|
42
|
+
} else if (existsSync(src)) {
|
|
43
|
+
writeFileSync(dest, readFileSync(src, 'utf-8'));
|
|
44
|
+
written.push({ name, file: dest, source: 'src' });
|
|
45
|
+
} else {
|
|
46
|
+
missing.push(name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { written, missing, themeOut };
|
|
50
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch content files and re-run production pipeline (debounced, Windows-safe).
|
|
3
|
+
*/
|
|
4
|
+
import { watch as fsWatch } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
export function watchProduction(rootPath, onChange, { debounceMs = 300, ignoreNames = ['node_modules', 'dist', '.git'] } = {}) {
|
|
8
|
+
const abs = resolve(rootPath);
|
|
9
|
+
let timer = null;
|
|
10
|
+
let closed = false;
|
|
11
|
+
|
|
12
|
+
const kick = (event, filename) => {
|
|
13
|
+
if (closed) return;
|
|
14
|
+
if (filename && ignoreNames.some((n) => String(filename).includes(n))) return;
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
timer = setTimeout(() => {
|
|
17
|
+
Promise.resolve()
|
|
18
|
+
.then(() => onChange({ event, filename }))
|
|
19
|
+
.catch((err) => console.error('[velinstyle production --watch]', err));
|
|
20
|
+
}, debounceMs);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let watcher;
|
|
24
|
+
try {
|
|
25
|
+
watcher = fsWatch(abs, { recursive: true }, kick);
|
|
26
|
+
} catch {
|
|
27
|
+
// recursive watch unsupported — watch root only
|
|
28
|
+
watcher = fsWatch(abs, kick);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
close() {
|
|
33
|
+
closed = true;
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
try { watcher.close(); } catch { /* ignore */ }
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
package/cli/review.js
CHANGED
|
@@ -225,6 +225,43 @@ export function reviewHtml(html, ctx = {}) {
|
|
|
225
225
|
const errors = issues.filter((i) => i.severity === 'error').length;
|
|
226
226
|
const warnings = issues.filter((i) => i.severity === 'warning').length;
|
|
227
227
|
|
|
228
|
+
// Optimization heuristics (soft) — full vendor without production output hints
|
|
229
|
+
const usesFullCss = /velinstyle\.min\.css|velinstyle\.css/i.test(text) && !/velin-production/i.test(text);
|
|
230
|
+
const usesFullIife = /velinstyle-components\.min\.js/i.test(text);
|
|
231
|
+
const unusedThemeLinks = [...text.matchAll(/themes\/([a-z0-9-]+)\.min\.css/gi)]
|
|
232
|
+
.map((m) => m[1].toLowerCase());
|
|
233
|
+
const themeAttrs = [...text.matchAll(/data-velin-theme\s*=\s*["']([^"']+)["']/gi)]
|
|
234
|
+
.flatMap((m) => m[1].toLowerCase().split(/[\s,|]+/));
|
|
235
|
+
const unusedThemes = unusedThemeLinks.filter((t) => themeAttrs.length && !themeAttrs.includes(t));
|
|
236
|
+
|
|
237
|
+
if (usesFullCss && usesFullIife) {
|
|
238
|
+
issues.push({
|
|
239
|
+
code: 'optimization.full-bundle',
|
|
240
|
+
severity: 'warning',
|
|
241
|
+
message: 'Full CSS + components IIFE without production output',
|
|
242
|
+
fix: 'Run `velinstyle build --production` and link dist/velin-production assets for publish.',
|
|
243
|
+
});
|
|
244
|
+
} else if (usesFullCss) {
|
|
245
|
+
issues.push({
|
|
246
|
+
code: 'optimization.full-css',
|
|
247
|
+
severity: 'warning',
|
|
248
|
+
message: 'Full VelinStyle CSS bundle linked',
|
|
249
|
+
fix: 'Prefer production CSS from `velinstyle production` for go-live.',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (unusedThemes.length) {
|
|
253
|
+
issues.push({
|
|
254
|
+
code: 'optimization.unused-themes',
|
|
255
|
+
severity: 'warning',
|
|
256
|
+
message: `${unusedThemes.length} theme stylesheet(s) not referenced by data-velin-theme`,
|
|
257
|
+
fix: `Remove unused theme links (${unusedThemes.slice(0, 5).join(', ')}) or run production theme trim.`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const optIssues = issues.filter((i) => i.code.startsWith('optimization'));
|
|
262
|
+
const unusedComponentHint = [...text.matchAll(/<(velin-[a-z0-9-]+)\b/gi)].length;
|
|
263
|
+
const classHits = [...text.matchAll(/\bvelin-[\w:-]+\b/g)].length;
|
|
264
|
+
|
|
228
265
|
const score = (base, penalty) => Math.max(0, Math.round((base - penalty) * 10) / 10);
|
|
229
266
|
|
|
230
267
|
let scores = {
|
|
@@ -234,6 +271,7 @@ export function reviewHtml(html, ctx = {}) {
|
|
|
234
271
|
performance: score(10, issues.filter((i) => i.code.startsWith('perf')).length * 2),
|
|
235
272
|
conversion: score(10, issues.filter((i) => i.code.startsWith('conversion')).length * 2),
|
|
236
273
|
visual: score(9, issues.filter((i) => i.code.startsWith('design')).length * 1.2),
|
|
274
|
+
optimization: score(10, optIssues.length * 2 + (unusedThemes.length ? Math.min(3, unusedThemes.length * 0.5) : 0)),
|
|
237
275
|
};
|
|
238
276
|
|
|
239
277
|
if (thin) {
|
|
@@ -250,10 +288,13 @@ export function reviewHtml(html, ctx = {}) {
|
|
|
250
288
|
if (profile === 'app') scores.seo = Math.min(scores.seo, 7);
|
|
251
289
|
}
|
|
252
290
|
|
|
291
|
+
const finalErrors = issues.filter((i) => i.severity === 'error').length;
|
|
292
|
+
const finalWarnings = issues.filter((i) => i.severity === 'warning').length;
|
|
293
|
+
|
|
253
294
|
const avg = Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length;
|
|
254
295
|
const promptScore = Math.max(0, Math.min(10, avg - (ctx.plan?.warnings?.length ? 1 : 0)));
|
|
255
296
|
|
|
256
|
-
const gate =
|
|
297
|
+
const gate = finalErrors > 0 ? 'fail' : finalWarnings > 0 ? 'warn' : 'pass';
|
|
257
298
|
|
|
258
299
|
return {
|
|
259
300
|
version: 1,
|
|
@@ -262,6 +303,12 @@ export function reviewHtml(html, ctx = {}) {
|
|
|
262
303
|
scores,
|
|
263
304
|
issues,
|
|
264
305
|
gate,
|
|
306
|
+
optimization: {
|
|
307
|
+
fullBundle: Boolean(usesFullCss && usesFullIife),
|
|
308
|
+
unusedThemes: unusedThemes.length,
|
|
309
|
+
componentTags: unusedComponentHint,
|
|
310
|
+
classTokens: classHits,
|
|
311
|
+
},
|
|
265
312
|
};
|
|
266
313
|
}
|
|
267
314
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '__fixtures__', 'atelier-library', 'showcases');
|
|
6
|
+
const items = [
|
|
7
|
+
{ id: '01-login', title: 'Fixture Login' },
|
|
8
|
+
{ id: '04-pricing', title: 'Fixture Pricing' },
|
|
9
|
+
{ id: '36-calendar', title: 'Fixture Calendar' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
for (const it of items) {
|
|
13
|
+
const d = join(root, it.id);
|
|
14
|
+
mkdirSync(d, { recursive: true });
|
|
15
|
+
writeFileSync(
|
|
16
|
+
join(d, 'index.html'),
|
|
17
|
+
`<!DOCTYPE html>
|
|
18
|
+
<html lang="en"><head><meta charset="UTF-8"><title>${it.title}</title>
|
|
19
|
+
<link rel="stylesheet" href="app.css"></head>
|
|
20
|
+
<body><div id="root" data-atelier-id="${it.id}"></div>
|
|
21
|
+
<script type="module" src="app.js"></script></body></html>
|
|
22
|
+
`,
|
|
23
|
+
);
|
|
24
|
+
writeFileSync(
|
|
25
|
+
join(d, 'app.js'),
|
|
26
|
+
`const root = document.getElementById('root');
|
|
27
|
+
root.innerHTML = '<section class="velin-section" data-fixture="${it.id}"><h1>${it.title}</h1></section>';
|
|
28
|
+
`,
|
|
29
|
+
);
|
|
30
|
+
writeFileSync(join(d, 'app.css'), `[data-fixture="${it.id}"] { padding: 1rem; }\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('fixtures at', root);
|
package/cli/security.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Velin Security Suite — CLI surface over Security Engine.
|
|
3
|
+
* velinstyle security doctor|scan|audit|publish|ci|deps|secrets|report
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
6
|
+
import { resolve, join } from 'path';
|
|
7
|
+
import {
|
|
8
|
+
runSecurityEngine,
|
|
9
|
+
toExperiencePayload,
|
|
10
|
+
toMarkdownReport,
|
|
11
|
+
gateLabel,
|
|
12
|
+
timelineRows,
|
|
13
|
+
} from '../core/security/index.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string[]} args
|
|
17
|
+
* @param {{ C: object, getArg: Function, hasFlag: Function, emitExperience: Function }} util
|
|
18
|
+
*/
|
|
19
|
+
export async function securityCmd(args, util) {
|
|
20
|
+
const { getArg, hasFlag, emitExperience } = util;
|
|
21
|
+
const sub = args[0] || 'doctor';
|
|
22
|
+
const rest = args.slice(1);
|
|
23
|
+
const target = rest.find((a) => !a.startsWith('-')) || '.';
|
|
24
|
+
const root = resolve(target);
|
|
25
|
+
const asJson = hasFlag('--json');
|
|
26
|
+
const strict = hasFlag('--strict');
|
|
27
|
+
const present = (payload) => emitExperience('security', payload);
|
|
28
|
+
|
|
29
|
+
if (sub === '--help' || sub === '-h') {
|
|
30
|
+
await present({
|
|
31
|
+
message: 'Usage: velinstyle security <doctor|scan|audit|publish|ci|deps|secrets|report> [path]',
|
|
32
|
+
suggestions: [
|
|
33
|
+
{ text: 'velinstyle security doctor .' },
|
|
34
|
+
{ text: 'velinstyle security deps .' },
|
|
35
|
+
{ text: 'velinstyle security publish' },
|
|
36
|
+
{ text: 'velinstyle security report --out ./security-report' },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(root)) {
|
|
44
|
+
await present({ status: 'ERROR', message: `Path not found: ${root}`, exitCode: 1 });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const mode = resolveMode(sub, hasFlag);
|
|
49
|
+
const result = await runSecurityEngine({
|
|
50
|
+
root,
|
|
51
|
+
mode,
|
|
52
|
+
includeReleaseHealth: mode === 'publish' || mode === 'audit' || hasFlag('--publish'),
|
|
53
|
+
includeDependencyHealth: true,
|
|
54
|
+
updateTimeline: sub !== 'report' || true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (sub === 'report') {
|
|
58
|
+
const out = getArg('--out') || getArg('--output') || getArg('-o') || join(root, '.velin', 'security', 'report');
|
|
59
|
+
mkdirSync(out, { recursive: true });
|
|
60
|
+
writeFileSync(join(out, 'security-report.json'), `${JSON.stringify(result, null, 2)}\n`);
|
|
61
|
+
writeFileSync(join(out, 'security-report.md'), toMarkdownReport(result));
|
|
62
|
+
if (asJson) {
|
|
63
|
+
console.log(JSON.stringify({ ok: result.ok, out, score: result.securityScore }, null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await present({
|
|
67
|
+
subtitle: 'report',
|
|
68
|
+
message: `Wrote security report → ${out}`,
|
|
69
|
+
items: [
|
|
70
|
+
{ label: 'Score', value: String(result.securityScore) },
|
|
71
|
+
{ label: 'Overall', value: result.overall },
|
|
72
|
+
],
|
|
73
|
+
...toExperiencePayload(result),
|
|
74
|
+
});
|
|
75
|
+
setExit(result, strict);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (asJson) {
|
|
80
|
+
console.log(JSON.stringify(result, null, 2));
|
|
81
|
+
setExit(result, strict);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const exp = toExperiencePayload(result);
|
|
86
|
+
const extra = [];
|
|
87
|
+
if (result.dependencyHealth) {
|
|
88
|
+
extra.push({
|
|
89
|
+
label: 'Dep Health',
|
|
90
|
+
value: `${result.dependencyHealth.score}% (${result.dependencyHealth.deprecated} deprecated, ${result.dependencyHealth.noLockfile ?? 0} no-lockfile, ${result.dependencyHealth.outdated} outdated-heuristic, ${result.dependencyHealth.critical} critical)`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (result.releaseHealth) {
|
|
94
|
+
extra.push({ label: 'Release', value: result.releaseHealth.status });
|
|
95
|
+
}
|
|
96
|
+
const tl = timelineRows(result.timeline || { entries: {} });
|
|
97
|
+
await present({
|
|
98
|
+
subtitle: sub,
|
|
99
|
+
status: result.ok ? (result.overallGate === 'warn' ? 'WARNING' : 'PASS') : 'ERROR',
|
|
100
|
+
message: exp.message,
|
|
101
|
+
items: [...(exp.items || []), ...extra],
|
|
102
|
+
table: exp.table,
|
|
103
|
+
score: exp.score,
|
|
104
|
+
notifications: exp.notifications,
|
|
105
|
+
findings: (result.findings || []).slice(0, 25).map((f) => ({
|
|
106
|
+
severity: f.severity,
|
|
107
|
+
message: `[${f.ruleId}] ${f.message}`,
|
|
108
|
+
path: f.path,
|
|
109
|
+
})),
|
|
110
|
+
suggestions: tl.length
|
|
111
|
+
? tl.map((r) => ({ text: `${r.title}: ${r.date}` }))
|
|
112
|
+
: [{ text: 'velinstyle security report --out ./.velin/security/report' }],
|
|
113
|
+
exitCode: exitCode(result, strict),
|
|
114
|
+
});
|
|
115
|
+
} catch (err) {
|
|
116
|
+
await present({
|
|
117
|
+
status: 'ERROR',
|
|
118
|
+
message: err?.message || String(err),
|
|
119
|
+
exitCode: 1,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveMode(sub, hasFlag) {
|
|
125
|
+
if (hasFlag('--repo')) return 'repo';
|
|
126
|
+
if (hasFlag('--consumer')) return 'consumer';
|
|
127
|
+
if (hasFlag('--ci')) return 'ci';
|
|
128
|
+
if (hasFlag('--publish')) return 'publish';
|
|
129
|
+
if (hasFlag('--deps')) return 'deps';
|
|
130
|
+
if (hasFlag('--secrets')) return 'secrets';
|
|
131
|
+
if (sub === 'doctor') return 'doctor';
|
|
132
|
+
if (sub === 'scan') return 'scan';
|
|
133
|
+
if (sub === 'audit') return 'audit';
|
|
134
|
+
if (sub === 'publish') return 'publish';
|
|
135
|
+
if (sub === 'ci') return 'ci';
|
|
136
|
+
if (sub === 'deps') return 'deps';
|
|
137
|
+
if (sub === 'secrets') return 'secrets';
|
|
138
|
+
if (sub === 'report') return 'audit';
|
|
139
|
+
return 'doctor';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function exitCode(result, strict) {
|
|
143
|
+
if (result.overallGate === 'fail') return 1;
|
|
144
|
+
if (strict && result.overallGate === 'warn') return 1;
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setExit(result, strict) {
|
|
149
|
+
const code = exitCode(result, strict);
|
|
150
|
+
if (code) process.exitCode = code;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check-pipeline step (used by `velinstyle check`).
|
|
155
|
+
* @param {string} path
|
|
156
|
+
* @param {{ quiet?: boolean, C?: object, strict?: boolean }} [opts]
|
|
157
|
+
*/
|
|
158
|
+
export async function securityCheckStep(path, { quiet, C, strict } = {}) {
|
|
159
|
+
try {
|
|
160
|
+
const root = resolve(path);
|
|
161
|
+
if (!existsSync(root)) return { ok: true, skipped: true };
|
|
162
|
+
|
|
163
|
+
const result = await runSecurityEngine({
|
|
164
|
+
root,
|
|
165
|
+
mode: 'doctor',
|
|
166
|
+
includeReleaseHealth: false,
|
|
167
|
+
includeDependencyHealth: true,
|
|
168
|
+
updateTimeline: true,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const fail = result.overallGate === 'fail' || (strict && result.overallGate === 'warn');
|
|
172
|
+
if (!quiet && C) {
|
|
173
|
+
const score = result.securityScore != null ? `${result.securityScore}%` : '—';
|
|
174
|
+
const gate = result.overallGate || '—';
|
|
175
|
+
const line = ` security: ${score} · ${gate} · ${result.overall || ''}`;
|
|
176
|
+
console.log(fail ? C.red(line) : result.overallGate === 'warn' ? C.yellow(line) : C.green(line));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
ok: !fail,
|
|
181
|
+
skipped: false,
|
|
182
|
+
securityScore: result.securityScore,
|
|
183
|
+
overall: result.overall,
|
|
184
|
+
overallGate: result.overallGate,
|
|
185
|
+
findings: result.findings?.length || 0,
|
|
186
|
+
};
|
|
187
|
+
} catch {
|
|
188
|
+
return { ok: true, skipped: true };
|
|
189
|
+
}
|
|
190
|
+
}
|