Setting the file. One moment.
Docs Survey Sync · Rp Target Wix · wix/skills · Skills Docs
ContentsBack to the top of the page Post
scripts/docs-survey-sync.js
scripts/ docs-survey-sync.js
JavaScript · 150 lines · 5 KB
15
const
path
=
require
(
'node:path'
);
16 const fs = require ( 'node:fs' );
17 const { knowledgeRoot , writeJson } = require ( '../lib/domain-knowledge.js' );
18
19 const DOCS_BASE = 'https://dev.wix.com/docs/' ;
20 const HEADING_RE = / ^ (# {1,6} ) \[ ( [ ^ \] ] + ) \]\( (https: \/\/ dev \. wix \. com \/ docs \/ [ ^ )] +? )(?: \. md) ? \) \s *$ / ;
21
22 async function fetchMenuMarkdown ( root ) {
23 const url = `${ DOCS_BASE }${ root }.md` ;
24 const response = await fetch (url, { redirect: 'follow' });
25 if ( ! response.ok) throw new Error ( `GET ${ url } -> HTTP ${ response . status }` );
26 return response. text ();
27 }
28
29 // A menu page lists its whole ancestry too (# Api Reference, ## Business
30 // Solutions, ...). Only headings strictly below the fetched root are the
31 // domain's own; among those, the surfaces are the deepest headings — the
32 // ones with no child headings.
33 function extractSurfaces ( markdown , root ) {
34 const headings = [];
35 let rootTitle = null ;
36 for ( const line of markdown. split ( ' \n ' )) {
37 const match = line. match ( HEADING_RE );
38 if ( ! match) continue ;
39 const docsPath = match[ 3 ]. slice ( DOCS_BASE . length );
40 if (docsPath === root) rootTitle = match[ 2 ];
41 if ( ! docsPath. startsWith ( `${ root }/` )) continue ;
42 headings. push ({ level: match[ 1 ]. length , title: match[ 2 ], docsPath });
43 }
44
45 // A flat tree (all methods as bullets, no sub-headings) is still one
46 // surface — the root itself. Never let a root vanish silently.
47 if (headings. length === 0 ) {
48 return [
49 {
50 id: root,
51 title: rootTitle || root. split ( '/' ). pop (),
52 url: `${ DOCS_BASE }${ root }` ,
53 group: null ,
54 },
55 ];
56 }
57
58 const surfaces = [];
59 for ( let i = 0 ; i < headings. length ; i += 1 ) {
60 const heading = headings[i];
61 const next = headings[i + 1 ];
62 const hasChildren = next && next.level > heading.level;
63 if (hasChildren) continue ;
64 const group = [];
65 for ( let j = i - 1 , level = heading.level; j >= 0 ; j -= 1 ) {
66 if (headings[j].level < level) {
67 group. unshift (headings[j].title);
68 level = headings[j].level;
69 }
70 }
71 surfaces. push ({
72 id: heading.docsPath,
73 title: heading.title,
74 url: `${ DOCS_BASE }${ heading . docsPath }` ,
75 group: group. join ( ' › ' ) || null ,
76 });
77 }
78 return surfaces;
79 }
80
81 function mergeSurvey ( existing , domain , roots , fetched ) {
82 const previous = new Map (((existing && existing.surfaces) || []). map (( entry ) => [entry.id, entry]));
83 const surfaces = fetched. map (( surface ) => {
84 const kept = previous. get (surface.id);
85 previous. delete (surface.id);
86 const verdictFields = kept
87 ? {
88 verdict: kept.verdict,
89 refs: kept.refs,
90 reason: kept.reason,
91 tracking: kept.tracking,
92 reviewedOn: kept.reviewedOn,
93 }
94 : { verdict: 'unreviewed' };
95 for ( const key of Object. keys (verdictFields)) {
96 if (verdictFields[key] === undefined ) delete verdictFields[key];
97 }
98 return { ... surface, ... verdictFields };
99 });
100 for ( const gone of previous. values ()) {
101 surfaces. push ({ ... gone, removed: true });
102 }
103 surfaces. sort (( a , b ) => a.id. localeCompare (b.id));
104 return {
105 schemaVersion: 1 ,
106 domain,
107 fetchedAt: new Date (). toISOString (). slice ( 0 , 10 ),
108 roots,
109 surfaces,
110 };
111 }
112
113 async function syncDomain ( domainsDir , domain ) {
114 const domainJsonPath = path. join (domainsDir, domain, 'domain.json' );
115 const domainJson = JSON . parse (fs. readFileSync (domainJsonPath, 'utf8' ));
116 const roots = domainJson.docsRoots;
117 if ( ! Array. isArray (roots) || roots. length === 0 ) {
118 throw new Error ( `${ domain }/domain.json has no docsRoots[]; declare the domain's docs menu roots first` );
119 }
120 const fetchedByRoot = await Promise . all (
121 roots. map (( root ) => fetchMenuMarkdown (root). then (( md ) => extractSurfaces (md, root))),
122 );
123 const fetched = fetchedByRoot. flat ();
124
125 const surveyPath = path. join (domainsDir, domain, 'docs-survey.json' );
126 const existing = fs. existsSync (surveyPath) ? JSON . parse (fs. readFileSync (surveyPath, 'utf8' )) : null ;
127 const survey = mergeSurvey (existing, domain, roots, fetched);
128 writeJson (surveyPath, survey);
129
130 const unreviewed = survey.surfaces. filter (( s ) => s.verdict === 'unreviewed' ). length ;
131 const removed = survey.surfaces. filter (( s ) => s.removed). length ;
132 process.stdout. write (
133 `${ domain }: ${ survey . surfaces . length } surfaces (${ unreviewed } unreviewed, ${ removed } removed) -> ${ path . relative ( process . cwd (), surveyPath ) } \n `
134 );
135 }
136
137 async function main () {
138 const domains = process.argv. slice ( 2 ). filter (( arg ) => ! arg. startsWith ( '--' ));
139 if (domains. length === 0 ) {
140 process.stderr. write ( 'Usage: docs-survey-sync.js <domain> [<domain>...] \n ' );
141 process. exit ( 2 );
142 }
143 const domainsDir = knowledgeRoot (path. resolve (__dirname, '..' ));
144 for ( const domain of domains) await syncDomain (domainsDir, domain);
145 }
146
147 main (). catch (( error ) => {
148 process.stderr. write ( `ERROR: ${ error . message } \n ` );
149 process. exit ( 1 );
150 });