@cldmv/slothlet 3.16.3 → 3.18.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.
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{resolveWrapper}from"#handlers/unified-wrapper";import{isFrameworkInternal}from"#handlers/framework-internals";import{compilePattern,expandBraces}from"@cldmv/slothlet/helpers/pattern-matcher";const ROOT_BUILTIN_NAMES=new Set(["shutdown","destroy"]);class RoutineManager extends ComponentBase{static slothletProperty="routineManager";constructor(slothlet){super(slothlet);this.raw=[];this.rawWrappers=new Map;this.recording=true;this.patternCache=new Map}reset(){this.raw=[];this.rawWrappers.clear();this.patternCache.clear()}get#routines(){return this.slothlet.config?.routines??[]}#moduleWrappers(moduleID){let inner=this.rawWrappers.get(moduleID);if(!inner){inner=new Map;this.rawWrappers.set(moduleID,inner)}return inner}#findRoutine(name){return this.#routines.find(routine=>routine.name===name)}#compile(pattern){let matcher=this.patternCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.patternCache.set(pattern,matcher)}return matcher}#matches(routine,entry){const{name,recursive}=routine;if(name.startsWith("^")){return this.#compile(name.slice(1))(entry.apiPath)}const endpoint=this.slothlet.handlers.ownership?.getModuleEndpoint(entry.moduleID);if(endpoint===void 0)return false;let relative;if(endpoint==="."||endpoint==="")relative=entry.apiPath;else if(entry.apiPath===endpoint)relative="";else if(entry.apiPath.startsWith(`${endpoint}.`))relative=entry.apiPath.slice(endpoint.length+1);else return false;if(relative==="")return false;if(this.#compile(name)(relative))return true;return recursive&&this.#compile(`**.${name}`)(relative)}#requiresDescent(routine){if(routine.name.startsWith("^"))return true;if(routine.recursive)return true;return routine.name.includes(".")}#isCurrentOwner(entry){const ownership=this.slothlet.handlers.ownership;if(!ownership)return true;const owner=ownership.getCurrentOwner(entry.apiPath);if(!owner)return true;return owner.moduleID===entry.moduleID}#applyStackFilter(entries){if(this.slothlet.config?.stackRoutines)return entries;return entries.filter(entry=>this.#isCurrentOwner(entry))}#contributorsFor(name){const routine=this.#findRoutine(name);if(!routine)return[];return this.#applyStackFilter(this.raw.filter(entry=>this.#matches(routine,entry)))}#groupByPath(entries){const groups=new Map;for(const entry of entries){let group=groups.get(entry.apiPath);if(!group){group=[];groups.set(entry.apiPath,group)}group.push(entry)}return groups}onImplCreated(data){if(!this.recording)return;if(this.#routines.length===0)return;const apiPath=data?.apiPath;if(typeof apiPath!=="string"||apiPath.length===0)return;const moduleID=data.moduleID;const fn=data.wrapper?.__impl;const existingIndex=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(typeof fn==="function"&&fn.__slothletRoutineStack===true){return}if(typeof fn!=="function"){if(existingIndex!==-1)this.raw.splice(existingIndex,1);this.rawWrappers.get(moduleID)?.delete(apiPath);return}const entry={apiPath,moduleID,fn};if(existingIndex===-1)this.raw.push(entry);else this.raw[existingIndex]=entry;const wrapper=resolveWrapper(data.__wrapperRef);if(wrapper)this.#moduleWrappers(moduleID).set(apiPath,wrapper);setImmediate(()=>{this.#reactivelyPatchStack(entry).catch(()=>{})})}async#reactivelyPatchStack(entry){if(this.slothlet.____buildDepth>0)return;const api=this.slothlet.api;if(!api)return;const lastDot=entry.apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":entry.apiPath.slice(0,lastDot);const key=lastDot===-1?entry.apiPath:entry.apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))return;let winner=null;let winnerGroup=null;const matchingRoutines=[];for(const routine of this.#routines){let group;try{group=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(routine,e)))}catch{continue}if(group.length===0)continue;matchingRoutines.push(routine);winner=routine;winnerGroup=group}if(!winner)return;const isCascadeSlot=parentPath===""&&key===winner.name;const contested=matchingRoutines.length>1;if(!isCascadeSlot&&winnerGroup.length<2&&!contested)return;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")return;if(this.slothlet.____buildDepth>0)return;if(this.slothlet.api!==api)return;const targetWrapper=resolveWrapper(target);if(targetWrapper&&targetWrapper.____slothletInternal?.invalid)return;let freshWinnerGroup;try{freshWinnerGroup=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(winner,e)))}catch{return}if(freshWinnerGroup.length===0)return;if(!isCascadeSlot&&freshWinnerGroup.length<2&&!contested)return;if(isCascadeSlot){let current2;try{current2=target[key]}catch{return}if(!(typeof current2==="function"&&current2.__slothletRoutineCascade===true&&current2.__slothletRoutineName===winner.name)){this.recording=false;try{target[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}return}let current;try{current=target[key]}catch{return}if(typeof current==="function"&&current.__slothletRoutineStack===true&&current.__slothletRoutineName===winner.name){return}this.recording=false;try{target[key]=this.#buildStackedCallable(entry.apiPath,winner)}catch{}finally{this.recording=true}}onImplRemoved(data){const apiPath=data?.apiPath;const moduleID=data?.moduleID;if(typeof apiPath!=="string"||!moduleID)return;this.raw=this.raw.filter(e=>!(e.apiPath===apiPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(apiPath)}pruneSubtree(apiPath,moduleID){const prefix=`${apiPath}.`;const matching=this.raw.filter(e=>e.moduleID===moduleID&&(e.apiPath===apiPath||e.apiPath.startsWith(prefix)));if(matching.length===0)return;this.raw=this.raw.filter(e=>!matching.includes(e));const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const entry of matching){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(moduleWrappers.get(entry.apiPath));moduleWrappers.delete(entry.apiPath)}}pruneModule(moduleID){this.raw=this.raw.filter(e=>e.moduleID!==moduleID);const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const wrapper of moduleWrappers.values()){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(wrapper)}this.rawWrappers.delete(moduleID)}snapshotRawEntries(moduleID){const snapshot=new Map;const moduleWrappers=this.rawWrappers.get(moduleID);this.raw.forEach((entry,index)=>{if(entry.moduleID===moduleID)snapshot.set(entry.apiPath,{fn:entry.fn,index,wrapper:moduleWrappers?.get(entry.apiPath)})});return snapshot}snapshotRawEntry(apiPath,moduleID){const index=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(index===-1)return void 0;return{fn:this.raw[index].fn,index,wrapper:this.rawWrappers.get(moduleID)?.get(apiPath)}}revertRawEntry(apiPath,moduleID,priorEntry){const idx=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(priorEntry!==void 0){const entry={apiPath,moduleID,fn:priorEntry.fn};if(idx!==-1)this.raw[idx]=entry;else this.raw.splice(Math.min(priorEntry.index,this.raw.length),0,entry);if(priorEntry.wrapper)this.#moduleWrappers(moduleID).set(apiPath,priorEntry.wrapper);else this.rawWrappers.get(moduleID)?.delete(apiPath)}else{if(idx!==-1)this.raw.splice(idx,1);this.rawWrappers.get(moduleID)?.delete(apiPath)}}revertSpeculativeSubtree(api,moduleID,path,priorEntries,visited=new WeakSet){if(!api||typeof api!=="object"&&typeof api!=="function")return;if(visited.has(api)){return}visited.add(api);const revert=revertPath=>{const prior=priorEntries.get(revertPath);if(prior){const idx=this.raw.findIndex(e=>e.apiPath===revertPath&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:revertPath,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:revertPath,moduleID,fn:prior.fn});if(prior.wrapper)this.#moduleWrappers(moduleID).set(revertPath,prior.wrapper);else this.rawWrappers.get(moduleID)?.delete(revertPath)}else{this.raw=this.raw.filter(e=>!(e.apiPath===revertPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(revertPath)}};if(path){revert(path)}for(const[key,value]of Object.entries(api)){const skipProps=["__metadata","__type","_materialize","_impl","____slothletInternal"];if(skipProps.includes(key)){continue}const childPath=path?`${path}.${key}`:key;if(typeof value==="function"||value&&typeof value==="object"){revert(childPath);if(typeof value==="object"&&!Array.isArray(value)){this.revertSpeculativeSubtree(value,moduleID,childPath,priorEntries,visited)}}}}revertSpeculativeState(moduleID,priorEntries){const currentEntries=this.raw.filter(e=>e.moduleID===moduleID);const allPaths=new Set([...currentEntries.map(e=>e.apiPath),...priorEntries.keys()]);const moduleWrappers=this.rawWrappers.get(moduleID);for(const path of allPaths){const prior=priorEntries.get(path);const currentEntry=currentEntries.find(e=>e.apiPath===path);if(!currentEntry||currentEntry.fn!==prior?.fn){moduleWrappers?.get(path)?.___invalidate();if(prior?.wrapper)moduleWrappers?.set(path,prior.wrapper);else moduleWrappers?.delete(path)}if(prior){const idx=this.raw.findIndex(e=>e.apiPath===path&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:path,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:path,moduleID,fn:prior.fn})}else{this.raw=this.raw.filter(e=>!(e.apiPath===path&&e.moduleID===moduleID))}}}async#runEntries(apiPath,entries,args=[]){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const receiver=parentPath===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,parentPath);if(receiver===void 0){return{results:[],failures:[]}}const results=[];const failures=[];const contextManager=this.slothlet.contextManager;const instanceID=this.slothlet.instanceID;const canEnterExtent=contextManager&&typeof contextManager.runInContext==="function"&&contextManager.instances?.has?.(instanceID);for(const{moduleID,fn}of entries){try{if(canEnterExtent){const wrapper=this.rawWrappers.get(moduleID)?.get(apiPath);results.push(await contextManager.runInContext(instanceID,fn,receiver,args,wrapper,true))}else{results.push(await Reflect.apply(fn,receiver,args))}}catch(error){failures.push({apiPath,moduleID,error})}}return{results,failures}}#throwAggregate(failures){const failureEntries=failures.map(({apiPath,moduleID})=>{const entry={apiPath,moduleID};Object.defineProperty(entry,"toString",{value:()=>`${apiPath} (${moduleID})`,enumerable:false});return entry});Object.defineProperty(failureEntries,"toString",{value:()=>failureEntries.map(String).join(", "),enumerable:false});throw new SlothletError("ROUTINE_FAILED",{apiPath:failures[0].apiPath,moduleID:failures[0].moduleID,count:failures.length,failures:failureEntries},failures[0].error)}async runPath(apiPath,args=[],routine=null){if(!this.slothlet.api)return void 0;const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath);const scopedEntries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;const entries=this.#applyStackFilter(scopedEntries);const{results,failures}=await this.#runEntries(apiPath,entries,args);if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#materializeTree(root){if(!root)return;const seen=new Set;const visit=async(obj,depth=0)=>{const objType=typeof obj;if(!obj||objType!=="object"&&objType!=="function"||depth>15||seen.has(obj))return;seen.add(obj);try{if(isFrameworkInternal(obj))return;const wrapper=resolveWrapper(obj);if(wrapper){if(wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return}}for(const key of Object.keys(wrapper)){if(!key.startsWith("____"))await visit(wrapper[key],depth+1)}return}for(const key of Object.keys(obj)){await visit(obj[key],depth+1)}}catch{}};const isApiRoot=root===this.slothlet.api;for(const key of Object.keys(root)){if(isApiRoot&&(key==="slothlet"||key==="shutdown"||key==="destroy"))continue;if(key.startsWith("____"))continue;await visit(root[key])}}async#materializeGlobPath(node,segments){if(node===null||node===void 0||segments.length===0)return;const nodeType=typeof node;if(nodeType!=="object"&&nodeType!=="function")return;const nodeWrapper=resolveWrapper(node);if(nodeWrapper&&nodeWrapper.____slothletInternal.mode==="lazy"&&!nodeWrapper.____slothletInternal.state.materialized){try{await nodeWrapper._materialize()}catch{return}}const[segment,...rest]=segments;if(segment==="**"){await this.#materializeTree(node);return}if(!/[*?]/.test(segment)){let child;try{child=node[segment]}catch{return}await this.#materializeGlobPath(child,rest);return}let keys;try{keys=Object.keys(node)}catch{return}const matches=this.#compile(segment);for(const key of keys){if(!matches(key))continue;let child;try{child=node[key]}catch{continue}await this.#materializeGlobPath(child,rest)}}async#materializeFor(routine){if(!this.#requiresDescent(routine))return;if(routine.name.startsWith("^")){await this.#materializeTree(this.slothlet.api);return}const ownership=this.slothlet.handlers.ownership;const endpoints=ownership?new Set(ownership.moduleEndpoints.values()):new Set;if(!routine.recursive&&!routine.name.startsWith("!")){const segmentChains=expandBraces(routine.name).map(alternative=>alternative.split("."));for(const endpoint of endpoints){const mountRoot=endpoint==="."||endpoint===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,endpoint);if(mountRoot===null||mountRoot===void 0)continue;for(const segments of segmentChains){await this.#materializeGlobPath(mountRoot,segments)}}return}if(endpoints.has(".")||endpoints.has("")){await this.#materializeTree(this.slothlet.api);return}for(const endpoint of endpoints){const mountRoot=await this.#resolveContainer(this.slothlet.api,endpoint);await this.#materializeTree(mountRoot)}}#orderPaths(apiPaths,order){if(order!=="depth")return apiPaths;return apiPaths.map((apiPath,index)=>({apiPath,index,depth:apiPath.split(".").length})).sort((a,b)=>b.depth-a.depth||a.index-b.index).map(entry=>entry.apiPath)}async runCascade(name,args=[],skipMaterialize=false){if(!this.slothlet.api)return void 0;const routine=this.#findRoutine(name);if(!routine)return void 0;if(!skipMaterialize)await this.#materializeFor(routine);const groups=this.#groupByPath(this.#contributorsFor(name));const orderedPaths=this.#orderPaths([...groups.keys()],routine.order);const results=[];const failures=[];for(const apiPath of orderedPaths){const outcome=await this.#runEntries(apiPath,groups.get(apiPath),args);results.push(outcome.results.length===1?outcome.results[0]:outcome.results);failures.push(...outcome.failures)}if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#runModeRoutines(mode){if(!this.slothlet.config?.autoRoutines)return;const routines=this.#routines.filter(routine=>routine.mode===mode);if(routines.length===0)return;for(const routine of routines){await this.#materializeFor(routine)}await this.rebuildStacks(this.slothlet.api);for(const routine of routines){await this.runCascade(routine.name,[],true)}}async runShutdownModeRoutines(){return this.#runModeRoutines("shutdown")}async runDestroyModeRoutines(){return this.#runModeRoutines("destroy")}async runStartupModeRoutines(){return this.#runModeRoutines("startup")}async#resolveContainer(api,path){if(path==="")return api;let node=api;for(const part of path.split(".")){if(node===null||node===void 0)return void 0;try{node=node[part]}catch{return void 0}const wrapper=resolveWrapper(node);if(wrapper&&wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return void 0}}}return node}#buildStackedCallable(apiPath,routine){const manager=this;const stacked=async function slothletRoutineStack(...args){return manager.runPath(apiPath,args,routine)};Object.defineProperty(stacked,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(stacked,"__slothletRoutineName",{value:routine.name,enumerable:false});return stacked}#buildCascadeCallable(name){const manager=this;const cascade=async function slothletRoutineCascade(...args){return manager.runCascade(name,args)};Object.defineProperty(cascade,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineCascade",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineName",{value:name,enumerable:false});return cascade}async rebuildStacks(api){if(!api||typeof api!=="object"&&typeof api!=="function")return;this.recording=false;try{for(const routine of this.#routines){const groups=this.#groupByPath(this.#contributorsFor(routine.name));for(const apiPath of groups.keys()){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const key=lastDot===-1?apiPath:apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))continue;if(parentPath===""&&key===routine.name)continue;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")continue;try{target[key]=this.#buildStackedCallable(apiPath,routine)}catch{}}if(ROOT_BUILTIN_NAMES.has(routine.name))continue;const cascade=this.#buildCascadeCallable(routine.name);try{api[routine.name]=cascade}catch{}}}finally{this.recording=true}}}export{RoutineManager};
17
+ import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{resolveWrapper}from"#handlers/unified-wrapper";import{isFrameworkInternal}from"#handlers/framework-internals";import{compilePattern,expandBraces}from"@cldmv/slothlet/helpers/pattern-matcher";const ROOT_BUILTIN_NAMES=new Set(["shutdown","destroy"]);class RoutineManager extends ComponentBase{static slothletProperty="routineManager";constructor(slothlet){super(slothlet);this.raw=[];this.rawWrappers=new Map;this.recording=true;this.patternCache=new Map}reset(){this.raw=[];this.rawWrappers.clear();this.patternCache.clear()}get#routines(){return this.slothlet.config?.routines??[]}#moduleWrappers(moduleID){let inner=this.rawWrappers.get(moduleID);if(!inner){inner=new Map;this.rawWrappers.set(moduleID,inner)}return inner}#findRoutine(name){return this.#routines.find(routine=>routine.name===name)}#compile(pattern){let matcher=this.patternCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.patternCache.set(pattern,matcher)}return matcher}#matches(routine,entry){const{name,recursive}=routine;if(name.startsWith("^")){return this.#compile(name.slice(1))(entry.apiPath)}const endpoint=this.slothlet.handlers.ownership?.getModuleEndpoint(entry.moduleID);if(endpoint===void 0)return false;let relative;if(endpoint==="."||endpoint==="")relative=entry.apiPath;else if(entry.apiPath===endpoint)relative="";else if(entry.apiPath.startsWith(`${endpoint}.`))relative=entry.apiPath.slice(endpoint.length+1);else return false;if(relative==="")return false;if(this.#compile(name)(relative))return true;return recursive&&this.#compile(`**.${name}`)(relative)}#requiresDescent(routine){if(routine.name.startsWith("^"))return true;if(routine.recursive)return true;return routine.name.includes(".")}#isCurrentOwner(entry){const ownership=this.slothlet.handlers.ownership;if(!ownership)return true;const owner=ownership.getCurrentOwner(entry.apiPath);if(!owner)return true;return owner.moduleID===entry.moduleID}#applyStackFilter(entries){if(this.slothlet.config?.stackRoutines)return entries;return entries.filter(entry=>this.#isCurrentOwner(entry))}#contributorsFor(name){const routine=this.#findRoutine(name);if(!routine)return[];return this.#applyStackFilter(this.raw.filter(entry=>this.#matches(routine,entry)))}#groupByPath(entries){const groups=new Map;for(const entry of entries){let group=groups.get(entry.apiPath);if(!group){group=[];groups.set(entry.apiPath,group)}group.push(entry)}return groups}onImplCreated(data){if(!this.recording)return;if(this.#routines.length===0)return;const apiPath=data?.apiPath;if(typeof apiPath!=="string"||apiPath.length===0)return;const moduleID=data.moduleID;const fn=data.wrapper?.__impl;const existingIndex=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(typeof fn==="function"&&fn.__slothletRoutineStack===true){return}if(typeof fn!=="function"){if(existingIndex!==-1)this.raw.splice(existingIndex,1);this.rawWrappers.get(moduleID)?.delete(apiPath);return}const entry={apiPath,moduleID,fn};if(existingIndex===-1)this.raw.push(entry);else this.raw[existingIndex]=entry;const wrapper=resolveWrapper(data.__wrapperRef);if(wrapper)this.#moduleWrappers(moduleID).set(apiPath,wrapper);setImmediate(()=>{this.#reactivelyPatchStack(entry).catch(()=>{})})}async#reactivelyPatchStack(entry){if(this.slothlet.____buildDepth>0)return;const api=this.slothlet.api;if(!api)return;const lastDot=entry.apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":entry.apiPath.slice(0,lastDot);const key=lastDot===-1?entry.apiPath:entry.apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))return;let winner=null;let winnerGroup=null;const matchingRoutines=[];for(const routine of this.#routines){let group;try{group=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(routine,e)))}catch{continue}if(group.length===0)continue;matchingRoutines.push(routine);winner=routine;winnerGroup=group}if(!winner)return;const isCascadeSlot=parentPath===""&&key===winner.name;const contested=matchingRoutines.length>1;if(!isCascadeSlot&&winnerGroup.length<2&&!contested)return;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")return;if(this.slothlet.____buildDepth>0)return;if(this.slothlet.api!==api)return;const targetWrapper=resolveWrapper(target);if(targetWrapper&&targetWrapper.____slothletInternal?.invalid)return;let freshWinnerGroup;try{freshWinnerGroup=this.#applyStackFilter(this.raw.filter(e=>e.apiPath===entry.apiPath&&this.#matches(winner,e)))}catch{return}if(freshWinnerGroup.length===0)return;if(!isCascadeSlot&&freshWinnerGroup.length<2&&!contested)return;if(isCascadeSlot){if(!winner.cascade)return;let current2;try{current2=target[key]}catch{return}if(!(typeof current2==="function"&&current2.__slothletRoutineCascade===true&&current2.__slothletRoutineName===winner.name)){this.recording=false;try{target[key]=this.#buildCascadeCallable(winner.name)}catch{}finally{this.recording=true}}return}let current;try{current=target[key]}catch{return}if(typeof current==="function"&&current.__slothletRoutineStack===true&&current.__slothletRoutineName===winner.name){return}this.recording=false;try{target[key]=this.#buildStackedCallable(entry.apiPath,winner)}catch{}finally{this.recording=true}}onImplRemoved(data){const apiPath=data?.apiPath;const moduleID=data?.moduleID;if(typeof apiPath!=="string"||!moduleID)return;this.raw=this.raw.filter(e=>!(e.apiPath===apiPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(apiPath)}pruneSubtree(apiPath,moduleID){const prefix=`${apiPath}.`;const matching=this.raw.filter(e=>e.moduleID===moduleID&&(e.apiPath===apiPath||e.apiPath.startsWith(prefix)));if(matching.length===0)return;this.raw=this.raw.filter(e=>!matching.includes(e));const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const entry of matching){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(moduleWrappers.get(entry.apiPath));moduleWrappers.delete(entry.apiPath)}}pruneModule(moduleID){this.raw=this.raw.filter(e=>e.moduleID!==moduleID);const moduleWrappers=this.rawWrappers.get(moduleID);if(!moduleWrappers)return;for(const wrapper of moduleWrappers.values()){this.slothlet.handlers.apiManager?.invalidateSpeculativeWrappers(wrapper)}this.rawWrappers.delete(moduleID)}snapshotRawEntries(moduleID){const snapshot=new Map;const moduleWrappers=this.rawWrappers.get(moduleID);this.raw.forEach((entry,index)=>{if(entry.moduleID===moduleID)snapshot.set(entry.apiPath,{fn:entry.fn,index,wrapper:moduleWrappers?.get(entry.apiPath)})});return snapshot}snapshotRawEntry(apiPath,moduleID){const index=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(index===-1)return void 0;return{fn:this.raw[index].fn,index,wrapper:this.rawWrappers.get(moduleID)?.get(apiPath)}}revertRawEntry(apiPath,moduleID,priorEntry){const idx=this.raw.findIndex(e=>e.apiPath===apiPath&&e.moduleID===moduleID);if(priorEntry!==void 0){const entry={apiPath,moduleID,fn:priorEntry.fn};if(idx!==-1)this.raw[idx]=entry;else this.raw.splice(Math.min(priorEntry.index,this.raw.length),0,entry);if(priorEntry.wrapper)this.#moduleWrappers(moduleID).set(apiPath,priorEntry.wrapper);else this.rawWrappers.get(moduleID)?.delete(apiPath)}else{if(idx!==-1)this.raw.splice(idx,1);this.rawWrappers.get(moduleID)?.delete(apiPath)}}revertSpeculativeSubtree(api,moduleID,path,priorEntries,visited=new WeakSet){if(!api||typeof api!=="object"&&typeof api!=="function")return;if(visited.has(api)){return}visited.add(api);const revert=revertPath=>{const prior=priorEntries.get(revertPath);if(prior){const idx=this.raw.findIndex(e=>e.apiPath===revertPath&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:revertPath,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:revertPath,moduleID,fn:prior.fn});if(prior.wrapper)this.#moduleWrappers(moduleID).set(revertPath,prior.wrapper);else this.rawWrappers.get(moduleID)?.delete(revertPath)}else{this.raw=this.raw.filter(e=>!(e.apiPath===revertPath&&e.moduleID===moduleID));this.rawWrappers.get(moduleID)?.delete(revertPath)}};if(path){revert(path)}for(const[key,value]of Object.entries(api)){const skipProps=["__metadata","__type","_materialize","_impl","____slothletInternal"];if(skipProps.includes(key)){continue}const childPath=path?`${path}.${key}`:key;if(typeof value==="function"||value&&typeof value==="object"){revert(childPath);if(typeof value==="object"&&!Array.isArray(value)){this.revertSpeculativeSubtree(value,moduleID,childPath,priorEntries,visited)}}}}revertSpeculativeState(moduleID,priorEntries){const currentEntries=this.raw.filter(e=>e.moduleID===moduleID);const allPaths=new Set([...currentEntries.map(e=>e.apiPath),...priorEntries.keys()]);const moduleWrappers=this.rawWrappers.get(moduleID);for(const path of allPaths){const prior=priorEntries.get(path);const currentEntry=currentEntries.find(e=>e.apiPath===path);if(!currentEntry||currentEntry.fn!==prior?.fn){moduleWrappers?.get(path)?.___invalidate();if(prior?.wrapper)moduleWrappers?.set(path,prior.wrapper);else moduleWrappers?.delete(path)}if(prior){const idx=this.raw.findIndex(e=>e.apiPath===path&&e.moduleID===moduleID);if(idx!==-1)this.raw[idx]={apiPath:path,moduleID,fn:prior.fn};else this.raw.splice(Math.min(prior.index,this.raw.length),0,{apiPath:path,moduleID,fn:prior.fn})}else{this.raw=this.raw.filter(e=>!(e.apiPath===path&&e.moduleID===moduleID))}}}async#runEntries(apiPath,entries,args=[]){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const receiver=parentPath===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,parentPath);if(receiver===void 0){return{results:[],failures:[]}}const results=[];const failures=[];const contextManager=this.slothlet.contextManager;const instanceID=this.slothlet.instanceID;const canEnterExtent=contextManager&&typeof contextManager.runInContext==="function"&&contextManager.instances?.has?.(instanceID);for(const{moduleID,fn}of entries){try{if(canEnterExtent){const wrapper=this.rawWrappers.get(moduleID)?.get(apiPath);results.push(await contextManager.runInContext(instanceID,fn,receiver,args,wrapper,true))}else{results.push(await Reflect.apply(fn,receiver,args))}}catch(error){failures.push({apiPath,moduleID,error})}}return{results,failures}}#throwAggregate(failures){const failureEntries=failures.map(({apiPath,moduleID})=>{const entry={apiPath,moduleID};Object.defineProperty(entry,"toString",{value:()=>`${apiPath} (${moduleID})`,enumerable:false});return entry});Object.defineProperty(failureEntries,"toString",{value:()=>failureEntries.map(String).join(", "),enumerable:false});throw new SlothletError("ROUTINE_FAILED",{apiPath:failures[0].apiPath,moduleID:failures[0].moduleID,count:failures.length,failures:failureEntries},failures[0].error)}async runPath(apiPath,args=[],routine=null){if(!this.slothlet.api)return void 0;const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath);const scopedEntries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;const entries=this.#applyStackFilter(scopedEntries);const{results,failures}=await this.#runEntries(apiPath,entries,args);if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async runPathFor(apiPath,key,args=[],routine=null){if(!this.slothlet.api)return void 0;const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath&&entry.moduleID===key);const entries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;if(entries.length===0){const available=this.contributorsAt(apiPath,routine);throw new SlothletError("INVALID_ARGUMENT",{argument:"key",expected:`a moduleID contributing "${routine?.name??"the routine"}" at "${apiPath}"${available.length?` (contributors: ${available.join(", ")})`:" (no contributors at this path)"}`,received:String(key),validationError:true},null,{validationError:true})}const{results,failures}=await this.#runEntries(apiPath,entries,args);if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}contributorsAt(apiPath,routine=null){const pathEntries=this.raw.filter(entry=>entry.apiPath===apiPath);const entries=routine?pathEntries.filter(entry=>this.#matches(routine,entry)):pathEntries;return entries.map(entry=>entry.moduleID)}async#materializeTree(root){if(!root)return;const seen=new Set;const visit=async(obj,depth=0)=>{const objType=typeof obj;if(!obj||objType!=="object"&&objType!=="function"||depth>15||seen.has(obj))return;seen.add(obj);try{if(isFrameworkInternal(obj))return;const wrapper=resolveWrapper(obj);if(wrapper){if(wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return}}for(const key of Object.keys(wrapper)){if(!key.startsWith("____"))await visit(wrapper[key],depth+1)}return}for(const key of Object.keys(obj)){await visit(obj[key],depth+1)}}catch{}};const isApiRoot=root===this.slothlet.api;for(const key of Object.keys(root)){if(isApiRoot&&(key==="slothlet"||key==="shutdown"||key==="destroy"))continue;if(key.startsWith("____"))continue;await visit(root[key])}}async#materializeGlobPath(node,segments){if(node===null||node===void 0||segments.length===0)return;const nodeType=typeof node;if(nodeType!=="object"&&nodeType!=="function")return;const nodeWrapper=resolveWrapper(node);if(nodeWrapper&&nodeWrapper.____slothletInternal.mode==="lazy"&&!nodeWrapper.____slothletInternal.state.materialized){try{await nodeWrapper._materialize()}catch{return}}const[segment,...rest]=segments;if(segment==="**"){await this.#materializeTree(node);return}if(!/[*?]/.test(segment)){let child;try{child=node[segment]}catch{return}await this.#materializeGlobPath(child,rest);return}let keys;try{keys=Object.keys(node)}catch{return}const matches=this.#compile(segment);for(const key of keys){if(!matches(key))continue;let child;try{child=node[key]}catch{continue}await this.#materializeGlobPath(child,rest)}}async#materializeFor(routine){if(!this.#requiresDescent(routine))return;if(routine.name.startsWith("^")){await this.#materializeTree(this.slothlet.api);return}const ownership=this.slothlet.handlers.ownership;const endpoints=ownership?new Set(ownership.moduleEndpoints.values()):new Set;if(!routine.recursive&&!routine.name.startsWith("!")){const segmentChains=expandBraces(routine.name).map(alternative=>alternative.split("."));for(const endpoint of endpoints){const mountRoot=endpoint==="."||endpoint===""?this.slothlet.api:await this.#resolveContainer(this.slothlet.api,endpoint);if(mountRoot===null||mountRoot===void 0)continue;for(const segments of segmentChains){await this.#materializeGlobPath(mountRoot,segments)}}return}if(endpoints.has(".")||endpoints.has("")){await this.#materializeTree(this.slothlet.api);return}for(const endpoint of endpoints){const mountRoot=await this.#resolveContainer(this.slothlet.api,endpoint);await this.#materializeTree(mountRoot)}}#orderPaths(apiPaths,order){if(order!=="depth")return apiPaths;return apiPaths.map((apiPath,index)=>({apiPath,index,depth:apiPath.split(".").length})).sort((a,b)=>b.depth-a.depth||a.index-b.index).map(entry=>entry.apiPath)}async runCascade(name,args=[],skipMaterialize=false){if(!this.slothlet.api)return void 0;const routine=this.#findRoutine(name);if(!routine)return void 0;if(!skipMaterialize)await this.#materializeFor(routine);const groups=this.#groupByPath(this.#contributorsFor(name));const orderedPaths=this.#orderPaths([...groups.keys()],routine.order);const results=[];const failures=[];for(const apiPath of orderedPaths){const outcome=await this.#runEntries(apiPath,groups.get(apiPath),args);results.push(outcome.results.length===1?outcome.results[0]:outcome.results);failures.push(...outcome.failures)}if(failures.length>0)this.#throwAggregate(failures);return results.length===1?results[0]:results}async#runModeRoutines(mode){if(!this.slothlet.config?.autoRoutines)return;const routines=this.#routines.filter(routine=>routine.mode===mode);if(routines.length===0)return;for(const routine of routines){await this.#materializeFor(routine)}await this.rebuildStacks(this.slothlet.api);for(const routine of routines){await this.runCascade(routine.name,[],true)}}async runShutdownModeRoutines(){return this.#runModeRoutines("shutdown")}async runDestroyModeRoutines(){return this.#runModeRoutines("destroy")}async runStartupModeRoutines(){return this.#runModeRoutines("startup")}async#resolveContainer(api,path){if(path==="")return api;let node=api;for(const part of path.split(".")){if(node===null||node===void 0)return void 0;try{node=node[part]}catch{return void 0}const wrapper=resolveWrapper(node);if(wrapper&&wrapper.____slothletInternal.mode==="lazy"&&!wrapper.____slothletInternal.state.materialized){try{await wrapper._materialize()}catch{return void 0}}}return node}#buildStackedCallable(apiPath,routine){const manager=this;const stacked=async function slothletRoutineStack(...args){return manager.runPath(apiPath,args,routine)};Object.defineProperty(stacked,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(stacked,"__slothletRoutineName",{value:routine.name,enumerable:false});Object.defineProperty(stacked,"for",{value:key=>async function slothletRoutineFor(...args){return manager.runPathFor(apiPath,key,args,routine)},enumerable:false});Object.defineProperty(stacked,"contributors",{get:()=>manager.contributorsAt(apiPath,routine),enumerable:false});return stacked}#buildCascadeCallable(name){const manager=this;const cascade=async function slothletRoutineCascade(...args){return manager.runCascade(name,args)};Object.defineProperty(cascade,"__slothletRoutineStack",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineCascade",{value:true,enumerable:false});Object.defineProperty(cascade,"__slothletRoutineName",{value:name,enumerable:false});return cascade}async rebuildStacks(api){if(!api||typeof api!=="object"&&typeof api!=="function")return;this.recording=false;try{for(const routine of this.#routines){const groups=this.#groupByPath(this.#contributorsFor(routine.name));for(const apiPath of groups.keys()){const lastDot=apiPath.lastIndexOf(".");const parentPath=lastDot===-1?"":apiPath.slice(0,lastDot);const key=lastDot===-1?apiPath:apiPath.slice(lastDot+1);if(parentPath===""&&ROOT_BUILTIN_NAMES.has(key))continue;if(parentPath===""&&key===routine.name)continue;const target=await this.#resolveContainer(api,parentPath);if(target===null||target===void 0||typeof target!=="object"&&typeof target!=="function")continue;try{target[key]=this.#buildStackedCallable(apiPath,routine)}catch{}}if(ROOT_BUILTIN_NAMES.has(routine.name))continue;if(!routine.cascade)continue;const cascade=this.#buildCascadeCallable(routine.name);try{api[routine.name]=cascade}catch{}}}finally{this.recording=true}}}export{RoutineManager};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{isNode as IS_NODE}from"@cldmv/slothlet/helpers/platform";import{DEFAULT_API_DEPTH,DEFAULT_ROUTINES}from"@cldmv/slothlet/helpers/defaults";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";const VALID_ROUTINE_MODES=new Set(["manual","startup","shutdown","destroy"]);const VALID_ROUTINE_ORDERS=new Set(["mount","depth"]);const DEFAULT_ROUTINE_ORDER_BY_MODE=Object.freeze({startup:"mount",shutdown:"depth",destroy:"depth",manual:"mount"});const COLLECT_LIFECYCLE_HOOKS_IMPLICIT_ROUTINES=Object.freeze([Object.freeze({name:"^**.shutdown",mode:"shutdown",order:"depth"}),Object.freeze({name:"^**.destroy",mode:"destroy",order:"depth"})]);const ROUTINE_NAME_RESERVED=new Set(["slothlet","__proto__","constructor","prototype"]);function normalizeHookConfig(hook){const hookConfig={enabled:false,pattern:"**",suppressErrors:false,pin:true};if(hook===true||hook===false){hookConfig.enabled=hook;hookConfig.pattern=hook?"**":null}else if(typeof hook==="string"){hookConfig.enabled=true;hookConfig.pattern=hook}else if(hook&&typeof hook==="object"){hookConfig.enabled=hook.enabled!==false;hookConfig.pattern=hook.pattern||"**";hookConfig.suppressErrors=hook.suppressErrors||false;hookConfig.pin=hook.pin!==false}return hookConfig}class Config extends ComponentBase{static slothletProperty="config";normalizeCollision(collision){const validModes=["skip","warn","replace","merge","merge-replace","error"];const defaultMode="merge";if(typeof collision==="string"){const normalized=collision.toLowerCase();const mode=validModes.includes(normalized)?normalized:defaultMode;return{initial:mode,api:mode}}if(collision&&typeof collision==="object"){const validateMode=m=>{if(!m)return defaultMode;const normalized=String(m).toLowerCase();return validModes.includes(normalized)?normalized:defaultMode};return{initial:validateMode(collision.initial),api:validateMode(collision.api)}}return{initial:defaultMode,api:defaultMode}}normalizeRuntime(runtime){if(!runtime||typeof runtime!=="string"){return"async"}const normalized=runtime.toLowerCase().trim();if(normalized==="async"||normalized==="asynclocal"||normalized==="asynclocalstorage"){return"async"}if(normalized==="live"||normalized==="livebindings"||normalized==="experimental"){return"live"}return"async"}normalizeMode(mode){if(!mode||typeof mode!=="string"){return"eager"}const normalized=mode.toLowerCase().trim();if(normalized==="lazy"||normalized==="deferred"||normalized==="proxy"){return"lazy"}if(normalized==="eager"||normalized==="immediate"||normalized==="preload"){return"eager"}return"eager"}normalizeMutations(mutations){const defaults={add:true,remove:true,reload:true,permissions:true};if(!mutations||typeof mutations!=="object"){return defaults}return{add:mutations.add===false?false:true,remove:mutations.remove===false?false:true,reload:mutations.reload===false?false:true,permissions:mutations.permissions===false?false:true}}normalizeDebug(debug){if(!debug){return{builder:false,api:false,index:false,modes:false,wrapper:false,ownership:false,context:false,initialization:false,materialize:false,versioning:false,permissions:false}}if(debug===true){return{builder:true,api:true,index:true,modes:true,wrapper:true,ownership:true,context:true,initialization:true,materialize:true,versioning:true,permissions:true}}if(typeof debug==="object"){return{builder:debug.builder||false,api:debug.api||false,index:debug.index||false,modes:debug.modes||false,wrapper:debug.wrapper||false,ownership:debug.ownership||false,context:debug.context||false,initialization:debug.initialization||false,materialize:debug.materialize||false,versioning:debug.versioning||false,permissions:debug.permissions||false}}return{builder:false,api:false,index:false,modes:false,wrapper:false,ownership:false,context:false,initialization:false,materialize:false,versioning:false,permissions:false}}normalizeEnvTarget(platform,hasManifest=false){if(platform==="browser")return"browser";if(platform==="node")return"node";if(hasManifest)return"browser";return IS_NODE?"node":"browser"}normalizeHook(hook){return normalizeHookConfig(hook)}transformConfig(config={}){const hasManifest=config.manifest!=null;const envTarget=this.normalizeEnvTarget(config.platform,hasManifest);const rawBase=config.base??config.dir;if(config.dir!==void 0&&config.base===void 0&&!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"dir",replacement:"base"})}if(!rawBase){throw new this.SlothletError("INVALID_CONFIG_DIR_MISSING",{},null,{validationError:true})}if(envTarget==="browser"){if(!config.manifest||typeof config.manifest!=="object"||Array.isArray(config.manifest)){throw new this.SlothletError("INVALID_CONFIG_BROWSER_REQUIRES_MANIFEST",{},null,{validationError:true})}if(!Array.isArray(config.manifest.files)||!Array.isArray(config.manifest.directories)){throw new this.SlothletError("INVALID_CONFIG_BROWSER_MANIFEST_INVALID",{received:typeof config.manifest},null,{validationError:true})}if(config.resolveModuleSpecifier!==void 0&&config.resolveModuleSpecifier!==null&&typeof config.resolveModuleSpecifier!=="function"){throw new this.SlothletError("INVALID_CONFIG_BROWSER_RESOLVE_SPECIFIER_INVALID",{received:typeof config.resolveModuleSpecifier},null,{validationError:true})}}const resolvedDir=envTarget==="browser"?rawBase:this.slothlet.helpers.resolver.resolvePathFromCaller(rawBase);let mutations=null;if(config.allowMutation===false){mutations={add:false,remove:false,reload:false};if(!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"allowMutation",replacement:"api.mutations: { add: false, remove: false, reload: false }"})}}let collision=null;if(config.collision&&!config.api?.collision){collision=this.normalizeCollision(config.collision);if(!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"collision",replacement:"api.collision"})}}const apiConfig=config.api||{};const finalCollision=apiConfig.collision?this.normalizeCollision(apiConfig.collision):collision||this.normalizeCollision(null);const finalMutations=apiConfig.mutations?this.normalizeMutations(apiConfig.mutations):mutations||this.normalizeMutations(null);let scopeConfig=config.scope;if(scopeConfig&&typeof scopeConfig==="object"&&scopeConfig.merge){const validMergeStrategies=["shallow","deep"];if(!validMergeStrategies.includes(scopeConfig.merge)){throw new this.SlothletError("INVALID_CONFIG",{option:"scope.merge",value:scopeConfig.merge,expected:validMergeStrategies.join(" or "),hint:`Invalid merge strategy: "${scopeConfig.merge}". Must be "shallow" or "deep".`,validationError:true},null,{validationError:true})}}const hookConfig=this.normalizeHook(config.hook);let trackingConfig={materialization:false};if(config.tracking===true||config.tracking===false){trackingConfig.materialization=config.tracking}else if(config.tracking&&typeof config.tracking==="object"){trackingConfig.materialization=config.tracking.materialization===true}if(config.backgroundMaterialize===true){trackingConfig.materialization=true}if(config.import!==void 0&&config.import!==null&&typeof config.import!=="function"){throw new this.SlothletError("INVALID_CONFIG_IMPORT",{received:typeof config.import,validationError:true})}if(config.versionDispatcher!==void 0&&config.versionDispatcher!==null){if(typeof config.versionDispatcher!=="string"&&typeof config.versionDispatcher!=="function"){throw new this.SlothletError("INVALID_CONFIG_VERSION_DISPATCHER",{received:typeof config.versionDispatcher,validationError:true})}}const permissionsConfig=this.normalizePermissions(config.permissions);const suppressFixes=this.normalizeSuppressFixes(config.suppressFixes,config.silent);let i18nConfig=null;if(config.i18n&&typeof config.i18n==="object"){i18nConfig={language:typeof config.i18n.language==="string"?config.i18n.language:void 0}}const lifecycleConfig=this.normalizeLifecycle(config.lifecycle);if(config.collectLifecycleHooks!==void 0&&config.autoRoutines===void 0&&!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"collectLifecycleHooks",replacement:"autoRoutines"})}const autoRoutines=config.autoRoutines!==void 0?config.autoRoutines===true:config.collectLifecycleHooks===true;const normalizedRoutines=this.normalizeRoutines(config.routines);const routines=config.collectLifecycleHooks===true?[...normalizedRoutines.filter(routine=>routine.mode!=="shutdown"&&routine.mode!=="destroy"),...COLLECT_LIFECYCLE_HOOKS_IMPLICIT_ROUTINES.filter(implicit=>!normalizedRoutines.some(routine=>routine.name===implicit.name)).map(implicit=>({...implicit,recursive:false}))]:normalizedRoutines;return{...config,base:resolvedDir,dir:resolvedDir,manifest:config.manifest??null,resolveModuleSpecifier:config.resolveModuleSpecifier??null,envTarget,mode:this.normalizeMode(config.mode),runtime:this.normalizeRuntime(config.runtime),apiDepth:config.apiDepth!==void 0?config.apiDepth:DEFAULT_API_DEPTH,reference:config.reference||null,context:config.context||null,i18n:i18nConfig,lifecycle:lifecycleConfig,routines,debug:this.normalizeDebug(config.debug),diagnostics:config.diagnostics===true,collectLifecycleHooks:config.collectLifecycleHooks===true,autoRoutines,stackRoutines:config.stackRoutines===true,hook:hookConfig,collision:finalCollision,api:{collision:finalCollision,mutations:finalMutations},scope:scopeConfig,tracking:trackingConfig,backgroundMaterialize:config.backgroundMaterialize===true,silent:config.silent===true,typescript:this.normalizeTypeScript(config.typescript),env:this.normalizeEnv(config.env),versionDispatcher:config.versionDispatcher??null,import:config.import??null,permissions:permissionsConfig,suppressFixes}}normalizeSuppressFixes(suppressFixes,silent){const KNOWN_FIX_IDS=new Set(["C03_116"]);const REPO_PR_BASE="https://github.com/CLDMV/slothlet/pull/";if(!Array.isArray(suppressFixes)||suppressFixes.length===0){return new Set}const result=new Set;for(const rule of suppressFixes){if(typeof rule!=="string"||!KNOWN_FIX_IDS.has(rule)){continue}result.add(rule);if(!silent){const prNumber=rule.split("_").pop();const url=`${REPO_PR_BASE}${prNumber}`;new this.SlothletWarning("WARN_SUPPRESS_FIX_ACTIVE",{rule,url})}}return result}normalizeTypeScript(typescript){if(!typescript){return null}if(typescript===true){return{enabled:true,mode:"fast"}}if(typeof typescript==="string"){const mode=typescript.toLowerCase();if(mode==="fast"||mode==="strict"){return{enabled:true,mode}}return{enabled:true,mode:"fast"}}if(typeof typescript==="object"){const mode=typescript.mode==="strict"?"strict":"fast";return{enabled:true,mode,types:typescript.types||null,target:typescript.target||"es2020",sourcemap:typescript.sourcemap||false}}return null}normalizeEnv(env){if(!env||typeof env!=="object"){return null}const include=Array.isArray(env.include)?env.include.filter(k=>typeof k==="string"):null;if(include&&include.length>0){return{include}}return null}normalizeLifecycle(lifecycle){if(lifecycle===void 0||lifecycle===null){return null}if(typeof lifecycle!=="object"||Array.isArray(lifecycle)){throw new this.SlothletError("INVALID_CONFIG",{option:"lifecycle",value:Array.isArray(lifecycle)?"array":typeof lifecycle,expected:"a plain object mapping event names to functions or arrays of functions",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const lifecycleProto=Object.getPrototypeOf(lifecycle);if(lifecycleProto!==null&&lifecycleProto!==Object.prototype){throw new this.SlothletError("INVALID_CONFIG",{option:"lifecycle",value:lifecycle?.constructor?.name?`${lifecycle.constructor.name} instance`:"non-plain object",expected:"a plain object mapping event names to functions or arrays of functions",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}for(const[event,handler]of Object.entries(lifecycle)){const handlers=Array.isArray(handler)?handler:[handler];for(const fn of handlers){if(typeof fn!=="function"){throw new this.SlothletError("INVALID_CONFIG",{option:`lifecycle["${event}"]`,value:typeof fn,expected:"a function or an array of functions",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}}}return lifecycle}normalizeRoutines(routines){if(routines===void 0){return DEFAULT_ROUTINES.map(entry=>({name:entry.name,mode:entry.mode,recursive:entry.recursive??false,order:entry.order??DEFAULT_ROUTINE_ORDER_BY_MODE[entry.mode]}))}if(routines===null){return[]}if(!Array.isArray(routines)){throw new this.SlothletError("INVALID_CONFIG",{option:"routines",value:typeof routines,expected:'an array of routine names/objects, e.g. ["initialize", "shutdown:shutdown", { name: "warmup" }]',hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}return routines.map((entry,index)=>{let name;let mode;if(typeof entry==="string"){const sepIndex=entry.indexOf(":");if(sepIndex===-1){name=entry;mode="manual"}else{name=entry.slice(0,sepIndex);mode=entry.slice(sepIndex+1)}}else if(entry&&typeof entry==="object"&&!Array.isArray(entry)){name=entry.name;mode=entry.mode??"manual"}else{throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}]`,value:Array.isArray(entry)?"array":typeof entry,expected:'a string ("name" or "name:mode") or an object ({ name, mode? })',hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}if(typeof name!=="string"||name.length===0){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].name`,value:typeof name,expected:"a non-empty string",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}if(ROUTINE_NAME_RESERVED.has(name)){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].name`,value:name,expected:`a name other than the reserved: ${[...ROUTINE_NAME_RESERVED].join(", ")}`,hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}if(!VALID_ROUTINE_MODES.has(mode)){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].mode`,value:String(mode),expected:`one of: ${[...VALID_ROUTINE_MODES].join(", ")}`,hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const recursive=typeof entry==="object"?entry.recursive??false:false;if(typeof recursive!=="boolean"){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].recursive`,value:typeof recursive,expected:"a boolean",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const pattern=name.startsWith("^")?name.slice(1):name;if(pattern.length===0){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].name`,value:name,expected:'a root-anchored name with a pattern after the `^` (a bare "^" matches nothing)',hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}try{compilePattern(pattern)}catch(error){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].name`,value:name,expected:`a name that compiles as a valid glob pattern (see helpers/pattern-matcher.mjs): ${error.message}`,hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const order=typeof entry==="object"&&entry.order!==void 0?entry.order:DEFAULT_ROUTINE_ORDER_BY_MODE[mode];if(!VALID_ROUTINE_ORDERS.has(order)){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].order`,value:String(order),expected:`one of: ${[...VALID_ROUTINE_ORDERS].join(", ")}`,hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}return{name,mode,recursive,order}})}normalizePermissions(permissions){if(!permissions||typeof permissions!=="object"){return null}let defaultPolicy;if(permissions.defaultPolicy==="deny"){defaultPolicy="deny"}else if(permissions.defaultPolicy==="allow"||permissions.defaultPolicy===void 0){defaultPolicy="allow"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.defaultPolicy",value:permissions.defaultPolicy,expected:'"allow" or "deny"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const enabled=permissions.enabled!==false;let audit;if(permissions.audit==="verbose"){audit="verbose"}else if(permissions.audit==="default"||permissions.audit===void 0){audit="default"}else if(permissions.audit===true){audit="default"}else if(permissions.audit===false){audit="default"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.audit",value:permissions.audit,expected:'"default" or "verbose"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let readGating;if(permissions.readGating===false){readGating=false}else if(permissions.readGating===true||permissions.readGating===void 0){readGating=true}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.readGating",value:permissions.readGating,expected:"boolean",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let failOpenOnAbsentCaller;if(permissions.failOpenOnAbsentCaller===true){failOpenOnAbsentCaller=true}else if(permissions.failOpenOnAbsentCaller===false||permissions.failOpenOnAbsentCaller===void 0){failOpenOnAbsentCaller=false}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.failOpenOnAbsentCaller",value:permissions.failOpenOnAbsentCaller,expected:"boolean",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}if(permissions.references!==void 0&&(typeof permissions.references!=="object"||permissions.references===null||Array.isArray(permissions.references))){throw new SlothletError("INVALID_CONFIG",{option:"permissions.references",value:permissions.references,expected:"object",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let capture;if(permissions.references?.capture===false){capture=false}else if(permissions.references?.capture===true||permissions.references?.capture===void 0){capture=true}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.references.capture",value:permissions.references.capture,expected:"boolean",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}if(permissions.rules!==void 0&&!Array.isArray(permissions.rules)){throw new SlothletError("INVALID_CONFIG",{option:"permissions.rules",value:permissions.rules,expected:"array",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const rules=Array.isArray(permissions.rules)?permissions.rules:[];if(permissions.private!==void 0&&(typeof permissions.private!=="object"||permissions.private===null||Array.isArray(permissions.private))){throw new SlothletError("INVALID_CONFIG",{option:"permissions.private",value:permissions.private,expected:"object",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let privateHost;if(permissions.private?.host==="allow"){privateHost="allow"}else if(permissions.private?.host==="deny"||permissions.private?.host===void 0){privateHost="deny"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.private.host",value:permissions.private.host,expected:'"deny" or "allow"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}return{defaultPolicy,enabled,audit,readGating,failOpenOnAbsentCaller,references:{capture},private:{host:privateHost},rules}}}export{Config,normalizeHookConfig};
17
+ import{ComponentBase}from"#factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{isNode as IS_NODE}from"@cldmv/slothlet/helpers/platform";import{DEFAULT_API_DEPTH,DEFAULT_ROUTINES}from"@cldmv/slothlet/helpers/defaults";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";const VALID_ROUTINE_MODES=new Set(["manual","startup","shutdown","destroy"]);const VALID_ROUTINE_ORDERS=new Set(["mount","depth"]);const DEFAULT_ROUTINE_ORDER_BY_MODE=Object.freeze({startup:"mount",shutdown:"depth",destroy:"depth",manual:"mount"});const COLLECT_LIFECYCLE_HOOKS_IMPLICIT_ROUTINES=Object.freeze([Object.freeze({name:"^**.shutdown",mode:"shutdown",order:"depth"}),Object.freeze({name:"^**.destroy",mode:"destroy",order:"depth"})]);const ROUTINE_NAME_RESERVED=new Set(["slothlet","__proto__","constructor","prototype"]);function normalizeHookConfig(hook){const hookConfig={enabled:false,pattern:"**",suppressErrors:false,pin:true};if(hook===true||hook===false){hookConfig.enabled=hook;hookConfig.pattern=hook?"**":null}else if(typeof hook==="string"){hookConfig.enabled=true;hookConfig.pattern=hook}else if(hook&&typeof hook==="object"){hookConfig.enabled=hook.enabled!==false;hookConfig.pattern=hook.pattern||"**";hookConfig.suppressErrors=hook.suppressErrors||false;hookConfig.pin=hook.pin!==false}return hookConfig}class Config extends ComponentBase{static slothletProperty="config";normalizeCollision(collision){const validModes=["skip","warn","replace","merge","merge-replace","error"];const defaultMode="merge";if(typeof collision==="string"){const normalized=collision.toLowerCase();const mode=validModes.includes(normalized)?normalized:defaultMode;return{initial:mode,api:mode}}if(collision&&typeof collision==="object"){const validateMode=m=>{if(!m)return defaultMode;const normalized=String(m).toLowerCase();return validModes.includes(normalized)?normalized:defaultMode};return{initial:validateMode(collision.initial),api:validateMode(collision.api)}}return{initial:defaultMode,api:defaultMode}}normalizeRuntime(runtime){if(!runtime||typeof runtime!=="string"){return"async"}const normalized=runtime.toLowerCase().trim();if(normalized==="async"||normalized==="asynclocal"||normalized==="asynclocalstorage"){return"async"}if(normalized==="live"||normalized==="livebindings"||normalized==="experimental"){return"live"}return"async"}normalizeMode(mode){if(!mode||typeof mode!=="string"){return"eager"}const normalized=mode.toLowerCase().trim();if(normalized==="lazy"||normalized==="deferred"||normalized==="proxy"){return"lazy"}if(normalized==="eager"||normalized==="immediate"||normalized==="preload"){return"eager"}return"eager"}normalizeMutations(mutations){const defaults={add:true,remove:true,reload:true,permissions:true,events:true,allowCollisionOverride:false};if(!mutations||typeof mutations!=="object"){return defaults}return{add:mutations.add===false?false:true,remove:mutations.remove===false?false:true,reload:mutations.reload===false?false:true,permissions:mutations.permissions===false?false:true,events:mutations.events===false?false:true,allowCollisionOverride:mutations.allowCollisionOverride===true?true:false}}normalizeDebug(debug){if(!debug){return{builder:false,api:false,index:false,modes:false,wrapper:false,ownership:false,context:false,initialization:false,materialize:false,versioning:false,permissions:false}}if(debug===true){return{builder:true,api:true,index:true,modes:true,wrapper:true,ownership:true,context:true,initialization:true,materialize:true,versioning:true,permissions:true}}if(typeof debug==="object"){return{builder:debug.builder||false,api:debug.api||false,index:debug.index||false,modes:debug.modes||false,wrapper:debug.wrapper||false,ownership:debug.ownership||false,context:debug.context||false,initialization:debug.initialization||false,materialize:debug.materialize||false,versioning:debug.versioning||false,permissions:debug.permissions||false}}return{builder:false,api:false,index:false,modes:false,wrapper:false,ownership:false,context:false,initialization:false,materialize:false,versioning:false,permissions:false}}normalizeEnvTarget(platform,hasManifest=false){if(platform==="browser")return"browser";if(platform==="node")return"node";if(hasManifest)return"browser";return IS_NODE?"node":"browser"}normalizeHook(hook){return normalizeHookConfig(hook)}transformConfig(config={}){const hasManifest=config.manifest!=null;const envTarget=this.normalizeEnvTarget(config.platform,hasManifest);const rawBase=config.base??config.dir;if(config.dir!==void 0&&config.base===void 0&&!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"dir",replacement:"base"})}if(!rawBase){throw new this.SlothletError("INVALID_CONFIG_DIR_MISSING",{},null,{validationError:true})}if(envTarget==="browser"){if(!config.manifest||typeof config.manifest!=="object"||Array.isArray(config.manifest)){throw new this.SlothletError("INVALID_CONFIG_BROWSER_REQUIRES_MANIFEST",{},null,{validationError:true})}if(!Array.isArray(config.manifest.files)||!Array.isArray(config.manifest.directories)){throw new this.SlothletError("INVALID_CONFIG_BROWSER_MANIFEST_INVALID",{received:typeof config.manifest},null,{validationError:true})}if(config.resolveModuleSpecifier!==void 0&&config.resolveModuleSpecifier!==null&&typeof config.resolveModuleSpecifier!=="function"){throw new this.SlothletError("INVALID_CONFIG_BROWSER_RESOLVE_SPECIFIER_INVALID",{received:typeof config.resolveModuleSpecifier},null,{validationError:true})}}const resolvedDir=envTarget==="browser"?rawBase:this.slothlet.helpers.resolver.resolvePathFromCaller(rawBase);let mutations=null;if(config.allowMutation===false){mutations={add:false,remove:false,reload:false};if(!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"allowMutation",replacement:"api.mutations: { add: false, remove: false, reload: false }"})}}let collision=null;if(config.collision&&!config.api?.collision){collision=this.normalizeCollision(config.collision);if(!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"collision",replacement:"api.collision"})}}const apiConfig=config.api||{};const finalCollision=apiConfig.collision?this.normalizeCollision(apiConfig.collision):collision||this.normalizeCollision(null);const finalMutations=apiConfig.mutations?this.normalizeMutations(apiConfig.mutations):mutations||this.normalizeMutations(null);let scopeConfig=config.scope;if(scopeConfig&&typeof scopeConfig==="object"&&scopeConfig.merge){const validMergeStrategies=["shallow","deep"];if(!validMergeStrategies.includes(scopeConfig.merge)){throw new this.SlothletError("INVALID_CONFIG",{option:"scope.merge",value:scopeConfig.merge,expected:validMergeStrategies.join(" or "),hint:`Invalid merge strategy: "${scopeConfig.merge}". Must be "shallow" or "deep".`,validationError:true},null,{validationError:true})}}const hookConfig=this.normalizeHook(config.hook);let trackingConfig={materialization:false};if(config.tracking===true||config.tracking===false){trackingConfig.materialization=config.tracking}else if(config.tracking&&typeof config.tracking==="object"){trackingConfig.materialization=config.tracking.materialization===true}if(config.backgroundMaterialize===true){trackingConfig.materialization=true}if(config.import!==void 0&&config.import!==null&&typeof config.import!=="function"){throw new this.SlothletError("INVALID_CONFIG_IMPORT",{received:typeof config.import,validationError:true})}if(config.versionDispatcher!==void 0&&config.versionDispatcher!==null){if(typeof config.versionDispatcher!=="string"&&typeof config.versionDispatcher!=="function"){throw new this.SlothletError("INVALID_CONFIG_VERSION_DISPATCHER",{received:typeof config.versionDispatcher,validationError:true})}}const permissionsConfig=this.normalizePermissions(config.permissions);const suppressFixes=this.normalizeSuppressFixes(config.suppressFixes,config.silent);let i18nConfig=null;if(config.i18n&&typeof config.i18n==="object"){i18nConfig={language:typeof config.i18n.language==="string"?config.i18n.language:void 0}}const lifecycleConfig=this.normalizeLifecycle(config.lifecycle);if(config.collectLifecycleHooks!==void 0&&config.autoRoutines===void 0&&!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"collectLifecycleHooks",replacement:"autoRoutines"})}const autoRoutines=config.autoRoutines!==void 0?config.autoRoutines===true:config.collectLifecycleHooks===true;const normalizedRoutines=this.normalizeRoutines(config.routines);const routines=config.collectLifecycleHooks===true?[...normalizedRoutines.filter(routine=>routine.mode!=="shutdown"&&routine.mode!=="destroy"),...COLLECT_LIFECYCLE_HOOKS_IMPLICIT_ROUTINES.filter(implicit=>!normalizedRoutines.some(routine=>routine.name===implicit.name)).map(implicit=>({...implicit,recursive:false}))]:normalizedRoutines;return{...config,base:resolvedDir,dir:resolvedDir,manifest:config.manifest??null,resolveModuleSpecifier:config.resolveModuleSpecifier??null,envTarget,mode:this.normalizeMode(config.mode),runtime:this.normalizeRuntime(config.runtime),apiDepth:config.apiDepth!==void 0?config.apiDepth:DEFAULT_API_DEPTH,reference:config.reference||null,context:config.context||null,i18n:i18nConfig,lifecycle:lifecycleConfig,routines,debug:this.normalizeDebug(config.debug),diagnostics:config.diagnostics===true,collectLifecycleHooks:config.collectLifecycleHooks===true,autoRoutines,stackRoutines:config.stackRoutines===true,hook:hookConfig,collision:finalCollision,api:{collision:finalCollision,mutations:finalMutations},scope:scopeConfig,tracking:trackingConfig,backgroundMaterialize:config.backgroundMaterialize===true,silent:config.silent===true,typescript:this.normalizeTypeScript(config.typescript),env:this.normalizeEnv(config.env),versionDispatcher:config.versionDispatcher??null,import:config.import??null,permissions:permissionsConfig,suppressFixes}}normalizeSuppressFixes(suppressFixes,silent){const KNOWN_FIX_IDS=new Set(["C03_116"]);const REPO_PR_BASE="https://github.com/CLDMV/slothlet/pull/";if(!Array.isArray(suppressFixes)||suppressFixes.length===0){return new Set}const result=new Set;for(const rule of suppressFixes){if(typeof rule!=="string"||!KNOWN_FIX_IDS.has(rule)){continue}result.add(rule);if(!silent){const prNumber=rule.split("_").pop();const url=`${REPO_PR_BASE}${prNumber}`;new this.SlothletWarning("WARN_SUPPRESS_FIX_ACTIVE",{rule,url})}}return result}normalizeTypeScript(typescript){if(!typescript){return null}if(typescript===true){return{enabled:true,mode:"fast"}}if(typeof typescript==="string"){const mode=typescript.toLowerCase();if(mode==="fast"||mode==="strict"){return{enabled:true,mode}}return{enabled:true,mode:"fast"}}if(typeof typescript==="object"){const mode=typescript.mode==="strict"?"strict":"fast";return{enabled:true,mode,types:typescript.types||null,target:typescript.target||"es2020",sourcemap:typescript.sourcemap||false}}return null}normalizeEnv(env){if(!env||typeof env!=="object"){return null}const include=Array.isArray(env.include)?env.include.filter(k=>typeof k==="string"):null;if(include&&include.length>0){return{include}}return null}normalizeLifecycle(lifecycle){if(lifecycle===void 0||lifecycle===null){return null}if(typeof lifecycle!=="object"||Array.isArray(lifecycle)){throw new this.SlothletError("INVALID_CONFIG",{option:"lifecycle",value:Array.isArray(lifecycle)?"array":typeof lifecycle,expected:"a plain object mapping event names to functions or arrays of functions",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const lifecycleProto=Object.getPrototypeOf(lifecycle);if(lifecycleProto!==null&&lifecycleProto!==Object.prototype){throw new this.SlothletError("INVALID_CONFIG",{option:"lifecycle",value:lifecycle?.constructor?.name?`${lifecycle.constructor.name} instance`:"non-plain object",expected:"a plain object mapping event names to functions or arrays of functions",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}for(const[event,handler]of Object.entries(lifecycle)){const handlers=Array.isArray(handler)?handler:[handler];for(const fn of handlers){if(typeof fn!=="function"){throw new this.SlothletError("INVALID_CONFIG",{option:`lifecycle["${event}"]`,value:typeof fn,expected:"a function or an array of functions",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}}}return lifecycle}normalizeRoutines(routines){if(routines===void 0){return DEFAULT_ROUTINES.map(entry=>({name:entry.name,mode:entry.mode,recursive:entry.recursive??false,order:entry.order??DEFAULT_ROUTINE_ORDER_BY_MODE[entry.mode],cascade:entry.cascade??true}))}if(routines===null){return[]}if(!Array.isArray(routines)){throw new this.SlothletError("INVALID_CONFIG",{option:"routines",value:typeof routines,expected:'an array of routine names/objects, e.g. ["initialize", "shutdown:shutdown", { name: "warmup" }]',hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}return routines.map((entry,index)=>{let name;let mode;if(typeof entry==="string"){const sepIndex=entry.indexOf(":");if(sepIndex===-1){name=entry;mode="manual"}else{name=entry.slice(0,sepIndex);mode=entry.slice(sepIndex+1)}}else if(entry&&typeof entry==="object"&&!Array.isArray(entry)){name=entry.name;mode=entry.mode??"manual"}else{throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}]`,value:Array.isArray(entry)?"array":typeof entry,expected:'a string ("name" or "name:mode") or an object ({ name, mode? })',hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}if(typeof name!=="string"||name.length===0){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].name`,value:typeof name,expected:"a non-empty string",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}if(ROUTINE_NAME_RESERVED.has(name)){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].name`,value:name,expected:`a name other than the reserved: ${[...ROUTINE_NAME_RESERVED].join(", ")}`,hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}if(!VALID_ROUTINE_MODES.has(mode)){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].mode`,value:String(mode),expected:`one of: ${[...VALID_ROUTINE_MODES].join(", ")}`,hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const recursive=typeof entry==="object"?entry.recursive??false:false;if(typeof recursive!=="boolean"){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].recursive`,value:typeof recursive,expected:"a boolean",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const pattern=name.startsWith("^")?name.slice(1):name;if(pattern.length===0){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].name`,value:name,expected:'a root-anchored name with a pattern after the `^` (a bare "^" matches nothing)',hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}try{compilePattern(pattern)}catch(error){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].name`,value:name,expected:`a name that compiles as a valid glob pattern (see helpers/pattern-matcher.mjs): ${error.message}`,hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const order=typeof entry==="object"&&entry.order!==void 0?entry.order:DEFAULT_ROUTINE_ORDER_BY_MODE[mode];if(!VALID_ROUTINE_ORDERS.has(order)){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].order`,value:String(order),expected:`one of: ${[...VALID_ROUTINE_ORDERS].join(", ")}`,hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}const cascade=typeof entry==="object"&&entry.cascade!==void 0?entry.cascade:true;if(typeof cascade!=="boolean"){throw new this.SlothletError("INVALID_CONFIG",{option:`routines[${index}].cascade`,value:typeof cascade,expected:"a boolean",hint:"HINT_INVALID_CONFIG",validationError:true},null,{validationError:true})}return{name,mode,recursive,order,cascade}})}normalizePermissions(permissions){if(!permissions||typeof permissions!=="object"){return null}let defaultPolicy;if(permissions.defaultPolicy==="deny"){defaultPolicy="deny"}else if(permissions.defaultPolicy==="allow"||permissions.defaultPolicy===void 0){defaultPolicy="allow"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.defaultPolicy",value:permissions.defaultPolicy,expected:'"allow" or "deny"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const enabled=permissions.enabled!==false;let audit;if(permissions.audit==="verbose"){audit="verbose"}else if(permissions.audit==="default"||permissions.audit===void 0){audit="default"}else if(permissions.audit===true){audit="default"}else if(permissions.audit===false){audit="default"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.audit",value:permissions.audit,expected:'"default" or "verbose"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let readGating;if(permissions.readGating===false){readGating=false}else if(permissions.readGating===true||permissions.readGating===void 0){readGating=true}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.readGating",value:permissions.readGating,expected:"boolean",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let failOpenOnAbsentCaller;if(permissions.failOpenOnAbsentCaller===true){failOpenOnAbsentCaller=true}else if(permissions.failOpenOnAbsentCaller===false||permissions.failOpenOnAbsentCaller===void 0){failOpenOnAbsentCaller=false}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.failOpenOnAbsentCaller",value:permissions.failOpenOnAbsentCaller,expected:"boolean",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}if(permissions.references!==void 0&&(typeof permissions.references!=="object"||permissions.references===null||Array.isArray(permissions.references))){throw new SlothletError("INVALID_CONFIG",{option:"permissions.references",value:permissions.references,expected:"object",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let capture;if(permissions.references?.capture===false){capture=false}else if(permissions.references?.capture===true||permissions.references?.capture===void 0){capture=true}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.references.capture",value:permissions.references.capture,expected:"boolean",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}if(permissions.rules!==void 0&&!Array.isArray(permissions.rules)){throw new SlothletError("INVALID_CONFIG",{option:"permissions.rules",value:permissions.rules,expected:"array",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const rules=Array.isArray(permissions.rules)?permissions.rules:[];if(permissions.private!==void 0&&(typeof permissions.private!=="object"||permissions.private===null||Array.isArray(permissions.private))){throw new SlothletError("INVALID_CONFIG",{option:"permissions.private",value:permissions.private,expected:"object",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let privateHost;if(permissions.private?.host==="allow"){privateHost="allow"}else if(permissions.private?.host==="deny"||permissions.private?.host===void 0){privateHost="deny"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.private.host",value:permissions.private.host,expected:'"deny" or "allow"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}if(permissions.events!==void 0&&(typeof permissions.events!=="object"||permissions.events===null||Array.isArray(permissions.events))){throw new SlothletError("INVALID_CONFIG",{option:"permissions.events",value:permissions.events,expected:"object",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let eventDefault;if(permissions.events?.default===void 0){eventDefault="notify"}else if(permissions.events.default==="deny"||permissions.events.default==="notify"||permissions.events.default==="allow"){eventDefault=permissions.events.default}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.events.default",value:permissions.events.default,expected:'"deny", "notify", or "allow"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}if(permissions.events?.rules!==void 0&&!Array.isArray(permissions.events.rules)){throw new SlothletError("INVALID_CONFIG",{option:"permissions.events.rules",value:permissions.events.rules,expected:"array",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const eventRules=Array.isArray(permissions.events?.rules)?permissions.events.rules:[];return{defaultPolicy,enabled,audit,readGating,failOpenOnAbsentCaller,references:{capture},private:{host:privateHost},rules,events:{default:eventDefault,rules:eventRules}}}}export{Config,normalizeHookConfig};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{SlothletError}from"@cldmv/slothlet/errors";import{t}from"@cldmv/slothlet/i18n";import{path}from"@cldmv/slothlet/helpers/platform";const RESERVED_MOUNTPATH_ROOTS=Object.freeze(new Set(["slothlet","shutdown","destroy"]));const ALLOWED_TOP_LEVEL_FIELDS=Object.freeze(new Set(["schemaVersion","name","version","description","mountPath","apiDir","kind","priority","dependencies","permissions","metadata"]));const PERMISSION_EFFECT_VALUES=Object.freeze(new Set(["allow","deny"]));function validateModuleManifest(manifest,packageContext){if(!manifest||typeof manifest!=="object"||Array.isArray(manifest)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName:packageContext?.packageName??"<unknown>",manifestPath:packageContext?.manifestPath??"<unknown>",reason:t("MODULE_MANIFEST_REASON_NOT_OBJECT")},null,{validationError:true})}const{packageName,packageVersion,packageDescription,packageRoot,manifestPath}=packageContext;for(const key of Object.keys(manifest)){if(!ALLOWED_TOP_LEVEL_FIELDS.has(key)){throw new SlothletError("MODULE_MANIFEST_UNKNOWN_FIELD",{packageName,manifestPath,field:key},null,{validationError:true})}}if(manifest.schemaVersion===void 0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MISSING_FIELD",{field:"schemaVersion"})},null,{validationError:true})}if(manifest.schemaVersion!==1){throw new SlothletError("MODULE_VERSION_UNSUPPORTED",{packageName,schemaVersion:String(manifest.schemaVersion)},null,{validationError:true})}if(manifest.mountPath===void 0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MISSING_FIELD",{field:"mountPath"})},null,{validationError:true})}const mountPathSegments=normalizeMountPath(manifest.mountPath,packageName,manifestPath);if(RESERVED_MOUNTPATH_ROOTS.has(mountPathSegments[0])){throw new SlothletError("MODULE_RESERVED_MOUNTPATH",{packageName,mountPathRoot:mountPathSegments[0],mountPath:mountPathSegments.join(".")},null,{validationError:true})}if(manifest.apiDir===void 0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MISSING_FIELD",{field:"apiDir"})},null,{validationError:true})}if(typeof manifest.apiDir!=="string"||manifest.apiDir.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"apiDir",expected:t("EXPECTED_NON_EMPTY_STRING")})},null,{validationError:true})}validateApiDirContainment(manifest.apiDir,packageRoot,packageName);if(manifest.name!==void 0){if(typeof manifest.name!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"name",expected:t("EXPECTED_STRING")})},null,{validationError:true})}if(manifest.name!==packageName){throw new SlothletError("MODULE_MANIFEST_NAME_MISMATCH",{packageName,manifestName:manifest.name},null,{validationError:true})}}if(manifest.version!==void 0){if(typeof manifest.version!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"version",expected:t("EXPECTED_STRING")})},null,{validationError:true})}if(manifest.version!==packageVersion){throw new SlothletError("MODULE_MANIFEST_VERSION_MISMATCH",{packageName,manifestVersion:manifest.version,packageVersion},null,{validationError:true})}}if(manifest.description!==void 0&&typeof manifest.description!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"description",expected:t("EXPECTED_STRING")})},null,{validationError:true})}if(manifest.kind!==void 0&&typeof manifest.kind!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"kind",expected:t("EXPECTED_STRING")})},null,{validationError:true})}if(manifest.priority!==void 0&&(typeof manifest.priority!=="number"||!Number.isFinite(manifest.priority))){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"priority",expected:t("EXPECTED_FINITE_NUMBER")})},null,{validationError:true})}if(manifest.dependencies!==void 0){if(typeof manifest.dependencies!=="object"||manifest.dependencies===null||Array.isArray(manifest.dependencies)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"dependencies",expected:t("EXPECTED_PLAIN_OBJECT")})},null,{validationError:true})}for(const[depName,depValue]of Object.entries(manifest.dependencies)){if(typeof depValue!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_DEPENDENCY_TYPE",{dependency:depName})},null,{validationError:true})}}}if(manifest.permissions!==void 0){validatePermissions(manifest.permissions,packageName,manifestPath)}if(manifest.metadata!==void 0){if(typeof manifest.metadata!=="object"||manifest.metadata===null||Array.isArray(manifest.metadata)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"metadata",expected:t("EXPECTED_PLAIN_OBJECT")})},null,{validationError:true})}}return{schemaVersion:1,name:packageName,version:packageVersion,description:manifest.description??packageDescription,mountPath:mountPathSegments,apiDir:manifest.apiDir,kind:manifest.kind,priority:manifest.priority??0,dependencies:manifest.dependencies,permissions:manifest.permissions,metadata:manifest.metadata}}function normalizeMountPath(mountPath,packageName,manifestPath){if(typeof mountPath==="string"){if(mountPath.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MOUNTPATH_SHAPE")},null,{validationError:true})}return mountPath.split(".")}if(Array.isArray(mountPath)){if(mountPath.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MOUNTPATH_SHAPE")},null,{validationError:true})}for(const segment of mountPath){if(typeof segment!=="string"||segment.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MOUNTPATH_ENTRIES")},null,{validationError:true})}}return mountPath.slice()}throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MOUNTPATH_SHAPE")},null,{validationError:true})}function validateApiDirContainment(apiDir,packageRoot,packageName){const resolved=path.resolve(packageRoot,apiDir);const rootWithSep=packageRoot.endsWith(path.sep)?packageRoot:packageRoot+path.sep;if(resolved!==packageRoot&&!resolved.startsWith(rootWithSep)){throw new SlothletError("MODULE_PATH_TRAVERSAL",{packageName,apiDir,resolvedPath:resolved,packageRoot},null,{validationError:true})}}function validatePermissions(permissions,packageName,manifestPath){if(!Array.isArray(permissions)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"permissions",expected:t("EXPECTED_ARRAY_OF_RULE_OBJECTS")})},null,{validationError:true})}for(let i=0;i<permissions.length;i++){const rule=permissions[i];if(!rule||typeof rule!=="object"||Array.isArray(rule)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_RULE",{index:i})},null,{validationError:true})}if(typeof rule.caller!=="string"||rule.caller.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_CALLER",{index:i})},null,{validationError:true})}if(typeof rule.target!=="string"||rule.target.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_TARGET",{index:i})},null,{validationError:true})}if(!PERMISSION_EFFECT_VALUES.has(rule.effect)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_EFFECT",{index:i})},null,{validationError:true})}}}export{validateModuleManifest};
17
+ import{SlothletError}from"@cldmv/slothlet/errors";import{t}from"@cldmv/slothlet/i18n";import{path}from"@cldmv/slothlet/helpers/platform";const RESERVED_MOUNTPATH_ROOTS=Object.freeze(new Set(["slothlet","shutdown","destroy"]));const ALLOWED_TOP_LEVEL_FIELDS=Object.freeze(new Set(["schemaVersion","name","version","description","mountPath","apiDir","kind","priority","dependencies","permissions","events","metadata"]));const PERMISSION_EFFECT_VALUES=Object.freeze(new Set(["allow","deny"]));const EVENT_EFFECT_VALUES=Object.freeze(new Set(["deny","notify","allow"]));function validateModuleManifest(manifest,packageContext){if(!manifest||typeof manifest!=="object"||Array.isArray(manifest)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName:packageContext?.packageName??"<unknown>",manifestPath:packageContext?.manifestPath??"<unknown>",reason:t("MODULE_MANIFEST_REASON_NOT_OBJECT")},null,{validationError:true})}const{packageName,packageVersion,packageDescription,packageRoot,manifestPath}=packageContext;for(const key of Object.keys(manifest)){if(!ALLOWED_TOP_LEVEL_FIELDS.has(key)){throw new SlothletError("MODULE_MANIFEST_UNKNOWN_FIELD",{packageName,manifestPath,field:key},null,{validationError:true})}}if(manifest.schemaVersion===void 0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MISSING_FIELD",{field:"schemaVersion"})},null,{validationError:true})}if(manifest.schemaVersion!==1){throw new SlothletError("MODULE_VERSION_UNSUPPORTED",{packageName,schemaVersion:String(manifest.schemaVersion)},null,{validationError:true})}if(manifest.mountPath===void 0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MISSING_FIELD",{field:"mountPath"})},null,{validationError:true})}const mountPathSegments=normalizeMountPath(manifest.mountPath,packageName,manifestPath);if(RESERVED_MOUNTPATH_ROOTS.has(mountPathSegments[0])){throw new SlothletError("MODULE_RESERVED_MOUNTPATH",{packageName,mountPathRoot:mountPathSegments[0],mountPath:mountPathSegments.join(".")},null,{validationError:true})}if(manifest.apiDir===void 0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MISSING_FIELD",{field:"apiDir"})},null,{validationError:true})}if(typeof manifest.apiDir!=="string"||manifest.apiDir.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"apiDir",expected:t("EXPECTED_NON_EMPTY_STRING")})},null,{validationError:true})}validateApiDirContainment(manifest.apiDir,packageRoot,packageName);if(manifest.name!==void 0){if(typeof manifest.name!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"name",expected:t("EXPECTED_STRING")})},null,{validationError:true})}if(manifest.name!==packageName){throw new SlothletError("MODULE_MANIFEST_NAME_MISMATCH",{packageName,manifestName:manifest.name},null,{validationError:true})}}if(manifest.version!==void 0){if(typeof manifest.version!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"version",expected:t("EXPECTED_STRING")})},null,{validationError:true})}if(manifest.version!==packageVersion){throw new SlothletError("MODULE_MANIFEST_VERSION_MISMATCH",{packageName,manifestVersion:manifest.version,packageVersion},null,{validationError:true})}}if(manifest.description!==void 0&&typeof manifest.description!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"description",expected:t("EXPECTED_STRING")})},null,{validationError:true})}if(manifest.kind!==void 0&&typeof manifest.kind!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"kind",expected:t("EXPECTED_STRING")})},null,{validationError:true})}if(manifest.priority!==void 0&&(typeof manifest.priority!=="number"||!Number.isFinite(manifest.priority))){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"priority",expected:t("EXPECTED_FINITE_NUMBER")})},null,{validationError:true})}if(manifest.dependencies!==void 0){if(typeof manifest.dependencies!=="object"||manifest.dependencies===null||Array.isArray(manifest.dependencies)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"dependencies",expected:t("EXPECTED_PLAIN_OBJECT")})},null,{validationError:true})}for(const[depName,depValue]of Object.entries(manifest.dependencies)){if(typeof depValue!=="string"){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_DEPENDENCY_TYPE",{dependency:depName})},null,{validationError:true})}}}if(manifest.permissions!==void 0){validatePermissions(manifest.permissions,packageName,manifestPath)}if(manifest.events!==void 0){validateEventRules(manifest.events,packageName,manifestPath)}if(manifest.metadata!==void 0){if(typeof manifest.metadata!=="object"||manifest.metadata===null||Array.isArray(manifest.metadata)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"metadata",expected:t("EXPECTED_PLAIN_OBJECT")})},null,{validationError:true})}}return{schemaVersion:1,name:packageName,version:packageVersion,description:manifest.description??packageDescription,mountPath:mountPathSegments,apiDir:manifest.apiDir,kind:manifest.kind,priority:manifest.priority??0,dependencies:manifest.dependencies,permissions:manifest.permissions,events:manifest.events,metadata:manifest.metadata}}function normalizeMountPath(mountPath,packageName,manifestPath){if(typeof mountPath==="string"){if(mountPath.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MOUNTPATH_SHAPE")},null,{validationError:true})}return mountPath.split(".")}if(Array.isArray(mountPath)){if(mountPath.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MOUNTPATH_SHAPE")},null,{validationError:true})}for(const segment of mountPath){if(typeof segment!=="string"||segment.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MOUNTPATH_ENTRIES")},null,{validationError:true})}}return mountPath.slice()}throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_MOUNTPATH_SHAPE")},null,{validationError:true})}function validateApiDirContainment(apiDir,packageRoot,packageName){const resolved=path.resolve(packageRoot,apiDir);const rootWithSep=packageRoot.endsWith(path.sep)?packageRoot:packageRoot+path.sep;if(resolved!==packageRoot&&!resolved.startsWith(rootWithSep)){throw new SlothletError("MODULE_PATH_TRAVERSAL",{packageName,apiDir,resolvedPath:resolved,packageRoot},null,{validationError:true})}}function validatePermissions(permissions,packageName,manifestPath){if(!Array.isArray(permissions)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"permissions",expected:t("EXPECTED_ARRAY_OF_RULE_OBJECTS")})},null,{validationError:true})}for(let i=0;i<permissions.length;i++){const rule=permissions[i];if(!rule||typeof rule!=="object"||Array.isArray(rule)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_RULE",{index:i})},null,{validationError:true})}if(typeof rule.caller!=="string"||rule.caller.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_CALLER",{index:i})},null,{validationError:true})}if(typeof rule.target!=="string"||rule.target.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_TARGET",{index:i})},null,{validationError:true})}if(!PERMISSION_EFFECT_VALUES.has(rule.effect)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_EFFECT",{index:i})},null,{validationError:true})}}}function validateEventRules(events,packageName,manifestPath){if(!Array.isArray(events)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_FIELD_TYPE",{field:"events",expected:t("EXPECTED_ARRAY_OF_RULE_OBJECTS")})},null,{validationError:true})}for(let i=0;i<events.length;i++){const rule=events[i];if(!rule||typeof rule!=="object"||Array.isArray(rule)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_RULE",{index:i})},null,{validationError:true})}if(typeof rule.caller!=="string"||rule.caller.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_PERMISSION_CALLER",{index:i})},null,{validationError:true})}if(typeof rule.event!=="string"||rule.event.length===0){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_EVENT_NAME",{index:i})},null,{validationError:true})}if(!EVENT_EFFECT_VALUES.has(rule.effect)){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_EVENT_EFFECT",{index:i})},null,{validationError:true})}}}export{validateModuleManifest};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{SlothletError}from"@cldmv/slothlet/errors";function compilePattern(pattern,options={}){const isNegation=pattern.startsWith("!");if(isNegation){pattern=pattern.slice(1);const matcher=compilePattern(pattern,options);return path=>!matcher(path)}const expanded=expandBraces(pattern,0,10,options);if(expanded.length>1){const matchers=expanded.map(p=>compilePattern(p,options));return path=>matchers.some(m=>m(path))}pattern=expanded[0];let regexPattern=pattern.replace(/[.+^$()|[\]\\]/g,"\\$&").replace(/\*\*/g,"__DOUBLESTAR__").replace(/\*/g,"[^.]*").replace(/__DOUBLESTAR__/g,".*").replace(/\?/g,".");regexPattern=`^${regexPattern}$`;const regex=new RegExp(regexPattern);return path=>regex.test(path)}function expandBraces(pattern,depth=0,maxDepth=10,options={}){if(depth>=maxDepth){if(options.onMaxDepth){options.onMaxDepth(maxDepth)}throw new SlothletError("BRACE_EXPANSION_MAX_DEPTH",{maxDepth,validationError:true})}const braceStart=pattern.indexOf("{");if(braceStart===-1){return[pattern]}let braceEnd=-1;let depthCount=1;for(let i=braceStart+1;i<pattern.length;i++){if(pattern[i]==="{")depthCount++;if(pattern[i]==="}"){depthCount--;if(depthCount===0){braceEnd=i;break}}}if(braceEnd===-1){return[pattern]}const prefix=pattern.slice(0,braceStart);const braceContent=pattern.slice(braceStart+1,braceEnd);const suffix=pattern.slice(braceEnd+1);const alternatives=splitBraceAlternatives(braceContent);const expanded=[];for(const alt of alternatives){const combined=prefix+alt+suffix;const recursiveExpanded=expandBraces(combined,depth+1,maxDepth,options);expanded.push(...recursiveExpanded)}return expanded}function splitBraceAlternatives(content){const alternatives=[];let current="";let depth=0;for(let i=0;i<content.length;i++){const char=content[i];if(char==="{"){depth++;current+=char}else if(char==="}"){depth--;current+=char}else if(char===","&&depth===0){alternatives.push(current);current=""}else{current+=char}}if(current){alternatives.push(current)}return alternatives}export{compilePattern,expandBraces,splitBraceAlternatives};
17
+ import{SlothletError}from"@cldmv/slothlet/errors";function compilePattern(pattern,options={}){const isNegation=pattern.startsWith("!")&&!pattern.startsWith("!(");if(isNegation){pattern=pattern.slice(1);const matcher=compilePattern(pattern,options);return path=>!matcher(path)}const expanded=expandBraces(pattern,0,10,options);if(expanded.length>1){const matchers=expanded.map(p=>compilePattern(p,options));return path=>matchers.some(m=>m(path))}pattern=expanded[0];const extglobs=[];pattern=pattern.replace(/!\(([^()]*)\)/g,(_match,body)=>{const alts=body.split("|").filter(alt=>alt.length>0).map(alt=>alt.replace(/[.+^$()|[\]\\*?{}]/g,"\\$&"));const guard=alts.length?`(?!(?:${alts.join("|")})(?:\\.|$))`:"";const token=`__EXTGLOB_${extglobs.length}__`;extglobs.push(`${guard}[^.]+`);return token});let regexPattern=pattern.replace(/[.+^$()|[\]\\]/g,"\\$&").replace(/\*\*/g,"__DOUBLESTAR__").replace(/\*/g,"[^.]*").replace(/__DOUBLESTAR__/g,".*").replace(/\?/g,".");extglobs.forEach((re,i)=>{regexPattern=regexPattern.replace(`__EXTGLOB_${i}__`,()=>re)});regexPattern=`^${regexPattern}$`;const regex=new RegExp(regexPattern);return path=>regex.test(path)}function expandBraces(pattern,depth=0,maxDepth=10,options={}){if(depth>=maxDepth){if(options.onMaxDepth){options.onMaxDepth(maxDepth)}throw new SlothletError("BRACE_EXPANSION_MAX_DEPTH",{maxDepth,validationError:true})}const braceStart=pattern.indexOf("{");if(braceStart===-1){return[pattern]}let braceEnd=-1;let depthCount=1;for(let i=braceStart+1;i<pattern.length;i++){if(pattern[i]==="{")depthCount++;if(pattern[i]==="}"){depthCount--;if(depthCount===0){braceEnd=i;break}}}if(braceEnd===-1){return[pattern]}const prefix=pattern.slice(0,braceStart);const braceContent=pattern.slice(braceStart+1,braceEnd);const suffix=pattern.slice(braceEnd+1);const alternatives=splitBraceAlternatives(braceContent);const expanded=[];for(const alt of alternatives){const combined=prefix+alt+suffix;const recursiveExpanded=expandBraces(combined,depth+1,maxDepth,options);expanded.push(...recursiveExpanded)}return expanded}function splitBraceAlternatives(content){const alternatives=[];let current="";let depth=0;for(let i=0;i<content.length;i++){const char=content[i];if(char==="{"){depth++;current+=char}else if(char==="}"){depth--;current+=char}else if(char===","&&depth===0){alternatives.push(current);current=""}else{current+=char}}if(current){alternatives.push(current)}return alternatives}export{compilePattern,expandBraces,splitBraceAlternatives};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"#factories/component-base";class Sanitize extends ComponentBase{static slothletProperty="sanitize";#compileGlobPattern(pattern,caseSensitive=true){if(pattern.startsWith("**")&&pattern.endsWith("**")&&pattern.length>4){const innerString=pattern.slice(2,-2);const escapedString=innerString.replace(/[.+^${}()|[\]\\*?]/g,"\\$&");const flags2=caseSensitive?"":"i";return new RegExp(`(?<=.)${escapedString}(?=.)`,flags2)}const regexPattern=pattern.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".");const flags=caseSensitive?"":"i";return new RegExp(`^${regexPattern}$`,flags)}#matchesAnyPattern(input,patterns,caseSensitive=false){for(const pattern of patterns){if(pattern.includes("*")||pattern.includes("?")){const regex=this.#compileGlobPattern(pattern,caseSensitive);if(regex&&regex.test(input))return true}else{const match=caseSensitive?input===pattern:input.toLowerCase()===pattern.toLowerCase();if(match)return true}}return false}#extractPatternLiterals(pattern){return pattern.split(/[*?]+/).filter(Boolean)}#applySegmentRules(segment,index,originalString,config){const{preserveAllUpper,preserveAllLower,leaveRules,leaveInsensitiveRules,upperRules,lowerRules}=config;if(this.#matchesAnyPattern(segment,leaveRules,true)){return segment}if(this.#matchesAnyPattern(segment,leaveInsensitiveRules,false)){return segment}if(preserveAllUpper&&segment===segment.toUpperCase()&&segment!==segment.toLowerCase()&&/[A-Z]/.test(segment)){return segment}if(preserveAllLower&&segment===segment.toLowerCase()&&segment!==segment.toUpperCase()&&/[a-z]/.test(segment)){return segment}for(const pattern of[...upperRules,...lowerRules]){if(pattern.includes("*")||pattern.includes("?")){const regex=this.#compileGlobPattern(pattern,false);if(regex&&regex.test(originalString)){const literals=this.#extractPatternLiterals(pattern);for(const literal of literals){const cleanLiteral=literal.replace(/[^A-Za-z0-9_$]/g,"").replace(/^_+|_+$/g,"");if(cleanLiteral&&segment.toLowerCase()===cleanLiteral.toLowerCase()){return upperRules.includes(pattern)?segment.toUpperCase():segment.toLowerCase()}}}}else{if(segment.toLowerCase()===pattern.toLowerCase()){return upperRules.includes(pattern)?segment.toUpperCase():segment.toLowerCase()}}}let transformed=this.#applyWithinSegmentPatterns(segment,upperRules,lowerRules);if(transformed!==segment){return transformed}return segment}#applyWithinSegmentPatterns(segment,upperRules,lowerRules){let result=segment;const applyBoundaryPattern=(pattern,toUpper)=>{if(pattern.startsWith("**")&&pattern.endsWith("**")&&pattern.length>4){const innerString=pattern.slice(2,-2);const innerRegex=new RegExp(innerString.replace(/[.+^${}()|[\]\\*?]/g,"\\$&"),"gi");const matches=[...result.matchAll(innerRegex)];for(const match of matches){const startPos=match.index;const endPos=startPos+match[0].length;const hasCharBefore=startPos>0;const hasCharAfter=endPos<result.length;if(hasCharBefore&&hasCharAfter){const replacement=toUpper?innerString.toUpperCase():innerString.toLowerCase();result=result.substring(0,startPos)+replacement+result.substring(endPos);break}}}else if(pattern.includes("*")&&!pattern.startsWith("**")){const literalParts=pattern.split("*").filter(Boolean);for(const literal of literalParts){const literalRegex=new RegExp(literal.replace(/[.+^${}()|[\]\\]/g,"\\$&"),"gi");const replacement=toUpper?literal.toUpperCase():literal.toLowerCase();result=result.replace(literalRegex,replacement)}}};upperRules.forEach(pattern=>applyBoundaryPattern(pattern,true));lowerRules.forEach(pattern=>applyBoundaryPattern(pattern,false));return result}sanitizePropertyName(input,options={}){const{lowerFirst=true,preserveAllUpper=false,preserveAllLower=false,rules={}}=options;const leaveRules=(rules.leave||[]).map(s=>String(s));const leaveInsensitiveRules=(rules.leaveInsensitive||[]).map(s=>String(s));const upperRules=(rules.upper||[]).map(s=>String(s));const lowerRules=(rules.lower||[]).map(s=>String(s));const originalString=String(input).trim();const isAllUpper=originalString===originalString.toUpperCase()&&originalString!==originalString.toLowerCase()&&/[A-Z]/.test(originalString);const isAllLower=originalString===originalString.toLowerCase()&&originalString!==originalString.toUpperCase()&&/[a-z]/.test(originalString);if(preserveAllUpper&&isAllUpper){return originalString}if(preserveAllLower&&isAllLower&&!/-/.test(originalString)){return originalString}let primarySegments=originalString.split(/[-]+|[^A-Za-z0-9_$]+/).filter(Boolean);if(primarySegments.length===0)return"_";while(primarySegments.length&&!/^[A-Za-z_$]/.test(primarySegments[0][0])){primarySegments[0]=primarySegments[0].replace(/^[^A-Za-z_$]+/,"");if(!primarySegments[0])primarySegments.shift()}if(primarySegments.length===0)return"_";const lowerRuleApplied=[];const processedPrimarySegments=primarySegments.map((primarySeg,primaryIdx)=>{const parts=primarySeg.split(/(_+)/);const processedParts=parts.map((part,partIdx)=>{if(partIdx%2===1)return part;if(!part)return part;const cleanSeg=part.replace(/[^A-Za-z0-9_$]/g,"");const config={preserveAllUpper,preserveAllLower,leaveRules,leaveInsensitiveRules,upperRules,lowerRules};const result2=this.#applySegmentRules(cleanSeg,0,originalString,config);const matchesLower=lowerRules.some(pattern=>{if(pattern.includes("*")||pattern.includes("?")){const regex=this.#compileGlobPattern(pattern,false);if(regex&&regex.test(originalString)){const literals=this.#extractPatternLiterals(pattern);for(const literal of literals){const cleanLiteral=literal.replace(/[^A-Za-z0-9_$]/g,"").replace(/^_+|_+$/g,"");if(cleanLiteral&&cleanSeg.toLowerCase()===cleanLiteral.toLowerCase()){return true}}}}else{if(cleanSeg.toLowerCase()===pattern.toLowerCase()){return true}}return false});if(matchesLower&&result2===cleanSeg.toLowerCase()){lowerRuleApplied[primaryIdx]=true}return result2});return processedParts.join("")});const camelCasedSegments=processedPrimarySegments.map((seg,idx)=>{const matchesLeave=this.#matchesAnyPattern(seg,leaveRules,true);const matchesLeaveInsensitive=this.#matchesAnyPattern(seg,leaveInsensitiveRules,false);const matchesUpper=this.#matchesAnyPattern(seg,upperRules,false);const hasUnderscores=seg.includes("_");const isAllUpper2=!hasUnderscores&&preserveAllUpper&&seg===seg.toUpperCase()&&seg!==seg.toLowerCase()&&/[A-Z]/.test(seg);const isAllLower2=!hasUnderscores&&preserveAllLower&&seg===seg.toLowerCase()&&seg!==seg.toUpperCase()&&/[a-z]/.test(seg);if(matchesLeave||matchesLeaveInsensitive||matchesUpper||isAllUpper2||isAllLower2){return seg}let transformed;if(idx===0){transformed=lowerFirst?seg[0].toLowerCase()+seg.slice(1):seg}else{if(lowerRuleApplied[idx]){transformed=seg}else{transformed=seg[0].toUpperCase()+seg.slice(1)}}return transformed});let result=camelCasedSegments.join("");result=result.replace(/[^A-Za-z0-9_$]/g,"");return result}getModuleId(filePath,baseDir){let relative=filePath.replace(baseDir,"").replace(/\\/g,"/");relative=relative.replace(/^\//,"");relative=relative.replace(/\.(mjs|cjs|js)$/,"");return relative}shouldPreserveFunctionCase(name){const preservePatterns=[/^[A-Z]{2,}$/,/[A-Z]{2,}/];return preservePatterns.some(pattern=>pattern.test(name))}}function sanitizePropertyName(input,options={}){const sanitizer=new Sanitize(null);return sanitizer.sanitizePropertyName(input,options)}export{Sanitize,sanitizePropertyName};
17
+ import{ComponentBase}from"#factories/component-base";class Sanitize extends ComponentBase{static slothletProperty="sanitize";#compileGlobPattern(pattern,caseSensitive=true){if(pattern.startsWith("**")&&pattern.endsWith("**")&&pattern.length>4){const innerString=pattern.slice(2,-2);const escapedString=innerString.replace(/[.+^${}()|[\]\\*?]/g,"\\$&");const flags2=caseSensitive?"":"i";return new RegExp(`(?<=.)${escapedString}(?=.)`,flags2)}const regexPattern=pattern.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".");const flags=caseSensitive?"":"i";return new RegExp(`^${regexPattern}$`,flags)}#matchesAnyPattern(input,patterns,caseSensitive=false){for(const pattern of patterns){if(pattern.includes("*")||pattern.includes("?")){const regex=this.#compileGlobPattern(pattern,caseSensitive);if(regex&&regex.test(input))return true}else{const match=caseSensitive?input===pattern:input.toLowerCase()===pattern.toLowerCase();if(match)return true}}return false}#extractPatternLiterals(pattern){return pattern.split(/[*?]+/).filter(Boolean)}#applySegmentRules(segment,index,originalString,config){const{preserveAllUpper,preserveAllLower,leaveRules,leaveInsensitiveRules,upperRules,lowerRules}=config;if(this.#matchesAnyPattern(segment,leaveRules,true)){return segment}if(this.#matchesAnyPattern(segment,leaveInsensitiveRules,false)){return segment}if(preserveAllUpper&&segment===segment.toUpperCase()&&segment!==segment.toLowerCase()&&/[A-Z]/.test(segment)){return segment}if(preserveAllLower&&segment===segment.toLowerCase()&&segment!==segment.toUpperCase()&&/[a-z]/.test(segment)){return segment}for(const pattern of[...upperRules,...lowerRules]){if(pattern.includes("*")||pattern.includes("?")){const regex=this.#compileGlobPattern(pattern,false);if(regex&&regex.test(originalString)){const literals=this.#extractPatternLiterals(pattern);for(const literal of literals){const cleanLiteral=literal.replace(/[^A-Za-z0-9_$]/g,"").replace(/^_+|_+$/g,"");if(cleanLiteral&&segment.toLowerCase()===cleanLiteral.toLowerCase()){return upperRules.includes(pattern)?segment.toUpperCase():segment.toLowerCase()}}}}else{if(segment.toLowerCase()===pattern.toLowerCase()){return upperRules.includes(pattern)?segment.toUpperCase():segment.toLowerCase()}}}let transformed=this.#applyWithinSegmentPatterns(segment,upperRules,lowerRules);if(transformed!==segment){return transformed}return segment}#applyWithinSegmentPatterns(segment,upperRules,lowerRules){let result=segment;const applyBoundaryPattern=(pattern,toUpper)=>{if(pattern.startsWith("**")&&pattern.endsWith("**")&&pattern.length>4){const innerString=pattern.slice(2,-2);const innerRegex=new RegExp(innerString.replace(/[.+^${}()|[\]\\*?]/g,"\\$&"),"gi");const matches=[...result.matchAll(innerRegex)];for(const match of matches){const startPos=match.index;const endPos=startPos+match[0].length;const hasCharBefore=startPos>0;const hasCharAfter=endPos<result.length;if(hasCharBefore&&hasCharAfter){const replacement=toUpper?innerString.toUpperCase():innerString.toLowerCase();result=result.substring(0,startPos)+replacement+result.substring(endPos);break}}}else if(pattern.includes("*")&&!pattern.startsWith("**")){const literalParts=pattern.split("*").filter(Boolean);for(const literal of literalParts){const literalRegex=new RegExp(literal.replace(/[.+^${}()|[\]\\]/g,"\\$&"),"gi");const replacement=toUpper?literal.toUpperCase():literal.toLowerCase();result=result.replace(literalRegex,replacement)}}};upperRules.forEach(pattern=>applyBoundaryPattern(pattern,true));lowerRules.forEach(pattern=>applyBoundaryPattern(pattern,false));return result}sanitizePropertyName(input,options={}){const{lowerFirst=true,preserveAllUpper=false,preserveAllLower=false,rules={}}=options;const leaveRules=(rules.leave||[]).map(s=>String(s));const leaveInsensitiveRules=(rules.leaveInsensitive||[]).map(s=>String(s));const upperRules=(rules.upper||[]).map(s=>String(s));const lowerRules=(rules.lower||[]).map(s=>String(s));const originalString=String(input).trim();const isAllUpper=originalString===originalString.toUpperCase()&&originalString!==originalString.toLowerCase()&&/[A-Z]/.test(originalString);const isAllLower=originalString===originalString.toLowerCase()&&originalString!==originalString.toUpperCase()&&/[a-z]/.test(originalString);if(preserveAllUpper&&isAllUpper&&!/-/.test(originalString)){return originalString}if(preserveAllLower&&isAllLower&&!/-/.test(originalString)){return originalString}let primarySegments=originalString.split(/[-]+|[^A-Za-z0-9_$]+/).filter(Boolean);if(primarySegments.length===0)return"_";while(primarySegments.length&&!/^[A-Za-z_$]/.test(primarySegments[0][0])){primarySegments[0]=primarySegments[0].replace(/^[^A-Za-z_$]+/,"");if(!primarySegments[0])primarySegments.shift()}if(primarySegments.length===0)return"_";const lowerRuleApplied=[];const processedPrimarySegments=primarySegments.map((primarySeg,primaryIdx)=>{const parts=primarySeg.split(/(_+)/);const processedParts=parts.map((part,partIdx)=>{if(partIdx%2===1)return part;if(!part)return part;const cleanSeg=part.replace(/[^A-Za-z0-9_$]/g,"");const config={preserveAllUpper,preserveAllLower,leaveRules,leaveInsensitiveRules,upperRules,lowerRules};const result2=this.#applySegmentRules(cleanSeg,0,originalString,config);const matchesLower=lowerRules.some(pattern=>{if(pattern.includes("*")||pattern.includes("?")){const regex=this.#compileGlobPattern(pattern,false);if(regex&&regex.test(originalString)){const literals=this.#extractPatternLiterals(pattern);for(const literal of literals){const cleanLiteral=literal.replace(/[^A-Za-z0-9_$]/g,"").replace(/^_+|_+$/g,"");if(cleanLiteral&&cleanSeg.toLowerCase()===cleanLiteral.toLowerCase()){return true}}}}else{if(cleanSeg.toLowerCase()===pattern.toLowerCase()){return true}}return false});if(matchesLower&&result2===cleanSeg.toLowerCase()){lowerRuleApplied[primaryIdx]=true}return result2});return processedParts.join("")});const camelCasedSegments=processedPrimarySegments.map((seg,idx)=>{const matchesLeave=this.#matchesAnyPattern(seg,leaveRules,true);const matchesLeaveInsensitive=this.#matchesAnyPattern(seg,leaveInsensitiveRules,false);const matchesUpper=this.#matchesAnyPattern(seg,upperRules,false);const hasUnderscores=seg.includes("_");const isAllUpper2=!hasUnderscores&&preserveAllUpper&&seg===seg.toUpperCase()&&seg!==seg.toLowerCase()&&/[A-Z]/.test(seg);const isAllLower2=!hasUnderscores&&preserveAllLower&&seg===seg.toLowerCase()&&seg!==seg.toUpperCase()&&/[a-z]/.test(seg);if(matchesLeave||matchesLeaveInsensitive||matchesUpper||isAllUpper2||isAllLower2){return seg}let transformed;if(idx===0){transformed=lowerFirst?seg[0].toLowerCase()+seg.slice(1):seg}else{if(lowerRuleApplied[idx]){transformed=seg}else{transformed=seg[0].toUpperCase()+seg.slice(1)}}return transformed});let result=camelCasedSegments.join("");result=result.replace(/[^A-Za-z0-9_$]/g,"");return result}getModuleId(filePath,baseDir){let relative=filePath.replace(baseDir,"").replace(/\\/g,"/");relative=relative.replace(/^\//,"");relative=relative.replace(/\.(mjs|cjs|js)$/,"");return relative}shouldPreserveFunctionCase(name){const preservePatterns=[/^[A-Z]{2,}$/,/[A-Z]{2,}/];return preservePatterns.some(pattern=>pattern.test(name))}}function sanitizePropertyName(input,options={}){const sanitizer=new Sanitize(null);return sanitizer.sanitizePropertyName(input,options)}export{Sanitize,sanitizePropertyName};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"#factories/component-base";class Utilities extends ComponentBase{static slothletProperty="utilities";isPlainObject(obj){if(typeof obj!=="object"||obj===null)return false;const proto=Object.getPrototypeOf(obj);return proto===null||proto===Object.prototype}deepMerge(target,source){if(!this.isPlainObject(target)||!this.isPlainObject(source)){return source}const result={...target};for(const key in source){if(Object.prototype.hasOwnProperty.call(source,key)){if(this.isPlainObject(source[key])){result[key]=this.deepMerge(this.isPlainObject(target[key])?target[key]:{},source[key])}else{result[key]=source[key]}}}return result}deepClone(obj){try{return structuredClone(obj)}catch{const objType=obj?.__type||typeof obj;if(obj===null||objType!=="object"&&objType!=="function")return obj;if(obj instanceof Date)return new Date(obj.getTime());if(Array.isArray(obj))return obj.map(item=>this.deepClone(item));const cloned={};for(const key in obj){try{cloned[key]=this.deepClone(obj[key])}catch{cloned[key]=obj[key]}}return cloned}}generateId(){return`slothlet_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}}export{Utilities};
17
+ import{ComponentBase}from"#factories/component-base";class Utilities extends ComponentBase{static slothletProperty="utilities";isPlainObject(obj){if(typeof obj!=="object"||obj===null)return false;const proto=Object.getPrototypeOf(obj);return proto===null||proto===Object.prototype}deepMerge(target,source){if(!this.isPlainObject(target)||!this.isPlainObject(source)){return source}const result={...target};for(const key in source){if(Object.prototype.hasOwnProperty.call(source,key)){if(this.isPlainObject(source[key])){result[key]=this.deepMerge(this.isPlainObject(target[key])?target[key]:{},source[key])}else{result[key]=source[key]}}}return result}deepClone(obj){try{return structuredClone(obj)}catch{const objType=obj?.__type||typeof obj;if(obj===null||objType!=="object"&&objType!=="function")return obj;if(typeof obj==="function")return obj;if(obj instanceof Date)return new Date(obj.getTime());if(Array.isArray(obj))return obj.map(item=>this.deepClone(item));const cloned={};for(const key in obj){try{cloned[key]=this.deepClone(obj[key])}catch{cloned[key]=obj[key]}}return cloned}}generateId(){return`slothlet_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}}export{Utilities};
@@ -49,6 +49,8 @@
49
49
  "HINT_RUNTIME_NO_ACTIVE_CONTEXT": "metadata.self() must be called from within a slothlet API function.",
50
50
  "INVALID_CONFIG_MUTATIONS_DISABLED": "Cannot perform '{operation}' - mutation is disabled. Set allowMutation: true to enable API modification operations (add/remove/reload).",
51
51
  "HINT_INVALID_CONFIG_MUTATIONS_DISABLED": "API mutation operations require allowMutation: true in the configuration. Use diagnostics: true to access these methods for testing without enabling actual mutations.",
52
+ "WARNING_API_ADD_OPTION_LOCKED": "'{option}' passed to api.add() was ignored — collision / internal policy is set at init, not per call. Use the `collision` config at slothlet() init, `forceOverwrite` for a targeted replace, or set api.mutations.allowCollisionOverride to true to honor per-call overrides.",
53
+ "HINT_WARNING_API_ADD_OPTION_LOCKED": "collisionMode, mutateExisting and recordHistory are locked so a runtime mount cannot bypass the instance's collision policy; forceOverwrite stays available for a targeted replace.",
52
54
  "CACHE_MODULEID_MISMATCH": "Cache entry moduleID mismatch: expected '{cacheKey}', but entry has '{entryModuleID}'. This indicates an internal cache inconsistency.",
53
55
  "HINT_CACHE_MODULEID_MISMATCH": "This is an internal error indicating cache corruption. Please report this issue with steps to reproduce.",
54
56
  "CACHE_NOT_FOUND": "Cache entry not found for moduleID '{moduleID}' during '{operation}' operation. The module may not be loaded or the cache may have been cleared.",
@@ -121,6 +123,8 @@
121
123
  "HINT_WARNING_COLLISION_TRIGGER_MATERIALIZE_ERROR": "This is a non-critical background error during collision handling. The lazy folder at the given path failed to materialize. Check that the module at that path is valid and can be loaded.",
122
124
  "WARNING_LIFECYCLE_HANDLER_ERROR": "Lifecycle event handler threw an error for event '{event}'. Other handlers for this event continued executing.",
123
125
  "HINT_WARNING_LIFECYCLE_HANDLER_ERROR": "A lifecycle handler registered for the '{event}' event threw an error. Check your lifecycle.on('{event}', ...) handler for bugs. Other handlers in the same event are not affected.",
126
+ "WARNING_EVENT_HANDLER_ERROR": "Event listener threw an error for event '{event}'. Other listeners for this event continued executing.",
127
+ "HINT_WARNING_EVENT_HANDLER_ERROR": "An event listener registered for the '{event}' event threw an error. Check your event.on('{event}', ...) listener for bugs. Other listeners in the same event are not affected.",
124
128
  "WARNING_MULTIPLE_ROOT_CONTRIBUTORS": "Multiple root-level default function exports detected: {rootContributors}. Each has been namespaced by filename (e.g., api.{firstContributor}()). Consider using a single root-level default export or moving files to subdirectories.",
125
129
  "HINT_WARNING_MULTIPLE_ROOT_CONTRIBUTORS": "Multiple files export default functions at the root level. Each has been namespaced (e.g., api.filename()). Consider consolidating into one root export or moving files to subdirectories for clearer organization.",
126
130
  "V3_CONFIG_DEPRECATED": "Configuration option '{option}' is deprecated and will be removed in v4. Use '{replacement}' instead.",
@@ -427,6 +431,8 @@
427
431
  "PERM_RULE_CALLER_REQUIRED": "rule.caller must be a non-empty string",
428
432
  "PERM_RULE_TARGET_REQUIRED": "rule.target must be a non-empty string",
429
433
  "PERM_RULE_EFFECT_INVALID": "rule.effect must be 'allow' or 'deny'",
434
+ "PERM_EVENT_RULE_EVENT_REQUIRED": "rule.event must be a non-empty string",
435
+ "PERM_EVENT_RULE_EFFECT_INVALID": "rule.effect must be 'deny', 'notify', or 'allow'",
430
436
  "PERM_RULE_CONDITION_INVALID": "rule.condition must be a plain object, a function, or an array where each entry is a plain object or function",
431
437
  "BRACE_EXPANSION_MAX_DEPTH": "Brace expansion exceeded maximum depth of {maxDepth}",
432
438
  "HINT_BRACE_EXPANSION_MAX_DEPTH": "Reduce nesting in your brace patterns or increase the maxDepth option.",
@@ -450,6 +456,8 @@
450
456
  "MODULE_MANIFEST_REASON_PERMISSION_CALLER": "permissions[{index}].caller must be a non-empty string",
451
457
  "MODULE_MANIFEST_REASON_PERMISSION_TARGET": "permissions[{index}].target must be a non-empty string",
452
458
  "MODULE_MANIFEST_REASON_PERMISSION_EFFECT": "permissions[{index}].effect must be \"allow\" or \"deny\"",
459
+ "MODULE_MANIFEST_REASON_EVENT_NAME": "events[{index}].event must be a non-empty string",
460
+ "MODULE_MANIFEST_REASON_EVENT_EFFECT": "events[{index}].effect must be \"deny\", \"notify\", or \"allow\"",
453
461
  "MODULE_MANIFEST_REASON_JSON_PARSE": "JSON parse error: {error}",
454
462
  "GENERATE_MANIFEST_DIR_INVALID": "generateManifest: dir must be a non-empty string, received {received}",
455
463
  "GENERATE_MANIFEST_DIR_UNREADABLE": "generateManifest: cannot read directory \"{dir}\": {reason}",
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"#factories/component-base";class Flatten extends ComponentBase{static slothletProperty="flatten";constructor(slothlet){super(slothlet)}#checkSelfReferential(mod,moduleName){if(!mod||typeof mod!=="object")return false;return mod[moduleName]===mod}async#checkMultiDefault(analysis,hasMultipleDefaults,t){if(!hasMultipleDefaults)return null;if(analysis.hasDefault){return{preserveAsNamespace:true,reason:await t("FLATTEN_REASON_MULTI_DEFAULT_WITH_DEFAULT")}}return{flattenToRoot:true,reason:await t("FLATTEN_REASON_MULTI_DEFAULT_WITHOUT_DEFAULT")}}#checkAutoFlatten(mod,moduleName,moduleKeys){if(moduleKeys.length!==1)return false;return moduleKeys[0]===moduleName}async getFlatteningDecision(options){const{mod,moduleName,categoryName,analysis,hasMultipleDefaults,moduleKeys,t}=options;const isAddapiFile=moduleName==="addapi";if(isAddapiFile){if(analysis.hasDefault&&analysis.defaultExportType==="object"&&moduleKeys.length>0){return{flattenToCategory:true,flattenType:"addapi-metadata-default",reason:await t("FLATTEN_REASON_ADDAPI_METADATA_DEFAULT")}}return{flattenToCategory:true,flattenType:"addapi-special-file",reason:await t("FLATTEN_REASON_ADDAPI_SPECIAL_FILE")}}if(this.#checkSelfReferential(mod,moduleName)){return{preserveAsNamespace:true,reason:await t("FLATTEN_REASON_SELF_REFERENTIAL")}}const multiDefaultDecision=await this.#checkMultiDefault(analysis,hasMultipleDefaults,t);if(multiDefaultDecision){if(multiDefaultDecision.preserveAsNamespace){const exportToCheck2=typeof mod==="function"?mod:mod?.default&&typeof mod.default==="function"?mod.default:null;if(exportToCheck2&&exportToCheck2.name&&exportToCheck2.name!=="default"){const normalizedFunctionName=exportToCheck2.name.toLowerCase().replace(/[-_]/g,"");const normalizedModuleName=moduleName.toLowerCase().replace(/[-_]/g,"");if(normalizedFunctionName===normalizedModuleName){multiDefaultDecision.preferredName=exportToCheck2.name}}}return multiDefaultDecision}if(this.#checkAutoFlatten(mod,moduleName,moduleKeys)){return{useAutoFlattening:true,reason:await t("FLATTEN_REASON_SINGLE_EXPORT_MATCHES_FILENAME")}}if(moduleName===categoryName){return{flattenToCategory:true,reason:await t("FLATTEN_REASON_FILENAME_MATCHES_CATEGORY")}}const exportToCheck=typeof mod==="function"?mod:mod?.default&&typeof mod.default==="function"?mod.default:null;if(exportToCheck&&exportToCheck.name&&exportToCheck.name!=="default"){const normalizedFunctionName=exportToCheck.name.toLowerCase().replace(/[-_]/g,"");const normalizedModuleName=moduleName.toLowerCase().replace(/[-_]/g,"");const functionNameMatchesFilename=normalizedFunctionName===normalizedModuleName;if(functionNameMatchesFilename){return{preserveAsNamespace:true,preferredName:exportToCheck.name,reason:await t("FLATTEN_REASON_PRESERVING_FUNCTION_NAME")}}}return{preserveAsNamespace:true,reason:await t("FLATTEN_REASON_DEFAULT_PRESERVE_NAMESPACE")}}processModuleForAPI(options){const{mod,decision,moduleName,propertyName,moduleKeys,analysis,file=null,collisionContext="initial",apiPathPrefix="",isSelfReferential=false,collisionModeOverride=null}=options;const isAddapiFile=moduleName==="addapi"||file&&file.name==="addapi"||file&&file.fullName&&["addapi.mjs","addapi.cjs","addapi.js","addapi.ts"].includes(file.fullName.toLowerCase());if(isAddapiFile&&analysis.hasDefault&&moduleKeys.length>0){const moduleContent2=mod.default;for(const key of moduleKeys){moduleContent2[key]=mod[key]}return{moduleContent:moduleContent2}}if(decision.useAutoFlattening){return{moduleContent:mod[moduleName]}}if(decision.flattenToRoot||decision.flattenToCategory){if(mod.default&&moduleKeys.length===0){return{moduleContent:mod.default}}if(mod.default&&moduleKeys.length>0){const moduleContent3=typeof mod.default==="function"?mod.default:{...mod.default};for(const key of moduleKeys){moduleContent3[key]=mod[key]}return{moduleContent:moduleContent3}}const moduleContent2={};for(const key of moduleKeys){moduleContent2[key]=mod[key]}return{moduleContent:moduleContent2}}if(isSelfReferential){return{moduleContent:mod[moduleName]||mod}}if(mod.default&&moduleKeys.length>0){if(typeof mod.default==="function"){const moduleContent3=this.slothlet.helpers.modesUtils.ensureNamedExportFunction(mod.default,propertyName);const collisionConfig=this.slothlet.config.api?.collision||this.slothlet.config.collision;const collisionMode=collisionModeOverride||(collisionContext==="initial"?collisionConfig.initial:collisionConfig.api);for(const key of moduleKeys){if(!this.shouldAttachNamedExport(key,mod[key],moduleContent3,mod.default)){continue}const hasExisting=Object.prototype.hasOwnProperty.call(moduleContent3,key);if(hasExisting){if(collisionMode==="merge"||collisionMode==="skip"){continue}else if(collisionMode==="error"){throw new this.slothlet.SlothletError("COLLISION_DEFAULT_EXPORT_ERROR",{key,apiPath:`${apiPathPrefix}.${propertyName}`},null,{validationError:true})}else if(collisionMode==="warn"){new this.slothlet.SlothletWarning("WARNING_COLLISION_DEFAULT_EXPORT_OVERWRITE",{key,apiPath:`${apiPathPrefix}.${propertyName}`})}}moduleContent3[key]=mod[key]}return{moduleContent:moduleContent3}}if(typeof mod.default==="object"&&mod.default!==null){const moduleContent3=mod.default;for(const key of moduleKeys){if(key in mod.default){continue}if(!this.shouldAttachNamedExport(key,mod[key],moduleContent3,mod.default)){continue}moduleContent3[key]=mod[key]}return{moduleContent:moduleContent3}}const moduleContent2={default:mod.default};for(const key of moduleKeys){moduleContent2[key]=mod[key]}return{moduleContent:moduleContent2}}if(mod.default&&moduleKeys.length===0){return{moduleContent:this.slothlet.helpers.modesUtils.ensureNamedExportFunction(mod.default,propertyName)}}const moduleContent={};for(const key of moduleKeys){moduleContent[key]=mod[key]}return{moduleContent}}async buildCategoryDecisions(options){const{categoryName,mod,moduleName,fileBaseName,analysis,moduleKeys,currentDepth,moduleFiles=[],t}=options;const decision={shouldFlatten:false,flattenType:"preserve",preferredName:null,reason:await t("FLATTEN_REASON_NO_CONDITIONS_MET")};const isAddapiFile=moduleName==="addapi"||fileBaseName==="addapi"||fileBaseName&&["addapi.mjs","addapi.cjs","addapi.js","addapi.ts"].includes(fileBaseName.toLowerCase());if(isAddapiFile){if(analysis.hasDefault&&analysis.defaultExportType==="object"&&moduleKeys.length>0){return{shouldFlatten:true,flattenType:"addapi-metadata-default",reason:await t("FLATTEN_REASON_ADDAPI_SPECIAL_FILE_PARENT")}}return{shouldFlatten:true,flattenType:"addapi-special-file",reason:await t("FLATTEN_REASON_ADDAPI_SPECIAL_FILE")}}if(moduleName===categoryName&&typeof mod==="function"&&currentDepth>0){return{shouldFlatten:true,flattenType:"function-folder-match",reason:await t("FLATTEN_REASON_FUNCTION_FOLDER_MATCH")}}if(analysis.hasDefault&&analysis.defaultExportType==="object"&&moduleName===categoryName&&currentDepth>0){return{shouldFlatten:true,flattenType:"default-export-flatten",reason:await t("FLATTEN_REASON_DEFAULT_OBJECT_EXPORT_FLATTEN")}}if(moduleName===categoryName&&mod&&typeof mod==="object"&&!Array.isArray(mod)&&currentDepth>0){if(moduleKeys.length===1&&moduleKeys[0]===moduleName){return{shouldFlatten:true,flattenType:"object-auto-flatten",reason:await t("FLATTEN_REASON_SINGLE_EXPORT_MATCHES_FILENAME")}}}if(fileBaseName===categoryName&&moduleKeys.length>0){return{shouldFlatten:true,flattenType:"filename-folder-match-flatten",reason:await t("FLATTEN_REASON_BASENAME_MATCHES_CATEGORY")}}if(moduleFiles.length===1&&currentDepth>0&&mod&&typeof mod==="object"&&!Array.isArray(mod)){const genericFilenames=["singlefile","index","main","default"];const isGenericFilename=genericFilenames.includes(moduleName.toLowerCase());if(moduleKeys.length===1&&isGenericFilename){return{shouldFlatten:true,flattenType:"parent-level-flatten",reason:await t("FLATTEN_REASON_GENERIC_FILENAME_SINGLE_EXPORT")}}}if(typeof mod==="function"&&mod.name&&currentDepth>0){const functionNameMatchesFolder=mod.name.toLowerCase()===categoryName.toLowerCase()||mod.name.toLowerCase().replace(/[-_]/g,"")===categoryName.toLowerCase().replace(/[-_]/g,"");if(functionNameMatchesFolder){return{shouldFlatten:true,flattenType:"function-folder-match",preferredName:mod.name,reason:await t("FLATTEN_REASON_FUNCTION_FOLDER_MATCH")}}}const exportToCheck=typeof mod==="function"?mod:mod?.default&&typeof mod.default==="function"?mod.default:null;if(exportToCheck&&exportToCheck.name&&exportToCheck.name!=="default"){const normalizedFunctionName=exportToCheck.name.toLowerCase().replace(/[-_]/g,"");const normalizedModuleName=moduleName.toLowerCase().replace(/[-_]/g,"");const functionNameMatchesFilename=normalizedFunctionName===normalizedModuleName;if(functionNameMatchesFilename){return{shouldFlatten:false,preferredName:exportToCheck.name,reason:await t("FLATTEN_REASON_PRESERVING_FUNCTION_NAME")}}}if(typeof mod==="function"&&(!mod.name||mod.name==="default"||mod.__slothletDefault===true)&&currentDepth>0){return{shouldFlatten:true,flattenType:"default-function",preferredName:categoryName,reason:await t("FLATTEN_REASON_DEFAULT_FUNCTION_EXPORT")}}if(moduleKeys.length===1&&moduleKeys[0]===moduleName){return{shouldFlatten:true,flattenType:"object-auto-flatten",preferredName:moduleName,reason:await t("FLATTEN_REASON_SINGLE_EXPORT_MATCHES_MODULE")}}return decision}shouldAttachNamedExport(key,value,defaultFunc,originalDefault){if(!key||key==="default"){return false}if(value===defaultFunc||value===originalDefault){return false}if(typeof defaultFunc==="function"&&key===defaultFunc.name){return false}if(typeof originalDefault==="function"&&key===originalDefault.name){return false}return true}}export{Flatten};
17
+ import{ComponentBase}from"#factories/component-base";class Flatten extends ComponentBase{static slothletProperty="flatten";constructor(slothlet){super(slothlet)}#checkSelfReferential(mod,moduleName){if(!mod||typeof mod!=="object")return false;return mod[moduleName]===mod}async#checkMultiDefault(analysis,hasMultipleDefaults,t){if(!hasMultipleDefaults)return null;if(analysis.hasDefault){return{preserveAsNamespace:true,reason:await t("FLATTEN_REASON_MULTI_DEFAULT_WITH_DEFAULT")}}return{flattenToRoot:true,reason:await t("FLATTEN_REASON_MULTI_DEFAULT_WITHOUT_DEFAULT")}}#checkAutoFlatten(mod,moduleName,moduleKeys){if(moduleKeys.length!==1)return false;return moduleKeys[0]===moduleName}async getFlatteningDecision(options){const{mod,moduleName,categoryName,analysis,hasMultipleDefaults,moduleKeys,t}=options;const isAddapiFile=moduleName==="addapi";if(isAddapiFile){if(analysis.hasDefault&&analysis.defaultExportType==="object"&&moduleKeys.length>0){return{flattenToCategory:true,flattenType:"addapi-metadata-default",reason:await t("FLATTEN_REASON_ADDAPI_METADATA_DEFAULT")}}return{flattenToCategory:true,flattenType:"addapi-special-file",reason:await t("FLATTEN_REASON_ADDAPI_SPECIAL_FILE")}}if(this.#checkSelfReferential(mod,moduleName)){return{preserveAsNamespace:true,reason:await t("FLATTEN_REASON_SELF_REFERENTIAL")}}const multiDefaultDecision=await this.#checkMultiDefault(analysis,hasMultipleDefaults,t);if(multiDefaultDecision){if(multiDefaultDecision.preserveAsNamespace){const exportToCheck2=typeof mod==="function"?mod:mod?.default&&typeof mod.default==="function"?mod.default:null;if(exportToCheck2&&exportToCheck2.name&&exportToCheck2.name!=="default"){const normalizedFunctionName=exportToCheck2.name.toLowerCase().replace(/[-_]/g,"");const normalizedModuleName=moduleName.toLowerCase().replace(/[-_]/g,"");if(normalizedFunctionName===normalizedModuleName){multiDefaultDecision.preferredName=exportToCheck2.name}}}return multiDefaultDecision}if(this.#checkAutoFlatten(mod,moduleName,moduleKeys)){return{useAutoFlattening:true,reason:await t("FLATTEN_REASON_SINGLE_EXPORT_MATCHES_FILENAME")}}if(moduleName===categoryName){return{flattenToCategory:true,reason:await t("FLATTEN_REASON_FILENAME_MATCHES_CATEGORY")}}const exportToCheck=typeof mod==="function"?mod:mod?.default&&typeof mod.default==="function"?mod.default:null;if(exportToCheck&&exportToCheck.name&&exportToCheck.name!=="default"){const normalizedFunctionName=exportToCheck.name.toLowerCase().replace(/[-_]/g,"");const normalizedModuleName=moduleName.toLowerCase().replace(/[-_]/g,"");const functionNameMatchesFilename=normalizedFunctionName===normalizedModuleName;if(functionNameMatchesFilename){return{preserveAsNamespace:true,preferredName:exportToCheck.name,reason:await t("FLATTEN_REASON_PRESERVING_FUNCTION_NAME")}}}return{preserveAsNamespace:true,reason:await t("FLATTEN_REASON_DEFAULT_PRESERVE_NAMESPACE")}}processModuleForAPI(options){const{mod,decision,moduleName,propertyName,moduleKeys,analysis,file=null,collisionContext="initial",apiPathPrefix="",isSelfReferential=false,collisionModeOverride=null}=options;const isAddapiFile=moduleName==="addapi"||file&&file.name==="addapi"||file&&file.fullName&&["addapi.mjs","addapi.cjs","addapi.js","addapi.ts"].includes(file.fullName.toLowerCase());if(isAddapiFile&&analysis.hasDefault&&moduleKeys.length>0){const moduleContent2=mod.default;for(const key of moduleKeys){moduleContent2[key]=mod[key]}return{moduleContent:moduleContent2}}if(decision.useAutoFlattening){return{moduleContent:mod[moduleName]}}if(decision.flattenToRoot||decision.flattenToCategory){if(mod.default&&moduleKeys.length===0){return{moduleContent:mod.default}}if(mod.default&&moduleKeys.length>0){const moduleContent3=typeof mod.default==="function"?mod.default:{...mod.default};for(const key of moduleKeys){moduleContent3[key]=mod[key]}return{moduleContent:moduleContent3}}const moduleContent2={};for(const key of moduleKeys){moduleContent2[key]=mod[key]}return{moduleContent:moduleContent2}}if(isSelfReferential){return{moduleContent:mod[moduleName]||mod}}if(mod.default&&moduleKeys.length>0){if(typeof mod.default==="function"){const moduleContent3=this.slothlet.helpers.modesUtils.ensureNamedExportFunction(mod.default,propertyName);const collisionConfig=this.slothlet.config.api?.collision||this.slothlet.config.collision;const collisionMode=collisionModeOverride||(collisionContext==="initial"?collisionConfig.initial:collisionConfig.api);for(const key of moduleKeys){if(!this.shouldAttachNamedExport(key,mod[key],moduleContent3,mod.default)){continue}const hasExisting=Object.prototype.hasOwnProperty.call(moduleContent3,key);if(hasExisting){if(collisionMode==="merge"||collisionMode==="skip"){continue}else if(collisionMode==="error"){throw new this.slothlet.SlothletError("COLLISION_DEFAULT_EXPORT_ERROR",{key,apiPath:`${apiPathPrefix}.${propertyName}`},null,{validationError:true})}else if(collisionMode==="warn"){new this.slothlet.SlothletWarning("WARNING_COLLISION_DEFAULT_EXPORT_OVERWRITE",{key,apiPath:`${apiPathPrefix}.${propertyName}`})}}moduleContent3[key]=mod[key]}return{moduleContent:moduleContent3}}if(typeof mod.default==="object"&&mod.default!==null){const moduleContent3=mod.default;const collisionConfig=this.slothlet.config.api?.collision||this.slothlet.config.collision;const collisionMode=collisionModeOverride||(collisionContext==="initial"?collisionConfig.initial:collisionConfig.api);for(const key of moduleKeys){if(!this.shouldAttachNamedExport(key,mod[key],moduleContent3,mod.default)){continue}const hasExisting=key in mod.default;if(hasExisting){if(collisionMode==="merge"||collisionMode==="skip"){continue}else if(collisionMode==="error"){throw new this.slothlet.SlothletError("COLLISION_DEFAULT_EXPORT_ERROR",{key,apiPath:`${apiPathPrefix}.${propertyName}`},null,{validationError:true})}else if(collisionMode==="warn"){new this.slothlet.SlothletWarning("WARNING_COLLISION_DEFAULT_EXPORT_OVERWRITE",{key,apiPath:`${apiPathPrefix}.${propertyName}`})}}moduleContent3[key]=mod[key]}return{moduleContent:moduleContent3}}const moduleContent2={default:mod.default};for(const key of moduleKeys){moduleContent2[key]=mod[key]}return{moduleContent:moduleContent2}}if(mod.default&&moduleKeys.length===0){return{moduleContent:this.slothlet.helpers.modesUtils.ensureNamedExportFunction(mod.default,propertyName)}}const moduleContent={};for(const key of moduleKeys){moduleContent[key]=mod[key]}return{moduleContent}}async buildCategoryDecisions(options){const{categoryName,mod,moduleName,fileBaseName,analysis,moduleKeys,currentDepth,moduleFiles=[],t}=options;const decision={shouldFlatten:false,flattenType:"preserve",preferredName:null,reason:await t("FLATTEN_REASON_NO_CONDITIONS_MET")};const isAddapiFile=moduleName==="addapi"||fileBaseName==="addapi"||fileBaseName&&["addapi.mjs","addapi.cjs","addapi.js","addapi.ts"].includes(fileBaseName.toLowerCase());if(isAddapiFile){if(analysis.hasDefault&&analysis.defaultExportType==="object"&&moduleKeys.length>0){return{shouldFlatten:true,flattenType:"addapi-metadata-default",reason:await t("FLATTEN_REASON_ADDAPI_SPECIAL_FILE_PARENT")}}return{shouldFlatten:true,flattenType:"addapi-special-file",reason:await t("FLATTEN_REASON_ADDAPI_SPECIAL_FILE")}}if(moduleName===categoryName&&typeof mod==="function"&&currentDepth>0){return{shouldFlatten:true,flattenType:"function-folder-match",reason:await t("FLATTEN_REASON_FUNCTION_FOLDER_MATCH")}}if(analysis.hasDefault&&analysis.defaultExportType==="object"&&moduleName===categoryName&&currentDepth>0){return{shouldFlatten:true,flattenType:"default-export-flatten",reason:await t("FLATTEN_REASON_DEFAULT_OBJECT_EXPORT_FLATTEN")}}if(moduleName===categoryName&&mod&&typeof mod==="object"&&!Array.isArray(mod)&&currentDepth>0){if(moduleKeys.length===1&&moduleKeys[0]===moduleName){return{shouldFlatten:true,flattenType:"object-auto-flatten",reason:await t("FLATTEN_REASON_SINGLE_EXPORT_MATCHES_FILENAME")}}}if(fileBaseName===categoryName&&moduleKeys.length>0){return{shouldFlatten:true,flattenType:"filename-folder-match-flatten",reason:await t("FLATTEN_REASON_BASENAME_MATCHES_CATEGORY")}}if(moduleFiles.length===1&&currentDepth>0&&mod&&typeof mod==="object"&&!Array.isArray(mod)){const genericFilenames=["singlefile","index","main","default"];const isGenericFilename=genericFilenames.includes(moduleName.toLowerCase());if(moduleKeys.length===1&&isGenericFilename){return{shouldFlatten:true,flattenType:"parent-level-flatten",reason:await t("FLATTEN_REASON_GENERIC_FILENAME_SINGLE_EXPORT")}}}if(typeof mod==="function"&&mod.name&&currentDepth>0){const functionNameMatchesFolder=mod.name.toLowerCase()===categoryName.toLowerCase()||mod.name.toLowerCase().replace(/[-_]/g,"")===categoryName.toLowerCase().replace(/[-_]/g,"");if(functionNameMatchesFolder){return{shouldFlatten:true,flattenType:"function-folder-match",preferredName:mod.name,reason:await t("FLATTEN_REASON_FUNCTION_FOLDER_MATCH")}}}const exportToCheck=typeof mod==="function"?mod:mod?.default&&typeof mod.default==="function"?mod.default:null;if(exportToCheck&&exportToCheck.name&&exportToCheck.name!=="default"){const normalizedFunctionName=exportToCheck.name.toLowerCase().replace(/[-_]/g,"");const normalizedModuleName=moduleName.toLowerCase().replace(/[-_]/g,"");const functionNameMatchesFilename=normalizedFunctionName===normalizedModuleName;if(functionNameMatchesFilename){return{shouldFlatten:false,preferredName:exportToCheck.name,reason:await t("FLATTEN_REASON_PRESERVING_FUNCTION_NAME")}}}if(typeof mod==="function"&&(!mod.name||mod.name==="default"||mod.__slothletDefault===true)&&currentDepth>0){return{shouldFlatten:true,flattenType:"default-function",preferredName:categoryName,reason:await t("FLATTEN_REASON_DEFAULT_FUNCTION_EXPORT")}}if(moduleKeys.length===1&&moduleKeys[0]===moduleName){return{shouldFlatten:true,flattenType:"object-auto-flatten",preferredName:moduleName,reason:await t("FLATTEN_REASON_SINGLE_EXPORT_MATCHES_MODULE")}}return decision}shouldAttachNamedExport(key,value,defaultFunc,originalDefault){if(!key||key==="default"){return false}if(value===defaultFunc||value===originalDefault){return false}if(typeof defaultFunc==="function"&&key===defaultFunc.name){return false}if(typeof originalDefault==="function"&&key===originalDefault.name){return false}return true}}export{Flatten};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"#factories/component-base";import{DEFAULT_API_DEPTH}from"@cldmv/slothlet/helpers/defaults";import{fsp,path,url,createRequire}from"@cldmv/slothlet/helpers/platform";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{isFrameworkReservedKey}from"#handlers/unified-wrapper";import{SlothletWarning}from"@cldmv/slothlet/errors";const RUNTIME_EXTERNALIZED=!("env"in import.meta);function warnIfCoverageWithoutImporter(config,{worker=globalThis.__vitest_worker__,externalized=RUNTIME_EXTERNALIZED}={}){if(worker?.config?.coverage?.enabled!==true)return false;if(!externalized)return false;if(typeof config?.import==="function")return false;if(config?.silent)return false;new SlothletWarning("WARNING_COVERAGE_IMPORTER_UNSET",{});return true}function compileHidden(globs){if(!globs)return null;const list=Array.isArray(globs)?globs:[globs];const rules=[];for(const g of list){if(typeof g!=="string"||g.length===0)continue;const negated=g.startsWith("!");const body=(negated?g.slice(1):g).replace(/\//g,".");if(body.length===0)continue;rules.push({negated,match:compilePattern(body)})}if(!rules.length)return null;return relDotted=>{let hidden=false;for(const rule of rules){if(rule.match(relDotted))hidden=!rule.negated}return hidden}}class Loader extends ComponentBase{static slothletProperty="loader";constructor(slothlet){super(slothlet)}async loadModule(filePath,instanceID,moduleID,cacheBust=null){try{if(this.slothlet.envTarget==="browser"){return this.#loadModuleBrowser(filePath)}if(filePath.endsWith(".cjs")){return this.#loadCJSIsolated(filePath)}const isTypeScript=filePath.endsWith(".ts")||filePath.endsWith(".mts");const typescriptConfig=this.slothlet.config?.typescript;let moduleUrl;if(isTypeScript&&typescriptConfig?.enabled){const mode=typescriptConfig.mode;if(mode==="strict"){if(!typescriptConfig.types?.output){throw new this.SlothletError("TS_STRICT_REQUIRES_OUTPUT",{},null,{validationError:true})}if(!typescriptConfig.types?.interfaceName){throw new this.SlothletError("TS_STRICT_REQUIRES_INTERFACE_NAME",{},null,{validationError:true})}if(!this.slothlet._typesGenerated){const{fork}=await import("child_process");const path2=await import("path");const{fileURLToPath}=await import("url");const __dirname=path2.dirname(fileURLToPath(import.meta.url));const scriptPath=path2.resolve(__dirname,"../../../tools/build/generate-types-worker.mjs");const childConfig=JSON.stringify({dir:this.slothlet.config.root||this.slothlet.config.dir,mode:"eager",typescript:{enabled:true,mode:"fast"},types:typescriptConfig.types});await new Promise((resolve,reject)=>{const child=fork(scriptPath,[],{stdio:["pipe","pipe","pipe","ipc"],env:{...process.env,SLOTHLET_CONFIG:childConfig}});let errorOutput="";child.stderr?.on("data",data=>{errorOutput+=data.toString()});child.on("message",msg=>{if(msg.type==="success"){this.slothlet._typesGenerated=true;resolve()}else if(msg.type==="error"){reject(new this.SlothletError("TS_TYPE_GENERATION_FAILED",{},{message:msg.error}))}});child.on("error",error=>{reject(new this.SlothletError("TS_TYPE_GENERATION_FORK_FAILED",{},error))});child.on("exit",code=>{if(code!==0&&!this.slothlet._typesGenerated){reject(new this.SlothletError("TS_TYPE_GENERATION_PROCESS_EXITED",{code,output:errorOutput},null,{validationError:true}))}})})}const{transformTypeScriptStrict,writeTransformedToCache,formatDiagnostics}=await import("@cldmv/slothlet/processors/typescript");const strictTransform=async tsPath=>{const result=await transformTypeScriptStrict(tsPath,{target:typescriptConfig.target,module:typescriptConfig.module,strict:typescriptConfig.strict,typeDefinitionPath:typescriptConfig.types.output,compilerOptions:typescriptConfig.compilerOptions});if(result.diagnostics&&result.diagnostics.length>0){const ts=await import("typescript");const errors=formatDiagnostics(result.diagnostics,ts.default);const error=new this.SlothletError("TS_TYPE_CHECK_ERRORS",{filePath:tsPath,errors:errors.join("\n")},null,{validationError:true});error.diagnostics=result.diagnostics;throw error}return result.code};const entryCode=await strictTransform(filePath);moduleUrl=await this.#buildTypescriptModuleUrl(writeTransformedToCache,filePath,entryCode,instanceID,moduleID,cacheBust,strictTransform)}else{const{transformTypeScript,writeTransformedToCache}=await import("@cldmv/slothlet/processors/typescript");const transformOptions={target:typescriptConfig.target,sourcemap:typescriptConfig.sourcemap};const transformedCode=await transformTypeScript(filePath,transformOptions);const transform=tsPath=>transformTypeScript(tsPath,transformOptions);moduleUrl=await this.#buildTypescriptModuleUrl(writeTransformedToCache,filePath,transformedCode,instanceID,moduleID,cacheBust,transform)}}else{const fileUrl=url.pathToFileURL(filePath).href;moduleUrl=`${fileUrl}?slothlet_instance=${instanceID}`;if(moduleID){moduleUrl+=`&module=${moduleID}`}if(cacheBust){moduleUrl+=`&_reload=${cacheBust}`}}const customImport=this.slothlet?.config?.import;const module=customImport?await customImport(moduleUrl):await import(moduleUrl);return module}catch(error){throw new this.SlothletError("MODULE_IMPORT_FAILED",{modulePath:filePath},error)}}#loadCJSIsolated(filePath){const requireFn=createRequire(filePath);const resolved=requireFn.resolve(filePath);delete requireFn.cache[resolved];const exports=requireFn(resolved);delete requireFn.cache[resolved];const namespace={default:exports};if(exports!==null&&typeof exports==="object"){for(const key of Object.keys(exports)){if(key!=="default"){namespace[key]=exports[key]}}}return namespace}async#buildTypescriptModuleUrl(writeTransformedToCache,filePath,code,instanceID,moduleID,cacheBust,transform){const{url:url2,cacheDir}=await writeTransformedToCache(filePath,code,instanceID,transform);(this.slothlet._typescriptCacheDirs??=new Set).add(cacheDir);let moduleUrl=`${url2}?slothlet_instance=${instanceID}`;if(moduleID){moduleUrl+=`&module=${moduleID}`}if(cacheBust){moduleUrl+=`&_reload=${cacheBust}`}return moduleUrl}async scanDirectory(dir,options={}){if(this.slothlet.envTarget==="browser"){return this.#scanDirectoryBrowser(dir,options)}const typescriptConfig=this.slothlet.config?.typescript;const defaultExtensions=[".mjs",".cjs",".js"];const typescriptExtensions=typescriptConfig?.enabled?[".ts",".mts"]:[];const allExtensions=[...defaultExtensions,...typescriptExtensions];const{recursive=true,extensions=allExtensions,isRootScan=true,currentDepth=0,maxDepth=DEFAULT_API_DEPTH,fileFilter=null,hidden=null,scanHiddenFolders=false,rootDir=dir}=options;const hiddenMatcher=typeof hidden==="function"?hidden:compileHidden(hidden);const apiRel=p=>path.relative(rootDir,p).split(path.sep).join(".");const hasHiddenPrefix=name=>name.startsWith(".")||name.startsWith("__");try{await fsp.stat(dir)}catch(error){throw new this.SlothletError("INVALID_DIRECTORY",{dir},error)}const structure={files:[],directories:[]};const entries=await fsp.readdir(dir,{withFileTypes:true});for(const entry of entries){const fullPath=path.join(dir,entry.name);if(entry.isDirectory()){if(fileFilter){continue}if(!scanHiddenFolders&&hasHiddenPrefix(entry.name)){continue}if(hiddenMatcher&&hiddenMatcher(apiRel(fullPath))){continue}if(recursive&&currentDepth<maxDepth){const subStructure=await this.scanDirectory(fullPath,{...options,isRootScan:false,currentDepth:currentDepth+1,hidden:hiddenMatcher,scanHiddenFolders,rootDir});if(subStructure.files.length===0&&subStructure.directories.length===0){continue}structure.directories.push({path:fullPath,name:entry.name,children:subStructure})}}else if(entry.isFile()){const ext=path.extname(entry.name);if(extensions.includes(ext)){if(hasHiddenPrefix(entry.name)){continue}if(fileFilter&&!fileFilter(entry.name)){continue}const nameWithoutExt=path.basename(entry.name,ext);if(hiddenMatcher&&hiddenMatcher(apiRel(path.join(dir,nameWithoutExt)))){continue}if(isFrameworkReservedKey(nameWithoutExt)){throw new this.SlothletError("MODULE_RESERVED_FILENAME",{file:entry.name,dir},null,{validationError:true})}structure.files.push({path:fullPath,name:nameWithoutExt,fullName:entry.name,moduleID:this.slothlet.helpers.sanitize.getModuleId(fullPath,dir)})}}}if(isRootScan&&structure.files.length===0&&structure.directories.length===0&&!this.____config?.silent){new this.SlothletWarning("WARN_DIRECTORY_EMPTY",{dir,resolvedPath:path.resolve(dir)})}return structure}#scanDirectoryBrowser(dir,options={}){const manifest=this.slothlet.config?.manifest;if(!manifest){throw new this.SlothletError("INVALID_CONFIG_BROWSER_REQUIRES_MANIFEST",{},null,{validationError:true})}const configBase=(this.slothlet.config?.dir||"").replace(/\/$/,"");let relativePath=(dir||"").replace(/\/$/,"");if(configBase&&relativePath.startsWith(configBase)){relativePath=relativePath.slice(configBase.length).replace(/^\/|\/$/g,"")}const isRoot=!relativePath||relativePath==="/"||relativePath===".";const node=isRoot?manifest:this.#findManifestNode(manifest,relativePath);if(!node){throw new this.SlothletError("INVALID_DIRECTORY",{dir},null)}return this.#manifestNodeToStructure(node,dir,options)}#findManifestNode(node,targetPath){const normalised=targetPath.replace(/\\/g,"/").replace(/^\/|\/$/g,"");for(const dir of node.directories||[]){const dirPath=(dir.path||dir.name||"").replace(/\\/g,"/").replace(/^\/|\/$/g,"");if(dirPath===normalised)return dir.children||dir;const found=this.#findManifestNode(dir.children||dir,normalised);if(found)return found}return null}#manifestNodeToStructure(node,rootPath,options={}){const{fileFilter=null}=options;const ALLOWED_EXTS=[".mjs",".cjs",".js"];const structure={files:[],directories:[]};for(const file of node.files||[]){const filePath=file.path||file.relativePath||"";const fullName=file.fullName||filePath.split("/").pop();const lastDot=fullName.lastIndexOf(".");const ext=lastDot>=0?fullName.slice(lastDot):"";if(!ALLOWED_EXTS.includes(ext))continue;if(fullName.startsWith("__"))continue;if(fileFilter&&!fileFilter(fullName))continue;const name=file.name||(lastDot>=0?fullName.slice(0,lastDot):fullName);if(isFrameworkReservedKey(name)){throw new this.SlothletError("MODULE_RESERVED_FILENAME",{file:fullName,dir:rootPath},null,{validationError:true})}structure.files.push({path:filePath,name,fullName,moduleID:this.slothlet.helpers.sanitize.getModuleId(filePath,rootPath)})}if(!fileFilter){for(const dir of node.directories||[]){const dirPath=dir.path||dir.name||"";structure.directories.push({path:dirPath,name:dir.name||dirPath.split("/").pop(),children:this.#manifestNodeToStructure(dir.children||dir,dirPath,options)})}}return structure}async#loadModuleBrowser(filePath){const resolveModuleSpecifier=this.slothlet.config?.resolveModuleSpecifier??(({path:p})=>{const base=this.slothlet.config?.base??this.slothlet.config?.dir??"";const leadingSlash=base.startsWith("/")?"":"/";const baseUrl=/^[a-zA-Z][\w+\-.]*:\/\//.test(base)?base.endsWith("/")?base:base+"/":"file://"+leadingSlash+base.replace(/\/?$/,"/");return new URL(p,baseUrl).href});const fullName=filePath.split("/").pop();const lastDot=fullName.lastIndexOf(".");const name=lastDot>=0?fullName.slice(0,lastDot):fullName;const specifier=resolveModuleSpecifier({path:filePath,name,fullName});const module=await import(specifier);return module}extractExports(module){const exports={};if(module.default!==void 0){exports.default=module.default}for(const key of Object.keys(module)){if(key!=="default"&&key!=="module.exports"&&typeof key==="string"){if(isFrameworkReservedKey(key)){throw new this.SlothletError("MODULE_RESERVED_EXPORT",{name:key},null,{validationError:true})}exports[key]=module[key]}}if(exports.default&&typeof exports.default==="object"&&exports.default!==null&&"default"in exports.default){const rootNamedKeys=Object.keys(exports).filter(k=>k!=="default"&&k!=="module.exports");const defaultKeys=Object.keys(exports.default).filter(k=>k!=="default");const isCJSPattern=rootNamedKeys.every(k=>k in exports.default);if(isCJSPattern&&defaultKeys.length>0){exports.default=exports.default.default}}return exports}}export{Loader,warnIfCoverageWithoutImporter};
17
+ import{ComponentBase}from"#factories/component-base";import{DEFAULT_API_DEPTH}from"@cldmv/slothlet/helpers/defaults";import{fsp,path,url,createRequire}from"@cldmv/slothlet/helpers/platform";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{isFrameworkReservedKey}from"#handlers/unified-wrapper";import{SlothletWarning}from"@cldmv/slothlet/errors";const RUNTIME_EXTERNALIZED=!("env"in import.meta);function warnIfCoverageWithoutImporter(config,{worker=globalThis.__vitest_worker__,externalized=RUNTIME_EXTERNALIZED}={}){if(worker?.config?.coverage?.enabled!==true)return false;if(!externalized)return false;if(typeof config?.import==="function")return false;if(config?.silent)return false;new SlothletWarning("WARNING_COVERAGE_IMPORTER_UNSET",{});return true}function compileHidden(globs){if(!globs)return null;const list=Array.isArray(globs)?globs:[globs];const rules=[];for(const g of list){if(typeof g!=="string"||g.length===0)continue;const negated=g.startsWith("!");const body=(negated?g.slice(1):g).replace(/\//g,".");if(body.length===0)continue;rules.push({negated,match:compilePattern(body)})}if(!rules.length)return null;return relDotted=>{let hidden=false;for(const rule of rules){if(rule.match(relDotted))hidden=!rule.negated}return hidden}}class Loader extends ComponentBase{static slothletProperty="loader";constructor(slothlet){super(slothlet)}async loadModule(filePath,instanceID,moduleID,cacheBust=null){try{if(this.slothlet.envTarget==="browser"){return this.#loadModuleBrowser(filePath)}if(filePath.endsWith(".cjs")){return this.#loadCJSIsolated(filePath)}const isTypeScript=filePath.endsWith(".ts")||filePath.endsWith(".mts");const typescriptConfig=this.slothlet.config?.typescript;let moduleUrl;if(isTypeScript&&typescriptConfig?.enabled){const mode=typescriptConfig.mode;if(mode==="strict"){if(!typescriptConfig.types?.output){throw new this.SlothletError("TS_STRICT_REQUIRES_OUTPUT",{},null,{validationError:true})}if(!typescriptConfig.types?.interfaceName){throw new this.SlothletError("TS_STRICT_REQUIRES_INTERFACE_NAME",{},null,{validationError:true})}if(!this.slothlet._typesGenerated){const{fork}=await import("child_process");const path2=await import("path");const{fileURLToPath}=await import("url");const __dirname=path2.dirname(fileURLToPath(import.meta.url));const scriptPath=path2.resolve(__dirname,"../../../tools/build/generate-types-worker.mjs");const childConfig=JSON.stringify({dir:this.slothlet.config.root||this.slothlet.config.dir,mode:"eager",typescript:{enabled:true,mode:"fast"},types:typescriptConfig.types});await new Promise((resolve,reject)=>{const child=fork(scriptPath,[],{stdio:["pipe","pipe","pipe","ipc"],env:{...process.env,SLOTHLET_CONFIG:childConfig}});let errorOutput="";child.stderr?.on("data",data=>{errorOutput+=data.toString()});child.on("message",msg=>{if(msg.type==="success"){this.slothlet._typesGenerated=true;resolve()}else if(msg.type==="error"){reject(new this.SlothletError("TS_TYPE_GENERATION_FAILED",{},{message:msg.error}))}});child.on("error",error=>{reject(new this.SlothletError("TS_TYPE_GENERATION_FORK_FAILED",{},error))});child.on("exit",code=>{if(code!==0&&!this.slothlet._typesGenerated){reject(new this.SlothletError("TS_TYPE_GENERATION_PROCESS_EXITED",{code,output:errorOutput},null,{validationError:true}))}})})}const{transformTypeScriptStrict,writeTransformedToCache,formatDiagnostics}=await import("@cldmv/slothlet/processors/typescript");const strictTransform=async tsPath=>{const result=await transformTypeScriptStrict(tsPath,{target:typescriptConfig.target,module:typescriptConfig.module,strict:typescriptConfig.strict,typeDefinitionPath:typescriptConfig.types.output,compilerOptions:typescriptConfig.compilerOptions});if(result.diagnostics&&result.diagnostics.length>0){const ts=await import("typescript");const errors=formatDiagnostics(result.diagnostics,ts.default);const error=new this.SlothletError("TS_TYPE_CHECK_ERRORS",{filePath:tsPath,errors:errors.join("\n")},null,{validationError:true});error.diagnostics=result.diagnostics;throw error}return result.code};const entryCode=await strictTransform(filePath);moduleUrl=await this.#buildTypescriptModuleUrl(writeTransformedToCache,filePath,entryCode,instanceID,moduleID,cacheBust,strictTransform)}else{const{transformTypeScript,writeTransformedToCache}=await import("@cldmv/slothlet/processors/typescript");const transformOptions={target:typescriptConfig.target,sourcemap:typescriptConfig.sourcemap};const transformedCode=await transformTypeScript(filePath,transformOptions);const transform=tsPath=>transformTypeScript(tsPath,transformOptions);moduleUrl=await this.#buildTypescriptModuleUrl(writeTransformedToCache,filePath,transformedCode,instanceID,moduleID,cacheBust,transform)}}else{const fileUrl=url.pathToFileURL(filePath).href;moduleUrl=`${fileUrl}?slothlet_instance=${instanceID}`;if(moduleID){moduleUrl+=`&module=${moduleID}`}if(cacheBust){moduleUrl+=`&_reload=${cacheBust}`}}const customImport=this.slothlet?.config?.import;const module=customImport?await customImport(moduleUrl):await import(moduleUrl);return module}catch(error){throw new this.SlothletError("MODULE_IMPORT_FAILED",{modulePath:filePath},error)}}#loadCJSIsolated(filePath){const requireFn=createRequire(filePath);const resolved=requireFn.resolve(filePath);delete requireFn.cache[resolved];const exports=requireFn(resolved);delete requireFn.cache[resolved];const namespace={default:exports};if(exports!==null&&typeof exports==="object"){for(const key of Object.keys(exports)){if(key!=="default"){namespace[key]=exports[key]}}}return namespace}async#buildTypescriptModuleUrl(writeTransformedToCache,filePath,code,instanceID,moduleID,cacheBust,transform){const{url:url2,cacheDir}=await writeTransformedToCache(filePath,code,instanceID,transform);(this.slothlet._typescriptCacheDirs??=new Set).add(cacheDir);let moduleUrl=`${url2}?slothlet_instance=${instanceID}`;if(moduleID){moduleUrl+=`&module=${moduleID}`}if(cacheBust){moduleUrl+=`&_reload=${cacheBust}`}return moduleUrl}async scanDirectory(dir,options={}){if(this.slothlet.envTarget==="browser"){return this.#scanDirectoryBrowser(dir,options)}const typescriptConfig=this.slothlet.config?.typescript;const defaultExtensions=[".mjs",".cjs",".js"];const typescriptExtensions=typescriptConfig?.enabled?[".ts",".mts"]:[];const allExtensions=[...defaultExtensions,...typescriptExtensions];const{recursive=true,extensions=allExtensions,isRootScan=true,currentDepth=0,maxDepth=DEFAULT_API_DEPTH,fileFilter=null,hidden=null,scanHiddenFolders=false,rootDir=dir}=options;const hiddenMatcher=typeof hidden==="function"?hidden:compileHidden(hidden);const apiRel=p=>path.relative(rootDir,p).split(path.sep).join(".");const hasHiddenPrefix=name=>name.startsWith(".")||name.startsWith("__");try{await fsp.stat(dir)}catch(error){throw new this.SlothletError("INVALID_DIRECTORY",{dir},error)}const structure={files:[],directories:[]};const entries=await fsp.readdir(dir,{withFileTypes:true});for(const entry of entries){const fullPath=path.join(dir,entry.name);if(entry.isDirectory()){if(fileFilter){continue}if(!scanHiddenFolders&&hasHiddenPrefix(entry.name)){continue}if(hiddenMatcher&&hiddenMatcher(apiRel(fullPath))){continue}if(recursive&&currentDepth<maxDepth){const subStructure=await this.scanDirectory(fullPath,{...options,isRootScan:false,currentDepth:currentDepth+1,hidden:hiddenMatcher,scanHiddenFolders,rootDir});if(subStructure.files.length===0&&subStructure.directories.length===0){continue}structure.directories.push({path:fullPath,name:entry.name,children:subStructure})}}else if(entry.isFile()){const ext=path.extname(entry.name);if(extensions.includes(ext)){if(hasHiddenPrefix(entry.name)){continue}if(fileFilter&&!fileFilter(entry.name)){continue}const nameWithoutExt=path.basename(entry.name,ext);if(hiddenMatcher&&hiddenMatcher(apiRel(path.join(dir,nameWithoutExt)))){continue}if(isFrameworkReservedKey(nameWithoutExt)){throw new this.SlothletError("MODULE_RESERVED_FILENAME",{file:entry.name,dir},null,{validationError:true})}structure.files.push({path:fullPath,name:nameWithoutExt,fullName:entry.name,moduleID:this.slothlet.helpers.sanitize.getModuleId(fullPath,dir)})}}}if(isRootScan&&structure.files.length===0&&structure.directories.length===0&&!this.____config?.silent){new this.SlothletWarning("WARN_DIRECTORY_EMPTY",{dir,resolvedPath:path.resolve(dir)})}return structure}#scanDirectoryBrowser(dir,options={}){const manifest=this.slothlet.config?.manifest;if(!manifest){throw new this.SlothletError("INVALID_CONFIG_BROWSER_REQUIRES_MANIFEST",{},null,{validationError:true})}const configBase=(this.slothlet.config?.dir||"").replace(/\/$/,"");let relativePath=(dir||"").replace(/\/$/,"");if(configBase&&relativePath.startsWith(configBase)){relativePath=relativePath.slice(configBase.length).replace(/^\/|\/$/g,"")}const isRoot=!relativePath||relativePath==="/"||relativePath===".";const node=isRoot?manifest:this.#findManifestNode(manifest,relativePath);if(!node){throw new this.SlothletError("INVALID_DIRECTORY",{dir},null)}return this.#manifestNodeToStructure(node,dir,options)}#findManifestNode(node,targetPath){const normalised=targetPath.replace(/\\/g,"/").replace(/^\/|\/$/g,"");for(const dir of node.directories||[]){const dirPath=(dir.path||dir.name||"").replace(/\\/g,"/").replace(/^\/|\/$/g,"");if(dirPath===normalised)return dir.children||dir;const found=this.#findManifestNode(dir.children||dir,normalised);if(found)return found}return null}#manifestNodeToStructure(node,rootPath,options={}){const{fileFilter=null,hidden=null,scanHiddenFolders=false,maxDepth=DEFAULT_API_DEPTH,currentDepth=0,apiPrefix=""}=options;const ALLOWED_EXTS=[".mjs",".cjs",".js"];const hiddenMatcher=typeof hidden==="function"?hidden:compileHidden(hidden);const hasHiddenPrefix=n=>n.startsWith(".")||n.startsWith("__");const structure={files:[],directories:[]};for(const file of node.files||[]){const filePath=file.path||file.relativePath||"";const fullName=file.fullName||filePath.split("/").pop();const lastDot=fullName.lastIndexOf(".");const ext=lastDot>=0?fullName.slice(lastDot):"";if(!ALLOWED_EXTS.includes(ext))continue;if(hasHiddenPrefix(fullName))continue;if(fileFilter&&!fileFilter(fullName))continue;const name=file.name||(lastDot>=0?fullName.slice(0,lastDot):fullName);if(hiddenMatcher&&hiddenMatcher(apiPrefix?`${apiPrefix}.${name}`:name))continue;if(isFrameworkReservedKey(name)){throw new this.SlothletError("MODULE_RESERVED_FILENAME",{file:fullName,dir:rootPath},null,{validationError:true})}structure.files.push({path:filePath,name,fullName,moduleID:this.slothlet.helpers.sanitize.getModuleId(filePath,rootPath)})}if(!fileFilter){for(const dir of node.directories||[]){const dirPath=dir.path||dir.name||"";const dirName=dir.name||dirPath.split("/").pop();if(!scanHiddenFolders&&hasHiddenPrefix(dirName))continue;const dirApiRel=apiPrefix?`${apiPrefix}.${dirName}`:dirName;if(hiddenMatcher&&hiddenMatcher(dirApiRel))continue;if(currentDepth>=maxDepth)continue;const children=this.#manifestNodeToStructure(dir.children||dir,dirPath,{...options,hidden:hiddenMatcher,currentDepth:currentDepth+1,apiPrefix:dirApiRel});if(children.files.length===0&&children.directories.length===0)continue;structure.directories.push({path:dirPath,name:dirName,children})}}return structure}async#loadModuleBrowser(filePath){const resolveModuleSpecifier=this.slothlet.config?.resolveModuleSpecifier??(({path:p})=>{const base=this.slothlet.config?.base??this.slothlet.config?.dir??"";const leadingSlash=base.startsWith("/")?"":"/";const baseUrl=/^[a-zA-Z][\w+\-.]*:\/\//.test(base)?base.endsWith("/")?base:base+"/":"file://"+leadingSlash+base.replace(/\/?$/,"/");return new URL(p,baseUrl).href});const fullName=filePath.split("/").pop();const lastDot=fullName.lastIndexOf(".");const name=lastDot>=0?fullName.slice(0,lastDot):fullName;const specifier=resolveModuleSpecifier({path:filePath,name,fullName});const module=await import(specifier);return module}extractExports(module){const exports={};if(module.default!==void 0){exports.default=module.default}for(const key of Object.keys(module)){if(key!=="default"&&key!=="module.exports"&&typeof key==="string"){if(isFrameworkReservedKey(key)){throw new this.SlothletError("MODULE_RESERVED_EXPORT",{name:key},null,{validationError:true})}exports[key]=module[key]}}if(exports.default&&typeof exports.default==="object"&&exports.default!==null&&"default"in exports.default){const rootNamedKeys=Object.keys(exports).filter(k=>k!=="default"&&k!=="module.exports");const defaultKeys=Object.keys(exports.default).filter(k=>k!=="default");const isCJSPattern=rootNamedKeys.every(k=>k in exports.default);if(isCJSPattern&&defaultKeys.length>0){exports.default=exports.default.default}}return exports}}export{Loader,warnIfCoverageWithoutImporter};