@@ -4,25 +4,29 @@ import { asyncEach, each } from '@semantic-ui/utils';
44import { readFileSync , writeFileSync } from 'fs' ;
55import { dirname , resolve } from 'path' ;
66import glob from 'tiny-glob' ;
7+ import { pathToFileURL } from 'url' ;
78import { build } from './lib/build.js' ;
89import { INTERNAL_CSS_BANNER } from './lib/config.js' ;
10+ import { validateSpec } from './lib/validate-spec.js' ;
911
1012/*
11- Generate component spec JS directly without intermediate JSON file
13+ Generate component spec JS from source spec
1214*/
13- const generateComponentSpecJS = async ( spec , plural = false , specSettings = { } ) => {
15+ const generateComponentSpecJS = async ( spec , plural = false , specSettings = { } , sourceFile = '' ) => {
1416 const readerSettings = {
1517 plural,
1618 ...specSettings ,
1719 } ;
1820 const reader = new SpecReader ( spec , readerSettings ) ;
1921 const componentSpec = reader . getWebComponentSpec ( ) ;
2022 const filename = plural
21- ? `${ spec ?. pluralTagName ?. replace ( 'ui-' , '' ) } - component.js`
23+ ? `${ spec ?. pluralTagName ?. replace ( 'ui-' , '' ) } . component.js`
2224 : 'component.js' ;
23- return `// Auto-generated from ${ spec ?. tagName ?. replace ( 'ui-' , '' ) || 'spec' } .json\nexport default ${
24- JSON . stringify ( componentSpec , null , 2 )
25- } ;\n`;
25+
26+ const sourceFileName = sourceFile
27+ ? sourceFile . split ( '/' ) . pop ( )
28+ : ( spec ?. tagName ?. replace ( 'ui-' , '' ) || 'spec' ) + '.spec.js' ;
29+ return `// Auto-generated from ${ sourceFileName } \nexport default ${ JSON . stringify ( componentSpec , null , 2 ) } ;\n` ;
2630} ;
2731
2832/*
@@ -51,41 +55,76 @@ export const buildUIDeps = async ({
5155 } ) ;
5256
5357 // External glob needed for proper negation support
54- const allFiles = await glob ( 'src/primitives/**/specs/*.json' ) ;
55- const entryPoints = allFiles . filter ( path => ! path . endsWith ( '-component.json' ) ) ;
58+ // Support both .json (legacy) and .spec.js (new) during transition
59+ const jsonFiles = await glob ( 'src/primitives/**/specs/*.json' ) ;
60+ const specJsFiles = await glob ( 'src/primitives/**/specs/*.spec.js' ) ;
61+
62+ const allFiles = [ ...jsonFiles , ...specJsFiles ] ;
63+ // Exclude all generated files (*.component.js, *.component.json, *.spec.json)
64+ const entryPoints = allFiles . filter ( path =>
65+ ! path . endsWith ( '.component.json' )
66+ && ! path . endsWith ( '.component.js' )
67+ && ! path . endsWith ( '.spec.json' ) // Generated JSON from .spec.js
68+ ) ;
5669
5770 const createComponentSpecs = async ( ) => {
5871 await asyncEach ( entryPoints , async ( entryPath ) => {
5972 try {
60- const contents = readFileSync ( entryPath , 'utf8' ) ;
61- const spec = JSON . parse ( contents ) ;
73+ let spec ;
74+ const isJsSpec = entryPath . endsWith ( '.spec.js' ) ;
75+
76+ if ( isJsSpec ) {
77+ // Load JS module with cache busting for watch mode
78+ const specModule = await import ( `${ pathToFileURL ( entryPath ) . href } ?t=${ Date . now ( ) } ` ) ;
79+ spec = specModule . default ;
80+
81+ // Validate JS specs are pure data
82+ validateSpec ( spec , entryPath ) ;
83+
84+ // Generate JSON snapshot for machine readability (LLMs, tooling)
85+ const jsonPath = entryPath . replace ( '.spec.js' , '.spec.json' ) ;
86+ const jsonContent = `${ JSON . stringify ( spec , null , 2 ) } \n` ;
87+ writeFileSync ( jsonPath , jsonContent ) ;
88+ }
89+ else {
90+ // Legacy JSON loading
91+ const contents = readFileSync ( entryPath , 'utf8' ) ;
92+ spec = JSON . parse ( contents ) ;
93+ }
6294
6395 // Generate component spec JS directly
64- const componentSpecJS = await generateComponentSpecJS ( spec , false ) ;
65- const componentJSPath = entryPath . replace ( '.json' , '-component.js' ) ;
96+ const componentSpecJS = await generateComponentSpecJS ( spec , false , { } , entryPath ) ;
97+ const componentJSPath = isJsSpec
98+ ? entryPath . replace ( '.spec.js' , '.component.js' )
99+ : entryPath . replace ( '.json' , '.component.js' ) ;
66100 writeFileSync ( componentJSPath , componentSpecJS ) ;
67101
68102 // Generate plural variant if supported
69103 if ( spec ?. supportsPlural ) {
70- const pluralComponentSpecJS = await generateComponentSpecJS ( spec , true ) ;
104+ const pluralComponentSpecJS = await generateComponentSpecJS ( spec , true , { } , entryPath ) ;
71105 const pluralName = spec ?. pluralTagName . replace ( 'ui-' , '' ) ;
72- const pluralJSPath = resolve ( dirname ( entryPath ) , `${ pluralName } - component.js` ) ;
106+ const pluralJSPath = resolve ( dirname ( entryPath ) , `${ pluralName } . component.js` ) ;
73107 writeFileSync ( pluralJSPath , pluralComponentSpecJS ) ;
74108 }
75109 }
76110 catch ( e ) {
77- // Silently skip malformed JSON files
111+ console . error ( `Error processing ${ entryPath } :` , e . message ) ;
112+ throw e ; // Don't silently skip errors in new system
78113 }
79114 } ) ;
80115 } ;
81116
82117 // Convert raw spec JSON to JS modules to avoid ESM JSON import compatibility issues
118+ // Note: .spec.js files don't need conversion - they're already JS modules
83119 const generateJSExportsFromSpecs = async ( ) => {
84120 await createComponentSpecs ( ) ;
85121
86- // Only process raw spec files (not component specs, which are generated directly above )
122+ // Only process legacy JSON spec files (not .spec.js/json or .component.js )
87123 const rawSpecFiles = await glob ( 'src/primitives/*/specs/*.json' ) ;
88- const filteredRawSpecs = rawSpecFiles . filter ( path => ! path . endsWith ( '-component.json' ) ) ;
124+ const filteredRawSpecs = rawSpecFiles . filter ( path =>
125+ ! path . endsWith ( '.component.json' )
126+ && ! path . endsWith ( '.spec.json' )
127+ ) ;
89128
90129 each ( filteredRawSpecs , ( jsonFile ) => {
91130 try {
@@ -105,22 +144,29 @@ export const buildUIDeps = async ({
105144
106145 const generateJSExports = generateJSExportsFromSpecs ( ) ;
107146
108- // Set up a separate esbuild watcher for JSON spec files
147+ // Set up a separate esbuild watcher for spec files
109148 let specWatcher ;
110149 if ( watch ) {
111- // Get all spec files to watch
112- const specsPattern = 'src/primitives/**/specs/*.json' ;
113- const watchedFiles = await glob ( specsPattern ) ;
114- const specFiles = watchedFiles . filter ( path => ! path . endsWith ( '-component.json' ) ) ;
115-
116- // Use esbuild to watch the JSON files by treating them as entry points
117- // with a plugin that rebuilds our spec JS files
118- if ( specFiles . length > 0 ) {
150+ // Get all spec files to watch (legacy .json and source .spec.js)
151+ const jsonSpecFiles = await glob ( 'src/primitives/**/specs/*.json' ) ;
152+ const jsSpecFiles = await glob ( 'src/primitives/**/specs/*.spec.js' ) ;
153+
154+ // Exclude all generated files from watch
155+ const watchedFiles = [ ...jsonSpecFiles , ...jsSpecFiles ] . filter (
156+ path =>
157+ ! path . endsWith ( '.component.json' )
158+ && ! path . endsWith ( '.component.js' )
159+ && ! path . endsWith ( '.spec.json' ) , // Don't watch generated JSON
160+ ) ;
161+
162+ // Use esbuild to watch the spec files by treating them as entry points
163+ // with a plugin that rebuilds our component specs
164+ if ( watchedFiles . length > 0 ) {
119165 specWatcher = build ( {
120166 watch,
121167 write : false , // Don't write output, just watch
122168 logLevel : 'silent' , // Suppress esbuild's own logs
123- entryPoints : specFiles ,
169+ entryPoints : watchedFiles ,
124170 outdir : '.temp-watch' , // Required by esbuild when multiple entry points
125171 plugins : [
126172 callbackPlugin ( {
0 commit comments