@stacksjs/desktop-build 0.75.420 → 0.75.422
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-launcher.d.ts +9 -0
- package/dist/agent-launcher.js +3 -3
- package/dist/macos-app.d.ts +2 -0
- package/dist/macos-app.js +1 -1
- package/dist/macos-bundle.js +1 -1
- package/package.json +8 -8
package/dist/agent-launcher.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { AgentHandler } from './agent';
|
|
2
|
+
/**
|
|
3
|
+
* Where a file or directory named in `macos.agent.resources` is: inside the
|
|
4
|
+
* built app's Resources, or in the project under `buddy dev:desktop`.
|
|
5
|
+
* Undefined outside a desktop agent, or for a name the config does not list.
|
|
6
|
+
*/
|
|
7
|
+
export declare function desktopResource(name: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
2
8
|
export declare function desktopAgentPaths(appName: string, home?: string): DesktopAgentPaths;
|
|
3
9
|
/** Take the lock, or return false when another live copy of the app holds it. A lock left by a crash is taken over. */
|
|
4
10
|
export declare function acquireInstanceLock(lockFile: string, pid?: number): boolean;
|
|
@@ -44,6 +50,8 @@ export declare const AGENT_MANIFEST_ENV: 'STACKS_DESKTOP_AGENT_MANIFEST';
|
|
|
44
50
|
* Support (`desktopAgentPaths().dataDir`), where a route keeps its own database.
|
|
45
51
|
*/
|
|
46
52
|
export declare const AGENT_DATA_DIR_ENV: 'STACKS_DESKTOP_DATA_DIR';
|
|
53
|
+
/** Set for the agent's routes before they load: `macos.agent.resources` as JSON, name to absolute path. */
|
|
54
|
+
export declare const AGENT_RESOURCES_ENV: 'STACKS_DESKTOP_RESOURCES';
|
|
47
55
|
/**
|
|
48
56
|
* The executable of a desktop app that runs its own API (`macos.agent`). It
|
|
49
57
|
* holds the single-instance lock, starts the agent, and spawns Craft's AppKit
|
|
@@ -64,6 +72,7 @@ export declare interface DesktopAgentManifest {
|
|
|
64
72
|
hostArguments: string[]
|
|
65
73
|
port?: number
|
|
66
74
|
staticDir?: string
|
|
75
|
+
resources?: Record<string, string>
|
|
67
76
|
projectRoot?: string
|
|
68
77
|
appRoutes: boolean
|
|
69
78
|
}
|
package/dist/agent-launcher.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{appendFileSync,existsSync,mkdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,isAbsolute,join,resolve}from"node:path";import process from"node:process";import{fileURLToPath}from"node:url";import{format}from"node:util";import{startDesktopAgent}from"./agent";export const AGENT_MANIFEST="desktop-agent.json",AGENT_MANIFEST_ENV="STACKS_DESKTOP_AGENT_MANIFEST",AGENT_DATA_DIR_ENV="STACKS_DESKTOP_DATA_DIR";export function desktopAgentPaths(appName,home=homedir()){const dataDir=join(home,"Library","Application Support",appName);return{dataDir,portFile:join(dataDir,"agent.port"),lockFile:join(dataDir,"agent.pid"),logFile:join(home,"Library","Logs",`${appName}.log`)}}export function acquireInstanceLock(lockFile,pid=process.pid){mkdirSync(dirname(lockFile),{recursive:!0});if(existsSync(lockFile)){const holder=Number(readFileSync(lockFile,"utf8").trim());if(holder&&holder!==pid)try{process.kill(holder,0);return!1}catch(error){if(error.code==="EPERM")return!1}}writeFileSync(lockFile,String(pid));return!0}export function releaseInstanceLock(lockFile,pid=process.pid){try{if(readFileSync(lockFile,"utf8").trim()===String(pid))rmSync(lockFile)}catch{}}export function logToFileWhenDetached(logFile,isTTY=Boolean(process.stdout.isTTY)){if(isTTY)return!1;mkdirSync(dirname(logFile),{recursive:!0});for(const level of["log","info","warn","error","debug"])console[level]=(...args)=>{try{appendFileSync(logFile,`${new Date().toISOString()} ${level==="log"?"":`[${level}] `}${format(...args)}
|
|
2
|
-
`)}catch{}};return!0}export function findAgentManifest(execPath=process.execPath,env=process.env){if(env[AGENT_MANIFEST_ENV])return resolve(env[AGENT_MANIFEST_ENV]);const bundled=join(dirname(execPath),"..","Resources",AGENT_MANIFEST);return existsSync(bundled)?bundled:null}export function readAgentManifest(file){const manifest=JSON.parse(readFileSync(file,"utf8")),base=dirname(file),at=(path)=>isAbsolute(path)?path:resolve(base,path);return{...manifest,host:at(manifest.host),nativeBundle:at(manifest.nativeBundle),...manifest.staticDir?{staticDir:at(manifest.staticDir)}:{}}}export function agentHostCommand(manifest,agentUrl){return[manifest.host,"--api-url",agentUrl,"--native-bundle",manifest.nativeBundle,...manifest.hostArguments]}export async function stacksAgentHandler(options){try{const{overridesReady}=await import("@stacksjs/config");await overridesReady}catch(error){console.warn(`[agent] config not loaded: ${error instanceof Error?error.message:String(error)}`)}if(options.appRoutes)try{const{injectGlobalAutoImports}=await import("@stacksjs/server");await injectGlobalAutoImports()}catch(error){console.warn(`[agent] models and framework globals not injected: ${error instanceof Error?error.message:String(error)}`)}const{provideActions,route,serverResponse}=await import("@stacksjs/router");if(options.actions)provideActions(options.actions);const module=options.entry?await options.entry():void 0;if(typeof module?.default==="function")return module.default;if(options.appRoutes)return(request)=>serverResponse(request);for(const file of options.routes??[])if(file.prefix||file.middleware?.length)await route.group({prefix:file.prefix,middleware:file.middleware?.length?file.middleware:void 0},async()=>{await file.load()});else await file.load();return(request)=>route.handleRequest(request)}export function renderAgentEntry(options={}){const launcher=options.launcherModule??fileURLToPath(import.meta.url),entry=options.agentEntry?`() => import(${JSON.stringify(options.agentEntry)})`:"undefined",routes=options.routes?.length?`[
|
|
1
|
+
import{appendFileSync,existsSync,mkdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,isAbsolute,join,resolve}from"node:path";import process from"node:process";import{fileURLToPath}from"node:url";import{format}from"node:util";import{startDesktopAgent}from"./agent";export const AGENT_MANIFEST="desktop-agent.json",AGENT_MANIFEST_ENV="STACKS_DESKTOP_AGENT_MANIFEST",AGENT_DATA_DIR_ENV="STACKS_DESKTOP_DATA_DIR",AGENT_RESOURCES_ENV="STACKS_DESKTOP_RESOURCES";export function desktopResource(name,env=process.env){try{const resources=JSON.parse(env[AGENT_RESOURCES_ENV]??"{}");return typeof resources[name]==="string"?resources[name]:void 0}catch{return}}export function desktopAgentPaths(appName,home=homedir()){const dataDir=join(home,"Library","Application Support",appName);return{dataDir,portFile:join(dataDir,"agent.port"),lockFile:join(dataDir,"agent.pid"),logFile:join(home,"Library","Logs",`${appName}.log`)}}export function acquireInstanceLock(lockFile,pid=process.pid){mkdirSync(dirname(lockFile),{recursive:!0});if(existsSync(lockFile)){const holder=Number(readFileSync(lockFile,"utf8").trim());if(holder&&holder!==pid)try{process.kill(holder,0);return!1}catch(error){if(error.code==="EPERM")return!1}}writeFileSync(lockFile,String(pid));return!0}export function releaseInstanceLock(lockFile,pid=process.pid){try{if(readFileSync(lockFile,"utf8").trim()===String(pid))rmSync(lockFile)}catch{}}export function logToFileWhenDetached(logFile,isTTY=Boolean(process.stdout.isTTY)){if(isTTY)return!1;mkdirSync(dirname(logFile),{recursive:!0});for(const level of["log","info","warn","error","debug"])console[level]=(...args)=>{try{appendFileSync(logFile,`${new Date().toISOString()} ${level==="log"?"":`[${level}] `}${format(...args)}
|
|
2
|
+
`)}catch{}};return!0}export function findAgentManifest(execPath=process.execPath,env=process.env){if(env[AGENT_MANIFEST_ENV])return resolve(env[AGENT_MANIFEST_ENV]);const bundled=join(dirname(execPath),"..","Resources",AGENT_MANIFEST);return existsSync(bundled)?bundled:null}export function readAgentManifest(file){const manifest=JSON.parse(readFileSync(file,"utf8")),base=dirname(file),at=(path)=>isAbsolute(path)?path:resolve(base,path);return{...manifest,host:at(manifest.host),nativeBundle:at(manifest.nativeBundle),...manifest.staticDir?{staticDir:at(manifest.staticDir)}:{},...manifest.resources?{resources:Object.fromEntries(Object.entries(manifest.resources).map(([name,path])=>[name,at(path)]))}:{}}}export function agentHostCommand(manifest,agentUrl){return[manifest.host,"--api-url",agentUrl,"--native-bundle",manifest.nativeBundle,...manifest.hostArguments]}export async function stacksAgentHandler(options){try{const{overridesReady}=await import("@stacksjs/config");await overridesReady}catch(error){console.warn(`[agent] config not loaded: ${error instanceof Error?error.message:String(error)}`)}if(options.appRoutes)try{const{injectGlobalAutoImports}=await import("@stacksjs/server");await injectGlobalAutoImports()}catch(error){console.warn(`[agent] models and framework globals not injected: ${error instanceof Error?error.message:String(error)}`)}const{provideActions,route,serverResponse}=await import("@stacksjs/router");if(options.actions)provideActions(options.actions);const module=options.entry?await options.entry():void 0;if(typeof module?.default==="function")return module.default;if(options.appRoutes)return(request)=>serverResponse(request);for(const file of options.routes??[])if(file.prefix||file.middleware?.length)await route.group({prefix:file.prefix,middleware:file.middleware?.length?file.middleware:void 0},async()=>{await file.load()});else await file.load();return(request)=>route.handleRequest(request)}export function renderAgentEntry(options={}){const launcher=options.launcherModule??fileURLToPath(import.meta.url),entry=options.agentEntry?`() => import(${JSON.stringify(options.agentEntry)})`:"undefined",routes=options.routes?.length?`[
|
|
3
3
|
${options.routes.map((file)=>` { load: () => import(${JSON.stringify(file.file)})${file.prefix?`, prefix: ${JSON.stringify(file.prefix)}`:""}${file.middleware?.length?`, middleware: ${JSON.stringify(file.middleware)}`:""} },`).join(`
|
|
4
4
|
`)}
|
|
5
5
|
]`:"[]",actionEntries=Object.entries(options.actions??{}),actions=actionEntries.length?`{
|
|
@@ -19,6 +19,6 @@ process.exit(await runDesktopAgentApp({
|
|
|
19
19
|
manifest,
|
|
20
20
|
handler: () => stacksAgentHandler({ entry: ${entry}, appRoutes: manifest.appRoutes, routes: ${routes}, actions: ${actions} }),
|
|
21
21
|
}))
|
|
22
|
-
`}export async function runDesktopAgentApp(options){const{manifest}=options,paths=options.paths??desktopAgentPaths(manifest.appName),tag=`[${manifest.appName}]`;if(options.logWhenDetached!==!1)logToFileWhenDetached(paths.logFile);if(!acquireInstanceLock(paths.lockFile)){console.log(`${tag} already running`);return 0}if(!existsSync(manifest.host)){console.error(`${tag} the AppKit host is missing: ${manifest.host}`);releaseInstanceLock(paths.lockFile);return 1}if(!existsSync(manifest.nativeBundle)){console.error(`${tag} the native screens are missing: ${manifest.nativeBundle}`);releaseInstanceLock(paths.lockFile);return 1}let agent;try{if(manifest.projectRoot&&manifest.appRoutes)process.chdir(manifest.projectRoot);process.env[AGENT_DATA_DIR_ENV]=paths.dataDir;agent=await startDesktopAgent({handler:await options.handler(),name:manifest.appName,version:manifest.version,port:manifest.port,portFile:paths.portFile,staticDir:manifest.staticDir})}catch(error){console.error(`${tag} could not start: ${error instanceof Error?error.stack??error.message:String(error)}`);releaseInstanceLock(paths.lockFile);return 1}console.log(`${tag} ${manifest.version} serving on ${agent.url}`);const command=agentHostCommand(manifest,agent.url),host=options.spawn?options.spawn(command):(()=>{const child=Bun.spawn(command,{stdio:["ignore","inherit","pipe"]});new Response(child.stderr).text().then((stderr)=>{const lines=stderr.split(`
|
|
22
|
+
`}export async function runDesktopAgentApp(options){const{manifest}=options,paths=options.paths??desktopAgentPaths(manifest.appName),tag=`[${manifest.appName}]`;if(options.logWhenDetached!==!1)logToFileWhenDetached(paths.logFile);if(!acquireInstanceLock(paths.lockFile)){console.log(`${tag} already running`);return 0}if(!existsSync(manifest.host)){console.error(`${tag} the AppKit host is missing: ${manifest.host}`);releaseInstanceLock(paths.lockFile);return 1}if(!existsSync(manifest.nativeBundle)){console.error(`${tag} the native screens are missing: ${manifest.nativeBundle}`);releaseInstanceLock(paths.lockFile);return 1}let agent;try{if(manifest.projectRoot&&manifest.appRoutes)process.chdir(manifest.projectRoot);process.env[AGENT_DATA_DIR_ENV]=paths.dataDir;process.env[AGENT_RESOURCES_ENV]=JSON.stringify(manifest.resources??{});agent=await startDesktopAgent({handler:await options.handler(),name:manifest.appName,version:manifest.version,port:manifest.port,portFile:paths.portFile,staticDir:manifest.staticDir})}catch(error){console.error(`${tag} could not start: ${error instanceof Error?error.stack??error.message:String(error)}`);releaseInstanceLock(paths.lockFile);return 1}console.log(`${tag} ${manifest.version} serving on ${agent.url}`);const command=agentHostCommand(manifest,agent.url),host=options.spawn?options.spawn(command):(()=>{const child=Bun.spawn(command,{stdio:["ignore","inherit","pipe"]});new Response(child.stderr).text().then((stderr)=>{const lines=stderr.split(`
|
|
23
23
|
`).filter((line)=>line.trim());if(lines.length)console.error(`[craft] ${lines.join(`
|
|
24
24
|
`)}`)});return{exited:child.exited,kill:()=>child.kill()}})();let stopped=!1;const stop=()=>{if(stopped)return;stopped=!0;host.kill();agent.stop();releaseInstanceLock(paths.lockFile)},onSignal=()=>{stop();process.exit(0)};for(const signal of["SIGTERM","SIGINT","SIGHUP"])process.on(signal,onSignal);const code=await host.exited;console.log(`${tag} window closed (${code})`);for(const signal of["SIGTERM","SIGINT","SIGHUP"])process.off(signal,onSignal);stop();return 0}
|
package/dist/macos-app.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export declare const MACOS_DEFAULTS: {
|
|
|
72
72
|
minHeight: 520
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
|
+
/** What the build writes into `Contents/Resources` itself, so `macos.agent.resources` cannot use these names. */
|
|
76
|
+
export declare const RESERVED_RESOURCES: ReadonlySet<string>;
|
|
75
77
|
/** What `defaultBrowser` declares the app opens, by Uniform Type Identifier. */
|
|
76
78
|
export declare const BROWSER_DOCUMENT_TYPES: ReadonlyArray<{ name: string, types: string[] }>;
|
|
77
79
|
/** The icon sizes `iconutil` wants in an `.iconset`, from one 1024 pixel PNG. */
|
package/dist/macos-app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync}from"node:fs";import{join,resolve}from"node:path";import{pathToFileURL}from"node:url";import{plistValue,renderUserlandPlistEntries,xmlEscape}from"./index";export const MACOS_DEFAULTS={minimumSystemVersion:"13.0",category:"public.app-category.productivity",nativeScreensDir:"resources/native",initialScreen:"Main",window:{width:1180,height:760,minWidth:820,minHeight:520}};export async function loadDesktopConfig(root){const file=join(root,"config/desktop.ts");if(!existsSync(file))return null;return(await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).default??null}export async function hasMacosDesktop(root){return Boolean((await loadDesktopConfig(root))?.macos)}export async function loadMacosDesktop(root,packageVersion="0.0.0"){const config=await loadDesktopConfig(root);return config?.macos?resolveMacosDesktop(config.macos,packageVersion):null}export function resolveMacosDesktop(config,packageVersion="0.0.0"){const version=config.version??packageVersion,agent=config.agent?config.agent===!0?{}:{...config.agent}:null;return{...config,agent,url:agent?"http://127.0.0.1":config.url??"",version,buildNumber:config.buildNumber??version.split(/[-+]/)[0],minimumSystemVersion:config.minimumSystemVersion??MACOS_DEFAULTS.minimumSystemVersion,category:config.category??MACOS_DEFAULTS.category,nativeScreensDir:config.nativeScreensDir??MACOS_DEFAULTS.nativeScreensDir,initialScreen:config.initialScreen??(Array.isArray(config.nativeScreens)?config.nativeScreens.includes("Main")?"Main":config.nativeScreens[0]:Object.keys(config.nativeScreens).includes("Main")?"Main":Object.keys(config.nativeScreens)[0])??MACOS_DEFAULTS.initialScreen,sandbox:config.sandbox!==!1,executable:config.appName}}export function validateMacosDesktop(config){const errors=[];if(!config.appName?.trim())errors.push("macos.appName is required");if(!/^[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z0-9.-]+$/.test(config.bundleId??""))errors.push("macos.bundleId must be a reverse-DNS identifier such as com.example.app");if(config.teamId!==void 0&&!/^[A-Z0-9]{10}$/.test(config.teamId))errors.push("macos.teamId must be the 10-character Apple Developer team ID");if(!/^\d+(?:\.\d+){0,2}$/.test(config.version))errors.push(`macos.version must be one to three numeric components, not "${config.version}"`);if(!/^[A-Za-z0-9.-]+$/.test(config.buildNumber))errors.push("macos.buildNumber must contain only letters, numbers, periods and hyphens");if(!/^\d+(?:\.\d+){0,2}$/.test(config.minimumSystemVersion))errors.push("macos.minimumSystemVersion must be a macOS version such as 13.0");if(!/^public\.app-category\.[a-z][a-z0-9-]*$/.test(config.category))errors.push("macos.category must be an LSApplicationCategoryType such as public.app-category.productivity");if(!config.agent)try{const url=new URL(config.url);if(!["http:","https:"].includes(url.protocol))errors.push("macos.url must be an http(s) origin")}catch{errors.push("macos.url must be the server origin the app talks to, e.g. https://example.com, or turn on macos.agent to run the API inside the app")}else
|
|
1
|
+
import{existsSync}from"node:fs";import{join,resolve}from"node:path";import{pathToFileURL}from"node:url";import{plistValue,renderUserlandPlistEntries,xmlEscape}from"./index";export const MACOS_DEFAULTS={minimumSystemVersion:"13.0",category:"public.app-category.productivity",nativeScreensDir:"resources/native",initialScreen:"Main",window:{width:1180,height:760,minWidth:820,minHeight:520}},RESERVED_RESOURCES=new Set(["web","native-screen.js","craft-app.json","desktop-agent.json","PrivacyInfo.xcprivacy","AppIcon.icns"]);export async function loadDesktopConfig(root){const file=join(root,"config/desktop.ts");if(!existsSync(file))return null;return(await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).default??null}export async function hasMacosDesktop(root){return Boolean((await loadDesktopConfig(root))?.macos)}export async function loadMacosDesktop(root,packageVersion="0.0.0"){const config=await loadDesktopConfig(root);return config?.macos?resolveMacosDesktop(config.macos,packageVersion):null}export function resolveMacosDesktop(config,packageVersion="0.0.0"){const version=config.version??packageVersion,agent=config.agent?config.agent===!0?{}:{...config.agent}:null;return{...config,agent,url:agent?"http://127.0.0.1":config.url??"",version,buildNumber:config.buildNumber??version.split(/[-+]/)[0],minimumSystemVersion:config.minimumSystemVersion??MACOS_DEFAULTS.minimumSystemVersion,category:config.category??MACOS_DEFAULTS.category,nativeScreensDir:config.nativeScreensDir??MACOS_DEFAULTS.nativeScreensDir,initialScreen:config.initialScreen??(Array.isArray(config.nativeScreens)?config.nativeScreens.includes("Main")?"Main":config.nativeScreens[0]:Object.keys(config.nativeScreens).includes("Main")?"Main":Object.keys(config.nativeScreens)[0])??MACOS_DEFAULTS.initialScreen,sandbox:config.sandbox!==!1,executable:config.appName}}export function validateMacosDesktop(config){const errors=[];if(!config.appName?.trim())errors.push("macos.appName is required");if(!/^[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z0-9.-]+$/.test(config.bundleId??""))errors.push("macos.bundleId must be a reverse-DNS identifier such as com.example.app");if(config.teamId!==void 0&&!/^[A-Z0-9]{10}$/.test(config.teamId))errors.push("macos.teamId must be the 10-character Apple Developer team ID");if(!/^\d+(?:\.\d+){0,2}$/.test(config.version))errors.push(`macos.version must be one to three numeric components, not "${config.version}"`);if(!/^[A-Za-z0-9.-]+$/.test(config.buildNumber))errors.push("macos.buildNumber must contain only letters, numbers, periods and hyphens");if(!/^\d+(?:\.\d+){0,2}$/.test(config.minimumSystemVersion))errors.push("macos.minimumSystemVersion must be a macOS version such as 13.0");if(!/^public\.app-category\.[a-z][a-z0-9-]*$/.test(config.category))errors.push("macos.category must be an LSApplicationCategoryType such as public.app-category.productivity");if(!config.agent)try{const url=new URL(config.url);if(!["http:","https:"].includes(url.protocol))errors.push("macos.url must be an http(s) origin")}catch{errors.push("macos.url must be the server origin the app talks to, e.g. https://example.com, or turn on macos.agent to run the API inside the app")}else{if(config.agent.port!==void 0&&!(Number.isInteger(config.agent.port)&&config.agent.port>1024&&config.agent.port<65536))errors.push("macos.agent.port must be a port above 1024");for(const[name,path]of Object.entries(config.agent.resources??{})){if(!/^[\w-][\w.-]*$/.test(name)||RESERVED_RESOURCES.has(name))errors.push(`macos.agent.resources: "${name}" must be a plain file name (letters, digits, . _ -) other than ${[...RESERVED_RESOURCES].join(", ")}`);if(typeof path!=="string"||!path.trim())errors.push(`macos.agent.resources.${name} must be a path in the project`)}}const lights=config.window?.trafficLights;if(lights!==void 0&&!(Array.isArray(lights)&&lights.length===2&&lights.every((value)=>Number.isFinite(value)&&value>=0)))errors.push("macos.window.trafficLights must be [x, y], two points from the window's top-left");for(const[index,item]of(config.dockMenu??[]).entries()){if(!item?.title?.trim())errors.push(`macos.dockMenu[${index}] needs a title`);if(!item?.action?.trim())errors.push(`macos.dockMenu[${index}] needs an action`)}const names=Object.keys(macosScreens(config));if(!names.length)errors.push("macos.nativeScreens must name at least one screen");if(names.length&&!names.includes(config.initialScreen))errors.push(`macos.initialScreen "${config.initialScreen}" is not one of the native screens (${names.join(", ")})`);for(const scheme of config.urlSchemes??[])if(!/^[a-z][a-z0-9+.-]*$/.test(scheme))errors.push(`macos.urlSchemes: "${scheme}" is not a valid URL scheme`);return errors}export function macosScreens(config){const dir=config.nativeScreensDir??MACOS_DEFAULTS.nativeScreensDir;if(Array.isArray(config.nativeScreens))return Object.fromEntries(config.nativeScreens.map((name)=>[name,`${dir}/${name}.stx`]));return{...config.nativeScreens}}export function renderHostConfig(config){const window={...MACOS_DEFAULTS.window,...config.window};return`${JSON.stringify({...config.agent?{}:{apiUrl:config.url.replace(/\/+$/,"")},title:config.appName,route:config.initialScreen,session:Boolean(config.agent),titlebarStyle:window.titlebarStyle??"standard",width:window.width,height:window.height,minWidth:window.minWidth,minHeight:window.minHeight,...window.trafficLights?{trafficLights:window.trafficLights}:{},...config.webkit?{webkit:!0}:{},...config.dockMenu?.length?{dockMenu:config.dockMenu.map((item)=>({title:item.title,action:item.action}))}:{}},null,2)}
|
|
2
2
|
`}export function hostArguments(config,nativeBundle){return["--api-url",config.url.replace(/\/+$/,""),"--native-bundle",nativeBundle,...windowArguments(config),"--no-session"]}export function agentHostArguments(config){return windowArguments(config)}function windowArguments(config){const window={...MACOS_DEFAULTS.window,...config.window};return["--title",config.appName,"--route",config.initialScreen,"--titlebar-style",window.titlebarStyle??"standard","--width",String(window.width),"--height",String(window.height),"--min-width",String(window.minWidth),"--min-height",String(window.minHeight),...window.trafficLights?["--traffic-lights",window.trafficLights.join(",")]:[]]}const OWNED_KEYS=["CFBundleIdentifier","CFBundleExecutable","CFBundleName","CFBundleDisplayName","CFBundleShortVersionString","CFBundleVersion","CFBundlePackageType","LSApplicationCategoryType","LSMinimumSystemVersion","CFBundleIconFile","CFBundleURLTypes"];export const BROWSER_DOCUMENT_TYPES=[{name:"HTML document",types:["public.html"]},{name:"XHTML document",types:["public.xhtml"]},{name:"Web archive",types:["com.apple.webarchive"]},{name:"PDF document",types:["com.adobe.pdf"]}];export function defaultUsageDescriptions(appName){return{camera:`${appName} uses the camera when you allow a page or feature to.`,microphone:`${appName} uses the microphone when you allow a page or feature to.`,location:`${appName} uses your location when you allow a page or feature to.`}}export function permissionUsageDescriptions(config){const permissions=config.permissions??{},defaults=defaultUsageDescriptions(config.appName),sentence=(value,fallback)=>typeof value==="string"&&value.trim()?value:value?fallback:void 0,usage={},camera=sentence(permissions.camera,defaults.camera);if(camera)usage.NSCameraUsageDescription=camera;const microphone=sentence(permissions.microphone,defaults.microphone);if(microphone)usage.NSMicrophoneUsageDescription=microphone;const location=sentence(permissions.location,defaults.location);if(location){usage.NSLocationUsageDescription=location;usage.NSLocationWhenInUseUsageDescription=location}return usage}export function renderMacosInfoPlist(config,options={}){const owns=config.defaultBrowser?[...OWNED_KEYS,"CFBundleDocumentTypes"]:OWNED_KEYS,declared={...permissionUsageDescriptions(config),...config.usageDescriptions,...config.infoPlist};if(config.defaultBrowser&&declared.NSAppTransportSecurity===void 0)declared.NSAppTransportSecurity={NSAllowsArbitraryLoadsInWebContent:!0};const{xml:extra,ignored}=renderUserlandPlistEntries(Object.fromEntries(Object.entries(declared).filter(([key])=>!owns.includes(key)))),owned=Object.keys(declared).filter((key)=>owns.includes(key)),urlTypes=[];if(config.urlSchemes?.length)urlTypes.push({CFBundleURLName:config.bundleId,CFBundleURLSchemes:config.urlSchemes});if(config.defaultBrowser)urlTypes.push({CFBundleURLName:"Web site URL",CFBundleTypeRole:"Viewer",CFBundleURLSchemes:["http","https"]});const schemes=urlTypes.length?` <key>CFBundleURLTypes</key>${plistValue(urlTypes," ")}
|
|
3
3
|
`:"",documents=config.defaultBrowser?` <key>CFBundleDocumentTypes</key>${plistValue(BROWSER_DOCUMENT_TYPES.map((type)=>({CFBundleTypeName:type.name,CFBundleTypeRole:"Viewer",LSHandlerRank:"Default",LSItemContentTypes:type.types}))," ")}
|
|
4
4
|
`:"",icon=options.iconFile?` <key>CFBundleIconFile</key><string>${xmlEscape(options.iconFile)}</string>
|
package/dist/macos-bundle.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{chmodSync,copyFileSync,cpSync,existsSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import{join,resolve}from"node:path";import{AGENT_MANIFEST}from"./agent-launcher";import{ICONSET_SIZES,renderHostConfig,renderMacosEntitlements,renderMacosHostEntitlements,renderMacosInfoPlist,renderPrivacyManifest}from"./macos-app";export const AGENT_HOST_EXECUTABLE="craft-runtime",runBundleCommand=(args)=>{const result=Bun.spawnSync(args,{stdout:"pipe",stderr:"pipe"});if(result.exitCode!==0)throw Error(`${args[0]} ${args.slice(1).join(" ")} exited with ${result.exitCode}: ${result.stderr.toString().trim()||result.stdout.toString().trim()}`)};export function makeIcns(png,output,scratch,run=runBundleCommand){const iconset=join(scratch,"AppIcon.iconset");rmSync(iconset,{recursive:!0,force:!0});mkdirSync(iconset,{recursive:!0});for(const{file,pixels}of ICONSET_SIZES)run(["sips","-z",String(pixels),String(pixels),png,"--out",join(iconset,file)]);run(["iconutil","-c","icns",iconset,"-o",output]);rmSync(iconset,{recursive:!0,force:!0})}export function assembleMacosApp(options){const run=options.run??runBundleCommand,{config}=options,notes=[],appPath=join(options.output,`${config.appName}.app`),contents=join(appPath,"Contents"),macos=join(contents,"MacOS"),resources=join(contents,"Resources");rmSync(appPath,{recursive:!0,force:!0});mkdirSync(macos,{recursive:!0});mkdirSync(resources,{recursive:!0});if(options.agent){if(!config.agent)throw Error("An agent launcher was given for an app whose config has no macos.agent");copyFileSync(options.agent.launcher,join(macos,config.executable));copyFileSync(options.host,join(macos,AGENT_HOST_EXECUTABLE));chmodSync(join(macos,AGENT_HOST_EXECUTABLE),493);const manifest={...options.agent.manifest,host:`../MacOS/${AGENT_HOST_EXECUTABLE}`,nativeBundle:"native-screen.js"};if(options.agent.manifest.staticDir){cpSync(options.agent.manifest.staticDir,join(resources,"web"),{recursive:!0});manifest.staticDir="web"}writeFileSync(join(resources,AGENT_MANIFEST),`${JSON.stringify(manifest,null,2)}
|
|
1
|
+
import{chmodSync,copyFileSync,cpSync,existsSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import{join,resolve}from"node:path";import{AGENT_MANIFEST}from"./agent-launcher";import{ICONSET_SIZES,renderHostConfig,renderMacosEntitlements,renderMacosHostEntitlements,renderMacosInfoPlist,renderPrivacyManifest}from"./macos-app";export const AGENT_HOST_EXECUTABLE="craft-runtime",runBundleCommand=(args)=>{const result=Bun.spawnSync(args,{stdout:"pipe",stderr:"pipe"});if(result.exitCode!==0)throw Error(`${args[0]} ${args.slice(1).join(" ")} exited with ${result.exitCode}: ${result.stderr.toString().trim()||result.stdout.toString().trim()}`)};export function makeIcns(png,output,scratch,run=runBundleCommand){const iconset=join(scratch,"AppIcon.iconset");rmSync(iconset,{recursive:!0,force:!0});mkdirSync(iconset,{recursive:!0});for(const{file,pixels}of ICONSET_SIZES)run(["sips","-z",String(pixels),String(pixels),png,"--out",join(iconset,file)]);run(["iconutil","-c","icns",iconset,"-o",output]);rmSync(iconset,{recursive:!0,force:!0})}export function assembleMacosApp(options){const run=options.run??runBundleCommand,{config}=options,notes=[],appPath=join(options.output,`${config.appName}.app`),contents=join(appPath,"Contents"),macos=join(contents,"MacOS"),resources=join(contents,"Resources");rmSync(appPath,{recursive:!0,force:!0});mkdirSync(macos,{recursive:!0});mkdirSync(resources,{recursive:!0});if(options.agent){if(!config.agent)throw Error("An agent launcher was given for an app whose config has no macos.agent");copyFileSync(options.agent.launcher,join(macos,config.executable));copyFileSync(options.host,join(macos,AGENT_HOST_EXECUTABLE));chmodSync(join(macos,AGENT_HOST_EXECUTABLE),493);const manifest={...options.agent.manifest,host:`../MacOS/${AGENT_HOST_EXECUTABLE}`,nativeBundle:"native-screen.js"};if(options.agent.manifest.staticDir){cpSync(options.agent.manifest.staticDir,join(resources,"web"),{recursive:!0});manifest.staticDir="web"}if(options.agent.manifest.resources){for(const[name,source]of Object.entries(options.agent.manifest.resources)){if(!existsSync(source))throw Error(`macos.agent.resources.${name} does not exist: ${source}`);cpSync(source,join(resources,name),{recursive:!0,dereference:!0})}manifest.resources=Object.fromEntries(Object.keys(options.agent.manifest.resources).map((name)=>[name,name]))}writeFileSync(join(resources,AGENT_MANIFEST),`${JSON.stringify(manifest,null,2)}
|
|
2
2
|
`)}else copyFileSync(options.host,join(macos,config.executable));chmodSync(join(macos,config.executable),493);copyFileSync(options.nativeBundle,join(resources,"native-screen.js"));writeFileSync(join(resources,"craft-app.json"),renderHostConfig(config));writeFileSync(join(resources,"PrivacyInfo.xcprivacy"),renderPrivacyManifest(config));let iconFile;if(config.icon){const icon=resolve(options.root,config.icon);if(!existsSync(icon))throw Error(`macos.icon does not exist: ${icon}`);if(icon.endsWith(".icns"))copyFileSync(icon,join(resources,"AppIcon.icns"));else makeIcns(icon,join(resources,"AppIcon.icns"),options.output,run);iconFile="AppIcon"}else notes.push("No macos.icon: the app has the generic icon, which the Mac App Store rejects.");const plist=renderMacosInfoPlist(config,{iconFile});writeFileSync(join(contents,"Info.plist"),plist.xml);if(options.distribution==="app-store"){if(!options.provisioningProfile)throw Error("A Mac App Store build needs a provisioning profile");const embedded=join(contents,"embedded.provisionprofile");copyFileSync(options.provisioningProfile,embedded);chmodSync(embedded,420)}const signed=options.unsandboxed?{...config,sandbox:!1}:config,entitlementsPath=join(options.output,`${config.appName}.entitlements`);writeFileSync(entitlementsPath,renderMacosEntitlements(signed,options.distribution));const hostEntitlements=join(options.output,`${config.appName}.host.entitlements`);if(options.agent)writeFileSync(hostEntitlements,renderMacosHostEntitlements(signed));run(["plutil","-lint",join(contents,"Info.plist"),join(resources,"PrivacyInfo.xcprivacy"),entitlementsPath,...options.agent?[hostEntitlements]:[]]);const identity=options.signingIdentity||"-";if(options.agent)run(["codesign","--force","--options","runtime",...identity==="-"?[]:["--timestamp"],"--entitlements",hostEntitlements,...options.keychain?["--keychain",options.keychain]:[],"--sign",identity,join(macos,AGENT_HOST_EXECUTABLE)]);run(["codesign","--force","--options","runtime",...identity==="-"?[]:["--timestamp"],"--entitlements",entitlementsPath,...options.keychain?["--keychain",options.keychain]:[],"--sign",identity,appPath]);run(["codesign","--verify","--deep","--strict",appPath]);return{appPath,entitlementsPath,ignoredPlistKeys:plist.ignored,notes}}export function parseDevelopmentIdentity(output){for(const line of output.split(`
|
|
3
3
|
`)){const match=/^\s*\d+\)\s+([0-9A-F]{40})\s+"Apple Development:/.exec(line);if(match)return match[1]}return}export function findDevelopmentIdentity(read=()=>Bun.spawnSync(["security","find-identity","-v","-p","codesigning"],{stdout:"pipe",stderr:"ignore"}).stdout.toString()){try{return parseDevelopmentIdentity(read())}catch{return}}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/desktop-build",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.422",
|
|
6
6
|
"description": "Build and launch Craft desktop applications for Stacks",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -73,15 +73,15 @@
|
|
|
73
73
|
"prepublishOnly": "bun run build"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@stacksjs/config": "0.75.
|
|
77
|
-
"@stacksjs/router": "0.75.
|
|
78
|
-
"@stacksjs/server": "0.75.
|
|
76
|
+
"@stacksjs/config": "0.75.422",
|
|
77
|
+
"@stacksjs/router": "0.75.422",
|
|
78
|
+
"@stacksjs/server": "0.75.422",
|
|
79
79
|
"craft-native": ">=0.0.420"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"@stacksjs/config": "0.75.
|
|
83
|
-
"@stacksjs/router": "0.75.
|
|
84
|
-
"@stacksjs/server": "0.75.
|
|
82
|
+
"@stacksjs/config": "0.75.422",
|
|
83
|
+
"@stacksjs/router": "0.75.422",
|
|
84
|
+
"@stacksjs/server": "0.75.422"
|
|
85
85
|
},
|
|
86
86
|
"peerDependenciesMeta": {
|
|
87
87
|
"@stacksjs/config": {
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
|
-
"@stacksjs/build": "0.75.
|
|
98
|
+
"@stacksjs/build": "0.75.422",
|
|
99
99
|
"better-dx": "^0.2.24"
|
|
100
100
|
}
|
|
101
101
|
}
|