@jxsuite/compiler 1.3.0 → 1.4.0
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/dist/cli.js +149 -149
- package/dist/compiler.js +1 -1
- package/package.json +8 -7
- package/src/site/asset-mounts.ts +140 -0
- package/src/site/image-transform.ts +25 -7
- package/src/site/site-build.ts +43 -1
package/dist/compiler.js
CHANGED
|
@@ -90,7 +90,7 @@ app.post('${X}/${Z}', async (c) => {
|
|
|
90
90
|
} catch (e) {
|
|
91
91
|
return c.json({ ok: false, error: e?.message ?? 'Server error' }, 500)
|
|
92
92
|
}
|
|
93
|
-
})`}var u8=c(()=>{e4();X0()});function sZ(Z){return Z instanceof Error?Z.message:String(Z)}function f8(Z,X,W){let G;try{G=JSON.parse(Z)}catch(Q){throw Error(`Failed to parse ${W} at ${X}: ${sZ(Q)}`,{cause:Q})}if(!F2(G))throw Error(`Invalid ${W} at ${X}: expected a JSON object`);return G}function w1(Z,X){return f8(Z,X,"Jx document")}function vX(Z,X){return f8(Z,X,"project config")}function yX(Z,X){return f8(Z,X,"class definition")}var c8=c(()=>{o2()});class O1{name;classPath;classDef;extensions;mediaType;documentKinds;exportTarget;remote;studio;project;server;connector;capabilities;#X;#Z=null;constructor(Z,X,W,G){this.name=Z,this.classPath=X,this.classDef=W,this.#X=G;let{format:Q}=W;this.extensions=(Q?.extensions??[]).map((J)=>J.toLowerCase()),this.mediaType=Q?.mediaType??null,this.documentKinds=Q?.documentKinds??[],this.exportTarget=Q?.exportTarget===!0,this.remote=Q?.remote===!0,this.studio=W.$studio??null,this.project=W.project??null,this.server=W.server??null,this.connector=W.connector??null,this.capabilities=tZ(W)}implementation(){if(!this.#Z){let Z=this.classDef.$implementation;if(!Z)return Promise.reject(Error(`Format class "${this.name}" has no $implementation to import`));this.#Z=this.#X.importModule(this.#X.resolvePath(this.classPath,Z))}return this.#Z}async call(Z,...X){let W=this.capabilities[Z];if(!W)throw Error(`Format class "${this.name}" does not declare a "${Z}" capability`);let G=await this.implementation(),Q=this.classDef.title??this.name,J=G[Q]??G.default?.[Q]??G.default,U=J?.[W.identifier];if(typeof U!=="function")throw TypeError(`Format class "${this.name}": implementation export "${Q}" has no static "${W.identifier}" method`);return await U.call(J,...X)}}class T1{#X;constructor(Z){this.#X=Z;for(let X of z6){let W=new Map;for(let G of Z){if(!G.capabilities[X])continue;for(let Q of G.extensions){let J=W.get(Q);if(J)throw Error(`Format conflict: "${J.name}" and "${G.name}" both claim "${Q}" with the "${X}" capability`);W.set(Q,G)}}}}get entries(){return this.#X}byExtension(Z,X){let W=X6(Z);return this.#X.find((G)=>G.extensions.includes(W)&&(!X||G.capabilities[X]))}byName(Z){return this.#X.find((X)=>X.name===Z)}withCapability(Z){return this.#X.filter((X)=>X.capabilities[Z])}documentExtensions(Z){let X=new Set;for(let W of this.#X){if(Z&&!W.documentKinds.includes(Z))continue;for(let G of W.extensions)X.add(G)}return X.delete(".json"),[...X]}has(Z){let X=X6(Z);return this.#X.some((W)=>W.extensions.includes(X))}}function tZ(Z){let X={},W=Z.$defs?.methods??{};for(let[G,Q]of Object.entries(W)){let J=Q.role;if(!J||!W6.includes(J))continue;let U={identifier:Q.identifier??G,timing:Q.timing??aZ};if(typeof Q.discriminator==="string")U.discriminator=Q.discriminator;X[J]=U}return X}function X6(Z){let X=Z.toLowerCase();return X.startsWith(".")?X:`.${X}`}var z6,W6,aZ;var d8=c(()=>{z6=["parse","serialize","discover","load"],W6=[...z6,"projectData","resolvePaths","lower","emit","mount","dialect","deploySchema","bindings","testConnection"],aZ=["compiler","server"]});class G6{#X;#Z;#z;constructor(Z){this.#X=Z,this.#Z=Z.flatMap((Q)=>Q.classes);let X=new Map;for(let Q of Z)for(let J of Q.classes){let U=X.get(J.name);if(U&&U!==Q.specifier)throw Error(`Extension conflict: "${U}" and "${Q.specifier}" both provide a class named "${J.name}"`);X.set(J.name,Q.specifier)}let W=new Map;for(let Q of this.#Z){if(!Q.project)continue;let J=W.get(Q.project.key);if(J)throw Error(`Extension conflict: "${J.name}" and "${Q.name}" both claim the project section "${Q.project.key}"`);W.set(Q.project.key,Q)}let G=new Map;for(let Q of this.#Z){if(!Q.server)continue;let{basePath:J}=Q.server;if(!J.startsWith("/_jx/"))throw Error(`Extension "${Q.name}": server basePath "${J}" must be under /_jx/`);let U=G.get(J);if(U)throw Error(`Extension conflict: "${U.name}" and "${Q.name}" both mount "${J}"`);G.set(J,Q)}this.#z=new T1(this.#Z.filter((Q)=>Q.extensions.length>0))}get extensions(){return this.#X}get classes(){return this.#Z}get formats(){return this.#z}byName(Z){return this.#Z.find((X)=>X.name===Z)}projectContributions(){return this.#Z.filter((Z)=>Z.project!==null)}byProjectKey(Z){return this.#Z.find((X)=>X.project?.key===Z)}byPathsDiscriminator(Z){return this.#Z.find((X)=>X.capabilities.resolvePaths?.discriminator===Z)}emitters(){return this.#Z.filter((Z)=>Z.capabilities.emit!==void 0)}serverMounts(){return this.#Z.filter((Z)=>Z.server!==null).toSorted((Z,X)=>(Z.server?.order??100)-(X.server?.order??100)||Z.name.localeCompare(X.name))}connectors(){return this.#Z.filter((Z)=>Z.connector!==null)}schemaFragments(Z){return this.#X.map((X)=>X.schemas[Z]).filter((X)=>typeof X==="string")}}async function Q6(Z,X,W){let G=[];for(let Q of Z??[])G.push(await eZ(Q,X,W));return new G6(G)}async function eZ(Z,X,W){let G;if(Z.startsWith("./")||Z.startsWith("../")){let K=X.resolvePath(W,Z);G=X.resolvePath(`${K}/package.json`,`./${_1}`)}else try{G=X.resolvePath(W,`${Z}/${_1}`)}catch(K){throw Error(`Extension "${Z}" is not resolvable: it must export ${_1} (is the package installed, and does its exports map include it?): ${p8(K)}`,{cause:K})}let Q;try{Q=await X.loadJson(G)}catch(K){throw Error(`Extension "${Z}": cannot read manifest ${G}: ${p8(K)}`,{cause:K})}if(!Q||typeof Q!=="object"||typeof Q.name!=="string")throw Error(`Extension "${Z}": ${_1} must declare a "name"`);if(!Z.startsWith(".")&&Q.name!==Z)throw Error(`Extension "${Z}": manifest name "${Q.name}" does not match the specifier`);let J=[];for(let[K,H]of Object.entries(Q.classes??{})){let Y=X.resolvePath(G,H),q;try{q=await X.loadJson(Y)}catch(B){throw Error(`Extension "${Z}": cannot read class "${K}" at ${Y}: ${p8(B)}`,{cause:B})}J.push(new O1(K,Y,q,X))}let U={};if(Q.schemas?.project)U.project=X.resolvePath(G,Q.schemas.project);if(Q.schemas?.document)U.document=X.resolvePath(G,Q.schemas.document);if(Q.schemas?.fields)U.fields=X.resolvePath(G,Q.schemas.fields);return{specifier:Z,manifestPath:G,manifest:Q,classes:J,schemas:U}}function p8(Z){return Z instanceof Error?Z.message:String(Z)}var _1="jx-extension.json";var J6=c(()=>{d8();d8()});var i8={};X9(i8,{unknownFormatError:()=>Q7,importImplementation:()=>K6,createNodeFormatIO:()=>V6,buildProjectFormatRegistry:()=>W7,buildProjectExtensionRegistry:()=>q6,buildExtensionsPayload:()=>G7});import{readFileSync as Y6}from"node:fs";import{dirname as Z7,isAbsolute as X7,resolve as l8,sep as N1}from"node:path";import{pathToFileURL as z7}from"node:url";import{createRequire as U6}from"node:module";async function K6(Z){let X=[Z],W=`${N1}src${N1}`,G=`${N1}dist${N1}`;if(Z.includes(W))X.push(Z.replace(W,G));if(Z.endsWith(".js"))X.push(`${Z.slice(0,-3)}.ts`);let Q;for(let J of X)try{return await import(z7(J).href)}catch(U){Q=U}throw Q instanceof Error?Q:Error(String(Q))}function V6(Z){return{importModule:(X)=>K6(X),loadJson:async(X)=>JSON.parse(Y6(X,"utf8")),resolvePath:(X,W)=>{if(W.startsWith("./")||W.startsWith("../"))return l8(Z7(X),W);if(X7(W))return W;let G=U6(l8(Z,"package.json"));try{return G.resolve(W)}catch{return U6(import.meta.url).resolve(W)}}}}async function q6(Z,X){return Q6(X?.extensions,V6(Z),l8(Z,"project.json"))}async function W7(Z,X){return(await q6(Z,X)).formats}function G7(Z){let X=[];for(let W of Z.extensions){let G={};if(W.schemas.project)try{G=JSON.parse(Y6(W.schemas.project,"utf8")).properties??{}}catch{G={}}let Q=[];for(let J of W.classes){if(!J.project)continue;Q.push({className:J.name,entrySchema:G[J.project.key]??null,project:J.project,studio:J.studio})}X.push({classes:W.classes.map((J)=>{let U=J.studio?.stateDefaults,K=J.project===null&&J.server===null&&J.connector===null&&J.extensions.length===0;return{name:J.name,path:J.classPath,...K?{state:!0}:{},...U&&typeof U==="object"?{stateDefaults:U}:{}}}),contributions:Q,name:W.manifest.name,specifier:W.specifier,...W.manifest.title===void 0?{}:{title:W.manifest.title},...W.manifest.description===void 0?{}:{description:W.manifest.description}})}return X}function Q7(Z,X){return Error(`No format class registered for "${X}" (${Z}). Enable an extension providing this format in project.json "extensions", e.g. "@jxsuite/parser".`)}var n8=c(()=>{J6()});async function B6(Z,X={}){let{resolveElementPath:W,$media:G,formats:Q}=X,J=[],U=new Set;async function K(H,Y){let q,B;if(typeof H==="string"){let{readFileSync:h}=await import("node:fs"),{resolve:f}=await import("node:path");if(B=Y?f(Y,H):f(H),U.has(B))return;if(U.add(B),B.endsWith(".json"))q=w1(h(B,"utf8"),B);else{let{extname:X2}=await import("node:path"),u=X2(B).toLowerCase(),b=Q?.byExtension?.(u,"parse");if(!b){let{unknownFormatError:o}=await Promise.resolve().then(() => (n8(),i8));throw o(B,u)}q=await b.call("parse",h(B,"utf8"))}}else{if(q=H,B=null,q.tagName&&U.has(q.tagName))return;if(q.tagName)U.add(q.tagName)}let{tagName:M}=q;if(!M||!M.includes("-"))throw Error(`compileElement: tagName "${M}" must contain a hyphen`);let{dirname:F}=await import("node:path"),D=B?F(B):null,w=[];if(Array.isArray(q.$elements))for(let h of q.$elements){let f=T2(h)?h.$ref:h;if(typeof f!=="string")continue;if(D)await K(f,D);let X2=W?W(f,D):f.replace(/\.json$/,".js");w.push(X2)}let A=J1(M);if(G)q.$media={...G,...q.$media};let{rewriteSrc:C}=X,E=y0(q,A,w,C?(h)=>C(h,D):void 0),y=B?B.replace(/\.json$/,".js"):`${M}.js`;J.push({content:E,path:y,tagName:M})}return await K(Z,X.basePath??null),{files:J}}async function J7(Z,X={}){let{title:W="Jx App",reactivitySrc:G="https://esm.sh/@vue/reactivity@3.5.13",litHtmlSrc:Q="https://esm.sh/lit-html@3.3.0"}=X,J=await B6(Z,X),U=J.files.at(-1);if(!U)throw Error("compileElementPage: no element modules were produced");let{basename:K}=await import("node:path"),H=K(U.path),Y=`<!DOCTYPE html>
|
|
93
|
+
})`}var u8=c(()=>{e4();X0()});function sZ(Z){return Z instanceof Error?Z.message:String(Z)}function f8(Z,X,W){let G;try{G=JSON.parse(Z)}catch(Q){throw Error(`Failed to parse ${W} at ${X}: ${sZ(Q)}`,{cause:Q})}if(!F2(G))throw Error(`Invalid ${W} at ${X}: expected a JSON object`);return G}function w1(Z,X){return f8(Z,X,"Jx document")}function vX(Z,X){return f8(Z,X,"project config")}function yX(Z,X){return f8(Z,X,"class definition")}var c8=c(()=>{o2()});class O1{name;classPath;classDef;extensions;mediaType;documentKinds;exportTarget;remote;studio;project;server;connector;capabilities;#X;#Z=null;constructor(Z,X,W,G){this.name=Z,this.classPath=X,this.classDef=W,this.#X=G;let{format:Q}=W;this.extensions=(Q?.extensions??[]).map((J)=>J.toLowerCase()),this.mediaType=Q?.mediaType??null,this.documentKinds=Q?.documentKinds??[],this.exportTarget=Q?.exportTarget===!0,this.remote=Q?.remote===!0,this.studio=W.$studio??null,this.project=W.project??null,this.server=W.server??null,this.connector=W.connector??null,this.capabilities=tZ(W)}implementation(){if(!this.#Z){let Z=this.classDef.$implementation;if(!Z)return Promise.reject(Error(`Format class "${this.name}" has no $implementation to import`));this.#Z=this.#X.importModule(this.#X.resolvePath(this.classPath,Z))}return this.#Z}async call(Z,...X){let W=this.capabilities[Z];if(!W)throw Error(`Format class "${this.name}" does not declare a "${Z}" capability`);let G=await this.implementation(),Q=this.classDef.title??this.name,J=G[Q]??G.default?.[Q]??G.default,U=J?.[W.identifier];if(typeof U!=="function")throw TypeError(`Format class "${this.name}": implementation export "${Q}" has no static "${W.identifier}" method`);return await U.call(J,...X)}}class T1{#X;constructor(Z){this.#X=Z;for(let X of z6){let W=new Map;for(let G of Z){if(!G.capabilities[X])continue;for(let Q of G.extensions){let J=W.get(Q);if(J)throw Error(`Format conflict: "${J.name}" and "${G.name}" both claim "${Q}" with the "${X}" capability`);W.set(Q,G)}}}}get entries(){return this.#X}byExtension(Z,X){let W=X6(Z);return this.#X.find((G)=>G.extensions.includes(W)&&(!X||G.capabilities[X]))}byName(Z){return this.#X.find((X)=>X.name===Z)}withCapability(Z){return this.#X.filter((X)=>X.capabilities[Z])}documentExtensions(Z){let X=new Set;for(let W of this.#X){if(Z&&!W.documentKinds.includes(Z))continue;for(let G of W.extensions)X.add(G)}return X.delete(".json"),[...X]}has(Z){let X=X6(Z);return this.#X.some((W)=>W.extensions.includes(X))}}function tZ(Z){let X={},W=Z.$defs?.methods??{};for(let[G,Q]of Object.entries(W)){let J=Q.role;if(!J||!W6.includes(J))continue;let U={identifier:Q.identifier??G,timing:Q.timing??aZ};if(typeof Q.discriminator==="string")U.discriminator=Q.discriminator;X[J]=U}return X}function X6(Z){let X=Z.toLowerCase();return X.startsWith(".")?X:`.${X}`}var z6,W6,aZ;var d8=c(()=>{z6=["parse","serialize","discover","load"],W6=[...z6,"projectData","resolvePaths","lower","emit","assets","mount","dialect","deploySchema","bindings","testConnection"],aZ=["compiler","server"]});class G6{#X;#Z;#z;constructor(Z){this.#X=Z,this.#Z=Z.flatMap((Q)=>Q.classes);let X=new Map;for(let Q of Z)for(let J of Q.classes){let U=X.get(J.name);if(U&&U!==Q.specifier)throw Error(`Extension conflict: "${U}" and "${Q.specifier}" both provide a class named "${J.name}"`);X.set(J.name,Q.specifier)}let W=new Map;for(let Q of this.#Z){if(!Q.project)continue;let J=W.get(Q.project.key);if(J)throw Error(`Extension conflict: "${J.name}" and "${Q.name}" both claim the project section "${Q.project.key}"`);W.set(Q.project.key,Q)}let G=new Map;for(let Q of this.#Z){if(!Q.server)continue;let{basePath:J}=Q.server;if(!J.startsWith("/_jx/"))throw Error(`Extension "${Q.name}": server basePath "${J}" must be under /_jx/`);let U=G.get(J);if(U)throw Error(`Extension conflict: "${U.name}" and "${Q.name}" both mount "${J}"`);G.set(J,Q)}this.#z=new T1(this.#Z.filter((Q)=>Q.extensions.length>0))}get extensions(){return this.#X}get classes(){return this.#Z}get formats(){return this.#z}byName(Z){return this.#Z.find((X)=>X.name===Z)}projectContributions(){return this.#Z.filter((Z)=>Z.project!==null)}byProjectKey(Z){return this.#Z.find((X)=>X.project?.key===Z)}byPathsDiscriminator(Z){return this.#Z.find((X)=>X.capabilities.resolvePaths?.discriminator===Z)}emitters(){return this.#Z.filter((Z)=>Z.capabilities.emit!==void 0)}assetProviders(){return this.#Z.filter((Z)=>Z.capabilities.assets!==void 0)}serverMounts(){return this.#Z.filter((Z)=>Z.server!==null).toSorted((Z,X)=>(Z.server?.order??100)-(X.server?.order??100)||Z.name.localeCompare(X.name))}connectors(){return this.#Z.filter((Z)=>Z.connector!==null)}schemaFragments(Z){return this.#X.map((X)=>X.schemas[Z]).filter((X)=>typeof X==="string")}}async function Q6(Z,X,W){let G=[];for(let Q of Z??[])G.push(await eZ(Q,X,W));return new G6(G)}async function eZ(Z,X,W){let G;if(Z.startsWith("./")||Z.startsWith("../")){let K=X.resolvePath(W,Z);G=X.resolvePath(`${K}/package.json`,`./${_1}`)}else try{G=X.resolvePath(W,`${Z}/${_1}`)}catch(K){throw Error(`Extension "${Z}" is not resolvable: it must export ${_1} (is the package installed, and does its exports map include it?): ${p8(K)}`,{cause:K})}let Q;try{Q=await X.loadJson(G)}catch(K){throw Error(`Extension "${Z}": cannot read manifest ${G}: ${p8(K)}`,{cause:K})}if(!Q||typeof Q!=="object"||typeof Q.name!=="string")throw Error(`Extension "${Z}": ${_1} must declare a "name"`);if(!Z.startsWith(".")&&Q.name!==Z)throw Error(`Extension "${Z}": manifest name "${Q.name}" does not match the specifier`);let J=[];for(let[K,H]of Object.entries(Q.classes??{})){let Y=X.resolvePath(G,H),q;try{q=await X.loadJson(Y)}catch(B){throw Error(`Extension "${Z}": cannot read class "${K}" at ${Y}: ${p8(B)}`,{cause:B})}J.push(new O1(K,Y,q,X))}let U={};if(Q.schemas?.project)U.project=X.resolvePath(G,Q.schemas.project);if(Q.schemas?.document)U.document=X.resolvePath(G,Q.schemas.document);if(Q.schemas?.fields)U.fields=X.resolvePath(G,Q.schemas.fields);return{specifier:Z,manifestPath:G,manifest:Q,classes:J,schemas:U}}function p8(Z){return Z instanceof Error?Z.message:String(Z)}var _1="jx-extension.json";var J6=c(()=>{d8();d8()});var i8={};X9(i8,{unknownFormatError:()=>Q7,importImplementation:()=>K6,createNodeFormatIO:()=>V6,buildProjectFormatRegistry:()=>W7,buildProjectExtensionRegistry:()=>q6,buildExtensionsPayload:()=>G7});import{readFileSync as Y6}from"node:fs";import{dirname as Z7,isAbsolute as X7,resolve as l8,sep as N1}from"node:path";import{pathToFileURL as z7}from"node:url";import{createRequire as U6}from"node:module";async function K6(Z){let X=[Z],W=`${N1}src${N1}`,G=`${N1}dist${N1}`;if(Z.includes(W))X.push(Z.replace(W,G));if(Z.endsWith(".js"))X.push(`${Z.slice(0,-3)}.ts`);let Q;for(let J of X)try{return await import(z7(J).href)}catch(U){Q=U}throw Q instanceof Error?Q:Error(String(Q))}function V6(Z){return{importModule:(X)=>K6(X),loadJson:async(X)=>JSON.parse(Y6(X,"utf8")),resolvePath:(X,W)=>{if(W.startsWith("./")||W.startsWith("../"))return l8(Z7(X),W);if(X7(W))return W;let G=U6(l8(Z,"package.json"));try{return G.resolve(W)}catch{return U6(import.meta.url).resolve(W)}}}}async function q6(Z,X){return Q6(X?.extensions,V6(Z),l8(Z,"project.json"))}async function W7(Z,X){return(await q6(Z,X)).formats}function G7(Z){let X=[];for(let W of Z.extensions){let G={};if(W.schemas.project)try{G=JSON.parse(Y6(W.schemas.project,"utf8")).properties??{}}catch{G={}}let Q=[];for(let J of W.classes){if(!J.project)continue;Q.push({className:J.name,entrySchema:G[J.project.key]??null,project:J.project,studio:J.studio})}X.push({classes:W.classes.map((J)=>{let U=J.studio?.stateDefaults,K=J.project===null&&J.server===null&&J.connector===null&&J.extensions.length===0;return{name:J.name,path:J.classPath,...K?{state:!0}:{},...U&&typeof U==="object"?{stateDefaults:U}:{}}}),contributions:Q,name:W.manifest.name,specifier:W.specifier,...W.manifest.title===void 0?{}:{title:W.manifest.title},...W.manifest.description===void 0?{}:{description:W.manifest.description}})}return X}function Q7(Z,X){return Error(`No format class registered for "${X}" (${Z}). Enable an extension providing this format in project.json "extensions", e.g. "@jxsuite/parser".`)}var n8=c(()=>{J6()});async function B6(Z,X={}){let{resolveElementPath:W,$media:G,formats:Q}=X,J=[],U=new Set;async function K(H,Y){let q,B;if(typeof H==="string"){let{readFileSync:h}=await import("node:fs"),{resolve:f}=await import("node:path");if(B=Y?f(Y,H):f(H),U.has(B))return;if(U.add(B),B.endsWith(".json"))q=w1(h(B,"utf8"),B);else{let{extname:X2}=await import("node:path"),u=X2(B).toLowerCase(),b=Q?.byExtension?.(u,"parse");if(!b){let{unknownFormatError:o}=await Promise.resolve().then(() => (n8(),i8));throw o(B,u)}q=await b.call("parse",h(B,"utf8"))}}else{if(q=H,B=null,q.tagName&&U.has(q.tagName))return;if(q.tagName)U.add(q.tagName)}let{tagName:M}=q;if(!M||!M.includes("-"))throw Error(`compileElement: tagName "${M}" must contain a hyphen`);let{dirname:F}=await import("node:path"),D=B?F(B):null,w=[];if(Array.isArray(q.$elements))for(let h of q.$elements){let f=T2(h)?h.$ref:h;if(typeof f!=="string")continue;if(D)await K(f,D);let X2=W?W(f,D):f.replace(/\.json$/,".js");w.push(X2)}let A=J1(M);if(G)q.$media={...G,...q.$media};let{rewriteSrc:C}=X,E=y0(q,A,w,C?(h)=>C(h,D):void 0),y=B?B.replace(/\.json$/,".js"):`${M}.js`;J.push({content:E,path:y,tagName:M})}return await K(Z,X.basePath??null),{files:J}}async function J7(Z,X={}){let{title:W="Jx App",reactivitySrc:G="https://esm.sh/@vue/reactivity@3.5.13",litHtmlSrc:Q="https://esm.sh/lit-html@3.3.0"}=X,J=await B6(Z,X),U=J.files.at(-1);if(!U)throw Error("compileElementPage: no element modules were produced");let{basename:K}=await import("node:path"),H=K(U.path),Y=`<!DOCTYPE html>
|
|
94
94
|
<html lang="en">
|
|
95
95
|
<head>
|
|
96
96
|
<meta charset="utf-8">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"type": "module",
|
|
20
20
|
"exports": {
|
|
21
21
|
".": "./src/compiler.ts",
|
|
22
|
+
"./asset-mounts": "./src/site/asset-mounts.ts",
|
|
22
23
|
"./format-host": "./src/site/format-host.ts",
|
|
23
24
|
"./project-sections": "./src/site/project-sections.ts",
|
|
24
25
|
"./schema-command": "./src/site/schema-command.ts",
|
|
@@ -45,16 +46,16 @@
|
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@apidevtools/json-schema-ref-parser": "^15.5.0",
|
|
48
|
-
"@jxsuite/create": "^1.2.
|
|
49
|
-
"@jxsuite/runtime": "^1.3.
|
|
50
|
-
"@jxsuite/schema": "^1.
|
|
49
|
+
"@jxsuite/create": "^1.2.1",
|
|
50
|
+
"@jxsuite/runtime": "^1.3.1",
|
|
51
|
+
"@jxsuite/schema": "^1.4.0",
|
|
51
52
|
"esbuild": "^0.28.1",
|
|
52
53
|
"sharp": "^0.35.3"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
56
|
-
"@jxsuite/connector": "^0.4.
|
|
57
|
-
"@jxsuite/parser": "^1.
|
|
56
|
+
"@happy-dom/global-registrator": "^20.11.1",
|
|
57
|
+
"@jxsuite/connector": "^0.4.1",
|
|
58
|
+
"@jxsuite/parser": "^1.4.0",
|
|
58
59
|
"@types/mdast": "^4.0.4"
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset-mounts — host side of the extension `assets` capability (specs/extensions.md §8.5).
|
|
3
|
+
*
|
|
4
|
+
* A section-owner class publishes directories that may sit outside the project root (an external
|
|
5
|
+
* content source's co-located images) at a stable site URL. The build resolves those URLs while
|
|
6
|
+
* optimizing images, collects the ones its compiled output actually references, and copies just
|
|
7
|
+
* those files into dist — the source directory itself is never mirrored, so entry files never leak
|
|
8
|
+
* into the build output.
|
|
9
|
+
*
|
|
10
|
+
* @module asset-mounts
|
|
11
|
+
* @docs framework/site/images
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { cpSync, existsSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { dirname, resolve } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
collectAssetUrls,
|
|
18
|
+
normalizeAssetPrefix,
|
|
19
|
+
resolveAssetUrl,
|
|
20
|
+
} from "@jxsuite/schema/asset-paths";
|
|
21
|
+
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
22
|
+
import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
23
|
+
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gather every asset mount contributed by the project's extensions.
|
|
27
|
+
*
|
|
28
|
+
* Gating matches `emit` (extensions.md §8.4): a class owning a project section contributes only
|
|
29
|
+
* when the project declares a non-empty value for that section. Mounts claiming the same URL prefix
|
|
30
|
+
* for different directories are a configuration error — the first wins and the clash is reported.
|
|
31
|
+
*
|
|
32
|
+
* @param {ExtensionRegistry} registry - The project's extension registry
|
|
33
|
+
* @param {ProjectConfig} projectConfig
|
|
34
|
+
* @param {string} projectRoot - Absolute project root
|
|
35
|
+
* @returns {Promise<{ mounts: AssetMount[]; errors: string[] }>}
|
|
36
|
+
*/
|
|
37
|
+
export async function loadAssetMounts(
|
|
38
|
+
registry: ExtensionRegistry,
|
|
39
|
+
projectConfig: ProjectConfig,
|
|
40
|
+
projectRoot: string,
|
|
41
|
+
): Promise<{ mounts: AssetMount[]; errors: string[] }> {
|
|
42
|
+
const mounts: AssetMount[] = [];
|
|
43
|
+
const errors: string[] = [];
|
|
44
|
+
const byPrefix = new Map<string, AssetMount>();
|
|
45
|
+
|
|
46
|
+
for (const entry of registry.assetProviders()) {
|
|
47
|
+
const sectionKey = entry.project?.key;
|
|
48
|
+
const sectionValue = sectionKey
|
|
49
|
+
? (projectConfig as unknown as Record<string, unknown>)[sectionKey]
|
|
50
|
+
: undefined;
|
|
51
|
+
if (
|
|
52
|
+
sectionKey &&
|
|
53
|
+
(sectionValue == null ||
|
|
54
|
+
(typeof sectionValue === "object" && Object.keys(sectionValue).length === 0))
|
|
55
|
+
) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const declared = (await entry.call("assets", sectionValue ?? null, {
|
|
60
|
+
projectConfig,
|
|
61
|
+
root: projectRoot,
|
|
62
|
+
})) as AssetMount[] | null;
|
|
63
|
+
for (const mount of declared ?? []) {
|
|
64
|
+
if (!mount?.urlPrefix || !mount.dir) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const normalized: AssetMount = {
|
|
68
|
+
dir: mount.dir,
|
|
69
|
+
urlPrefix: normalizeAssetPrefix(mount.urlPrefix),
|
|
70
|
+
};
|
|
71
|
+
const prior = byPrefix.get(normalized.urlPrefix);
|
|
72
|
+
if (prior) {
|
|
73
|
+
if (prior.dir !== normalized.dir) {
|
|
74
|
+
errors.push(
|
|
75
|
+
`Asset mount conflict: "${normalized.urlPrefix}" is claimed by both ` +
|
|
76
|
+
`"${prior.dir}" and "${normalized.dir}"`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
byPrefix.set(normalized.urlPrefix, normalized);
|
|
82
|
+
mounts.push(normalized);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
errors.push(`Error in ${entry.name}.assets: ${(error as Error).message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { errors, mounts };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Add every mounted URL referenced by a compiled artifact to `into`.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} text - Compiled HTML or CSS
|
|
96
|
+
* @param {readonly AssetMount[]} mounts
|
|
97
|
+
* @param {Set<string>} into - Accumulator across the whole build
|
|
98
|
+
*/
|
|
99
|
+
export function collectAssetRefs(text: string, mounts: readonly AssetMount[], into: Set<string>) {
|
|
100
|
+
if (mounts.length === 0 || !text) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const url of collectAssetUrls(text, mounts)) {
|
|
104
|
+
into.add(url);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Copy referenced mounted assets into the build output, each at its own URL path.
|
|
110
|
+
*
|
|
111
|
+
* @param {Iterable<string>} urls - Mounted URLs collected from the compiled output
|
|
112
|
+
* @param {readonly AssetMount[]} mounts
|
|
113
|
+
* @param {string} outDir - Absolute build output directory
|
|
114
|
+
* @returns {{ copied: number; missing: string[] }} Files written, and URLs that resolved to nothing
|
|
115
|
+
*/
|
|
116
|
+
export function copyMountedAssets(
|
|
117
|
+
urls: Iterable<string>,
|
|
118
|
+
mounts: readonly AssetMount[],
|
|
119
|
+
outDir: string,
|
|
120
|
+
): { copied: number; missing: string[] } {
|
|
121
|
+
let copied = 0;
|
|
122
|
+
const missing: string[] = [];
|
|
123
|
+
|
|
124
|
+
for (const url of urls) {
|
|
125
|
+
// The single containment gate is resolveAssetUrl: it decodes once and refuses `.`/`..`,
|
|
126
|
+
// Empty segments, and undecodable input, so a surviving URL cannot escape either the mount
|
|
127
|
+
// Directory it reads from or the outDir path it writes to.
|
|
128
|
+
const source = resolveAssetUrl(mounts, url);
|
|
129
|
+
if (!source || !existsSync(source)) {
|
|
130
|
+
missing.push(url);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const target = resolve(outDir, decodeURIComponent(url).replace(/^\//, ""));
|
|
134
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
135
|
+
cpSync(source, target);
|
|
136
|
+
copied += 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { copied, missing };
|
|
140
|
+
}
|
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
processImage,
|
|
17
17
|
} from "./image-optimizer.ts";
|
|
18
18
|
import { getCached, getImageCacheDir, setCached } from "./image-cache.ts";
|
|
19
|
+
import { resolveAssetUrl } from "@jxsuite/schema/asset-paths";
|
|
19
20
|
|
|
21
|
+
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
20
22
|
import type { ImageConfig, ImageManifest } from "./image-optimizer.ts";
|
|
21
23
|
import type { JxDocument, JxMutableNode } from "@jxsuite/schema/types";
|
|
22
24
|
import type { CacheManifest } from "./image-cache.ts";
|
|
@@ -193,13 +195,20 @@ function isAllowedRemote(src: string, config: ImageConfig) {
|
|
|
193
195
|
/**
|
|
194
196
|
* Resolve a src path to an absolute filesystem path.
|
|
195
197
|
*
|
|
196
|
-
*
|
|
198
|
+
* Extension asset mounts win first — they publish directories that may sit outside the project root
|
|
199
|
+
* (extensions.md §8.5), so a mounted src has no other resolution. Otherwise paths starting with "/"
|
|
200
|
+
* resolve into `public/`, and relative ones from the project root.
|
|
197
201
|
*
|
|
198
202
|
* @param {string} src
|
|
199
203
|
* @param {string} projectRoot
|
|
204
|
+
* @param {readonly AssetMount[]} [mounts]
|
|
200
205
|
* @returns {string}
|
|
201
206
|
*/
|
|
202
|
-
function resolveImagePath(src: string, projectRoot: string) {
|
|
207
|
+
function resolveImagePath(src: string, projectRoot: string, mounts?: readonly AssetMount[]) {
|
|
208
|
+
const mounted = mounts?.length ? resolveAssetUrl(mounts, src) : null;
|
|
209
|
+
if (mounted) {
|
|
210
|
+
return mounted;
|
|
211
|
+
}
|
|
203
212
|
if (src.startsWith("/")) {
|
|
204
213
|
return resolve(projectRoot, "public", src.slice(1));
|
|
205
214
|
}
|
|
@@ -218,6 +227,7 @@ function resolveImagePath(src: string, projectRoot: string) {
|
|
|
218
227
|
* @param {string} projectRoot
|
|
219
228
|
* @param {CacheManifest | null} cache - Variant cache (build mode); unused in cloudflare mode
|
|
220
229
|
* @param {ImageMetaCache} [metaCache] - Dimension/hash memo (cloudflare mode)
|
|
230
|
+
* @param {readonly AssetMount[]} [mounts] - Extension asset mounts (extensions.md §8.5)
|
|
221
231
|
* @returns {Promise<{ imageRefs: Map<string, ImageManifest> }>}
|
|
222
232
|
*/
|
|
223
233
|
export async function transformImageNodes(
|
|
@@ -226,6 +236,7 @@ export async function transformImageNodes(
|
|
|
226
236
|
projectRoot: string,
|
|
227
237
|
cache: CacheManifest | null,
|
|
228
238
|
metaCache?: ImageMetaCache,
|
|
239
|
+
mounts: readonly AssetMount[] = [],
|
|
229
240
|
) {
|
|
230
241
|
const imageRefs = new Map<string, ImageManifest>();
|
|
231
242
|
|
|
@@ -234,7 +245,7 @@ export async function transformImageNodes(
|
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
const meta = metaCache ?? new Map();
|
|
237
|
-
await walkAndTransform(doc, config, projectRoot, cache, meta, imageRefs);
|
|
248
|
+
await walkAndTransform(doc, config, projectRoot, cache, meta, imageRefs, mounts);
|
|
238
249
|
|
|
239
250
|
return { imageRefs };
|
|
240
251
|
}
|
|
@@ -246,6 +257,7 @@ export async function transformImageNodes(
|
|
|
246
257
|
* @param {CacheManifest | null} cache
|
|
247
258
|
* @param {ImageMetaCache} metaCache
|
|
248
259
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
260
|
+
* @param {readonly AssetMount[]} mounts
|
|
249
261
|
*/
|
|
250
262
|
async function walkAndTransform(
|
|
251
263
|
node: JxMutableNode | JxDocument,
|
|
@@ -254,13 +266,14 @@ async function walkAndTransform(
|
|
|
254
266
|
cache: CacheManifest | null,
|
|
255
267
|
metaCache: ImageMetaCache,
|
|
256
268
|
imageRefs: Map<string, ImageManifest>,
|
|
269
|
+
mounts: readonly AssetMount[],
|
|
257
270
|
) {
|
|
258
271
|
if (!node || typeof node !== "object") {
|
|
259
272
|
return;
|
|
260
273
|
}
|
|
261
274
|
|
|
262
275
|
if (node.tagName === "img") {
|
|
263
|
-
await transformImgNode(node, config, projectRoot, cache, metaCache, imageRefs);
|
|
276
|
+
await transformImgNode(node, config, projectRoot, cache, metaCache, imageRefs, mounts);
|
|
264
277
|
}
|
|
265
278
|
|
|
266
279
|
if (typeof node.innerHTML === "string" && node.innerHTML.includes("<img")) {
|
|
@@ -271,6 +284,7 @@ async function walkAndTransform(
|
|
|
271
284
|
cache,
|
|
272
285
|
metaCache,
|
|
273
286
|
imageRefs,
|
|
287
|
+
mounts,
|
|
274
288
|
);
|
|
275
289
|
}
|
|
276
290
|
|
|
@@ -279,7 +293,7 @@ async function walkAndTransform(
|
|
|
279
293
|
if (typeof child === "string") {
|
|
280
294
|
continue;
|
|
281
295
|
}
|
|
282
|
-
await walkAndTransform(child, config, projectRoot, cache, metaCache, imageRefs);
|
|
296
|
+
await walkAndTransform(child, config, projectRoot, cache, metaCache, imageRefs, mounts);
|
|
283
297
|
}
|
|
284
298
|
}
|
|
285
299
|
}
|
|
@@ -291,6 +305,7 @@ async function walkAndTransform(
|
|
|
291
305
|
* @param {CacheManifest | null} cache
|
|
292
306
|
* @param {ImageMetaCache} metaCache
|
|
293
307
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
308
|
+
* @param {readonly AssetMount[]} mounts
|
|
294
309
|
*/
|
|
295
310
|
async function transformImgNode(
|
|
296
311
|
node: JxMutableNode | JxDocument,
|
|
@@ -299,6 +314,7 @@ async function transformImgNode(
|
|
|
299
314
|
cache: CacheManifest | null,
|
|
300
315
|
metaCache: ImageMetaCache,
|
|
301
316
|
imageRefs: Map<string, ImageManifest>,
|
|
317
|
+
mounts: readonly AssetMount[],
|
|
302
318
|
) {
|
|
303
319
|
if (!node.attributes) {
|
|
304
320
|
node.attributes = {};
|
|
@@ -325,7 +341,7 @@ async function transformImgNode(
|
|
|
325
341
|
// Fit=scale-down avoid upscaling past the source size.
|
|
326
342
|
srcset = buildCloudflareSrcset(src, config, Infinity, null);
|
|
327
343
|
} else {
|
|
328
|
-
const absoluteSrc = resolveImagePath(src, projectRoot);
|
|
344
|
+
const absoluteSrc = resolveImagePath(src, projectRoot, mounts);
|
|
329
345
|
if (!existsSync(absoluteSrc)) {
|
|
330
346
|
return;
|
|
331
347
|
}
|
|
@@ -382,6 +398,7 @@ const DATA_NO_OPT_RE = /\bdata-no-optimize\b/;
|
|
|
382
398
|
* @param {CacheManifest | null} cache
|
|
383
399
|
* @param {ImageMetaCache} metaCache
|
|
384
400
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
401
|
+
* @param {readonly AssetMount[]} mounts
|
|
385
402
|
* @returns {Promise<string>}
|
|
386
403
|
*/
|
|
387
404
|
async function transformInnerHtmlImages(
|
|
@@ -391,6 +408,7 @@ async function transformInnerHtmlImages(
|
|
|
391
408
|
cache: CacheManifest | null,
|
|
392
409
|
metaCache: ImageMetaCache,
|
|
393
410
|
imageRefs: Map<string, ImageManifest>,
|
|
411
|
+
mounts: readonly AssetMount[],
|
|
394
412
|
) {
|
|
395
413
|
/** @type {{ match: string; replacement: string }[]} */
|
|
396
414
|
const replacements = [];
|
|
@@ -418,7 +436,7 @@ async function transformInnerHtmlImages(
|
|
|
418
436
|
continue;
|
|
419
437
|
}
|
|
420
438
|
|
|
421
|
-
const absoluteSrc = remote ? null : resolveImagePath(src, projectRoot);
|
|
439
|
+
const absoluteSrc = remote ? null : resolveImagePath(src, projectRoot, mounts);
|
|
422
440
|
if (absoluteSrc && !existsSync(absoluteSrc)) {
|
|
423
441
|
continue;
|
|
424
442
|
}
|
package/src/site/site-build.ts
CHANGED
|
@@ -32,6 +32,8 @@ import { loadProjectConfig } from "./site-loader.ts";
|
|
|
32
32
|
import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
|
|
33
33
|
import { buildProjectExtensionRegistry } from "./format-host.ts";
|
|
34
34
|
import { loadProjectSections } from "./project-sections.ts";
|
|
35
|
+
import { collectAssetRefs, copyMountedAssets, loadAssetMounts } from "./asset-mounts.ts";
|
|
36
|
+
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
35
37
|
import type { FormatEntry, FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
36
38
|
import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
37
39
|
import { resolveLayout } from "./layout-resolver.ts";
|
|
@@ -208,6 +210,26 @@ export async function buildSite(
|
|
|
208
210
|
log(` Loaded ${sectionKeys.length} section(s): ${sectionKeys.join(", ")}`);
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
// ── 3c. Extension asset mounts (extensions.md §8.5) ─────────────────────
|
|
214
|
+
// Directories published at a site URL — typically an external content source's co-located
|
|
215
|
+
// Images. They resolve for image optimization below, and the files pages actually reference
|
|
216
|
+
// Are copied into dist in step 7c.
|
|
217
|
+
const { errors: mountErrors, mounts: assetMounts } = await loadAssetMounts(
|
|
218
|
+
registry,
|
|
219
|
+
projectConfig,
|
|
220
|
+
projectRoot,
|
|
221
|
+
);
|
|
222
|
+
errors.push(...mountErrors);
|
|
223
|
+
for (const message of mountErrors) {
|
|
224
|
+
console.error(message);
|
|
225
|
+
}
|
|
226
|
+
if (assetMounts.length > 0) {
|
|
227
|
+
log(
|
|
228
|
+
` Mounted ${assetMounts.length} asset dir(s): ${assetMounts.map((m) => m.urlPrefix).join(", ")}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const assetRefs = new Set<string>();
|
|
232
|
+
|
|
211
233
|
// ── 4. Expand dynamic routes ────────────────────────────────────────────
|
|
212
234
|
const routes = await expandDynamicRoutes(
|
|
213
235
|
staticRoutes,
|
|
@@ -262,6 +284,7 @@ export async function buildSite(
|
|
|
262
284
|
const css = buildComponentCSS(doc.tagName, doc.style, doc, projectConfig.$media ?? {});
|
|
263
285
|
if (css) {
|
|
264
286
|
componentCSS.set(doc.tagName, css);
|
|
287
|
+
collectAssetRefs(css, assetMounts, assetRefs);
|
|
265
288
|
writeFileSync(resolve(componentOutDir, `${doc.tagName}.css`), css, "utf8");
|
|
266
289
|
fileCount += 1;
|
|
267
290
|
}
|
|
@@ -325,6 +348,7 @@ export async function buildSite(
|
|
|
325
348
|
formatRegistry,
|
|
326
349
|
registry,
|
|
327
350
|
rewriteSidecarSrc,
|
|
351
|
+
assetMounts,
|
|
328
352
|
);
|
|
329
353
|
|
|
330
354
|
// Determine which component tags are fully static (for script omission)
|
|
@@ -345,6 +369,10 @@ export async function buildSite(
|
|
|
345
369
|
);
|
|
346
370
|
}
|
|
347
371
|
|
|
372
|
+
// Mounted assets this page actually references — scanned from the finished HTML so a
|
|
373
|
+
// Reference is caught wherever it came from (markdown image, hand-authored page, $head).
|
|
374
|
+
collectAssetRefs(result.html, assetMounts, assetRefs);
|
|
375
|
+
|
|
348
376
|
// Determine output path
|
|
349
377
|
const outPath = routeToOutputPath(route.urlPattern, outDir, trailingSlash);
|
|
350
378
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
@@ -568,7 +596,19 @@ export async function buildSite(
|
|
|
568
596
|
console.warn("sitemap.xml skipped — set `url` in project.json to enable sitemap generation.");
|
|
569
597
|
}
|
|
570
598
|
|
|
571
|
-
// ── 7c. Copy
|
|
599
|
+
// ── 7c. Copy referenced mounted assets (extensions.md §8.5) ─────────────
|
|
600
|
+
// Only files the compiled output references are copied, so a content source's entry files
|
|
601
|
+
// Never land in dist. Runs before the public/ copy, which keeps shadowing everything.
|
|
602
|
+
if (assetRefs.size > 0) {
|
|
603
|
+
const { copied, missing } = copyMountedAssets(assetRefs, assetMounts, outDir);
|
|
604
|
+
fileCount += copied;
|
|
605
|
+
log(`Copying ${copied} mounted asset(s)...`);
|
|
606
|
+
for (const url of missing) {
|
|
607
|
+
console.warn(`Referenced asset not found: ${url}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ── 7c(ii). Copy public/ assets ─────────────────────────────────────────
|
|
572
612
|
if (existsSync(publicDir)) {
|
|
573
613
|
log("Copying public/ assets...");
|
|
574
614
|
cpSync(publicDir, outDir, { recursive: true });
|
|
@@ -630,6 +670,7 @@ async function compilePage(
|
|
|
630
670
|
formatRegistry?: FormatRegistry,
|
|
631
671
|
registry?: ExtensionRegistry,
|
|
632
672
|
rewriteSidecarSrc?: (specifier: string, docDir: string | null) => string,
|
|
673
|
+
assetMounts: readonly AssetMount[] = [],
|
|
633
674
|
) {
|
|
634
675
|
// Load the raw page document (.json natively, other formats via the registry)
|
|
635
676
|
const pageDoc = await readPageDocument(route.sourcePath as string, formatRegistry);
|
|
@@ -744,6 +785,7 @@ async function compilePage(
|
|
|
744
785
|
projectRoot,
|
|
745
786
|
imageCache,
|
|
746
787
|
imageMetaCache ?? undefined,
|
|
788
|
+
assetMounts,
|
|
747
789
|
);
|
|
748
790
|
}
|
|
749
791
|
|