@fragments-sdk/cli 0.7.4 → 0.7.5
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/LICENSE +1 -4
- package/dist/bin.js +33 -14
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-NEJ2FBTN.js → chunk-CR3XHBGM.js} +2 -2
- package/dist/{chunk-S56I5FST.js → chunk-EFQ7SIBX.js} +582 -107
- package/dist/chunk-EFQ7SIBX.js.map +1 -0
- package/dist/{chunk-UXLGIGSX.js → chunk-GIC3I2KZ.js} +2 -2
- package/dist/{chunk-R6IZZSE7.js → chunk-JZNATKQA.js} +9 -3
- package/dist/chunk-JZNATKQA.js.map +1 -0
- package/dist/{chunk-P33AKQJW.js → chunk-SFWZ4K7C.js} +8 -2
- package/dist/{chunk-P33AKQJW.js.map → chunk-SFWZ4K7C.js.map} +1 -1
- package/dist/{core-3NMNCLFW.js → core-T7BDYEGO.js} +3 -3
- package/dist/{generate-23VLX7QN.js → generate-C2DKFCFJ.js} +4 -4
- package/dist/index.d.ts +28 -2
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/{init-VYVYMVHH.js → init-O3FCHEPN.js} +22 -6
- package/dist/init-O3FCHEPN.js.map +1 -0
- package/dist/mcp-bin.js +3 -3
- package/dist/{scan-FZR6YVI5.js → scan-IYTZDUKG.js} +5 -5
- package/dist/{service-CFFBHW4X.js → service-VA6XKADO.js} +3 -3
- package/dist/{static-viewer-VA2JXSCX.js → static-viewer-5N42MBDR.js} +3 -3
- package/dist/{test-VTD7R6G2.js → test-OMMDWL2W.js} +3 -3
- package/dist/{tokens-7JA5CPDL.js → tokens-6VJAHFIG.js} +4 -4
- package/dist/{viewer-WXTDDQGK.js → viewer-IVP5XC7U.js} +22 -14
- package/dist/viewer-IVP5XC7U.js.map +1 -0
- package/package.json +4 -2
- package/src/bin.ts +4 -0
- package/src/commands/add.ts +6 -0
- package/src/commands/init.ts +18 -2
- package/src/commands/validate.ts +24 -2
- package/src/core/config.ts +6 -0
- package/src/core/index.ts +1 -0
- package/src/core/schema.ts +6 -0
- package/src/core/types.ts +21 -0
- package/src/index.ts +2 -1
- package/src/service/snippet-validation.test.ts +209 -0
- package/src/service/snippet-validation.ts +635 -0
- package/src/validators.ts +53 -5
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -0
- package/src/viewer/components/CodePanel.naming.test.tsx +60 -0
- package/src/viewer/components/CodePanel.tsx +76 -468
- package/src/viewer/components/Layout.tsx +1 -1
- package/src/viewer/utils/a11y-fixes.ts +24 -9
- package/src/viewer/vite-plugin.ts +9 -1
- package/dist/chunk-R6IZZSE7.js.map +0 -1
- package/dist/chunk-S56I5FST.js.map +0 -1
- package/dist/init-VYVYMVHH.js.map +0 -1
- package/dist/viewer-WXTDDQGK.js.map +0 -1
- /package/dist/{chunk-NEJ2FBTN.js.map → chunk-CR3XHBGM.js.map} +0 -0
- /package/dist/{chunk-UXLGIGSX.js.map → chunk-GIC3I2KZ.js.map} +0 -0
- /package/dist/{core-3NMNCLFW.js.map → core-T7BDYEGO.js.map} +0 -0
- /package/dist/{generate-23VLX7QN.js.map → generate-C2DKFCFJ.js.map} +0 -0
- /package/dist/{scan-FZR6YVI5.js.map → scan-IYTZDUKG.js.map} +0 -0
- /package/dist/{service-CFFBHW4X.js.map → service-VA6XKADO.js.map} +0 -0
- /package/dist/{static-viewer-VA2JXSCX.js.map → static-viewer-5N42MBDR.js.map} +0 -0
- /package/dist/{test-VTD7R6G2.js.map → test-OMMDWL2W.js.map} +0 -0
- /package/dist/{tokens-7JA5CPDL.js.map → tokens-6VJAHFIG.js.map} +0 -0
package/src/core/types.ts
CHANGED
|
@@ -426,6 +426,24 @@ export interface CIConfig {
|
|
|
426
426
|
jsonOutput?: boolean;
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Snippet policy configuration.
|
|
431
|
+
* Controls snippet/render quality enforcement in `fragments validate`.
|
|
432
|
+
*/
|
|
433
|
+
export interface SnippetPolicyConfig {
|
|
434
|
+
/** Validation mode: warn (non-blocking) or error (blocking). Default: warn */
|
|
435
|
+
mode?: "warn" | "error";
|
|
436
|
+
|
|
437
|
+
/** Validate snippet strings only, or snippet strings + render functions. Default: snippet+render */
|
|
438
|
+
scope?: "snippet" | "snippet+render";
|
|
439
|
+
|
|
440
|
+
/** Require authored snippets to be full, copy-pasteable examples with imports. Default: true */
|
|
441
|
+
requireFullSnippet?: boolean;
|
|
442
|
+
|
|
443
|
+
/** Allow these external modules for JSX components in snippets/renders. */
|
|
444
|
+
allowedExternalModules?: string[];
|
|
445
|
+
}
|
|
446
|
+
|
|
429
447
|
/**
|
|
430
448
|
* Config file structure
|
|
431
449
|
*/
|
|
@@ -465,6 +483,9 @@ export interface FragmentsConfig {
|
|
|
465
483
|
|
|
466
484
|
/** CI pipeline configuration */
|
|
467
485
|
ci?: CIConfig;
|
|
486
|
+
|
|
487
|
+
/** Snippet/render policy validation */
|
|
488
|
+
snippets?: SnippetPolicyConfig;
|
|
468
489
|
}
|
|
469
490
|
|
|
470
491
|
/**
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,12 @@ export {
|
|
|
9
9
|
export type { DiscoveredFile } from "./core/node.js";
|
|
10
10
|
|
|
11
11
|
// Validators
|
|
12
|
-
export { validateSchema, validateCoverage, validateAll } from "./validators.js";
|
|
12
|
+
export { validateSchema, validateCoverage, validateAll, validateSnippets } from "./validators.js";
|
|
13
13
|
export type {
|
|
14
14
|
ValidationResult,
|
|
15
15
|
ValidationError,
|
|
16
16
|
ValidationWarning,
|
|
17
|
+
ValidationRunOptions,
|
|
17
18
|
} from "./validators.js";
|
|
18
19
|
|
|
19
20
|
// Build
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import type { FragmentsConfig } from '../core/types.js';
|
|
6
|
+
import { validateSnippetPolicy } from './snippet-validation.js';
|
|
7
|
+
|
|
8
|
+
async function createTempProject(): Promise<string> {
|
|
9
|
+
return mkdtemp(resolve(tmpdir(), 'fragments-snippets-'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function writeFragment(projectDir: string, componentName: string, content: string): Promise<void> {
|
|
13
|
+
const dir = resolve(projectDir, 'src/components', componentName);
|
|
14
|
+
await mkdir(dir, { recursive: true });
|
|
15
|
+
await writeFile(resolve(dir, `${componentName}.fragment.tsx`), content);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const baseConfig: FragmentsConfig = {
|
|
19
|
+
include: ['src/**/*.fragment.tsx'],
|
|
20
|
+
exclude: ['**/node_modules/**'],
|
|
21
|
+
framework: 'react',
|
|
22
|
+
snippets: {
|
|
23
|
+
mode: 'warn',
|
|
24
|
+
scope: 'snippet+render',
|
|
25
|
+
requireFullSnippet: true,
|
|
26
|
+
allowedExternalModules: ['@phosphor-icons/react'],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let lastProjectDir: string | null = null;
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
if (lastProjectDir) {
|
|
34
|
+
await rm(lastProjectDir, { recursive: true, force: true });
|
|
35
|
+
lastProjectDir = null;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('validateSnippetPolicy', () => {
|
|
40
|
+
it('reports snippet and render violations in warn mode', async () => {
|
|
41
|
+
const projectDir = await createTempProject();
|
|
42
|
+
lastProjectDir = projectDir;
|
|
43
|
+
|
|
44
|
+
await writeFragment(
|
|
45
|
+
projectDir,
|
|
46
|
+
'Button',
|
|
47
|
+
`import React from 'react';
|
|
48
|
+
import { defineFragment } from '@fragments/core';
|
|
49
|
+
import { Button } from '.';
|
|
50
|
+
|
|
51
|
+
export default defineFragment({
|
|
52
|
+
component: Button,
|
|
53
|
+
meta: { name: 'Button', description: 'Button', category: 'actions' },
|
|
54
|
+
usage: { when: ['x'], whenNot: ['y'] },
|
|
55
|
+
props: {},
|
|
56
|
+
variants: [
|
|
57
|
+
{
|
|
58
|
+
name: 'Default',
|
|
59
|
+
description: 'Default',
|
|
60
|
+
code: '<Button />',
|
|
61
|
+
render: () => (
|
|
62
|
+
<div style={{ display: 'flex' }}>
|
|
63
|
+
<Button>Click</Button>
|
|
64
|
+
</div>
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
`,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const result = await validateSnippetPolicy(baseConfig, projectDir);
|
|
73
|
+
|
|
74
|
+
expect(result.errors).toHaveLength(0);
|
|
75
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
76
|
+
expect(result.warnings.some((w) => w.message.includes('full snippet required (missing import statement)'))).toBe(true);
|
|
77
|
+
expect(result.warnings.some((w) => w.message.includes('inline style usage is not allowed'))).toBe(true);
|
|
78
|
+
expect(result.warnings.some((w) => w.message.includes('raw HTML tags are not allowed'))).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('allows approved external JSX modules in snippets', async () => {
|
|
82
|
+
const projectDir = await createTempProject();
|
|
83
|
+
lastProjectDir = projectDir;
|
|
84
|
+
|
|
85
|
+
await writeFragment(
|
|
86
|
+
projectDir,
|
|
87
|
+
'Icon',
|
|
88
|
+
`import React from 'react';
|
|
89
|
+
import { defineFragment } from '@fragments/core';
|
|
90
|
+
import { Icon } from '.';
|
|
91
|
+
|
|
92
|
+
export default defineFragment({
|
|
93
|
+
component: Icon,
|
|
94
|
+
meta: { name: 'Icon', description: 'Icon', category: 'display' },
|
|
95
|
+
usage: { when: ['x'], whenNot: ['y'] },
|
|
96
|
+
props: {},
|
|
97
|
+
variants: [
|
|
98
|
+
{
|
|
99
|
+
name: 'Default',
|
|
100
|
+
description: 'Default',
|
|
101
|
+
code: \`import { Icon } from '@fragments-sdk/ui';
|
|
102
|
+
import { House } from '@phosphor-icons/react';
|
|
103
|
+
|
|
104
|
+
<Icon icon={House} />
|
|
105
|
+
<House />\`,
|
|
106
|
+
render: () => <Icon icon={() => null} />,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const result = await validateSnippetPolicy(baseConfig, projectDir);
|
|
114
|
+
|
|
115
|
+
expect(result.errors).toHaveLength(0);
|
|
116
|
+
expect(result.warnings).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('does not treat PascalCase component names like Header as intrinsic html tags', async () => {
|
|
120
|
+
const projectDir = await createTempProject();
|
|
121
|
+
lastProjectDir = projectDir;
|
|
122
|
+
|
|
123
|
+
await writeFragment(
|
|
124
|
+
projectDir,
|
|
125
|
+
'Header',
|
|
126
|
+
`import React from 'react';
|
|
127
|
+
import { defineFragment } from '@fragments/core';
|
|
128
|
+
import { Header } from '.';
|
|
129
|
+
|
|
130
|
+
export default defineFragment({
|
|
131
|
+
component: Header,
|
|
132
|
+
meta: { name: 'Header', description: 'Header', category: 'layout' },
|
|
133
|
+
usage: { when: ['x'], whenNot: ['y'] },
|
|
134
|
+
props: {},
|
|
135
|
+
variants: [
|
|
136
|
+
{
|
|
137
|
+
name: 'Default',
|
|
138
|
+
description: 'Default',
|
|
139
|
+
code: \`import { Header } from '@/components/Header';
|
|
140
|
+
|
|
141
|
+
<Header>
|
|
142
|
+
<Header.Brand>MyApp</Header.Brand>
|
|
143
|
+
</Header>\`,
|
|
144
|
+
render: () => (
|
|
145
|
+
<Header>
|
|
146
|
+
<Header.Brand>MyApp</Header.Brand>
|
|
147
|
+
</Header>
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const result = await validateSnippetPolicy(baseConfig, projectDir);
|
|
156
|
+
|
|
157
|
+
expect(result.errors).toHaveLength(0);
|
|
158
|
+
expect(result.warnings).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('supports error mode and alphabetical component batch filtering', async () => {
|
|
162
|
+
const projectDir = await createTempProject();
|
|
163
|
+
lastProjectDir = projectDir;
|
|
164
|
+
|
|
165
|
+
await writeFragment(
|
|
166
|
+
projectDir,
|
|
167
|
+
'Alpha',
|
|
168
|
+
`import React from 'react';
|
|
169
|
+
import { defineFragment } from '@fragments/core';
|
|
170
|
+
import { Alpha } from '.';
|
|
171
|
+
|
|
172
|
+
export default defineFragment({
|
|
173
|
+
component: Alpha,
|
|
174
|
+
meta: { name: 'Alpha', description: 'Alpha', category: 'display' },
|
|
175
|
+
usage: { when: ['x'], whenNot: ['y'] },
|
|
176
|
+
props: {},
|
|
177
|
+
variants: [{ name: 'Default', description: 'Default', render: () => <Alpha /> }],
|
|
178
|
+
});
|
|
179
|
+
`,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
await writeFragment(
|
|
183
|
+
projectDir,
|
|
184
|
+
'Beta',
|
|
185
|
+
`import React from 'react';
|
|
186
|
+
import { defineFragment } from '@fragments/core';
|
|
187
|
+
import { Beta } from '.';
|
|
188
|
+
|
|
189
|
+
export default defineFragment({
|
|
190
|
+
component: Beta,
|
|
191
|
+
meta: { name: 'Beta', description: 'Beta', category: 'display' },
|
|
192
|
+
usage: { when: ['x'], whenNot: ['y'] },
|
|
193
|
+
props: {},
|
|
194
|
+
variants: [{ name: 'Default', description: 'Default', render: () => <Beta /> }],
|
|
195
|
+
});
|
|
196
|
+
`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const result = await validateSnippetPolicy(baseConfig, projectDir, {
|
|
200
|
+
mode: 'error',
|
|
201
|
+
componentStart: 'Beta',
|
|
202
|
+
componentLimit: 1,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result.warnings).toHaveLength(0);
|
|
206
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
207
|
+
expect(result.errors.every((issue) => issue.file.includes('Beta.fragment.tsx'))).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|