Setting the file. One moment.
Templates · Wix Headless Kit · wix/skills · Skills Docs
ContentsBack to the top of the page install/ templates.mjs
JavaScript · 183 lines · 10 KB
14 // from (skills-lock.json's `source`, default wix/skills), at the branch or tag the install
15 // named (its `ref`, falling back to the default branch when that ref no longer exists), else
16 // the repository's default branch: a skill installed from the repository tracks the repository,
17 // so the templates match the kit beside them. A kit that came from a package (no skills-lock.json
18 // above it) fetches at the release tag in `install/pins.json` (`templates.ref`, kept equal to the
19 // package version by the release flow), the code it was released with; the package carries the
20 // templates skill beside the kit anyway, so that fetch is the rare path.
21 // `WIX_HEADLESS_FAST_TEMPLATES_REF=<branch|tag|sha>` overrides all of it.
22 // The cache stays with the project: its `.gitignore` leaves out only the composed `project/`
23 // folders (the scaffolds with their lockfiles, read once, at create or attach) and the repository
24 // tooling, so the code layers, playbooks, seeds and readers are committed at the commit the project
25 // was built from, recorded in `.source`. A caller that needs a `project/` folder passes it as
26 // `need`; when a committed copy lacks it, that part is fetched at the same commit.
27 import { cpSync, existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs" ;
28 import { tmpdir } from "node:os" ;
29 import { basename, dirname, join, resolve } from "node:path" ;
30 import { spawnSync } from "node:child_process" ;
31 import { fileURLToPath } from "node:url" ;
32
33 export const SKILL_ROOT = resolve ( dirname ( fileURLToPath ( import . meta .url)), ".." );
34 export const DEFAULT_REPO = "https://github.com/wix/skills.git" ;
35 /** The templates skill's folder in the repository. */
36 export const TEMPLATES_PATH = "skills/wix-headless-templates" ;
37 /** Pinned versions, shared by the install scripts; `templates.ref` is the release tag the fetch defaults to. */
38 export const PINS = JSON . parse ( readFileSync ( new URL ( "./pins.json" , import . meta .url), "utf8" ));
39
40 const git = ( args , opts = {}) => spawnSync ( "git" , args, { encoding: "utf8" , timeout: 180_000 , ... opts });
41
42 // The repository and ref the skill was installed from: the nearest skills-lock.json above the
43 // skill folder, its entry for this skill. "wix/skills" → the default branch of that repository;
44 // "https://github.com/owner/repo/tree/<ref>" → that ref.
45 export function installSource () {
46 const skill = basename ( SKILL_ROOT );
47 let dir = SKILL_ROOT ;
48 for ( let i = 0 ; i < 6 ; i ++ ) {
49 const p = join (dir, "skills-lock.json" );
50 if ( existsSync (p)) {
51 try {
52 const entry = JSON . parse ( readFileSync (p, "utf8" )).skills?.[skill];
53 if ( typeof entry?.source === "string" && entry.source) {
54 const parsed = parseSource (entry.source);
55 // `ref` is the branch or tag the install named (skills-lock.json v1 keeps it beside `source`)
56 return { ... parsed, ref: parsed.ref ?? ( typeof entry.ref === "string" && entry.ref ? entry.ref : null ), fromLock: true };
57 }
58 } catch { /* fall through to the default */ }
59 break ;
60 }
61 const up = dirname (dir);
62 if (up === dir) break ;
63 dir = up;
64 }
65 return { repo: DEFAULT_REPO , ref: null , fromLock: false };
66 }
67
68 function parseSource ( src ) {
69 const m = src. match ( / ^ (?:https ? : \/\/ github \. com \/ ) ? ( [ ^ /\s] + ) \/ ( [ ^ /\s#@] +? )(?: \. git) ? (?: \/ tree \/ ( [ ^ \s] + )) ?$ / );
70 if ( ! m) return { repo: DEFAULT_REPO , ref: null };
71 return { repo: `https://github.com/${ m [ 1 ] }/${ m [ 2 ] }.git` , ref: m[ 3 ] ?? null };
72 }
73
74 export function listVerticals ( dir ) {
75 return readdirSync (dir, { withFileTypes: true })
76 . filter (( d ) => d. isDirectory () && d.name !== "shared" && d.name !== "blank" && existsSync ( join (dir, d.name, "app" )))
77 . map (( d ) => d.name)
78 . sort ();
79 }
80
81 export function templatesDir ({ refresh = false , need = null } = {}) {
82 const checkout = resolve ( SKILL_ROOT , ".." , "wix-headless-templates" );
83 if ( existsSync ( join (checkout, "shared" , "app" ))) return checkout;
84 const cache = join ( SKILL_ROOT , "templates" );
85 if ( ! refresh && existsSync ( join (cache, "shared" , "app" ))) {
86 if (need && ! existsSync ( join (cache, need))) fetchIgnoredPart (cache, need);
87 return cache;
88 }
89 fetchTemplates (cache);
90 return cache;
91 }
92
93 // What a project's repository does not carry: the composed scaffolds (heavy, read once) and the
94 // repository's own tooling. Everything else in the cache is committed with the project.
95 const IGNORED = [ "*/project/" , "blank/" , "compose.mjs" ];
96
97 // A committed copy lacks the ignored parts. Fetch them at the commit the copy came from, so the
98 // scaffold a create copies matches the code committed beside it; the committed files are untouched.
99 function fetchIgnoredPart ( cache , need ) {
100 const src = templatesSource (cache);
101 const { repo } = installSource ();
102 const r = cloneSparse (src.repo ?? repo, src.commit ?? process.env. WIX_HEADLESS_FAST_TEMPLATES_REF ?? null );
103 const tmp = r.tmp;
104 if (r.status !== 0 || ! existsSync ( join (tmp, TEMPLATES_PATH , need))) {
105 rmSync (tmp, { recursive: true , force: true });
106 throw new Error ( `could not fetch ${ TEMPLATES_PATH }/${ need } from ${ src . repo ?? repo }${ src . commit ? ` @ ${ src . commit . slice ( 0 , 7 ) }` : ""}: ${ ( r . stderr || r . stdout || "not in the clone" ). trim (). slice ( - 300 ) }` );
107 }
108 for ( const part of [ "blank" , ... readdirSync ( join (tmp, TEMPLATES_PATH ), { withFileTypes: true }). filter (( d ) => d. isDirectory () && existsSync ( join (tmp, TEMPLATES_PATH , d.name, "project" ))). map (( d ) => `${ d . name }/project` )]) {
109 const from = join (tmp, TEMPLATES_PATH , part);
110 if ( existsSync (from) && ! existsSync ( join (cache, part))) cpSync (from, join (cache, part), { recursive: true });
111 }
112 rmSync (tmp, { recursive: true , force: true });
113 }
114
115 export function templatesSource ( dir ) {
116 const p = join (dir, ".source" );
117 if ( existsSync (p)) { try { return JSON . parse ( readFileSync (p, "utf8" )); } catch { /* below */ } }
118 return { checkout: dir };
119 }
120
121 function fetchTemplates ( cache ) {
122 const { repo , ref : lockRef , fromLock } = installSource ();
123 const envRef = process.env. WIX_HEADLESS_FAST_TEMPLATES_REF || null ;
124 // Installed from the repository (a lock above the skill) and no ref named: the default branch, so
125 // the templates match the kit. No lock at all is a package install: the release tag it shipped with.
126 const pinnedRef = fromLock ? null : ( PINS .templates?.ref || null );
127 let ref = envRef || lockRef || pinnedRef || null ;
128 let r = cloneSparse (repo, ref);
129 // The branch the skill was installed from can be gone by the time the code is first needed
130 // (merged and deleted). The lock's ref then falls back to the default branch; an explicit env
131 // ref does not.
132 let fellBack = false ;
133 if (r.status !== 0 && ref && ! envRef && /not found | couldn't find remote ref | unknown revision/ i . test (r.stderr || "" )) {
134 // (the pinned tag, too: a fork or a pre-release checkout may not carry it)
135 rmSync (r.tmp, { recursive: true , force: true });
136 fellBack = true ; ref = null ;
137 r = cloneSparse (repo, null );
138 }
139 const tmp = r.tmp;
140 if (r.error?.code === "ENOENT" ) {
141 rmSync (tmp, { recursive: true , force: true });
142 throw new Error ( "git is not installed or not on PATH; the shipped code is fetched with a git clone of the skill's repository" );
143 }
144 if (r.status !== 0 || ! existsSync ( join (tmp, TEMPLATES_PATH , "shared" , "app" ))) {
145 rmSync (tmp, { recursive: true , force: true });
146 throw new Error ( `could not fetch ${ TEMPLATES_PATH }/ from ${ repo }${ ref ? ` @ ${ ref }` : ""}: ${ ( r . stderr || r . stdout || `no ${ TEMPLATES_PATH }/shared/app in the clone` ). trim (). slice ( - 400 ) }` );
147 }
148 const commit = git ([ "-C" , tmp, "rev-parse" , "HEAD" ]).stdout. trim ();
149 rmSync (cache, { recursive: true , force: true });
150 mkdirSync ( dirname (cache), { recursive: true });
151 try { renameSync ( join (tmp, TEMPLATES_PATH ), cache); }
152 catch { cpSync ( join (tmp, TEMPLATES_PATH ), cache, { recursive: true }); }
153 rmSync (tmp, { recursive: true , force: true });
154 writeFileSync ( join (cache, ".gitignore" ), IGNORED . join ( " \n " ) + " \n " );
155 writeFileSync ( join (cache, ".source" ), JSON . stringify ({ repo, ref: ref ?? (fellBack ? `default branch (${ lockRef || pinnedRef } not found)` : "default branch" ), commit, fetchedAt: new Date (). toISOString () }, null , 2 ) + " \n " );
156 }
157
158 // A sparse, shallow clone of the templates skill's folder at a ref (branch, tag, or commit id) into a temp dir.
159 function cloneSparse ( repo , ref ) {
160 const tmp = mkdtempSync ( join ( tmpdir (), "wix-headless-kit-templates-" ));
161 let r;
162 if (ref && / ^ [0-9a-f] {40}$ / i . test (ref)) {
163 // a commit: shallow-fetch just it (GitHub serves any reachable commit by id)
164 for ( const args of [[ "init" , "-q" , tmp], [ "-C" , tmp, "remote" , "add" , "origin" , repo], [ "-C" , tmp, "fetch" , "-q" , "--depth" , "1" , "--filter=blob:none" , "origin" , ref], [ "-C" , tmp, "sparse-checkout" , "set" , TEMPLATES_PATH ], [ "-C" , tmp, "checkout" , "-q" , "FETCH_HEAD" ]]) {
165 r = git (args);
166 if (r.status !== 0 ) break ;
167 }
168 } else {
169 r = git ([ "clone" , "--quiet" , "--depth" , "1" , "--filter=blob:none" , "--sparse" , ... (ref ? [ "-b" , ref] : []), repo, tmp]);
170 if (r.status === 0 ) r = git ([ "-C" , tmp, "sparse-checkout" , "set" , TEMPLATES_PATH ]);
171 }
172 return { ... r, tmp };
173 }
174
175 if (process.argv[ 1 ] && resolve (process.argv[ 1 ]) === fileURLToPath ( import . meta .url)) {
176 try {
177 const dir = templatesDir ({ refresh: process.argv. includes ( "--refresh" ) });
178 console. log ( JSON . stringify ({ templates: dir, source: templatesSource (dir), verticals: listVerticals (dir) }));
179 } catch (e) {
180 console. log ( JSON . stringify ({ error: e.message }));
181 process. exit ( 1 );
182 }
183 }