← Files ShopifyARCHIVED FILE
skills/shopify-liquid/scripts/validate.mjs
13.3 KB · Sep 30, 2026 · 23:18 UTC
#!/usr/bin/env node
import{access as ce}from"fs/promises";import{readFileSync as pe}from"fs";import{join as ue,normalize as $}from"path";import{parseArgs as fe}from"util";import{check as me,extractDocDefinition as de,FileType as he,recommended as ve,Severity as E,SourceCodeType as ge,toSchema as M,toSourceCode as ye}from"@shopify/theme-check-common";import{ThemeLiquidDocsManager as Se}from"@shopify/theme-check-docs-updater";import{themeCheckRun as Ee}from"@shopify/theme-check-node";import{randomUUID as Ie}from"crypto";import{readFileSync as $e,statSync as Re}from"node:fs";import{homedir as De}from"node:os";import{join as S}from"node:path";var le=Object.defineProperty,l=(e,t)=>le(e,"name",{value:t,configurable:!0});function P(e){return e.some(t=>t.result==="failed")}l(P,"hasFailedValidation");function I(e){return e.map(t=>({artifactId:t.artifactId||`artifact-${Ie()}`,revision:t.revision??1}))}l(I,"extractArtifactsFromItems");function O(e,t){return e.map((r,a)=>{let i=t[a];return i?{...r,artifactId:i.artifactId,artifactRevision:i.revision}:r})}l(O,"attachArtifactIds");function k(e,t="Items"){let r=P(e),a=e.some(n=>n.result==="inform"),i;r?i="\u274C INVALID":a?i="\u26A0\uFE0F VALID (with warnings)":i="\u2705 VALID";let s=`## Validation Summary
`;return s+=`**Overall Status:** ${i}
`,s+=`**Total ${t}:** ${e.length}
`,s+=`## Detailed Results
`,e.forEach((n,p)=>{let u;n.result==="success"?u="\u2705":n.result==="inform"?u="\u26A0\uFE0F":u="\u274C",s+=`### ${t.slice(0,-1)} ${p+1}
`,n.artifactId&&(s+=`**Artifact ID:** ${n.artifactId}`,n.artifactRevision&&(s+=`
**Revision:** ${n.artifactRevision}`),s+=`
*Use same ID & increment revision when retrying on an improvement of this artifact*
`),s+=`**Status:** ${u} ${n.result.toUpperCase()}
`,s+=`**Details:** ${n.resultDetail}
`}),s}l(k,"formatValidationResult");function G(e){return Object.fromEntries(Object.entries(e).map(([t,r])=>{let{description:a,valueName:i,choices:s,...n}=r;return[t,n]}))}l(G,"toParseArgsOptions");var H={model:{type:"string",valueName:"name",description:"Agent model name"},"client-name":{type:"string",valueName:"name",description:"Agent client name"},"client-version":{type:"string",valueName:"version",description:"Agent client version"}},B={"user-prompt-base64":{type:"string",valueName:"base64",description:"Base64-encoded triggering prompt"},"session-id":{type:"string",valueName:"id",description:"Agent session identifier"},"tool-use-id":{type:"string",valueName:"id",description:"Agent tool-use identifier"}},Oe={"artifact-id":{type:"string",valueName:"id",description:"Stable artifact identifier"},revision:{type:"string",valueName:"number",description:"Positive artifact revision"},...H,...B,json:{type:"boolean",description:"Emit machine-readable JSON"}},Ne={help:{type:"boolean",short:"h",description:"Show this help"}};function q(e,t){let r=new Set([...Object.keys(H),...Object.keys(B),"artifact-id","revision"]),a=Object.entries(t).filter(([s])=>!r.has(s)).map(([s,n])=>{let p=n.type==="string"?n.valueName||n.choices?.join("|")||"value":void 0,u=`--${s}${p?` <${p}>`:""}`;return[n.short?`-${n.short}, ${u}`:u,n.description]}),i=Math.max(...a.map(([s])=>s.length));return["Usage:",...e.map(s=>` ${s}`),"","Options:",...a.map(([s,n])=>` ${s.padEnd(i)} ${n}`)].join(`
`)}l(q,"formatCliHelp");function Y(){let e=[process.env.CLAUDE_SESSION_ID,process.env.CLAUDE_CODE_SESSION_ID,process.env.CURSOR_SESSION_ID,process.env.COPILOT_SESSION_ID];for(let t of e)if(typeof t=="string"&&t.length>0)return t}l(Y,"readHostSessionId");var _e="https://shopify.dev/",Te="https://shopify-dev.shop.dev/";function j(e){return`https://shopify-dev-staging${e}.shopifycloud.com/`}l(j,"stagingHost");function C(e){let t=e?.env??process.env,r=t.SHOPIFY_DEV_STAGING_SERVER_NUMBER?.trim();if(r){if(!/^\d+$/.test(r))throw new Error(`SHOPIFY_DEV_STAGING_SERVER_NUMBER must be a positive integer; got: "${r}"`);let i=Number(r);if(!Number.isSafeInteger(i)||i<=0)throw new Error(`SHOPIFY_DEV_STAGING_SERVER_NUMBER must be a positive integer; got: "${r}"`);let s=t.MINERVA_TOKEN;if(!s){let n=j(i).replace(/\/$/,"");throw new Error(`SHOPIFY_DEV_STAGING_SERVER_NUMBER=${i} is set but no Minerva token is available. Staging servers are behind Minerva. Get a token via:
export MINERVA_TOKEN=$(devx minerva-auth --client-id 0oa1bphetnkOusboI0x8 --audience ${n})`)}return{url:j(i),headers:{Cookie:`MINERVA_TOKEN=${s}`}}}let a=t.SHOPIFY_DEV_INSTRUMENTATION_URL?.trim();return a&&e?.uri?.startsWith("/mcp/usage")?{url:a,headers:{}}:t.DEV&&t.DEV!=="false"?{url:Te,headers:{}}:{url:_e,headers:{}}}l(C,"resolveShopifyDevBaseUrl");async function J(e,t){let r,a={};if(e.startsWith("http://")||e.startsWith("https://"))r=new URL(e);else{let s=C({uri:e});r=new URL(e,s.url),a=s.headers}t?.parameters&&Object.entries(t.parameters).forEach(([s,n])=>{r.searchParams.append(s,n)});let i=await fetch(r.toString(),{method:t?.method||"GET",headers:{Accept:"application/json","Cache-Control":"no-cache","X-Shopify-Surface":"mcp","X-Shopify-MCP-Version":t?.instrumentation?.packageVersion||"","X-Shopify-Timestamp":t?.instrumentation?.timestamp||"",...a,...t?.headers},...t?.body&&{body:t.body}});if(!i.ok){let s;try{s=await i.text()}catch{}throw new Error(s?`HTTP ${i.status}: ${s}`:`HTTP error! status: ${i.status}`)}return await i.text()}l(J,"shopifyDevFetch");var _="shopify-ai-toolkit",T="opt-out",we="SHOPIFY_AI_TOOLKIT_OPT_OUT_FILE",Ae=new Set(["false","0","no","off"]);function U(e){return{env:e?.env??process.env,platform:e?.platform??process.platform,homeDir:e&&"homeDir"in e?e.homeDir:W(e?.env??process.env)}}l(U,"resolveContext");function W(e){let t=e.HOME?.trim()||e.USERPROFILE?.trim();if(t)return t;try{return De()||void 0}catch{return}}l(W,"resolveHomeDir");function X(e){let{env:t,platform:r,homeDir:a}=U(e),i=[],s=t[we]?.trim();s&&i.push(s);let n=t.XDG_CONFIG_HOME?.trim();if(n&&i.push(S(n,_,T)),a&&(i.push(S(a,".config",_,T)),r==="darwin"&&i.push(S(a,"Library","Application Support",_,T))),r==="win32"){let p=t.APPDATA?.trim()||(a?S(a,"AppData","Roaming"):void 0);p&&i.push(S(p,_,T))}return[...new Set(i.filter(p=>!!p))]}l(X,"telemetryOptOutFileCandidates");function K(e){if(e.OPT_OUT_INSTRUMENTATION?.trim().toLowerCase()==="true")return!0;let t=e.DO_NOT_TRACK?.trim().toLowerCase();return t==="1"||t==="true"}l(K,"envSaysOptOut");function z(e){try{if(!Re(e).isFile())return!1}catch{return!1}try{let t=$e(e,"utf8").trim().toLowerCase();return!Ae.has(t)}catch{return!0}}l(z,"fileSaysOptOut");function Q(e){try{let t=U(e);return K(t.env)?!0:X(t).some(z)}catch{return!1}}l(Q,"isTelemetryOptedOut");function Z(e){return{...e?.api&&{api:e.api},...e?.api_version&&{api_version:e.api_version},...e?.resolve_api_version&&{resolve_api_version:e.resolve_api_version}}}l(Z,"nonEmptyUsageMetadata");function be(){return Q()}l(be,"isInstrumentationDisabled");function ee(e){if(!(typeof e!="string"||e.length===0))try{let t=Buffer.from(e,"base64").toString("utf8");return t.length>0?t:void 0}catch{return}}l(ee,"decodeUserPrompt");async function R(e,t,r,a){}l(R,"reportValidation");var D=["assets","blocks","config","layout","locales","sections","snippets","templates"],x=["theme","app"],te={"theme-path":{type:"string",valueName:"path",description:"Theme directory for full-app mode"},files:{type:"string",valueName:"paths",description:"Comma-separated relative theme files"},filename:{type:"string",valueName:"name",description:"Virtual filename for stateless mode"},filetype:{type:"string",valueName:"type",choices:D,description:`Theme file type: ${D.join(", ")} (default: sections)`},context:{type:"string",choices:x,description:"Theme context (default: theme)"},code:{type:"string",short:"c",valueName:"liquid",description:"Liquid source string"},file:{type:"string",short:"f",valueName:"path",description:"Read Liquid source from a file"},...Oe,...Ne},Ve=q(["scripts/validate.mjs --theme-path <path> --files <rel1,rel2,...> [options]","scripts/validate.mjs --filename <name.liquid> --code '<liquid>' [options]","scripts/validate.mjs --filename <name.liquid> --file <path> [options]"],te),{values:o}=fe({options:G(te)});o.help&&(console.log(Ve),process.exit(0));var F=ee(o["user-prompt-base64"]),ie;async function re(e,t){let r=ue(e,".theme-check.yml");try{await ce(r)}catch{r=void 0}let a=await Ee(e,r,n=>console.error(n)),i={},s={};for(let n of a.offenses)(i[n.uri]??=[]).push(w(n)),L(n)&&(s[n.uri]=!0);return t.map(n=>{let p=Object.keys(i).find(u=>$(u).endsWith($(n)));if(p){let u=i[p].join(`
`);return s[p]?{result:"failed",resultDetail:`${n}:
${u}`}:{result:"inform",resultDetail:`${n} passed all checks (with non-error findings):
${u}`}}return{result:"success",resultDetail:`${n} passed all checks.`}})}l(re,"validateFullApp");var je=class{constructor(e){this.theme=e}static{l(this,"MockFileSystem")}async readFile(e){let t=this.theme[e];if(!t)throw new Error(`File not found: ${e}`);return t}async readDirectory(){return[]}async stat(e){let t=this.theme[e];if(!t)throw new Error(`File not found: ${e}`);return{type:he.File,size:t.length}}};async function se(e,t,r,a){let i={[`file:///${t}/${e}`]:a},s=new Set(["TranslationKeyExists","ValidSchemaTranslations","MissingTemplate","MissingAsset","ValidStaticBlockType","AssetSizeAppBlockCSS","AssetSizeAppBlockJavaScript"]),n={checks:ve.filter(c=>!s.has(c.meta?.code??"")),settings:{},rootUri:"file:///",context:r},p=new Se,u=Object.entries(i).filter(([c])=>c.endsWith(".liquid")||c.endsWith(".json")).map(([c,m])=>ye(c,m,void 0)),f=await me(u,n,{fs:new je(i),themeDocset:p,jsonValidationSet:p,getBlockSchema:l(async c=>{let m=`file:///blocks/${c}.liquid`,d=u.find(h=>h.uri===m);if(d)return M(r,m,d,async()=>!0)},"getBlockSchema"),getSectionSchema:l(async c=>{let m=`file:///sections/${c}.liquid`,d=u.find(h=>h.uri===m);if(d)return M(r,m,d,async()=>!0)},"getSectionSchema"),async getDocDefinition(c){let m=u.find(d=>$(d.uri).endsWith($(c)));if(!(!m||m.type!==ge.LiquidHtml))return de(m.uri,m.ast)}});return f.filter(L).length===0?f.length===0?{result:"success",resultDetail:`${e} passed all checks.`}:{result:"inform",resultDetail:`${e} passed all checks (with ${f.length} non-error finding(s)):
`+f.map(c=>w(c)).join(`
`)}:{result:"failed",resultDetail:f.map(c=>w(c)).join(`
`)}}l(se,"validateCodeblock");function ne(e){switch(e){case E.WARNING:return"WARNING";case E.INFO:return"INFO";case E.ERROR:default:return"ERROR"}}l(ne,"severityLabel");function w(e){let t=e.start.line+1,r=e.start.character+1,a=`${ne(e.severity)} [line ${t}, col ${r}]: ${e.message}`;return e.suggest&&e.suggest.length>0?`${a}; SUGGESTED FIXES: ${e.suggest.map(i=>i.message).join(" OR ")}.`:a}l(w,"formatOffense");function L(e){return(e.severity??E.ERROR)===E.ERROR}l(L,"isFailingOffense");function N(e){if(!e)return;let t=Number.parseInt(e,10);return Number.isFinite(t)&&t>0?t:void 0}l(N,"parseRevision");function g(e,t=1){let r=Array.from({length:t}).map(()=>({artifactId:o["artifact-id"],revision:N(o.revision)})),a=I(r),i=O(r.map(()=>({result:"failed",resultDetail:e})),a);return{responses:i,text:k(i,"Files")}}l(g,"formatErrorResponse");function A(e,t){let r=k(e,"Files");return console.log(o.json?JSON.stringify({success:t,responses:e}):r),r}l(A,"emit");async function oe(){if(o["theme-path"]){let f=o["theme-path"],c=(o.files??"").split(",").map(v=>v.trim()).filter(Boolean);if(c.length===0){let{responses:v,text:V}=g("--files must list at least one relative file path");console.log(o.json?JSON.stringify({success:!1,responses:v}):V),process.exit(1)}let m=await re(f,c),d=I(c.map(()=>({artifactId:o["artifact-id"],revision:N(o.revision)}))),h=O(m,d),y=m.every(v=>v.result!=="failed"),b=A(h,y);await R("validate_theme",b,{model:o.model,clientName:o["client-name"],clientVersion:o["client-version"],user_prompt:F,sessionId:o["session-id"],toolUseId:o["tool-use-id"],themePath:f,files:c,artifactId:d[0]?.artifactId,revision:d[0]?.revision}),process.exit(y?0:1);return}let e=o.filename;if(!e){let{responses:f,text:c}=g("Provide either --theme-path (full app mode) or --filename (stateless mode)");console.log(o.json?JSON.stringify({success:!1,responses:f}):c),process.exit(1)}let t=o.code;if(o.file&&(t=pe(o.file,"utf-8")),ie=t,!t){let{responses:f,text:c}=g("Provide --code or --file with the codeblock content");console.log(o.json?JSON.stringify({success:!1,responses:f}):c),process.exit(1)}let r=o.filetype??"sections";if(!D.includes(r)){let{responses:f,text:c}=g(`Invalid --filetype "${r}". Valid values: ${D.join(", ")}`);console.log(o.json?JSON.stringify({success:!1,responses:f}):c),process.exit(1)}let a=o.context??"theme";if(!x.includes(a)){let{responses:f,text:c}=g(`Invalid --context "${a}". Valid values: ${x.join(", ")}`);console.log(o.json?JSON.stringify({success:!1,responses:f}):c),process.exit(1)}let[i]=I([{artifactId:o["artifact-id"],revision:N(o.revision)}]),s=await se(e,r,a,t),n=O([s],[i]),p=s.result!=="failed",u=A(n,p);await R("validate_theme",u,{model:o.model,clientName:o["client-name"],clientVersion:o["client-version"],user_prompt:F,sessionId:o["session-id"],toolUseId:o["tool-use-id"],filename:e,filetype:r,context:a,code:t,artifactId:i.artifactId,revision:i.revision}),process.exit(p?0:1)}l(oe,"main");oe().catch(async e=>{let[t]=I([{artifactId:o["artifact-id"],revision:N(o.revision)}]),r=O([{result:"failed",resultDetail:e instanceof Error?e.message:String(e)}],[t]),a=A(r,!1);await R("validate_theme",a,{model:o.model,clientName:o["client-name"],clientVersion:o["client-version"],user_prompt:F,sessionId:o["session-id"],toolUseId:o["tool-use-id"],filename:o.filename,filetype:o.filetype,context:o.context,code:ie,artifactId:t.artifactId,revision:t.revision}),process.exit(1)});
SHA-256: ae05a4e6f04ac6ce239094e6c1c3ff99af048db259a3bf7f3a54bb0e7be9574a