@stacksjs/actions 0.75.226 → 0.75.233
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/build/android-config.d.ts +3 -0
- package/dist/build/android-config.js +1 -1
- package/dist/build/android.js +1 -1
- package/dist/build/ios-config.d.ts +1 -0
- package/dist/build/ios-config.js +1 -1
- package/dist/build/ios-shell.js +3 -0
- package/dist/build/ios.js +1 -1
- package/dist/build/native-screens.d.ts +5 -1
- package/dist/build/native-screens.js +1 -1
- package/dist/bump.js +1 -1
- package/dist/dev/api.js +1 -1
- package/dist/generate/lib-entries.js +1 -1
- package/dist/library/build.js +1 -1
- package/dist/pantry-lock.d.ts +1 -1
- package/dist/pantry-lock.js +1 -1
- package/dist/release-preflight.js +2 -2
- package/dist/release.js +1 -1
- package/dist/serve/api.js +1 -1
- package/package.json +43 -43
|
@@ -30,6 +30,8 @@ export declare const CRAFT_ANDROID_FLOORS: {
|
|
|
30
30
|
/** Verified HTTPS hosts and ordered website path exclusions. */
|
|
31
31
|
appLinks: '0.0.201';
|
|
32
32
|
bluetooth: '0.0.178';
|
|
33
|
+
/** CraftHeartRate.kt: Bluetooth heart-rate straps, the same `craft.heartRate` as iOS. */
|
|
34
|
+
heartRate: '0.0.247';
|
|
33
35
|
/** Health Connect for native screens (workouts, sleep, heart rate, mindful, nutrition). */
|
|
34
36
|
healthConnect: '0.0.169';
|
|
35
37
|
/** WorkManager background refresh for the app script. */
|
|
@@ -65,6 +67,7 @@ export declare interface CraftAndroidConfig {
|
|
|
65
67
|
googleServicesFile?: string
|
|
66
68
|
mapsApiKey?: string
|
|
67
69
|
enableBluetooth?: boolean
|
|
70
|
+
enableHeartRate?: boolean
|
|
68
71
|
enableHealthConnect?: boolean
|
|
69
72
|
renderer?: AndroidMobileConfig['renderer']
|
|
70
73
|
nativeScreens?: Record<string, string>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{NATIVE_APP_ROUTE,normalizeMobileUrl,toCraftCrashReporting}from"./ios-config";const CAPABILITY_KEYS={speechRecognition:"enableSpeechRecognition",haptics:"enableHaptics",share:"enableShare",camera:"enableCamera",biometric:"enableBiometric",pushNotifications:"enablePushNotifications",secureStorage:"enableSecureStorage",geolocation:"enableGeolocation",backgroundLocation:"enableBackgroundLocation",keepAwake:"enableKeepAwake",deepLinks:"enableDeepLinks",healthConnect:"enableHealthConnect",bluetooth:"enableBluetooth",localNetwork:"enableLocalNetwork",localDatabase:"enableLocalDatabase",localNotifications:"enableLocalNotifications"};export const CRAFT_ANDROID_FLOORS={nativeShell:"0.0.169",appLinks:"0.0.201",bluetooth:"0.0.178",healthConnect:"0.0.169",backgroundRefresh:"0.0.169",appBundle:"0.0.169",crashReporting:"0.0.179",notificationIcon:"0.0.241"};export function assertCraftSupportsAndroidMapProvider(config,providers){if(config.mapProvider&&!providers?.includes(config.mapProvider))throw Error(`The resolved Craft Android builder does not advertise native map provider '${config.mapProvider}'. Upgrade craft-native to a published SDK supporting that provider before building this app.`)}export function androidCraftFeatures(config,options={}){const features=[];if(config.renderer==="native"&&Object.keys(config.nativeScreens??{}).length)features.push("nativeShell");if(config.appLinks?.length&&config.capabilities?.deepLinks)features.push("appLinks");if(config.capabilities?.bluetooth)features.push("bluetooth");if(config.capabilities?.healthConnect&&config.renderer==="native")features.push("healthConnect");if(config.backgroundRefresh?.enabled)features.push("backgroundRefresh");if(options.appBundle)features.push("appBundle");if(config.crashReporting?.key)features.push("crashReporting");if(config.notificationIcon||config.notificationColor)features.push("notificationIcon");return features}export function assertCraftSupportsAndroid(config,version,source,options={}){const missing=androidCraftFeatures(config,options).filter((feature)=>!Bun.semver.satisfies(version,`>=${CRAFT_ANDROID_FLOORS[feature]}`));if(!missing.length)return;const needed=missing.map((feature)=>CRAFT_ANDROID_FLOORS[feature]).reduce((max,next)=>Bun.semver.order(next,max)>0?next:max,"0.0.0");throw Error(`The Craft Android builder is craft-native ${version} (${source}), which would build the app without ${missing.map((feature)=>`${feature} (needs >= ${CRAFT_ANDROID_FLOORS[feature]})`).join(", ")}. Install craft-native >= ${needed}: look for an older copy nested under @stacksjs/mobile in bun.lock, or raise the app's own craft-native pin.`)}export function toCraftAndroidConfig(config){const devServerURL=normalizeMobileUrl(config.url),trustedOrigins=new Set(config.trustedOrigins??[]);if(devServerURL)trustedOrigins.add(new URL(devServerURL).origin);const native=config.renderer==="native",craft={mapProvider:config.mapProvider,nativeFonts:config.nativeFonts,appName:config.appName,packageName:config.packageName,version:config.version,versionCode:config.versionCode,minSdk:config.minSdk,targetSdk:config.targetSdk,darkMode:config.darkMode,appearance:config.appearance,backgroundColor:config.backgroundColor,backgroundColorDark:config.backgroundColorDark,devServerURL,trustedOrigins:[...trustedOrigins],urlSchemes:config.urlSchemes,appLinks:config.appLinks,appIconPath:config.appIcon,notificationColor:config.notificationColor,googleServicesFile:config.googleServicesFile,mapsApiKey:config.mapsApiKey,renderer:config.renderer,nativeScreens:native?config.nativeScreens:void 0,nativeApp:native&&config.nativeApp?NATIVE_APP_ROUTE:void 0,tabs:native?config.tabs:void 0,backgroundRefresh:native?config.backgroundRefresh:void 0,healthConnect:config.capabilities?.healthConnect?config.healthConnect:void 0,privacyPolicyURL:config.privacyPolicyUrl,crashReporting:toCraftCrashReporting(config.crashReporting)};for(const[key,nativeKey]of Object.entries(CAPABILITY_KEYS)){const enabled=config.capabilities?.[key];if(enabled!==void 0)craft[nativeKey]=enabled}if(config.capabilities?.backgroundLocation)craft.enableGeolocation=!0;for(const key of Object.keys(craft))if(craft[key]===void 0)delete craft[key];return craft}export function validateAndroidMobileConfig(config){if(config.mapProvider!==void 0&&config.mapProvider!=="google"&&config.mapProvider!=="maplibre")throw Error(`android.mapProvider must be 'google' or 'maplibre', not ${String(config.mapProvider)}`);if(config.mapProvider&&config.renderer!=="native")throw Error("android.mapProvider configures native map views: set android.renderer to 'native'");if(!config.appName?.trim())throw Error("config/mobile.ts must define android.appName");if(!/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/.test(config.packageName))throw Error(`Invalid Android package name: ${config.packageName}`);if(config.notificationColor!==void 0&&!/^#[0-9a-f]{6}$/i.test(config.notificationColor))throw Error(`android.notificationColor must be #RRGGBB, not ${String(config.notificationColor)}`);if(config.notificationIcon!==void 0&&!/\.(png|xml)$/i.test(config.notificationIcon))throw Error("android.notificationIcon must be an Android vector drawable (.xml) or a white-on-transparent .png");if(config.mapsApiKey!==void 0&&(typeof config.mapsApiKey!=="string"||!config.mapsApiKey.trim()))throw Error("android.mapsApiKey must be a non-empty Google Maps Android SDK key");if(config.renderer!==void 0&&config.renderer!=="web"&&config.renderer!=="native")throw Error(`android.renderer must be 'web' or 'native', not ${String(config.renderer)}`);if(config.renderer==="native"){if(!Object.keys(config.nativeScreens??{}).length)throw Error("android.renderer 'native' draws every screen natively: list them in android.nativeScreens");if(config.webAssets)throw Error("android.renderer 'native' has no web view to show android.webAssets in: set android.url to the app's site instead");if(config.fallbackWebAssets)throw Error("android.fallbackWebAssets is the web view's offline page; android.renderer 'native' has no web view");if(!config.url)throw Error("android.renderer 'native' needs android.url: the app's site, where its API answers")}else{if(config.url&&config.webAssets)throw Error("Choose either android.url or android.webAssets in config/mobile.ts, not both");if(!config.url&&!config.webAssets)throw Error("config/mobile.ts must define android.url or android.webAssets");if(config.fallbackWebAssets&&!config.url)throw Error("android.fallbackWebAssets requires android.url")}if(config.nativeApp&&config.renderer!=="native")throw Error("android.nativeApp runs beside native screens: set android.renderer to 'native'");for(const tab of config.tabs??[])if(!tab.id?.startsWith("/"))throw Error(`An android.tabs entry needs an id that is an app path, like /m (got ${String(tab.id)})`);if(config.url){const normalized=normalizeMobileUrl(config.url),url=new URL(normalized),isLocal=["localhost","127.0.0.1","10.0.2.2"].includes(url.hostname);if(url.protocol!=="https:"&&!isLocal)throw Error("android.url must use HTTPS outside local development")}if(config.capabilities?.healthConnect&&(config.minSdk??26)<26)throw Error("Android Health Connect requires android.minSdk 26 or newer");if(config.healthConnect&&!config.capabilities?.healthConnect)throw Error("android.healthConnect lists Health Connect data, but android.capabilities.healthConnect is off");const minutes=config.backgroundRefresh?.minimumIntervalMinutes;if(minutes!==void 0&&(!Number.isFinite(minutes)||minutes<15))throw Error("android.backgroundRefresh.minimumIntervalMinutes is at least 15 (WorkManager's shortest period)");for(const scheme of config.urlSchemes??[])if(!/^[a-z][a-z0-9+.-]*$/i.test(scheme))throw Error(`Invalid Android URL scheme: ${scheme}`)}export function androidRelease(config){if(config.crashReporting?.release)return config.crashReporting.release;return`${config.version||"1.0.0"}+${config.versionCode||1}`}export async function uploadAndroidMapping(mappingPath,host,release,token,send=fetch){if(!token)return{ok:!1,message:"BUGHQ_UPLOAD_TOKEN is not set, so the R8 mapping was not uploaded and this build's Android crashes will show obfuscated names."};const file=Bun.file(mappingPath);if(!await file.exists())return{ok:!1,message:`No R8 mapping at ${mappingPath}, so nothing was uploaded.`};try{const body=Bun.gzipSync(new Uint8Array(await file.arrayBuffer())),response=await send(`${host.replace(/\/$/,"")}/api/debug-files?kind=proguard&release=${encodeURIComponent(release)}`,{method:"POST",headers:{Authorization:`Bearer ${token}`,"Content-Type":"application/octet-stream"},body});if(!response.ok)return{ok:!1,message:`bughq refused the R8 mapping (${response.status}): ${(await response.text()).slice(0,200)}`};return{ok:!0,message:`Uploaded the R8 mapping for ${release} to bughq.`}}catch(error){return{ok:!1,message:`Could not upload the R8 mapping: ${error instanceof Error?error.message:String(error)}`}}}
|
|
1
|
+
import{NATIVE_APP_ROUTE,normalizeMobileUrl,toCraftCrashReporting}from"./ios-config";const CAPABILITY_KEYS={speechRecognition:"enableSpeechRecognition",haptics:"enableHaptics",share:"enableShare",camera:"enableCamera",biometric:"enableBiometric",pushNotifications:"enablePushNotifications",secureStorage:"enableSecureStorage",geolocation:"enableGeolocation",backgroundLocation:"enableBackgroundLocation",keepAwake:"enableKeepAwake",deepLinks:"enableDeepLinks",healthConnect:"enableHealthConnect",bluetooth:"enableBluetooth",heartRate:"enableHeartRate",localNetwork:"enableLocalNetwork",localDatabase:"enableLocalDatabase",localNotifications:"enableLocalNotifications"};export const CRAFT_ANDROID_FLOORS={nativeShell:"0.0.169",appLinks:"0.0.201",bluetooth:"0.0.178",heartRate:"0.0.247",healthConnect:"0.0.169",backgroundRefresh:"0.0.169",appBundle:"0.0.169",crashReporting:"0.0.179",notificationIcon:"0.0.241"};export function assertCraftSupportsAndroidMapProvider(config,providers){if(config.mapProvider&&!providers?.includes(config.mapProvider))throw Error(`The resolved Craft Android builder does not advertise native map provider '${config.mapProvider}'. Upgrade craft-native to a published SDK supporting that provider before building this app.`)}export function androidCraftFeatures(config,options={}){const features=[];if(config.renderer==="native"&&Object.keys(config.nativeScreens??{}).length)features.push("nativeShell");if(config.appLinks?.length&&config.capabilities?.deepLinks)features.push("appLinks");if(config.capabilities?.bluetooth)features.push("bluetooth");if(config.capabilities?.heartRate)features.push("heartRate");if(config.capabilities?.healthConnect&&config.renderer==="native")features.push("healthConnect");if(config.backgroundRefresh?.enabled)features.push("backgroundRefresh");if(options.appBundle)features.push("appBundle");if(config.crashReporting?.key)features.push("crashReporting");if(config.notificationIcon||config.notificationColor)features.push("notificationIcon");return features}export function assertCraftSupportsAndroid(config,version,source,options={}){const missing=androidCraftFeatures(config,options).filter((feature)=>!Bun.semver.satisfies(version,`>=${CRAFT_ANDROID_FLOORS[feature]}`));if(!missing.length)return;const needed=missing.map((feature)=>CRAFT_ANDROID_FLOORS[feature]).reduce((max,next)=>Bun.semver.order(next,max)>0?next:max,"0.0.0");throw Error(`The Craft Android builder is craft-native ${version} (${source}), which would build the app without ${missing.map((feature)=>`${feature} (needs >= ${CRAFT_ANDROID_FLOORS[feature]})`).join(", ")}. Install craft-native >= ${needed}: look for an older copy nested under @stacksjs/mobile in bun.lock, or raise the app's own craft-native pin.`)}export function toCraftAndroidConfig(config){const devServerURL=normalizeMobileUrl(config.url),trustedOrigins=new Set(config.trustedOrigins??[]);if(devServerURL)trustedOrigins.add(new URL(devServerURL).origin);const native=config.renderer==="native",craft={mapProvider:config.mapProvider,nativeFonts:config.nativeFonts,appName:config.appName,packageName:config.packageName,version:config.version,versionCode:config.versionCode,minSdk:config.minSdk,targetSdk:config.targetSdk,darkMode:config.darkMode,appearance:config.appearance,backgroundColor:config.backgroundColor,backgroundColorDark:config.backgroundColorDark,devServerURL,trustedOrigins:[...trustedOrigins],urlSchemes:config.urlSchemes,appLinks:config.appLinks,appIconPath:config.appIcon,notificationColor:config.notificationColor,googleServicesFile:config.googleServicesFile,mapsApiKey:config.mapsApiKey,renderer:config.renderer,nativeScreens:native?config.nativeScreens:void 0,nativeApp:native&&config.nativeApp?NATIVE_APP_ROUTE:void 0,tabs:native?config.tabs:void 0,backgroundRefresh:native?config.backgroundRefresh:void 0,healthConnect:config.capabilities?.healthConnect?config.healthConnect:void 0,privacyPolicyURL:config.privacyPolicyUrl,crashReporting:toCraftCrashReporting(config.crashReporting)};for(const[key,nativeKey]of Object.entries(CAPABILITY_KEYS)){const enabled=config.capabilities?.[key];if(enabled!==void 0)craft[nativeKey]=enabled}if(config.capabilities?.backgroundLocation)craft.enableGeolocation=!0;for(const key of Object.keys(craft))if(craft[key]===void 0)delete craft[key];return craft}export function validateAndroidMobileConfig(config){if(config.mapProvider!==void 0&&config.mapProvider!=="google"&&config.mapProvider!=="maplibre")throw Error(`android.mapProvider must be 'google' or 'maplibre', not ${String(config.mapProvider)}`);if(config.mapProvider&&config.renderer!=="native")throw Error("android.mapProvider configures native map views: set android.renderer to 'native'");if(!config.appName?.trim())throw Error("config/mobile.ts must define android.appName");if(!/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/.test(config.packageName))throw Error(`Invalid Android package name: ${config.packageName}`);if(config.notificationColor!==void 0&&!/^#[0-9a-f]{6}$/i.test(config.notificationColor))throw Error(`android.notificationColor must be #RRGGBB, not ${String(config.notificationColor)}`);if(config.notificationIcon!==void 0&&!/\.(png|xml)$/i.test(config.notificationIcon))throw Error("android.notificationIcon must be an Android vector drawable (.xml) or a white-on-transparent .png");if(config.mapsApiKey!==void 0&&(typeof config.mapsApiKey!=="string"||!config.mapsApiKey.trim()))throw Error("android.mapsApiKey must be a non-empty Google Maps Android SDK key");if(config.renderer!==void 0&&config.renderer!=="web"&&config.renderer!=="native")throw Error(`android.renderer must be 'web' or 'native', not ${String(config.renderer)}`);if(config.renderer==="native"){if(!Object.keys(config.nativeScreens??{}).length)throw Error("android.renderer 'native' draws every screen natively: list them in android.nativeScreens");if(config.webAssets)throw Error("android.renderer 'native' has no web view to show android.webAssets in: set android.url to the app's site instead");if(config.fallbackWebAssets)throw Error("android.fallbackWebAssets is the web view's offline page; android.renderer 'native' has no web view");if(!config.url)throw Error("android.renderer 'native' needs android.url: the app's site, where its API answers")}else{if(config.url&&config.webAssets)throw Error("Choose either android.url or android.webAssets in config/mobile.ts, not both");if(!config.url&&!config.webAssets)throw Error("config/mobile.ts must define android.url or android.webAssets");if(config.fallbackWebAssets&&!config.url)throw Error("android.fallbackWebAssets requires android.url")}if(config.nativeApp&&config.renderer!=="native")throw Error("android.nativeApp runs beside native screens: set android.renderer to 'native'");for(const tab of config.tabs??[])if(!tab.id?.startsWith("/"))throw Error(`An android.tabs entry needs an id that is an app path, like /m (got ${String(tab.id)})`);if(config.url){const normalized=normalizeMobileUrl(config.url),url=new URL(normalized),isLocal=["localhost","127.0.0.1","10.0.2.2"].includes(url.hostname);if(url.protocol!=="https:"&&!isLocal)throw Error("android.url must use HTTPS outside local development")}if(config.capabilities?.healthConnect&&(config.minSdk??26)<26)throw Error("Android Health Connect requires android.minSdk 26 or newer");if(config.healthConnect&&!config.capabilities?.healthConnect)throw Error("android.healthConnect lists Health Connect data, but android.capabilities.healthConnect is off");const minutes=config.backgroundRefresh?.minimumIntervalMinutes;if(minutes!==void 0&&(!Number.isFinite(minutes)||minutes<15))throw Error("android.backgroundRefresh.minimumIntervalMinutes is at least 15 (WorkManager's shortest period)");for(const scheme of config.urlSchemes??[])if(!/^[a-z][a-z0-9+.-]*$/i.test(scheme))throw Error(`Invalid Android URL scheme: ${scheme}`)}export function androidRelease(config){if(config.crashReporting?.release)return config.crashReporting.release;return`${config.version||"1.0.0"}+${config.versionCode||1}`}export async function uploadAndroidMapping(mappingPath,host,release,token,send=fetch){if(!token)return{ok:!1,message:"BUGHQ_UPLOAD_TOKEN is not set, so the R8 mapping was not uploaded and this build's Android crashes will show obfuscated names."};const file=Bun.file(mappingPath);if(!await file.exists())return{ok:!1,message:`No R8 mapping at ${mappingPath}, so nothing was uploaded.`};try{const body=Bun.gzipSync(new Uint8Array(await file.arrayBuffer())),response=await send(`${host.replace(/\/$/,"")}/api/debug-files?kind=proguard&release=${encodeURIComponent(release)}`,{method:"POST",headers:{Authorization:`Bearer ${token}`,"Content-Type":"application/octet-stream"},body});if(!response.ok)return{ok:!1,message:`bughq refused the R8 mapping (${response.status}): ${(await response.text()).slice(0,200)}`};return{ok:!0,message:`Uploaded the R8 mapping for ${release} to bughq.`}}catch(error){return{ok:!1,message:`Could not upload the R8 mapping: ${error instanceof Error?error.message:String(error)}`}}}
|
package/dist/build/android.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{pathToFileURL}from"node:url";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{craftBuilderSpecifier,craftBuilderVersion}from"./craft-entry";import{resolveCraftBuilderProvenance}from"./craft-provenance";import{androidRelease,assertCraftSupportsAndroid,assertCraftSupportsAndroidMapProvider,toCraftAndroidConfig,uploadAndroidMapping,validateAndroidMobileConfig}from"./android-config";import{resolveCrashReporting,resolveMobilePath,resolveNativeFonts,assertCraftSupportsNativeFonts}from"./ios-config";import{compileNativeScreens}from"./native-screens";import{writeAndroidIconTable}from"./android-icons";process.exitCode=1;function craftAndroidEntry(){const explicit=process.env.CRAFT_ANDROID_SRC;return explicit?pathToFileURL(explicit).href:craftBuilderSpecifier("android",projectPath())}async function loadCraftAndroidBuilder(){try{return await import(craftAndroidEntry())}catch(error){throw Error("Craft Android builder is unavailable. Install the current craft-native package or set CRAFT_ANDROID_SRC to packages/android/src/index.ts in a Craft checkout.",{cause:error})}}const configPath=projectPath("config/mobile.ts");if(!existsSync(configPath))throw Error("Missing config/mobile.ts. Add an android section before running `buddy build:android`.");const configModule=await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`),androidSection=configModule.default.android;if(!androidSection)throw Error("config/mobile.ts must define an android section");const config={...androidSection,crashReporting:resolveCrashReporting(configModule.default.crashReporting,androidSection.crashReporting)};validateAndroidMobileConfig(config);const output=resolveMobilePath(projectPath(),config.output)??storagePath("framework/mobile/android"),webAssets=resolveMobilePath(projectPath(),config.webAssets),fallbackWebAssets=resolveMobilePath(projectPath(),config.fallbackWebAssets),craftConfig=toCraftAndroidConfig(config);craftConfig.nativeFonts=resolveNativeFonts(config.nativeFonts,projectPath());craftConfig.appIconPath=resolveMobilePath(projectPath(),config.appIcon);craftConfig.notificationIconPath=resolveMobilePath(projectPath(),config.notificationIcon);craftConfig.googleServicesFile=resolveMobilePath(projectPath(),config.googleServicesFile);if(process.env.NODE_ENV==="production"&&config.capabilities?.pushNotifications&&!craftConfig.googleServicesFile)throw Error("Production Android push notifications require android.googleServicesFile in config/mobile.ts");const appBundle=process.env.STACKS_ANDROID_BUNDLE==="1",release=appBundle||process.env.NODE_ENV==="production",native=config.renderer==="native",builder=await loadCraftAndroidBuilder(),nativeBundle=native?await compileNativeScreens(config,projectPath(),output,void 0,"android",{routes:builder.nativeRouteBundles===!0}):null;if(nativeBundle){for(const diagnostic of nativeBundle.diagnostics??[])log.warn(`${diagnostic.file?`${diagnostic.file}: `:""}${diagnostic.message}`);log.info(`Compiled ${Object.keys(config.nativeScreens??{}).length} native screen path(s) into ${nativeBundle.outFile}`)}const{LUCIDE_TO_SF}=native?await import("@stacksjs/stx/native"):{LUCIDE_TO_SF:{}},iconsPath=native?writeAndroidIconTable(projectPath(),output,LUCIDE_TO_SF):null;if(native&&!iconsPath)log.warn("No @iconify-json/lucide in the app: Android draws its own glyphs for the screens' icons.");const craftVersion=craftBuilderVersion(craftAndroidEntry(),projectPath());assertCraftSupportsNativeFonts(config.nativeFonts,craftVersion?.version);if(craftVersion)assertCraftSupportsAndroid(config,craftVersion.version,craftVersion.manifest,{appBundle});else log.warn(`Could not read the craft-native version behind ${craftAndroidEntry()}, so the build cannot check that it supports what config/mobile.ts asks for.`);assertCraftSupportsAndroidMapProvider(config,builder.nativeMapProviders);await builder.init({name:config.appName,packageName:config.packageName,output,config:craftConfig});await builder.build(native?{output,nativeBundlePath:nativeBundle.outFile,iconsPath:iconsPath??void 0,release,bundle:appBundle,compile:process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"}:{output,htmlPath:webAssets??fallbackWebAssets,devServer:craftConfig.devServerURL,release,bundle:appBundle,compile:process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"});if(release&&config.crashReporting?.key&&config.crashReporting.uploadSymbols!==!1&&process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"){const upload=await uploadAndroidMapping(`${output}/app/build/outputs/mapping/release/mapping.txt`,config.crashReporting.host??"https://bughq.org",androidRelease(config),process.env.BUGHQ_UPLOAD_TOKEN);if(upload.ok)log.info(upload.message);else log.warn(upload.message)}const sourceRevision=Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),generatedConfig=JSON.parse(readFileSync(`${output}/craft.config.json`,"utf8"));writeFileSync(`${output}/stacks-mobile.json`,`${JSON.stringify({schemaVersion:"1.0.0",platform:"android",sourceRevision,source:native?{kind:"native",site:craftConfig.devServerURL}:webAssets?{kind:"bundled",path:webAssets}:{kind:"remote",url:craftConfig.devServerURL,fallback:fallbackWebAssets?{kind:"bundled",path:fallbackWebAssets}:void 0},capabilities:config.capabilities??{},builder:resolveCraftBuilderProvenance(process.env.CRAFT_ANDROID_SRC),craft:generatedConfig},null,2)}
|
|
1
|
+
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{pathToFileURL}from"node:url";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{craftBuilderSpecifier,craftBuilderVersion}from"./craft-entry";import{resolveCraftBuilderProvenance}from"./craft-provenance";import{androidRelease,assertCraftSupportsAndroid,assertCraftSupportsAndroidMapProvider,toCraftAndroidConfig,uploadAndroidMapping,validateAndroidMobileConfig}from"./android-config";import{resolveCrashReporting,resolveMobilePath,resolveNativeFonts,assertCraftSupportsNativeFonts}from"./ios-config";import{compileNativeScreens}from"./native-screens";import{writeAndroidIconTable}from"./android-icons";process.exitCode=1;function craftAndroidEntry(){const explicit=process.env.CRAFT_ANDROID_SRC;return explicit?pathToFileURL(explicit).href:craftBuilderSpecifier("android",projectPath())}async function loadCraftAndroidBuilder(){try{return await import(craftAndroidEntry())}catch(error){throw Error("Craft Android builder is unavailable. Install the current craft-native package or set CRAFT_ANDROID_SRC to packages/android/src/index.ts in a Craft checkout.",{cause:error})}}const configPath=projectPath("config/mobile.ts");if(!existsSync(configPath))throw Error("Missing config/mobile.ts. Add an android section before running `buddy build:android`.");const configModule=await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`),androidSection=configModule.default.android;if(!androidSection)throw Error("config/mobile.ts must define an android section");const config={...androidSection,crashReporting:resolveCrashReporting(configModule.default.crashReporting,androidSection.crashReporting)};validateAndroidMobileConfig(config);const output=resolveMobilePath(projectPath(),config.output)??storagePath("framework/mobile/android"),webAssets=resolveMobilePath(projectPath(),config.webAssets),fallbackWebAssets=resolveMobilePath(projectPath(),config.fallbackWebAssets),craftConfig=toCraftAndroidConfig(config);craftConfig.nativeFonts=resolveNativeFonts(config.nativeFonts,projectPath());craftConfig.appIconPath=resolveMobilePath(projectPath(),config.appIcon);craftConfig.notificationIconPath=resolveMobilePath(projectPath(),config.notificationIcon);craftConfig.googleServicesFile=resolveMobilePath(projectPath(),config.googleServicesFile);if(process.env.NODE_ENV==="production"&&config.capabilities?.pushNotifications&&!craftConfig.googleServicesFile)throw Error("Production Android push notifications require android.googleServicesFile in config/mobile.ts");const appBundle=process.env.STACKS_ANDROID_BUNDLE==="1",release=appBundle||process.env.NODE_ENV==="production",native=config.renderer==="native",builder=await loadCraftAndroidBuilder(),nativeBundle=native?await compileNativeScreens(config,projectPath(),output,void 0,"android",{routes:builder.nativeRouteBundles===!0,runtime:builder.nativeRouteRuntime===!0,messages:builder.nativeMessages===!0}):null;if(nativeBundle){for(const diagnostic of nativeBundle.diagnostics??[])log.warn(`${diagnostic.file?`${diagnostic.file}: `:""}${diagnostic.message}`);log.info(`Compiled ${Object.keys(config.nativeScreens??{}).length} native screen path(s) into ${nativeBundle.outFile}`)}const{LUCIDE_TO_SF}=native?await import("@stacksjs/stx/native"):{LUCIDE_TO_SF:{}},iconsPath=native?writeAndroidIconTable(projectPath(),output,LUCIDE_TO_SF):null;if(native&&!iconsPath)log.warn("No @iconify-json/lucide in the app: Android draws its own glyphs for the screens' icons.");const craftVersion=craftBuilderVersion(craftAndroidEntry(),projectPath());assertCraftSupportsNativeFonts(config.nativeFonts,craftVersion?.version);if(craftVersion)assertCraftSupportsAndroid(config,craftVersion.version,craftVersion.manifest,{appBundle});else log.warn(`Could not read the craft-native version behind ${craftAndroidEntry()}, so the build cannot check that it supports what config/mobile.ts asks for.`);assertCraftSupportsAndroidMapProvider(config,builder.nativeMapProviders);await builder.init({name:config.appName,packageName:config.packageName,output,config:craftConfig});await builder.build(native?{output,nativeBundlePath:nativeBundle.outFile,iconsPath:iconsPath??void 0,release,bundle:appBundle,compile:process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"}:{output,htmlPath:webAssets??fallbackWebAssets,devServer:craftConfig.devServerURL,release,bundle:appBundle,compile:process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"});if(release&&config.crashReporting?.key&&config.crashReporting.uploadSymbols!==!1&&process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"){const upload=await uploadAndroidMapping(`${output}/app/build/outputs/mapping/release/mapping.txt`,config.crashReporting.host??"https://bughq.org",androidRelease(config),process.env.BUGHQ_UPLOAD_TOKEN);if(upload.ok)log.info(upload.message);else log.warn(upload.message)}const sourceRevision=Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),generatedConfig=JSON.parse(readFileSync(`${output}/craft.config.json`,"utf8"));writeFileSync(`${output}/stacks-mobile.json`,`${JSON.stringify({schemaVersion:"1.0.0",platform:"android",sourceRevision,source:native?{kind:"native",site:craftConfig.devServerURL}:webAssets?{kind:"bundled",path:webAssets}:{kind:"remote",url:craftConfig.devServerURL,fallback:fallbackWebAssets?{kind:"bundled",path:fallbackWebAssets}:void 0},capabilities:config.capabilities??{},builder:resolveCraftBuilderProvenance(process.env.CRAFT_ANDROID_SRC),craft:generatedConfig},null,2)}
|
|
2
2
|
`);log.success(`Built the Craft Android project in ${output}`);await log.flush();process.exitCode=0;
|
|
@@ -146,6 +146,7 @@ export declare interface CraftIosConfig {
|
|
|
146
146
|
nativeScreens?: Record<string, string>
|
|
147
147
|
nativeBundle?: string
|
|
148
148
|
nativeApp?: string
|
|
149
|
+
nativeScriptThread?: IosMobileConfig['nativeScriptThread']
|
|
149
150
|
tabs?: IosMobileConfig['tabs']
|
|
150
151
|
shareStorage?: Record<string, string>
|
|
151
152
|
appGroups?: string[]
|
package/dist/build/ios-config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{isAbsolute,join,relative,resolve}from"node:path";import{spotlightActivityTypes}from"@stacksjs/mobile";export const NATIVE_APP_ROUTE="__app";const CAPABILITY_KEYS={speechRecognition:"enableSpeechRecognition",haptics:"enableHaptics",share:"enableShare",camera:"enableCamera",biometric:"enableBiometric",pushNotifications:"enablePushNotifications",secureStorage:"enableSecureStorage",geolocation:"enableGeolocation",backgroundLocation:"enableBackgroundLocation",clipboard:"enableClipboard",contacts:"enableContacts",calendar:"enableCalendar",localNotifications:"enableLocalNotifications",inAppPurchase:"enableInAppPurchase",keepAwake:"enableKeepAwake",orientationLock:"enableOrientationLock",deepLinks:"enableDeepLinks",qrScanner:"enableQRScanner",localNetwork:"enableLocalNetwork",filePicker:"enableFilePicker",fileDownload:"enableFileDownload",socialAuth:"enableSocialAuth",audioRecording:"enableAudioRecording",videoRecording:"enableVideoRecording",motionSensors:"enableMotionSensors",localDatabase:"enableLocalDatabase",bluetooth:"enableBluetooth",nfc:"enableNFC",healthKit:"enableHealthKit",appleMusic:"enableAppleMusic",heartRate:"enableHeartRate",backgroundAudio:"enableBackgroundAudio",scheduledWorkouts:"enableScheduledWorkouts",liveActivities:"enableLiveActivities",watchApp:"enableWatchApp",backgroundTasks:"enableBackgroundTasks",screenCapture:"enableScreenCapture",pdfViewer:"enablePDFViewer",augmentedReality:"enableAR",machineLearning:"enableMLKit"};export const CRAFT_CAPABILITY_FLOORS={appleMusic:"0.0.144",heartRate:"0.0.152",backgroundAudio:"0.0.162",scheduledWorkouts:"0.0.163"};export function unsupportedCraftCapabilities(capabilities,version){const missing=[];for(const[capability,needs]of Object.entries(CRAFT_CAPABILITY_FLOORS))if(capabilities?.[capability]&&!Bun.semver.satisfies(version,`>=${needs}`))missing.push({capability,needs});return missing}export function assertCraftSupportsCapabilities(capabilities,version,source){const missing=unsupportedCraftCapabilities(capabilities,version);if(missing.length===0)return;const needed=missing.reduce((max,{needs})=>Bun.semver.order(needs,max)>0?needs:max,"0.0.0");throw Error(`The Craft iOS builder is craft-native ${version} (${source}), which would build the app without ${missing.map(({capability,needs})=>`ios.capabilities.${capability} (needs >= ${needs})`).join(", ")}. Install craft-native >= ${needed}: look for an older copy nested under @stacksjs/mobile in bun.lock, or raise the app's own craft-native pin.`)}export const CRAFT_CRASH_REPORTING_FLOOR="0.0.179";export function resolveCrashReporting(shared,platform){const set=(config)=>Object.fromEntries(Object.entries(config??{}).filter(([,value])=>value!==void 0&&value!=="")),merged={...set(shared),...set(platform)};if(!merged.key?.trim())return;if(merged.host){const url=new URL(normalizeMobileUrl(merged.host)),local=["localhost","127.0.0.1","::1","10.0.2.2"].includes(url.hostname);if(url.protocol!=="https:"&&!local)throw Error("crashReporting.host must use HTTPS outside local development");merged.host=url.origin}return merged}export function toCraftCrashReporting(config){if(!config?.key)return;const craft={key:config.key,host:config.host,environment:config.environment,release:config.release,sessions:config.sessions};for(const key of Object.keys(craft))if(craft[key]===void 0)delete craft[key];return craft}export function assertCraftSupportsCrashReporting(config,version,source){if(!config?.key||Bun.semver.satisfies(version,`>=${CRAFT_CRASH_REPORTING_FLOOR}`))return;throw Error(`The Craft builder is craft-native ${version} (${source}), which would build the app without crash reporting (crashReporting needs >= ${CRAFT_CRASH_REPORTING_FLOOR}). Install craft-native >= ${CRAFT_CRASH_REPORTING_FLOOR}: look for an older copy nested under @stacksjs/mobile in bun.lock, or raise the app's own craft-native pin.`)}export function normalizeMobileUrl(value){const input=value?.trim();if(!input)return;return new URL(/^https?:\/\//i.test(input)?input:`https://${input}`).toString().replace(/\/$/,"")}export function portablePaths(value,projectDir,appRoot){if(typeof value==="string")return isAbsolute(value)&&!relative(appRoot,value).startsWith("..")?relative(projectDir,value)||".":value;if(Array.isArray(value))return value.map((item)=>portablePaths(item,projectDir,appRoot));if(value&&typeof value==="object"){const out={};for(const[key,item]of Object.entries(value))out[key]=typeof item==="string"&&!/(?:path(?:dark)?|bundle)$|^(?:src|license)$/i.test(key)?item:portablePaths(item,projectDir,appRoot);return out}return value}export function resolveMobilePath(root,value){if(!value)return;return isAbsolute(value)?value:resolve(root,value)}export function resolveSwipeNavigation(config){if(config.swipeNavigation!==void 0)return config.swipeNavigation;return config.swipeBack==="webview"}export function toCraftIosConfig(config){const devServerURL=normalizeMobileUrl(config.url),trustedOrigins=new Set(config.trustedOrigins??[]);if(devServerURL)trustedOrigins.add(new URL(devServerURL).origin);const declaresAppBoundDomains=(config.appBoundDomains?.length??0)>0,craft={nativeFonts:config.nativeFonts,appName:config.appName,bundleId:config.bundleId,version:config.version,buildNumber:config.buildNumber,darkMode:config.darkMode,appearance:config.appearance,backgroundColor:config.backgroundColor,backgroundColorDark:config.backgroundColorDark,swipeNavigation:resolveSwipeNavigation(config),allowsLinkPreview:config.allowsLinkPreview,keyboardAccessory:config.keyboardAccessory,disableZoom:config.disableZoom,splashMaxSeconds:config.splashMaxSeconds,requestTimeoutSeconds:config.requestTimeoutSeconds,backgroundRefresh:config.backgroundRefresh,iosVersion:config.deploymentTarget,watchosVersion:config.watchDeploymentTarget,teamId:config.teamId,devServerURL,urlSchemes:config.urlSchemes,appShortcuts:config.appShortcuts,trustedOrigins:[...trustedOrigins],appBoundDomains:config.appBoundDomains,limitNavigationsToAppBoundDomains:config.limitNavigationsToAppBoundDomains??(declaresAppBoundDomains||void 0),associatedDomains:config.associatedDomains,renderer:config.renderer,nativeScreens:config.nativeScreens,nativeApp:config.nativeApp?NATIVE_APP_ROUTE:void 0,tabs:config.tabs,shareStorage:config.shareStorage,appGroups:config.appGroups,appIconPath:config.appIcon,privacy:config.privacy,usageDescriptions:config.usageDescriptions,orientations:config.orientations,deviceFamilies:config.deviceFamilies,crashReporting:toCraftCrashReporting(config.crashReporting)};for(const[key,nativeKey]of Object.entries(CAPABILITY_KEYS)){const enabled=config.capabilities?.[key];if(enabled!==void 0)craft[nativeKey]=enabled}if(config.capabilities?.backgroundLocation)craft.enableGeolocation=!0;for(const key of Object.keys(craft))if(craft[key]===void 0)delete craft[key];return craft}export function validateIosMobileConfig(config){if(!config.appName?.trim())throw Error("config/mobile.ts must define ios.appName");if(!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))throw Error(`Invalid iOS bundle identifier: ${config.bundleId}`);if(config.url&&config.webAssets)throw Error("Choose either ios.url or ios.webAssets in config/mobile.ts, not both");if(!config.url&&!config.webAssets)throw Error("config/mobile.ts must define ios.url or ios.webAssets");if(config.renderer!==void 0&&config.renderer!=="web"&&config.renderer!=="native")throw Error(`ios.renderer must be 'web' or 'native', not ${String(config.renderer)}`);if(config.renderer==="native"){if(!Object.keys(config.nativeScreens??{}).length)throw Error("ios.renderer 'native' draws every screen natively: list them in ios.nativeScreens");if(config.webAssets)throw Error("ios.renderer 'native' has no web view to show ios.webAssets in: set ios.url to the app's site instead");if(config.fallbackWebAssets)throw Error("ios.fallbackWebAssets is the web view's offline page; ios.renderer 'native' has no web view")}if(config.fallbackWebAssets&&!config.url)throw Error("ios.fallbackWebAssets requires ios.url");if(config.deviceFamilies&&(config.deviceFamilies.length===0||config.deviceFamilies.some((family)=>family!=="iphone"&&family!=="ipad")))throw Error("ios.deviceFamilies must contain iphone and/or ipad");if(config.url){const url=new URL(normalizeMobileUrl(config.url)),isLocal=["localhost","127.0.0.1","::1"].includes(url.hostname);if(url.protocol!=="https:"&&!isLocal)throw Error("ios.url must use HTTPS outside local development")}for(const domain of config.associatedDomains??[])if(!/^(applinks|webcredentials|activitycontinuation):[^/\s]+$/.test(domain))throw Error(`Invalid iOS associated domain: ${domain}`);for(const key of["splashMaxSeconds","requestTimeoutSeconds"]){const value=config[key];if(value!==void 0&&(!Number.isFinite(value)||value<=0))throw Error(`ios.${key} must be a positive number of seconds`)}const refresh=config.backgroundRefresh;if(refresh){if(refresh.identifier!==void 0&&!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(refresh.identifier))throw Error(`Invalid iOS background refresh identifier: ${refresh.identifier}`);if(refresh.minimumIntervalMinutes!==void 0&&(!Number.isFinite(refresh.minimumIntervalMinutes)||refresh.minimumIntervalMinutes<=0))throw Error("ios.backgroundRefresh.minimumIntervalMinutes must be a positive number of minutes")}if(config.capabilities?.watchApp){const target=Number.parseFloat(config.watchDeploymentTarget??"9.0");if(!Number.isFinite(target)||target<9)throw Error("ios.watchDeploymentTarget must be watchOS 9.0 or newer")}}const ACTIVITY_TYPES_BLOCK=/[ \t]*<key>NSUserActivityTypes<\/key>\s*<array>[\s\S]*?<\/array>\n?/;export function withActivityTypes(plist,types){const entries=types.map((type)=>` <string>${escapeXml(type)}</string>`).join(`
|
|
1
|
+
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{isAbsolute,join,relative,resolve}from"node:path";import{spotlightActivityTypes}from"@stacksjs/mobile";export const NATIVE_APP_ROUTE="__app";const CAPABILITY_KEYS={speechRecognition:"enableSpeechRecognition",haptics:"enableHaptics",share:"enableShare",camera:"enableCamera",biometric:"enableBiometric",pushNotifications:"enablePushNotifications",secureStorage:"enableSecureStorage",geolocation:"enableGeolocation",backgroundLocation:"enableBackgroundLocation",clipboard:"enableClipboard",contacts:"enableContacts",calendar:"enableCalendar",localNotifications:"enableLocalNotifications",inAppPurchase:"enableInAppPurchase",keepAwake:"enableKeepAwake",orientationLock:"enableOrientationLock",deepLinks:"enableDeepLinks",qrScanner:"enableQRScanner",localNetwork:"enableLocalNetwork",filePicker:"enableFilePicker",fileDownload:"enableFileDownload",socialAuth:"enableSocialAuth",audioRecording:"enableAudioRecording",videoRecording:"enableVideoRecording",motionSensors:"enableMotionSensors",localDatabase:"enableLocalDatabase",bluetooth:"enableBluetooth",nfc:"enableNFC",healthKit:"enableHealthKit",appleMusic:"enableAppleMusic",heartRate:"enableHeartRate",backgroundAudio:"enableBackgroundAudio",scheduledWorkouts:"enableScheduledWorkouts",liveActivities:"enableLiveActivities",watchApp:"enableWatchApp",backgroundTasks:"enableBackgroundTasks",screenCapture:"enableScreenCapture",pdfViewer:"enablePDFViewer",augmentedReality:"enableAR",machineLearning:"enableMLKit"};export const CRAFT_CAPABILITY_FLOORS={appleMusic:"0.0.144",heartRate:"0.0.152",backgroundAudio:"0.0.162",scheduledWorkouts:"0.0.163"};export function unsupportedCraftCapabilities(capabilities,version){const missing=[];for(const[capability,needs]of Object.entries(CRAFT_CAPABILITY_FLOORS))if(capabilities?.[capability]&&!Bun.semver.satisfies(version,`>=${needs}`))missing.push({capability,needs});return missing}export function assertCraftSupportsCapabilities(capabilities,version,source){const missing=unsupportedCraftCapabilities(capabilities,version);if(missing.length===0)return;const needed=missing.reduce((max,{needs})=>Bun.semver.order(needs,max)>0?needs:max,"0.0.0");throw Error(`The Craft iOS builder is craft-native ${version} (${source}), which would build the app without ${missing.map(({capability,needs})=>`ios.capabilities.${capability} (needs >= ${needs})`).join(", ")}. Install craft-native >= ${needed}: look for an older copy nested under @stacksjs/mobile in bun.lock, or raise the app's own craft-native pin.`)}export const CRAFT_CRASH_REPORTING_FLOOR="0.0.179";export function resolveCrashReporting(shared,platform){const set=(config)=>Object.fromEntries(Object.entries(config??{}).filter(([,value])=>value!==void 0&&value!=="")),merged={...set(shared),...set(platform)};if(!merged.key?.trim())return;if(merged.host){const url=new URL(normalizeMobileUrl(merged.host)),local=["localhost","127.0.0.1","::1","10.0.2.2"].includes(url.hostname);if(url.protocol!=="https:"&&!local)throw Error("crashReporting.host must use HTTPS outside local development");merged.host=url.origin}return merged}export function toCraftCrashReporting(config){if(!config?.key)return;const craft={key:config.key,host:config.host,environment:config.environment,release:config.release,sessions:config.sessions};for(const key of Object.keys(craft))if(craft[key]===void 0)delete craft[key];return craft}export function assertCraftSupportsCrashReporting(config,version,source){if(!config?.key||Bun.semver.satisfies(version,`>=${CRAFT_CRASH_REPORTING_FLOOR}`))return;throw Error(`The Craft builder is craft-native ${version} (${source}), which would build the app without crash reporting (crashReporting needs >= ${CRAFT_CRASH_REPORTING_FLOOR}). Install craft-native >= ${CRAFT_CRASH_REPORTING_FLOOR}: look for an older copy nested under @stacksjs/mobile in bun.lock, or raise the app's own craft-native pin.`)}export function normalizeMobileUrl(value){const input=value?.trim();if(!input)return;return new URL(/^https?:\/\//i.test(input)?input:`https://${input}`).toString().replace(/\/$/,"")}export function portablePaths(value,projectDir,appRoot){if(typeof value==="string")return isAbsolute(value)&&!relative(appRoot,value).startsWith("..")?relative(projectDir,value)||".":value;if(Array.isArray(value))return value.map((item)=>portablePaths(item,projectDir,appRoot));if(value&&typeof value==="object"){const out={};for(const[key,item]of Object.entries(value))out[key]=typeof item==="string"&&!/(?:path(?:dark)?|bundle)$|^(?:src|license)$/i.test(key)?item:portablePaths(item,projectDir,appRoot);return out}return value}export function resolveMobilePath(root,value){if(!value)return;return isAbsolute(value)?value:resolve(root,value)}export function resolveSwipeNavigation(config){if(config.swipeNavigation!==void 0)return config.swipeNavigation;return config.swipeBack==="webview"}export function toCraftIosConfig(config){const devServerURL=normalizeMobileUrl(config.url),trustedOrigins=new Set(config.trustedOrigins??[]);if(devServerURL)trustedOrigins.add(new URL(devServerURL).origin);const declaresAppBoundDomains=(config.appBoundDomains?.length??0)>0,craft={nativeFonts:config.nativeFonts,appName:config.appName,bundleId:config.bundleId,version:config.version,buildNumber:config.buildNumber,darkMode:config.darkMode,appearance:config.appearance,backgroundColor:config.backgroundColor,backgroundColorDark:config.backgroundColorDark,swipeNavigation:resolveSwipeNavigation(config),allowsLinkPreview:config.allowsLinkPreview,keyboardAccessory:config.keyboardAccessory,disableZoom:config.disableZoom,splashMaxSeconds:config.splashMaxSeconds,requestTimeoutSeconds:config.requestTimeoutSeconds,backgroundRefresh:config.backgroundRefresh,iosVersion:config.deploymentTarget,watchosVersion:config.watchDeploymentTarget,teamId:config.teamId,devServerURL,urlSchemes:config.urlSchemes,appShortcuts:config.appShortcuts,trustedOrigins:[...trustedOrigins],appBoundDomains:config.appBoundDomains,limitNavigationsToAppBoundDomains:config.limitNavigationsToAppBoundDomains??(declaresAppBoundDomains||void 0),associatedDomains:config.associatedDomains,renderer:config.renderer,nativeScreens:config.nativeScreens,nativeApp:config.nativeApp?NATIVE_APP_ROUTE:void 0,nativeScriptThread:config.nativeScriptThread,tabs:config.tabs,shareStorage:config.shareStorage,appGroups:config.appGroups,appIconPath:config.appIcon,privacy:config.privacy,usageDescriptions:config.usageDescriptions,orientations:config.orientations,deviceFamilies:config.deviceFamilies,crashReporting:toCraftCrashReporting(config.crashReporting)};for(const[key,nativeKey]of Object.entries(CAPABILITY_KEYS)){const enabled=config.capabilities?.[key];if(enabled!==void 0)craft[nativeKey]=enabled}if(config.capabilities?.backgroundLocation)craft.enableGeolocation=!0;for(const key of Object.keys(craft))if(craft[key]===void 0)delete craft[key];return craft}export function validateIosMobileConfig(config){if(!config.appName?.trim())throw Error("config/mobile.ts must define ios.appName");if(!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))throw Error(`Invalid iOS bundle identifier: ${config.bundleId}`);if(config.url&&config.webAssets)throw Error("Choose either ios.url or ios.webAssets in config/mobile.ts, not both");if(!config.url&&!config.webAssets)throw Error("config/mobile.ts must define ios.url or ios.webAssets");if(config.renderer!==void 0&&config.renderer!=="web"&&config.renderer!=="native")throw Error(`ios.renderer must be 'web' or 'native', not ${String(config.renderer)}`);if(config.renderer==="native"){if(!Object.keys(config.nativeScreens??{}).length)throw Error("ios.renderer 'native' draws every screen natively: list them in ios.nativeScreens");if(config.webAssets)throw Error("ios.renderer 'native' has no web view to show ios.webAssets in: set ios.url to the app's site instead");if(config.fallbackWebAssets)throw Error("ios.fallbackWebAssets is the web view's offline page; ios.renderer 'native' has no web view")}if(config.fallbackWebAssets&&!config.url)throw Error("ios.fallbackWebAssets requires ios.url");if(config.deviceFamilies&&(config.deviceFamilies.length===0||config.deviceFamilies.some((family)=>family!=="iphone"&&family!=="ipad")))throw Error("ios.deviceFamilies must contain iphone and/or ipad");if(config.url){const url=new URL(normalizeMobileUrl(config.url)),isLocal=["localhost","127.0.0.1","::1"].includes(url.hostname);if(url.protocol!=="https:"&&!isLocal)throw Error("ios.url must use HTTPS outside local development")}for(const domain of config.associatedDomains??[])if(!/^(applinks|webcredentials|activitycontinuation):[^/\s]+$/.test(domain))throw Error(`Invalid iOS associated domain: ${domain}`);for(const key of["splashMaxSeconds","requestTimeoutSeconds"]){const value=config[key];if(value!==void 0&&(!Number.isFinite(value)||value<=0))throw Error(`ios.${key} must be a positive number of seconds`)}const refresh=config.backgroundRefresh;if(refresh){if(refresh.identifier!==void 0&&!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(refresh.identifier))throw Error(`Invalid iOS background refresh identifier: ${refresh.identifier}`);if(refresh.minimumIntervalMinutes!==void 0&&(!Number.isFinite(refresh.minimumIntervalMinutes)||refresh.minimumIntervalMinutes<=0))throw Error("ios.backgroundRefresh.minimumIntervalMinutes must be a positive number of minutes")}if(config.capabilities?.watchApp){const target=Number.parseFloat(config.watchDeploymentTarget??"9.0");if(!Number.isFinite(target)||target<9)throw Error("ios.watchDeploymentTarget must be watchOS 9.0 or newer")}}const ACTIVITY_TYPES_BLOCK=/[ \t]*<key>NSUserActivityTypes<\/key>\s*<array>[\s\S]*?<\/array>\n?/;export function withActivityTypes(plist,types){const entries=types.map((type)=>` <string>${escapeXml(type)}</string>`).join(`
|
|
2
2
|
`),block=types.length===0?"":` <key>NSUserActivityTypes</key>
|
|
3
3
|
<array>
|
|
4
4
|
${entries}
|
package/dist/build/ios-shell.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import{chmodSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import{join}from"node:path";export function writeIosShellSupport(output,config,bunVersion=Bun.version,crashReporting){const version=config.shell?.bunVersion??bunVersion;if(!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(version))throw Error("ios.shell.bunVersion must be a Bun version such as 1.3.14");writeFileSync(join(output,".gitignore"),`# Native sources are restored from the locked Craft package.
|
|
2
2
|
*.swift
|
|
3
|
+
# Compiled screens ship from dist/; this is the compiler's copy.
|
|
4
|
+
/native-screens.js
|
|
5
|
+
/native-screens/
|
|
3
6
|
`);const scripts=join(output,"ci_scripts");mkdirSync(scripts,{recursive:!0});writeFileSync(join(scripts,"bunfig.toml"),`preload = []
|
|
4
7
|
`);const path=join(scripts,"ci_post_clone.sh");writeFileSync(path,`#!/bin/sh
|
|
5
8
|
set -eu
|
package/dist/build/ios.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{pathToFileURL}from"node:url";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{craftBuilderSpecifier,craftBuilderVersion}from"./craft-entry";import{resolveCraftBuilderProvenance}from"./craft-provenance";import{assertCraftSupportsAppShortcuts,assertCraftSupportsCapabilities,assertCraftSupportsCrashReporting,portablePaths,resolveCrashReporting,resolveMobilePath,resolveNativeFonts,assertCraftSupportsNativeFonts,toCraftIosConfig,validateIosMobileConfig,writeIosActivityTypes}from"./ios-config";import{generateXcodeProject,resolveXcodegen}from"./ios-toolchain";import{compileNativeScreens}from"./native-screens";import{writeIosShellSupport}from"./ios-shell";process.exitCode=1;function craftIosEntry(){const explicit=process.env.CRAFT_IOS_SRC;return explicit?pathToFileURL(explicit).href:craftBuilderSpecifier("ios",projectPath())}async function loadCraftIosBuilder(){try{return await import(craftIosEntry())}catch(error){throw Error("Craft iOS builder is unavailable. Install the current craft-native package or set CRAFT_IOS_SRC to packages/ios/src/index.ts in a Craft checkout.",{cause:error})}}const configPath=projectPath("config/mobile.ts");if(!existsSync(configPath))throw Error("Missing config/mobile.ts. Add an ios section before running `buddy build:ios`.");const configModule=await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`),mobile=configModule.default,config=mobile.ios;validateIosMobileConfig(config);if(config.swipeNavigation===!0&&config.swipeBack!=="webview")log.warn("ios.swipeNavigation turns on WebKit's edge swipe alongside the stx router's own. Remove it, or set ios.swipeBack: 'webview' for an app not on the router.");const output=resolveMobilePath(projectPath(),config.output)??storagePath("framework/mobile/ios"),generateProject=process.env.STACKS_IOS_SKIP_XCODEGEN!=="1",xcodegen=generateProject?await resolveXcodegen(projectPath()):null,webAssets=resolveMobilePath(projectPath(),config.webAssets),fallbackWebAssets=resolveMobilePath(projectPath(),config.fallbackWebAssets),crashReporting=resolveCrashReporting(mobile.crashReporting,config.crashReporting),craftConfig=toCraftIosConfig({...config,crashReporting});craftConfig.nativeFonts=resolveNativeFonts(config.nativeFonts,projectPath());craftConfig.appIconPath=resolveMobilePath(projectPath(),config.appIcon);craftConfig.splashImagePath=resolveMobilePath(projectPath(),config.splash?.image);craftConfig.splashImagePathDark=resolveMobilePath(projectPath(),config.splash?.imageDark);const builder=await loadCraftIosBuilder(),nativeBundle=await compileNativeScreens(config,projectPath(),output,void 0,"ios",{routes:builder.nativeRouteBundles===!0});if(nativeBundle){craftConfig.nativeBundle=nativeBundle.outFile;for(const diagnostic of nativeBundle.diagnostics??[])log.warn(`${diagnostic.file?`${diagnostic.file}: `:""}${diagnostic.message}`);log.info(`Compiled ${Object.keys(config.nativeScreens??{}).length} native screen path(s) into ${nativeBundle.outFile}`)}const craftVersion=craftBuilderVersion(craftIosEntry(),projectPath());assertCraftSupportsNativeFonts(config.nativeFonts,craftVersion?.version);assertCraftSupportsAppShortcuts(config.appShortcuts,craftVersion?.version);if(craftVersion){assertCraftSupportsCapabilities(config.capabilities,craftVersion.version,craftVersion.manifest);assertCraftSupportsCrashReporting(crashReporting,craftVersion.version,craftVersion.manifest)}else log.warn(`Could not read the craft-native version behind ${craftIosEntry()}, so the build cannot check that it supports every capability config/mobile.ts turns on.`);await builder.init({name:config.appName,bundleId:config.bundleId,teamId:config.teamId,output,config:craftConfig});const native=config.renderer==="native";if(native&&!nativeBundle)throw Error("ios.renderer 'native' needs native screens to draw: list them in ios.nativeScreens");await builder.build(native?{output,nativeBundlePath:nativeBundle.outFile,generateProject:!1}:{output,htmlPath:webAssets??fallbackWebAssets,devServer:craftConfig.devServerURL,generateProject:!1});if(xcodegen){generateXcodeProject(xcodegen,output);log.info(`Generated the Xcode project with xcodegen ${xcodegen.version} from Pantry`)}writeIosShellSupport(output,config,void 0,crashReporting);if(crashReporting)log.info(`Crash reporting to ${crashReporting.host??"https://bughq.org"}${crashReporting.uploadSymbols===!1?"":": dSYMs upload from Xcode Cloud when BUGHQ_UPLOAD_TOKEN is set in the workflow"}`);const activityTypes=writeIosActivityTypes(output,mobile);if(activityTypes)log.info(`Declared ${activityTypes.length} activity types, so a tapped Spotlight or Siri entry reaches the app`);const sourceRevision=Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),craftConfigPath=`${output}/craft.config.json`,generatedConfig=portablePaths(JSON.parse(readFileSync(craftConfigPath,"utf8")),output,projectPath());writeFileSync(craftConfigPath,`${JSON.stringify(generatedConfig,null,2)}
|
|
1
|
+
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{pathToFileURL}from"node:url";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{craftBuilderSpecifier,craftBuilderVersion}from"./craft-entry";import{resolveCraftBuilderProvenance}from"./craft-provenance";import{assertCraftSupportsAppShortcuts,assertCraftSupportsCapabilities,assertCraftSupportsCrashReporting,portablePaths,resolveCrashReporting,resolveMobilePath,resolveNativeFonts,assertCraftSupportsNativeFonts,toCraftIosConfig,validateIosMobileConfig,writeIosActivityTypes}from"./ios-config";import{generateXcodeProject,resolveXcodegen}from"./ios-toolchain";import{compileNativeScreens}from"./native-screens";import{writeIosShellSupport}from"./ios-shell";process.exitCode=1;function craftIosEntry(){const explicit=process.env.CRAFT_IOS_SRC;return explicit?pathToFileURL(explicit).href:craftBuilderSpecifier("ios",projectPath())}async function loadCraftIosBuilder(){try{return await import(craftIosEntry())}catch(error){throw Error("Craft iOS builder is unavailable. Install the current craft-native package or set CRAFT_IOS_SRC to packages/ios/src/index.ts in a Craft checkout.",{cause:error})}}const configPath=projectPath("config/mobile.ts");if(!existsSync(configPath))throw Error("Missing config/mobile.ts. Add an ios section before running `buddy build:ios`.");const configModule=await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`),mobile=configModule.default,config=mobile.ios;validateIosMobileConfig(config);if(config.swipeNavigation===!0&&config.swipeBack!=="webview")log.warn("ios.swipeNavigation turns on WebKit's edge swipe alongside the stx router's own. Remove it, or set ios.swipeBack: 'webview' for an app not on the router.");const output=resolveMobilePath(projectPath(),config.output)??storagePath("framework/mobile/ios"),generateProject=process.env.STACKS_IOS_SKIP_XCODEGEN!=="1",xcodegen=generateProject?await resolveXcodegen(projectPath()):null,webAssets=resolveMobilePath(projectPath(),config.webAssets),fallbackWebAssets=resolveMobilePath(projectPath(),config.fallbackWebAssets),crashReporting=resolveCrashReporting(mobile.crashReporting,config.crashReporting),craftConfig=toCraftIosConfig({...config,crashReporting});craftConfig.nativeFonts=resolveNativeFonts(config.nativeFonts,projectPath());craftConfig.appIconPath=resolveMobilePath(projectPath(),config.appIcon);craftConfig.splashImagePath=resolveMobilePath(projectPath(),config.splash?.image);craftConfig.splashImagePathDark=resolveMobilePath(projectPath(),config.splash?.imageDark);const builder=await loadCraftIosBuilder(),nativeBundle=await compileNativeScreens(config,projectPath(),output,void 0,"ios",{routes:builder.nativeRouteBundles===!0,runtime:builder.nativeRouteRuntime===!0,messages:builder.nativeMessages===!0});if(nativeBundle){craftConfig.nativeBundle=nativeBundle.outFile;for(const diagnostic of nativeBundle.diagnostics??[])log.warn(`${diagnostic.file?`${diagnostic.file}: `:""}${diagnostic.message}`);log.info(`Compiled ${Object.keys(config.nativeScreens??{}).length} native screen path(s) into ${nativeBundle.outFile}`)}const craftVersion=craftBuilderVersion(craftIosEntry(),projectPath());assertCraftSupportsNativeFonts(config.nativeFonts,craftVersion?.version);assertCraftSupportsAppShortcuts(config.appShortcuts,craftVersion?.version);if(craftVersion){assertCraftSupportsCapabilities(config.capabilities,craftVersion.version,craftVersion.manifest);assertCraftSupportsCrashReporting(crashReporting,craftVersion.version,craftVersion.manifest)}else log.warn(`Could not read the craft-native version behind ${craftIosEntry()}, so the build cannot check that it supports every capability config/mobile.ts turns on.`);await builder.init({name:config.appName,bundleId:config.bundleId,teamId:config.teamId,output,config:craftConfig});const native=config.renderer==="native";if(native&&!nativeBundle)throw Error("ios.renderer 'native' needs native screens to draw: list them in ios.nativeScreens");await builder.build(native?{output,nativeBundlePath:nativeBundle.outFile,generateProject:!1}:{output,htmlPath:webAssets??fallbackWebAssets,devServer:craftConfig.devServerURL,generateProject:!1});if(xcodegen){generateXcodeProject(xcodegen,output);log.info(`Generated the Xcode project with xcodegen ${xcodegen.version} from Pantry`)}writeIosShellSupport(output,config,void 0,crashReporting);if(crashReporting)log.info(`Crash reporting to ${crashReporting.host??"https://bughq.org"}${crashReporting.uploadSymbols===!1?"":": dSYMs upload from Xcode Cloud when BUGHQ_UPLOAD_TOKEN is set in the workflow"}`);const activityTypes=writeIosActivityTypes(output,mobile);if(activityTypes)log.info(`Declared ${activityTypes.length} activity types, so a tapped Spotlight or Siri entry reaches the app`);const sourceRevision=Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),craftConfigPath=`${output}/craft.config.json`,generatedConfig=portablePaths(JSON.parse(readFileSync(craftConfigPath,"utf8")),output,projectPath());writeFileSync(craftConfigPath,`${JSON.stringify(generatedConfig,null,2)}
|
|
2
2
|
`);writeFileSync(`${output}/stacks-mobile.json`,`${JSON.stringify(portablePaths({schemaVersion:"1.0.0",platform:"ios",sourceRevision,source:native?{kind:"native",site:craftConfig.devServerURL}:webAssets?{kind:"bundled",path:webAssets}:{kind:"remote",url:craftConfig.devServerURL,fallback:fallbackWebAssets?{kind:"bundled",path:fallbackWebAssets}:void 0},capabilities:config.capabilities??{},builder:resolveCraftBuilderProvenance(process.env.CRAFT_IOS_SRC),craft:generatedConfig},output,projectPath()),null,2)}
|
|
3
3
|
`);log.success(`Built the Craft iOS project in ${output}`);await log.flush();process.exitCode=0;
|
|
@@ -19,7 +19,7 @@ export declare function nativeAppFile(config: Pick<IosMobileConfig, 'nativeApp'>
|
|
|
19
19
|
* app script parse all of them first: ~170 ms of main thread per screen for
|
|
20
20
|
* HQ's 17 MB. Otherwise, the one-file bundle.
|
|
21
21
|
*/
|
|
22
|
-
export declare function compileNativeScreens(config: Pick<IosMobileConfig, 'nativeScreens' | 'nativeScreensDir' | 'nativeApp'>, root: string, output: string, compile?: CompileNativeBundle, section?: 'ios' | 'android', options?: { routes?: boolean }): Promise<{ outFile: string, diagnostics: NativeBundleResult['diagnostics'] } | null>;
|
|
22
|
+
export declare function compileNativeScreens(config: Pick<IosMobileConfig, 'nativeScreens' | 'nativeScreensDir' | 'nativeApp' | 'nativeMessages'>, root: string, output: string, compile?: CompileNativeBundle, section?: 'ios' | 'android', options?: { routes?: boolean, runtime?: boolean, messages?: boolean }): Promise<{ outFile: string, diagnostics: NativeBundleResult['diagnostics'] } | null>;
|
|
23
23
|
/** What stx's native compiler answers (`@stacksjs/stx/native`). */
|
|
24
24
|
declare interface NativeBundleResult {
|
|
25
25
|
outFile?: string
|
|
@@ -37,4 +37,8 @@ declare type CompileNativeBundle = (options: {
|
|
|
37
37
|
root?: string
|
|
38
38
|
/** The app script, run once beside the screens (stx 0.2.504 and later). */
|
|
39
39
|
app?: string
|
|
40
|
+
/** The runtime as a file of its own beside the route files (stx 0.2.578 and later). */
|
|
41
|
+
sharedRuntime?: boolean
|
|
42
|
+
/** Messages a language at a time, beside the route files (stx 0.2.578 and later). */
|
|
43
|
+
messages?: { dir: string, languages: string[] }
|
|
40
44
|
}) => Promise<NativeBundleResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import{join,resolve}from"node:path";export function nativeScreenFiles(config,root,section="ios"){const dir=resolve(root,config.nativeScreensDir??"resources/native"),files={};for(const name of Object.values(config.nativeScreens??{})){if(files[name])continue;const file=join(dir,`${name}.stx`);if(!existsSync(file))throw Error(`${section}.nativeScreens names the screen "${name}", but ${file} does not exist.`);files[name]=file}return files}export function nativeAppFile(config,root,section="ios"){if(!config.nativeApp)return null;const file=resolve(root,config.nativeApp);if(!existsSync(file))throw Error(`${section}.nativeApp names ${config.nativeApp}, but ${file} does not exist.`);return file}export async function compileNativeScreens(config,root,output,compile,section="ios",options={}){const screens=nativeScreenFiles(config,root,section),names=Object.keys(screens),app=nativeAppFile(config,root,section);if(!names.length){if(app)throw Error(`${section}.nativeApp runs beside native screens: list them in ${section}.nativeScreens`);return null}const compileBundle=compile??(await import("@stacksjs/stx/native")).compileNativeBundle;mkdirSync(output,{recursive:!0});const outFile=join(output,"native-screens.js"),routesDir=join(output,"native-screens");rmSync(options.routes?outFile:routesDir,{recursive:!0,force:!0});if(options.routes){rmSync(routesDir,{recursive:!0,force:!0});const result=await compileBundle({screens,initialScreen:names[0],minify:!0,routesDir,root,...app?{app}:{}});if(result.routes&&Object.keys(result.routes).length)return{outFile:routesDir,diagnostics:result.diagnostics??[]};if(!result.code)throw Error("stx compiled no native bundle: it wrote no route files and returned no code");writeFileSync(outFile,result.code);return{outFile,diagnostics:result.diagnostics??[]}}const result=await compileBundle({screens,initialScreen:names[0],minify:!0,outFile,root,...app?{app}:{}});return{outFile:result.outFile||outFile,diagnostics:result.diagnostics??[]}}
|
|
1
|
+
import{existsSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import{join,resolve}from"node:path";export function nativeScreenFiles(config,root,section="ios"){const dir=resolve(root,config.nativeScreensDir??"resources/native"),files={};for(const name of Object.values(config.nativeScreens??{})){if(files[name])continue;const file=join(dir,`${name}.stx`);if(!existsSync(file))throw Error(`${section}.nativeScreens names the screen "${name}", but ${file} does not exist.`);files[name]=file}return files}export function nativeAppFile(config,root,section="ios"){if(!config.nativeApp)return null;const file=resolve(root,config.nativeApp);if(!existsSync(file))throw Error(`${section}.nativeApp names ${config.nativeApp}, but ${file} does not exist.`);return file}export async function compileNativeScreens(config,root,output,compile,section="ios",options={}){const screens=nativeScreenFiles(config,root,section),names=Object.keys(screens),app=nativeAppFile(config,root,section);if(!names.length){if(app)throw Error(`${section}.nativeApp runs beside native screens: list them in ${section}.nativeScreens`);return null}const compileBundle=compile??(await import("@stacksjs/stx/native")).compileNativeBundle;mkdirSync(output,{recursive:!0});const outFile=join(output,"native-screens.js"),routesDir=join(output,"native-screens");rmSync(options.routes?outFile:routesDir,{recursive:!0,force:!0});if(options.routes){rmSync(routesDir,{recursive:!0,force:!0});const sharedRuntime=options.runtime===!0,messages=sharedRuntime&&options.messages&&config.nativeMessages?{dir:config.nativeMessages.dir,languages:[...config.nativeMessages.languages]}:void 0,result=await compileBundle({screens,initialScreen:names[0],minify:!0,routesDir,root,...sharedRuntime?{sharedRuntime}:{},...app?{app}:{},...messages?{messages}:{}});if(result.routes&&Object.keys(result.routes).length)return{outFile:routesDir,diagnostics:result.diagnostics??[]};if(!result.code)throw Error("stx compiled no native bundle: it wrote no route files and returned no code");writeFileSync(outFile,result.code);return{outFile,diagnostics:result.diagnostics??[]}}const result=await compileBundle({screens,initialScreen:names[0],minify:!0,outFile,root,...app?{app}:{}});return{outFile:result.outFile||outFile,diagnostics:result.diagnostics??[]}}
|
package/dist/bump.js
CHANGED
|
@@ -2,7 +2,7 @@ import{execSync,log,parseOptions}from"@stacksjs/cli";import{path as p}from"@stac
|
|
|
2
2
|
`).slice(0,4).join(`
|
|
3
3
|
`)))content=content.replace(/^(#\s.*\n+)?/,(match)=>`${match??""}## v${nextVersion}
|
|
4
4
|
|
|
5
|
-
`);writeFileSync(changelogPath,content)}await writeChangelog();if(!isDryRun&&isFrameworkRelease)pinLockstepDeps(nextVersion);async function refreshPantryLock(){const lockPath=p.projectPath("pantry.lock");if(!existsSync(lockPath))return;const previousLock=readFileSync(lockPath);try{await execSync(["pantry","install","--ignore-scripts","--quiet"],{cwd:p.projectPath(),stdin:"inherit"});if(!existsSync(lockPath))throw Error("Pantry completed without writing pantry.lock")}catch(error){writeFileSync(lockPath,previousLock);throw Error("Release aborted: Pantry could not refresh pantry.lock without lifecycle scripts. The previous lockfile was restored.",{cause:error})}const violations=pantryLockViolations(readFileSync(lockPath,"utf8"),previousLock.toString("utf8"));if(violations.length>0){writeFileSync(lockPath,previousLock);throw Error(`Release aborted: refreshing pantry.lock produced a lockfile that does not describe this workspace, so the previous one has been restored.
|
|
5
|
+
`);writeFileSync(changelogPath,content)}await writeChangelog();if(!isDryRun&&isFrameworkRelease)pinLockstepDeps(nextVersion);async function refreshPantryLock(){const lockPath=p.projectPath("pantry.lock");if(!existsSync(lockPath))return;const previousLock=readFileSync(lockPath);try{await execSync(["pantry","install","--ignore-scripts","--quiet"],{cwd:p.projectPath(),stdin:"inherit",throwOnError:!0});if(!existsSync(lockPath))throw Error("Pantry completed without writing pantry.lock")}catch(error){writeFileSync(lockPath,previousLock);throw Error("Release aborted: Pantry could not refresh pantry.lock without lifecycle scripts. The previous lockfile was restored.",{cause:error})}const declarations=existsSync(bunLockPath)?Bun.JSONC.parse(readFileSync(bunLockPath,"utf8")).workspaces:void 0,violations=pantryLockViolations(readFileSync(lockPath,"utf8"),previousLock.toString("utf8"),declarations);if(violations.length>0){writeFileSync(lockPath,previousLock);throw Error(`Release aborted: refreshing pantry.lock produced a lockfile that does not describe this workspace, so the previous one has been restored.
|
|
6
6
|
`+violations.map((violation)=>` - ${violation}`).join(`
|
|
7
7
|
`)+"\n\nThis is platform divergence in `pantry install`, not something the version bump did. Regenerate pantry.lock on the platform CI validates against, commit that on its own, then re-run the release.")}}const bunLockPath=p.projectPath("bun.lock"),pantryLockPath=p.projectPath("pantry.lock"),committedBunLock=existsSync(bunLockPath)?readFileSync(bunLockPath):null,committedPantryLock=existsSync(pantryLockPath)?readFileSync(pantryLockPath):null;function restoreCommittedLockfiles(){if(committedBunLock)writeFileSync(bunLockPath,committedBunLock);if(committedPantryLock)writeFileSync(pantryLockPath,committedPantryLock)}if(!isDryRun&&committedBunLock){const lockPath=bunLockPath;unlinkSync(lockPath);try{await execSync(["bun","install","--lockfile-only"],{cwd:p.projectPath(),stdin:"inherit"})}catch(error){restoreCommittedLockfiles();throw error}if(!existsSync(lockPath)){restoreCommittedLockfiles();throw Error("Release aborted: `bun install --lockfile-only` wrote no lockfile, so the previous one has been restored. This usually means a dependency range this bump just raised names a version that is not published yet - release that package first, then re-run.")}}if(!isDryRun)try{await refreshPantryLock()}catch(error){restoreCommittedLockfiles();throw error}if(!isDryRun&&committedBunLock){const lockPath=bunLockPath,expectedLockfileVersion=lockfileVersion(committedBunLock.toString("utf8")),regeneratedLock=readFileSync(lockPath,"utf8"),producedVersion=lockfileVersion(regeneratedLock);if(expectedLockfileVersion==null||producedVersion!==expectedLockfileVersion){restoreCommittedLockfiles();const remedy=producedVersion!=null&&expectedLockfileVersion!=null&&producedVersion>expectedLockfileVersion?"the committed lockfile predates the Bun this repository declares. Regenerate it once with that toolchain (`bun install --lockfile-only`) and commit the result as its own change, then re-run the release.":"this machine's Bun is older than the one that wrote the committed lockfile. Re-run the release with the repository's declared Bun toolchain through `pantry install`.";throw Error(`Release aborted: regenerating bun.lock produced lockfileVersion ${producedVersion??"unknown"}, but the committed one is v${expectedLockfileVersion??"unknown"} \u2014 ${remedy}`)}}function lockstepPackages(version){const names=new Set;for(const entry of readdirSync(p.frameworkPath("core"),{withFileTypes:!0})){if(!entry.isDirectory())continue;const manifest=p.frameworkPath(`core/${entry.name}/package.json`);if(!existsSync(manifest))continue;const pkg=JSON.parse(readFileSync(manifest,"utf-8"));if(pkg.name?.startsWith("@stacksjs/")&&pkg.version===version)names.add(pkg.name)}return names}function pinLockstepDeps(version){const lockstep=lockstepPackages(version),next=version,manifests=[p.frameworkPath("core/package.json"),...readdirSync(p.frameworkPath("core"),{withFileTypes:!0}).filter((entry)=>entry.isDirectory()).map((entry)=>p.frameworkPath(`core/${entry.name}/package.json`)).filter((path)=>existsSync(path))];let pinned=0;for(const manifestPath of manifests){const manifest=JSON.parse(readFileSync(manifestPath,"utf-8"));let changed=!1;for(const field of["dependencies","devDependencies"]){const deps=manifest[field];if(!deps)continue;for(const name of Object.keys(deps)){if(!lockstep.has(name)||deps[name].startsWith("workspace:")||deps[name]===next)continue;deps[name]=next;changed=!0;pinned++}}if(changed)writeFileSync(manifestPath,`${JSON.stringify(manifest,null,2)}
|
|
8
8
|
`)}if(pinned>0)log.debug(`Pinned ${pinned} lockstep core dep(s) to ${version}`)}async function stageReleaseArtifacts(){const pathspecs=isFrameworkRelease?[":(glob)storage/framework/**/package.json","package.json"]:["package.json"];for(const file of["CHANGELOG.md","bun.lock","pantry.lock"]){if(!existsSync(p.projectPath(file)))continue;if(await isGitIgnored(file))continue;pathspecs.push(file)}await git(["add","--",...pathspecs]);const leftover=(await git(["status","--porcelain"])).split(`
|
package/dist/dev/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync}from"node:fs";import process from"node:process";import{parseOptions}from"@stacksjs/cli";import{config,overridesReady}from"@stacksjs/config";import{path}from"@stacksjs/path";import{disableViewRouting,route}from"@stacksjs/router";import{autoImportsAreStale,generateAutoImportFiles,generateServerAutoImportTypes,injectGlobalAutoImports}from"@stacksjs/server";import{resolveApiHost}from"../helpers/api-host";import{exitWithParent}from"./exit-with-parent";const _options=parseOptions();exitWithParent();await generateServerAutoImportTypes();await overridesReady;const port=Number(process.env.PORT_API)||config.ports?.api||3008,hostname=resolveApiHost();try{const{discoverPackages}=await import("../discover-packages");await discoverPackages()}catch{}const modelsIndex=path.storagePath("framework/auto-imports/models.ts");if(!existsSync(modelsIndex)||autoImportsAreStale())await generateAutoImportFiles();await injectGlobalAutoImports();try{const typesenseDev=path.appPath("Lib/typesense-dev.ts");if(existsSync(typesenseDev)){const mod=await import(typesenseDev);if(typeof mod.ensureTypesenseRunning==="function")await mod.ensureTypesenseRunning()}}catch(err){const{log}=await import("@stacksjs/cli");log.warn(`[api:dev] search engine bootstrap skipped: ${err.message}`)}try{const listenerPath=path.appPath("Listener.ts");if(existsSync(listenerPath)){const listener=await import(listenerPath);if(typeof listener.handleEvents==="function")await listener.handleEvents()}}catch(err){const{log}=await import("@stacksjs/cli");log.warn(`[api:dev] failed to bootstrap event listeners - dispatched events will be ignored: ${err.message}`)}try{const{startModelMigrationWatcher}=await import("./migrate-watcher");startModelMigrationWatcher()}catch(err){const{log}=await import("@stacksjs/cli");log.debug(`[api:dev] auto-migrate watcher not started: ${err.message}`)}function resolveDefaultsCorsPath(){const published=path.appPath("Middleware/Cors.ts");if(existsSync(published))return published;const vendored=path.frameworkPath("defaults/app/Middleware/Cors.ts");if(existsSync(vendored))return vendored;try{return`${Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd()).replace(/package\.json$/,"")}app/Middleware/Cors.ts`}catch{return vendored}}const corsMod=await import(resolveDefaultsCorsPath()),corsMiddleware=corsMod.default;route.use(corsMiddleware.toRouterHandler());const SAFE_REQUEST_ID=/^[a-zA-Z0-9._-]{8,128}$/;route.use(async(request,next)=>{const inbound=request?.headers?.get?.("x-request-id")||request?.headers?.get?.("X-Request-ID")||"",id=typeof inbound==="string"&&SAFE_REQUEST_ID.test(inbound)?inbound:crypto.randomUUID();request._requestId=id;request._startNs=process.hrtime.bigint();const response=await next();if(response&&response.status===404&&(response.headers.get("content-type")||"").includes("json"))try{const body=await response.clone().json();if(body&&(body.message==="Not Found"||body.error==="Not Found")){const url=new URL(request.url),enriched={...body,path:url.pathname,method:request.method,request_id:id},headers=new Headers(response.headers);headers.set("X-Request-ID",id);return new Response(JSON.stringify(enriched),{status:404,headers})}}catch{}return response});await route.importRoutes();disableViewRouting(route.bunRouter);try{await route.serve({port,hostname});console.log(`
|
|
1
|
+
import{existsSync}from"node:fs";import process from"node:process";import{parseOptions}from"@stacksjs/cli";import{config,overridesReady}from"@stacksjs/config";import{path}from"@stacksjs/path";import{disableViewRouting,route}from"@stacksjs/router";import{autoImportsAreStale,generateAutoImportFiles,generateServerAutoImportTypes,injectGlobalAutoImports,warnShadowedSecurityDefaults}from"@stacksjs/server";import{resolveApiHost}from"../helpers/api-host";import{exitWithParent}from"./exit-with-parent";const _options=parseOptions();exitWithParent();await generateServerAutoImportTypes();await overridesReady;const port=Number(process.env.PORT_API)||config.ports?.api||3008,hostname=resolveApiHost();try{const{discoverPackages}=await import("../discover-packages");await discoverPackages()}catch{}const modelsIndex=path.storagePath("framework/auto-imports/models.ts");if(!existsSync(modelsIndex)||autoImportsAreStale())await generateAutoImportFiles();await injectGlobalAutoImports();warnShadowedSecurityDefaults();try{const typesenseDev=path.appPath("Lib/typesense-dev.ts");if(existsSync(typesenseDev)){const mod=await import(typesenseDev);if(typeof mod.ensureTypesenseRunning==="function")await mod.ensureTypesenseRunning()}}catch(err){const{log}=await import("@stacksjs/cli");log.warn(`[api:dev] search engine bootstrap skipped: ${err.message}`)}try{const listenerPath=path.appPath("Listener.ts");if(existsSync(listenerPath)){const listener=await import(listenerPath);if(typeof listener.handleEvents==="function")await listener.handleEvents()}}catch(err){const{log}=await import("@stacksjs/cli");log.warn(`[api:dev] failed to bootstrap event listeners - dispatched events will be ignored: ${err.message}`)}try{const{startModelMigrationWatcher}=await import("./migrate-watcher");startModelMigrationWatcher()}catch(err){const{log}=await import("@stacksjs/cli");log.debug(`[api:dev] auto-migrate watcher not started: ${err.message}`)}function resolveDefaultsCorsPath(){const published=path.appPath("Middleware/Cors.ts");if(existsSync(published))return published;const vendored=path.frameworkPath("defaults/app/Middleware/Cors.ts");if(existsSync(vendored))return vendored;try{return`${Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd()).replace(/package\.json$/,"")}app/Middleware/Cors.ts`}catch{return vendored}}const corsMod=await import(resolveDefaultsCorsPath()),corsMiddleware=corsMod.default;route.use(corsMiddleware.toRouterHandler());const SAFE_REQUEST_ID=/^[a-zA-Z0-9._-]{8,128}$/;route.use(async(request,next)=>{const inbound=request?.headers?.get?.("x-request-id")||request?.headers?.get?.("X-Request-ID")||"",id=typeof inbound==="string"&&SAFE_REQUEST_ID.test(inbound)?inbound:crypto.randomUUID();request._requestId=id;request._startNs=process.hrtime.bigint();const response=await next();if(response&&response.status===404&&(response.headers.get("content-type")||"").includes("json"))try{const body=await response.clone().json();if(body&&(body.message==="Not Found"||body.error==="Not Found")){const url=new URL(request.url),enriched={...body,path:url.pathname,method:request.method,request_id:id},headers=new Headers(response.headers);headers.set("X-Request-ID",id);return new Response(JSON.stringify(enriched),{status:404,headers})}}catch{}return response});await route.importRoutes();disableViewRouting(route.bunRouter);try{await route.serve({port,hostname});console.log(`
|
|
2
2
|
\u279C API server ready: http://${hostname}:${port}
|
|
3
3
|
`)}catch(err){if((err?.code||err?.errno)==="EADDRINUSE"||String(err?.message||"").includes("EADDRINUSE")){console.error(`
|
|
4
4
|
[dev/api] Port ${port} is already in use. Kill the other process and re-run \`./buddy dev\`, or set PORT_API to another port.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{library}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{generateLibraryPackages}from"../library/build";export async function generateLibEntries(){if(library?.releaseable===!1){log.info("library.releaseable is false. Skipping library entry points.");return}log.info("Generating library entry points...");const reports=await generateLibraryPackages({allowEmpty:!0});for(const report of reports)log.success(`Generated ${report.name} (${report.kind}, ${report.sources} sources)`)}await generateLibEntries();
|
|
1
|
+
import{awaitConfig,library}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{generateLibraryPackages}from"../library/build";export async function generateLibEntries(){await awaitConfig();if(library?.releaseable===!1){log.info("library.releaseable is false. Skipping library entry points.");return}log.info("Generating library entry points...");const reports=await generateLibraryPackages({allowEmpty:!0});for(const report of reports)log.success(`Generated ${report.name} (${report.kind}, ${report.sources} sources)`)}await generateLibEntries();
|
package/dist/library/build.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{mkdir,rm}from"node:fs/promises";import{dirname,relative,resolve}from"node:path";import{transpilePackage,validateDeclarations,validateRuntimeExports}from"@stacksjs/build";import{library}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{projectPath}from"@stacksjs/path";import{buildComponentLibrary}from"@stacksjs/stx";import{functionEntryData}from"./entries";import{ambientGlobalsUsed,stxAmbientGlobals}from"./globals";import{libraryManifest,serializeManifest}from"./manifest";import{LibraryConfigError,resolveLibraryPackages}from"./packages";export async function buildLibraryPackages(options={}){const config=options.config??library,kinds=options.kinds,compile=options.compile??!0,all=await resolveLibraryPackages(config,{onUnmatched:options.allowEmpty?"skip":"error"}),packages=kinds?all.filter((pkg)=>kinds.includes(pkg.kind)):all;if(!packages.length){if(options.allowEmpty){log.info("No library packages are configured. Nothing to build.");return[]}throw new LibraryConfigError(kinds?`No ${kinds.join("/")} packages are configured. Add one to \`packages\` in config/library.ts.`:"No library packages are configured. Add one to `packages` in config/library.ts.")}const version=await projectVersion(),reports=[];for(const pkg of packages){const verb=compile?"Building":"Generating";log.info(`${verb} ${pkg.name} (${pkg.kind}, ${pkg.sources.length} source${pkg.sources.length===1?"":"s"})...`);if(compile)await rm(resolve(pkg.dir,"dist"),{recursive:!0,force:!0});await mkdir(pkg.dir,{recursive:!0});if(pkg.kind==="functions")await buildFunctionPackage(pkg,config,version,compile);else await buildComponentPackage(pkg,config,version,compile);reports.push({name:pkg.name,kind:pkg.kind,dir:pkg.dir,sources:pkg.sources.length});if(compile)log.success(`Built ${pkg.name} \u2192 ${relative(projectPath(),pkg.dir)}/dist`)}return reports}export async function generateLibraryPackages(options={}){return buildLibraryPackages({...options,compile:!1})}async function buildFunctionPackage(pkg,config,version,compile){await assertPublishable(pkg);const srcDir=resolve(pkg.dir,"src");await rm(srcDir,{recursive:!0,force:!0});const staged=[];for(const source of pkg.sources){const destination=resolve(srcDir,relative(pkg.sourceDir,source));await mkdir(dirname(destination),{recursive:!0});await Bun.write(destination,await Bun.file(source).text());staged.push(destination)}const entry=resolve(srcDir,"index.ts");if(staged.includes(entry))throw new LibraryConfigError(`Package "${pkg.name}" claims resources/functions/index.ts, which collides with the generated barrel. Rename it or exclude it.`);await Bun.write(entry,functionEntryData(pkg,staged));await writeManifest(pkg,config,version);if(!compile)return;await transpilePackage({dir:pkg.dir,pkgName:pkg.name,external:["@stacksjs/*","bun",...Object.keys(pkg.dependencies),...Object.keys(pkg.peerDependencies)]});await validateDeclarations(pkg.dir);await validateRuntimeExports(pkg.dir)}async function buildComponentPackage(pkg,config,version,compile){if(!compile){await writeManifest(pkg,config,version);return}const result=await buildComponentLibrary({inputDir:pkg.sourceDir,outputDir:resolve(pkg.dir,"dist"),prefix:pkg.prefix,components:pkg.sources.map((source)=>{const file=relative(pkg.sourceDir,source),stem=file.replace(/\.stx$/,"").split("/").at(-1);return pkg.names[stem]?{file,name:pkg.names[stem]}:{file}}),progressive:!0,manifest:!0,declarations:!0,css:!0,bundle:!0,minify:!0,sourcemap:pkg.sourcemap?"external":!1});log.debug(`${pkg.name}: compiled ${result.components.length} components (${result.totalBytes} bytes)`);await writeManifest(pkg,config,version)}async function assertPublishable(pkg){if(pkg.runtime==="stx")return;const globals=await stxAmbientGlobals();if(!globals.size)return;const offenders=[];for(const source of pkg.sources){const used=ambientGlobalsUsed(await Bun.file(source).text(),globals);if(used.length)offenders.push(` ${relative(projectPath(),source)} \u2192 ${used.join(", ")}`)}if(!offenders.length)return;throw new LibraryConfigError(`Package "${pkg.name}" uses stx's ambient globals, which no module exports:
|
|
1
|
+
import{mkdir,rm}from"node:fs/promises";import{dirname,relative,resolve}from"node:path";import{transpilePackage,validateDeclarations,validateRuntimeExports}from"@stacksjs/build";import{awaitConfig,library}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{projectPath}from"@stacksjs/path";import{buildComponentLibrary}from"@stacksjs/stx";import{functionEntryData}from"./entries";import{ambientGlobalsUsed,stxAmbientGlobals}from"./globals";import{libraryManifest,serializeManifest}from"./manifest";import{LibraryConfigError,resolveLibraryPackages}from"./packages";export async function buildLibraryPackages(options={}){if(!options.config)await awaitConfig();const config=options.config??library,kinds=options.kinds,compile=options.compile??!0,all=await resolveLibraryPackages(config,{onUnmatched:options.allowEmpty?"skip":"error"}),packages=kinds?all.filter((pkg)=>kinds.includes(pkg.kind)):all;if(!packages.length){if(options.allowEmpty){log.info("No library packages are configured. Nothing to build.");return[]}throw new LibraryConfigError(kinds?`No ${kinds.join("/")} packages are configured. Add one to \`packages\` in config/library.ts.`:"No library packages are configured. Add one to `packages` in config/library.ts.")}const version=await projectVersion(),reports=[];for(const pkg of packages){const verb=compile?"Building":"Generating";log.info(`${verb} ${pkg.name} (${pkg.kind}, ${pkg.sources.length} source${pkg.sources.length===1?"":"s"})...`);if(compile)await rm(resolve(pkg.dir,"dist"),{recursive:!0,force:!0});await mkdir(pkg.dir,{recursive:!0});if(pkg.kind==="functions")await buildFunctionPackage(pkg,config,version,compile);else await buildComponentPackage(pkg,config,version,compile);reports.push({name:pkg.name,kind:pkg.kind,dir:pkg.dir,sources:pkg.sources.length});if(compile)log.success(`Built ${pkg.name} \u2192 ${relative(projectPath(),pkg.dir)}/dist`)}return reports}export async function generateLibraryPackages(options={}){return buildLibraryPackages({...options,compile:!1})}async function buildFunctionPackage(pkg,config,version,compile){await assertPublishable(pkg);const srcDir=resolve(pkg.dir,"src");await rm(srcDir,{recursive:!0,force:!0});const staged=[];for(const source of pkg.sources){const destination=resolve(srcDir,relative(pkg.sourceDir,source));await mkdir(dirname(destination),{recursive:!0});await Bun.write(destination,await Bun.file(source).text());staged.push(destination)}const entry=resolve(srcDir,"index.ts");if(staged.includes(entry))throw new LibraryConfigError(`Package "${pkg.name}" claims resources/functions/index.ts, which collides with the generated barrel. Rename it or exclude it.`);await Bun.write(entry,functionEntryData(pkg,staged));await writeManifest(pkg,config,version);if(!compile)return;await transpilePackage({dir:pkg.dir,pkgName:pkg.name,external:["@stacksjs/*","bun",...Object.keys(pkg.dependencies),...Object.keys(pkg.peerDependencies)]});await validateDeclarations(pkg.dir);await validateRuntimeExports(pkg.dir)}async function buildComponentPackage(pkg,config,version,compile){if(!compile){await writeManifest(pkg,config,version);return}const result=await buildComponentLibrary({inputDir:pkg.sourceDir,outputDir:resolve(pkg.dir,"dist"),prefix:pkg.prefix,components:pkg.sources.map((source)=>{const file=relative(pkg.sourceDir,source),stem=file.replace(/\.stx$/,"").split("/").at(-1);return pkg.names[stem]?{file,name:pkg.names[stem]}:{file}}),progressive:!0,manifest:!0,declarations:!0,css:!0,bundle:!0,minify:!0,sourcemap:pkg.sourcemap?"external":!1});log.debug(`${pkg.name}: compiled ${result.components.length} components (${result.totalBytes} bytes)`);await writeManifest(pkg,config,version)}async function assertPublishable(pkg){if(pkg.runtime==="stx")return;const globals=await stxAmbientGlobals();if(!globals.size)return;const offenders=[];for(const source of pkg.sources){const used=ambientGlobalsUsed(await Bun.file(source).text(),globals);if(used.length)offenders.push(` ${relative(projectPath(),source)} \u2192 ${used.join(", ")}`)}if(!offenders.length)return;throw new LibraryConfigError(`Package "${pkg.name}" uses stx's ambient globals, which no module exports:
|
|
2
2
|
${offenders.join(`
|
|
3
3
|
`)}
|
|
4
4
|
|
package/dist/pantry-lock.d.ts
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* at once. An operator who fixes one pin, re-runs a long release and hits the
|
|
6
6
|
* next one learns to distrust the check.
|
|
7
7
|
*/
|
|
8
|
-
export declare function pantryLockViolations(lock: string, baseline: string | null): string[];
|
|
8
|
+
export declare function pantryLockViolations(lock: string, baseline: string | null, declarations?: Record<string, Record<string, unknown>>): string[];
|
package/dist/pantry-lock.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const DEPENDENCY_FIELDS=["dependencies","devDependencies","peerDependencies","optionalDependencies"],UNCOMPARABLE_RANGE=/^(?:workspace|file|link|git|github|npm|catalog):/;function splitPackageKey(key){const at=key.lastIndexOf("@");if(at<=0)return null;return{name:key.slice(0,at),version:key.slice(at+1)}}function resolvedVersions(packages){const resolved=new Map;for(const key of Object.keys(packages)){const parsed=splitPackageKey(key);if(!parsed)continue;const versions=resolved.get(parsed.name);if(versions)versions.push(parsed.version);else resolved.set(parsed.name,[parsed.version])}return resolved}function systemBlockProblems(root,baseline){if(!baseline)return[];let baselineSystem;try{baselineSystem=JSON.parse(baseline).workspaces?.[""]?.system}catch{return[]}if(!baselineSystem||typeof baselineSystem!=="object")return[];const expected=baselineSystem,system=root?.system;if(!system||typeof system!=="object")return[`the root "system" block was dropped (it pinned ${Object.keys(expected).join(", ")}), so CI would have no toolchain to provision`];const present=system;return Object.keys(expected).filter((pin)=>!(pin in present)).map((pin)=>`the root "system" block lost its "${pin}" pin`)}export function pantryLockViolations(lock,baseline){let parsed;try{parsed=JSON.parse(lock)}catch(error){return[`it is not valid JSON (${error instanceof Error?error.message:String(error)})`]}const problems=[],workspaces=parsed.workspaces??{},resolved=resolvedVersions(parsed.packages??{});for(const[workspacePath,workspace]of Object.entries(workspaces))for(const field of DEPENDENCY_FIELDS){const declared=workspace[field];if(!declared||typeof declared!=="object")continue;for(const[name,range]of Object.entries(declared)){if(typeof range!=="string"||UNCOMPARABLE_RANGE.test(range))continue;const versions=resolved.get(name);if(!versions)continue;if(!versions.some((version)=>Bun.semver.satisfies(version,range)))problems.push(`${name} is pinned at ${versions.join(", ")}, which does not satisfy "${range}" declared by ${workspacePath||"the workspace root"} (${field})`)}}problems.push(...systemBlockProblems(workspaces[""],baseline));return problems}
|
|
1
|
+
const DEPENDENCY_FIELDS=["dependencies","devDependencies","peerDependencies","optionalDependencies"],UNCOMPARABLE_RANGE=/^(?:workspace|file|link|git|github|npm|catalog):/;function splitPackageKey(key){const at=key.lastIndexOf("@");if(at<=0)return null;return{name:key.slice(0,at),version:key.slice(at+1)}}function resolvedVersions(packages){const resolved=new Map;for(const key of Object.keys(packages)){const parsed=splitPackageKey(key);if(!parsed)continue;const versions=resolved.get(parsed.name);if(versions)versions.push(parsed.version);else resolved.set(parsed.name,[parsed.version])}return resolved}function systemBlockProblems(root,baseline){if(!baseline)return[];let baselineSystem;try{baselineSystem=JSON.parse(baseline).workspaces?.[""]?.system}catch{return[]}if(!baselineSystem||typeof baselineSystem!=="object")return[];const expected=baselineSystem,system=root?.system;if(!system||typeof system!=="object")return[`the root "system" block was dropped (it pinned ${Object.keys(expected).join(", ")}), so CI would have no toolchain to provision`];const present=system;return Object.keys(expected).filter((pin)=>!(pin in present)).map((pin)=>`the root "system" block lost its "${pin}" pin`)}export function pantryLockViolations(lock,baseline,declarations){let parsed;try{parsed=JSON.parse(lock)}catch(error){return[`it is not valid JSON (${error instanceof Error?error.message:String(error)})`]}const problems=[],workspaces=parsed.workspaces??{},resolved=resolvedVersions(parsed.packages??{});if(declarations){for(const[workspacePath,current]of Object.entries(declarations)){const recorded=workspaces[workspacePath],label=workspacePath||"the workspace root";if(!recorded){problems.push(`${label}: current workspace is missing from pantry.lock`);continue}for(const field of["name","version"])if(current[field]!==recorded[field])problems.push(`${label}: ${field} is ${JSON.stringify(current[field])} but pantry.lock records ${JSON.stringify(recorded[field])}`);for(const field of["dependencies","devDependencies"]){const expected=current[field]??{},locked=recorded[field]??{};for(const[name,range]of Object.entries(expected))if(range!==locked[name])problems.push(`${label}: ${field}.${name} is ${JSON.stringify(range)} but pantry.lock records ${JSON.stringify(locked[name])}`);for(const name of Object.keys(locked))if(!(name in expected))problems.push(`${label}: pantry.lock records undeclared ${field}.${name}`)}}for(const workspacePath of Object.keys(workspaces))if(!(workspacePath in declarations))problems.push(`${workspacePath||"the workspace root"}: pantry.lock records a workspace absent from current declarations`)}for(const[workspacePath,workspace]of Object.entries(workspaces))for(const field of DEPENDENCY_FIELDS){const declared=workspace[field];if(!declared||typeof declared!=="object")continue;for(const[name,range]of Object.entries(declared)){if(typeof range!=="string"||UNCOMPARABLE_RANGE.test(range))continue;const versions=resolved.get(name);if(!versions)continue;if(!versions.some((version)=>Bun.semver.satisfies(version,range)))problems.push(`${name} is pinned at ${versions.join(", ")}, which does not satisfy "${range}" declared by ${workspacePath||"the workspace root"} (${field})`)}}problems.push(...systemBlockProblems(workspaces[""],baseline));return problems}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{readFileSync}from"node:fs";import{join}from"node:path";export const BYPASS_ENV="RELEASE_GATE_BYPASS";export function pinnedCheckScripts(scripts){return Object.keys(scripts).filter((name)=>/^docs:(?:[\w:-]+:)?check$/.test(name)).sort()}export function readPinnedChecks(cwd){try{const pkg=JSON.parse(readFileSync(join(cwd,"package.json"),"utf8"));return pinnedCheckScripts(pkg.scripts??{})}catch{return[]}}function isBypassed(env){const value=env[BYPASS_ENV];return value==="1"||value==="true"}export async function runPinnedChecks(options={cwd:process.cwd()}){const env=options.env??process.env;if(isBypassed(env))return{ran:[],failures:[],bypassed:!0};const scripts=options.scripts??readPinnedChecks(options.cwd);if(scripts.length===0)return{ran:[],failures:[],bypassed:!1};const results=await Promise.all(scripts.map(async(script)=>{const proc=Bun.spawn(["bun","run",script],{cwd:options.cwd,stdout:"pipe",stderr:"pipe"}),[stdout,stderr,exitCode]=await Promise.all([new Response(proc.stdout).text(),new Response(proc.stderr).text(),proc.exited]);return{script,exitCode,output:`${stdout}${stderr}`.trim()}}));return{ran:scripts,failures:results.filter((r)=>r.exitCode!==0).map((r)=>({script:r.script,output:r.output})),bypassed:!1}}export function formatPreflightFailure(failures){const
|
|
2
|
-
`).map((line)=>line.trim()).filter((line)=>line&&!line.startsWith("[env]")&&!line.startsWith("$ ")&&!line.startsWith("error: script"));for(const
|
|
1
|
+
import{readFileSync}from"node:fs";import{join}from"node:path";export const BYPASS_ENV="RELEASE_GATE_BYPASS";export function pinnedCheckScripts(scripts){return Object.keys(scripts).filter((name)=>/^docs:(?:[\w:-]+:)?check$/.test(name)).sort()}export function readPinnedChecks(cwd){try{const pkg=JSON.parse(readFileSync(join(cwd,"package.json"),"utf8"));return pinnedCheckScripts(pkg.scripts??{})}catch{return[]}}function isBypassed(env){const value=env[BYPASS_ENV];return value==="1"||value==="true"}export async function runPinnedChecks(options={cwd:process.cwd()}){const env=options.env??process.env;if(isBypassed(env))return{ran:[],failures:[],bypassed:!0};const scripts=options.scripts??readPinnedChecks(options.cwd);if(scripts.length===0)return{ran:[],failures:[],bypassed:!1};const results=await Promise.all(scripts.map(async(script)=>{const proc=Bun.spawn(["bun","run",script],{cwd:options.cwd,stdout:"pipe",stderr:"pipe"}),[stdout,stderr,exitCode]=await Promise.all([new Response(proc.stdout).text(),new Response(proc.stderr).text(),proc.exited]);return{script,exitCode,output:`${stdout}${stderr}`.trim()}}));return{ran:scripts,failures:results.filter((r)=>r.exitCode!==0).map((r)=>({script:r.script,output:r.output})),bypassed:!1}}export function formatPreflightFailure(failures){const diagnosticLines=(output)=>output.split(`
|
|
2
|
+
`).map((line)=>line.trim()).filter((line)=>line&&!line.startsWith("[env]")&&!line.startsWith("$ ")&&!line.startsWith("error: script")),lines=[`Release blocked: ${failures.length} pinned check${failures.length===1?"":"s"} failed.`,""];for(const failure of failures){lines.push(` ${failure.script}`);const informative=diagnosticLines(failure.output.replace(/^\[env-plugin\] Errors: \[\r?\n[\s\S]*?^\][ \t]*\r?$/gm,"")),diagnostics=informative.length?informative:diagnosticLines(failure.output);for(const line of diagnostics.slice(0,4))lines.push(` ${line}`);lines.push("")}lines.push("Nothing was bumped, committed, or tagged.");lines.push(`Regenerate the affected artifacts and retry, or set ${BYPASS_ENV}=1 to release anyway.`);return lines.join(`
|
|
3
3
|
`)}
|
package/dist/release.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseOptions}from"@stacksjs/cli";import{app}from"@stacksjs/config";import{Action}from"@stacksjs/enums";import{log}from"@stacksjs/logging";import{projectPath}from"@stacksjs/path";import{runActions}from"
|
|
1
|
+
import{parseOptions}from"@stacksjs/cli";import{app}from"@stacksjs/config";import{Action}from"@stacksjs/enums";import{log}from"@stacksjs/logging";import{projectPath}from"@stacksjs/path";import{runActions}from"./helpers/utils";import{BYPASS_ENV,formatPreflightFailure,runPinnedChecks}from"./release-preflight";const raw=parseOptions()??{},passthrough={};for(const[k,v]of Object.entries(raw)){if(k==="--"||k==="_")continue;passthrough[k]=v}const isDryRun=passthrough.dryRun===!0||passthrough.dryRun==="true",actions=[Action.GenerateLibraryEntries,Action.Bump],preflight=await runPinnedChecks({cwd:projectPath()});if(preflight.bypassed)log.warn(`${BYPASS_ENV} is set, so the pinned checks were skipped for this release.`);else if(preflight.failures.length>0){await log.flush();await log.exit(formatPreflightFailure(preflight.failures),1)}const result=await runActions(actions,{cwd:projectPath(),...passthrough});if(result&&result.isErr)await log.exit(`Release failed: ${result.error?.message??String(result.error)}`,1);log.success(isDryRun?`Dry run complete for ${app.name}. Nothing was committed, tagged or pushed.`:`Successfully released ${app.name}`);
|
package/dist/serve/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync}from"node:fs";import{dirname,join}from"node:path";import process from"node:process";import{config,overridesReady}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{appPath,frameworkPath}from"@stacksjs/path";import{disableViewRouting,route}from"@stacksjs/router";import{ensureDiscoveredPackages}from"../discover-packages";import{resolveApiHost}from"../helpers/api-host";function resolveDefaultsFile(rel){if(rel.startsWith("app/")){const published=appPath(rel.slice(4));if(existsSync(published))return published}const vendored=frameworkPath(`defaults/${rel}`);if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return join(dirname(pkgJson),rel)}catch{return vendored}}await overridesReady;const port=Number(process.env.PORT)||config.ports?.api||3008,hostname=resolveApiHost();log.info("[Stacks API] Starting server...");log.info(`[Stacks API] Environment: ${process.env.APP_ENV||"development"}`);const corsMod=await import(resolveDefaultsFile("app/Middleware/Cors.ts")),corsMiddleware=corsMod.default;route.use(corsMiddleware.toRouterHandler());await ensureDiscoveredPackages();const{reportStripeAvailability}=await import("@stacksjs/payments");reportStripeAvailability();const{autoImportsAreStale,generateAutoImportFiles,injectGlobalAutoImports}=await import("@stacksjs/server");try{if(autoImportsAreStale()){log.info("[Stacks API] Installed packages moved since the auto-import barrel was written; rebuilding it.");await generateAutoImportFiles({declarations:!1})}}catch(error){log.warn(`[Stacks API] Could not refresh the auto-import barrel: ${error instanceof Error?error.message:String(error)}`)}await injectGlobalAutoImports();await route.importRoutes();try{const{describeUnforwardableRoutes,resolveApiProxyRules,unforwardableRoutes}=await import("@stacksjs/server"),{listRootMountedAppRoutes}=await import("@stacksjs/router"),unreachable=unforwardableRoutes(listRootMountedAppRoutes().filter((entry)=>entry.method==="GET"||entry.method==="HEAD"),resolveApiProxyRules(config.server?.proxy));if(unreachable.length>0)log.warn(describeUnforwardableRoutes(unreachable))}catch(error){log.debug(`[Stacks API] Route reachability check skipped: ${error instanceof Error?error.message:String(error)}`)}disableViewRouting(route.bunRouter);const isDeployed=["production","staging","development"].includes((process.env.APP_ENV||"").toLowerCase()),server=await route.serve({port,hostname,reusePort:isDeployed});process.on("SIGTERM",()=>{const graceMs=Number(process.env.SHUTDOWN_GRACE_MS)||15000;setTimeout(()=>process.exit(0),graceMs).unref();Promise.resolve(server?.stop()).then(()=>process.exit(0))});log.info(`[Stacks API] Server running at http://${hostname}:${port}`);
|
|
1
|
+
import{existsSync}from"node:fs";import{dirname,join}from"node:path";import process from"node:process";import{config,overridesReady}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{appPath,frameworkPath}from"@stacksjs/path";import{disableViewRouting,route}from"@stacksjs/router";import{ensureDiscoveredPackages}from"../discover-packages";import{resolveApiHost}from"../helpers/api-host";function resolveDefaultsFile(rel){if(rel.startsWith("app/")){const published=appPath(rel.slice(4));if(existsSync(published))return published}const vendored=frameworkPath(`defaults/${rel}`);if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return join(dirname(pkgJson),rel)}catch{return vendored}}await overridesReady;const port=Number(process.env.PORT)||config.ports?.api||3008,hostname=resolveApiHost();log.info("[Stacks API] Starting server...");log.info(`[Stacks API] Environment: ${process.env.APP_ENV||"development"}`);const corsMod=await import(resolveDefaultsFile("app/Middleware/Cors.ts")),corsMiddleware=corsMod.default;route.use(corsMiddleware.toRouterHandler());await ensureDiscoveredPackages();const{reportStripeAvailability}=await import("@stacksjs/payments");reportStripeAvailability();const{autoImportsAreStale,generateAutoImportFiles,injectGlobalAutoImports,warnShadowedSecurityDefaults}=await import("@stacksjs/server");try{if(autoImportsAreStale()){log.info("[Stacks API] Installed packages moved since the auto-import barrel was written; rebuilding it.");await generateAutoImportFiles({declarations:!1})}}catch(error){log.warn(`[Stacks API] Could not refresh the auto-import barrel: ${error instanceof Error?error.message:String(error)}`)}await injectGlobalAutoImports();warnShadowedSecurityDefaults();await route.importRoutes();try{const{describeUnforwardableRoutes,resolveApiProxyRules,unforwardableRoutes}=await import("@stacksjs/server"),{listRootMountedAppRoutes}=await import("@stacksjs/router"),unreachable=unforwardableRoutes(listRootMountedAppRoutes().filter((entry)=>entry.method==="GET"||entry.method==="HEAD"),resolveApiProxyRules(config.server?.proxy));if(unreachable.length>0)log.warn(describeUnforwardableRoutes(unreachable))}catch(error){log.debug(`[Stacks API] Route reachability check skipped: ${error instanceof Error?error.message:String(error)}`)}disableViewRouting(route.bunRouter);const isDeployed=["production","staging","development"].includes((process.env.APP_ENV||"").toLowerCase()),server=await route.serve({port,hostname,reusePort:isDeployed});process.on("SIGTERM",()=>{const graceMs=Number(process.env.SHUTDOWN_GRACE_MS)||15000;setTimeout(()=>process.exit(0),graceMs).unref();Promise.resolve(server?.stop()).then(()=>process.exit(0))});log.info(`[Stacks API] Server running at http://${hostname}:${port}`);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/actions",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.233",
|
|
6
6
|
"description": "The Stacks actions.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -66,55 +66,55 @@
|
|
|
66
66
|
"prepublishOnly": "bun run build"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@stacksjs/action-runner": "0.75.
|
|
70
|
-
"@stacksjs/ai": "0.75.
|
|
71
|
-
"@stacksjs/analytics": "0.75.
|
|
72
|
-
"@stacksjs/api": "0.75.
|
|
73
|
-
"@stacksjs/auth": "0.75.
|
|
74
|
-
"@stacksjs/build": "0.75.
|
|
69
|
+
"@stacksjs/action-runner": "0.75.233",
|
|
70
|
+
"@stacksjs/ai": "0.75.233",
|
|
71
|
+
"@stacksjs/analytics": "0.75.233",
|
|
72
|
+
"@stacksjs/api": "0.75.233",
|
|
73
|
+
"@stacksjs/auth": "0.75.233",
|
|
74
|
+
"@stacksjs/build": "0.75.233",
|
|
75
75
|
"@stacksjs/bumpx": "^0.2.21",
|
|
76
76
|
"@stacksjs/bunpress": "^0.2.15",
|
|
77
|
-
"@stacksjs/cli": "0.75.
|
|
78
|
-
"@stacksjs/cloud": "0.75.
|
|
79
|
-
"@stacksjs/cms": "0.75.
|
|
80
|
-
"@stacksjs/components": "^0.2.
|
|
81
|
-
"@stacksjs/config": "0.75.
|
|
82
|
-
"@stacksjs/database": "0.75.
|
|
83
|
-
"@stacksjs/desktop-build": "0.75.
|
|
84
|
-
"@stacksjs/dns": "0.75.
|
|
85
|
-
"@stacksjs/enums": "0.75.
|
|
86
|
-
"@stacksjs/env": "0.75.
|
|
87
|
-
"@stacksjs/error-handling": "0.75.
|
|
88
|
-
"@stacksjs/github": "0.75.
|
|
77
|
+
"@stacksjs/cli": "0.75.233",
|
|
78
|
+
"@stacksjs/cloud": "0.75.233",
|
|
79
|
+
"@stacksjs/cms": "0.75.233",
|
|
80
|
+
"@stacksjs/components": "^0.2.574",
|
|
81
|
+
"@stacksjs/config": "0.75.233",
|
|
82
|
+
"@stacksjs/database": "0.75.233",
|
|
83
|
+
"@stacksjs/desktop-build": "0.75.233",
|
|
84
|
+
"@stacksjs/dns": "0.75.233",
|
|
85
|
+
"@stacksjs/enums": "0.75.233",
|
|
86
|
+
"@stacksjs/env": "0.75.233",
|
|
87
|
+
"@stacksjs/error-handling": "0.75.233",
|
|
88
|
+
"@stacksjs/github": "0.75.233",
|
|
89
89
|
"@stacksjs/gitit": "^0.2.5",
|
|
90
|
-
"@stacksjs/i18n": "0.75.
|
|
91
|
-
"@stacksjs/image": "0.75.
|
|
92
|
-
"@stacksjs/logging": "0.75.
|
|
90
|
+
"@stacksjs/i18n": "0.75.233",
|
|
91
|
+
"@stacksjs/image": "0.75.233",
|
|
92
|
+
"@stacksjs/logging": "0.75.233",
|
|
93
93
|
"@stacksjs/logsmith": "^0.2.11",
|
|
94
|
-
"@stacksjs/mobile": "0.75.
|
|
95
|
-
"@stacksjs/orm": "0.75.
|
|
96
|
-
"@stacksjs/path": "0.75.
|
|
97
|
-
"@stacksjs/payments": "0.75.
|
|
98
|
-
"@stacksjs/queue": "0.75.
|
|
99
|
-
"@stacksjs/registry": "0.75.
|
|
100
|
-
"@stacksjs/router": "0.75.
|
|
94
|
+
"@stacksjs/mobile": "0.75.233",
|
|
95
|
+
"@stacksjs/orm": "0.75.233",
|
|
96
|
+
"@stacksjs/path": "0.75.233",
|
|
97
|
+
"@stacksjs/payments": "0.75.233",
|
|
98
|
+
"@stacksjs/queue": "0.75.233",
|
|
99
|
+
"@stacksjs/registry": "0.75.233",
|
|
100
|
+
"@stacksjs/router": "0.75.233",
|
|
101
101
|
"@stacksjs/rpx": "^0.11.54",
|
|
102
|
-
"@stacksjs/scheduler": "0.75.
|
|
103
|
-
"@stacksjs/search-engine": "0.75.
|
|
104
|
-
"@stacksjs/security": "0.75.
|
|
105
|
-
"@stacksjs/server": "0.75.
|
|
106
|
-
"@stacksjs/sites": "0.75.
|
|
107
|
-
"@stacksjs/storage": "0.75.
|
|
108
|
-
"@stacksjs/strings": "0.75.
|
|
109
|
-
"@stacksjs/stx": "^0.2.
|
|
102
|
+
"@stacksjs/scheduler": "0.75.233",
|
|
103
|
+
"@stacksjs/search-engine": "0.75.233",
|
|
104
|
+
"@stacksjs/security": "0.75.233",
|
|
105
|
+
"@stacksjs/server": "0.75.233",
|
|
106
|
+
"@stacksjs/sites": "0.75.233",
|
|
107
|
+
"@stacksjs/storage": "0.75.233",
|
|
108
|
+
"@stacksjs/strings": "0.75.233",
|
|
109
|
+
"@stacksjs/stx": "^0.2.574",
|
|
110
110
|
"@stacksjs/tlsx": "^0.13.19",
|
|
111
111
|
"@stacksjs/ts-cloud": "^0.16.58",
|
|
112
112
|
"@stacksjs/ts-md": "^0.1.1",
|
|
113
|
-
"@stacksjs/tunnel": "0.75.
|
|
114
|
-
"@stacksjs/types": "0.75.
|
|
115
|
-
"@stacksjs/utils": "0.75.
|
|
116
|
-
"@stacksjs/whois": "0.75.
|
|
117
|
-
"bun-plugin-stx": "^0.2.
|
|
113
|
+
"@stacksjs/tunnel": "0.75.233",
|
|
114
|
+
"@stacksjs/types": "0.75.233",
|
|
115
|
+
"@stacksjs/utils": "0.75.233",
|
|
116
|
+
"@stacksjs/whois": "0.75.233",
|
|
117
|
+
"bun-plugin-stx": "^0.2.574"
|
|
118
118
|
},
|
|
119
119
|
"peerDependencies": {
|
|
120
120
|
"craft-native": ">=0.0.163",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
}
|
|
130
130
|
},
|
|
131
131
|
"devDependencies": {
|
|
132
|
-
"@stacksjs/validation": "0.75.
|
|
132
|
+
"@stacksjs/validation": "0.75.233",
|
|
133
133
|
"better-dx": "^0.2.24"
|
|
134
134
|
}
|
|
135
135
|
}
|