@stacksjs/actions 0.75.262 → 0.75.264

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.
@@ -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";import{reportMobileBuildTarget}from"./mobile-target";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});reportMobileBuildTarget({renderer:config.renderer,site:craftConfig.devServerURL,nativeBundle:nativeBundle?.outFile,webAssets,fallbackWebAssets},log);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";import{reportMobileBuildTarget}from"./mobile-target";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});reportMobileBuildTarget({renderer:config.renderer,site:craftConfig.devServerURL,nativeBundle:nativeBundle?.outFile,webAssets,fallbackWebAssets,production:release},log);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;
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";import{reportMobileBuildTarget}from"./mobile-target";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");reportMobileBuildTarget({renderer:config.renderer,site:craftConfig.devServerURL,nativeBundle:nativeBundle?.outFile,webAssets,fallbackWebAssets},log);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";import{reportMobileBuildTarget}from"./mobile-target";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");reportMobileBuildTarget({renderer:config.renderer,site:craftConfig.devServerURL,nativeBundle:nativeBundle?.outFile,webAssets,fallbackWebAssets,production:process.env.NODE_ENV==="production"},log);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;
@@ -1,9 +1,16 @@
1
+ /**
2
+ * Whether a site is the developer's own machine or network: loopback, the
3
+ * Android emulator's alias for the host, a `.localhost` or `.test` name, or a
4
+ * private address a physical device reaches the laptop on.
5
+ */
6
+ export declare function isLocalSite(site: URL): boolean;
1
7
  /** Describe the resolved build inputs, never credentials embedded in a URL. */
2
- export declare function reportMobileBuildTarget(target: MobileBuildTarget, log: { info: (message: string) => unknown }): void;
8
+ export declare function reportMobileBuildTarget(target: MobileBuildTarget, log: { info: (message: string) => unknown, warn: (message: string) => unknown }): void;
3
9
  declare interface MobileBuildTarget {
4
10
  renderer?: 'web' | 'native'
5
11
  site?: string
6
12
  nativeBundle?: string
7
13
  webAssets?: string
8
14
  fallbackWebAssets?: string
15
+ production?: boolean
9
16
  }
@@ -1 +1 @@
1
- export function reportMobileBuildTarget(target,log){const site=target.site?new URL(target.site):void 0;if(site){site.username="";site.password="";site.search="";site.hash=""}if(target.renderer==="native"){log.info(`Native screens: ${target.nativeBundle}`);if(site)log.info(`API site: ${site}`)}else if(target.webAssets)log.info(`Bundled web source: ${target.webAssets}`);else{if(site)log.info(`Web source: ${site}`);if(target.fallbackWebAssets)log.info(`Offline web fallback: ${target.fallbackWebAssets}`)}}
1
+ export function isLocalSite(site){const host=site.hostname.replace(/^\[|\]$/g,"");if(["localhost","127.0.0.1","::1","10.0.2.2"].includes(host))return!0;if(host.endsWith(".localhost")||host.endsWith(".test")||host.endsWith(".local"))return!0;const octets=host.split(".").map(Number);if(octets.length!==4||octets.some((octet)=>!Number.isInteger(octet)))return!1;const[a,b]=octets;return a===10||a===127||a===192&&b===168||a===172&&b>=16&&b<=31}export function reportMobileBuildTarget(target,log){const site=target.site?new URL(target.site):void 0;if(site){site.username="";site.password="";site.search="";site.hash=""}if(target.renderer==="native"){log.info(`Native screens: ${target.nativeBundle}`);if(site)log.info(`API site: ${site}`)}else if(target.webAssets)log.info(`Bundled web source: ${target.webAssets}`);else{if(site)log.info(`Web source: ${site}`);if(target.fallbackWebAssets)log.info(`Offline web fallback: ${target.fallbackWebAssets}`)}if(site&&!target.production&&!isLocalSite(site)&&(target.renderer==="native"||!target.webAssets))log.warn(`This development build talks to ${site}, which is not a local server. To build against your dev server, point the app's url in config/mobile.ts at it (for example http://localhost:3000). Build with NODE_ENV=production for a release.`)}
@@ -30,7 +30,7 @@ export declare function nativeAppFile(config: Pick<IosMobileConfig, 'nativeApp'>
30
30
  * app script parse all of them first: ~170 ms of main thread per screen for
31
31
  * HQ's 17 MB. Otherwise, the one-file bundle.
32
32
  */
33
- 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>;
33
+ export declare function compileNativeScreens(config: Pick<IosMobileConfig, 'nativeScreens' | 'nativeScreensDir' | 'nativeApp' | 'nativeMessages' | 'shareStorage'>, root: string, output: string, compile?: CompileNativeBundle, section?: 'ios' | 'android', options?: { routes?: boolean, runtime?: boolean, messages?: boolean }): Promise<{ outFile: string, diagnostics: NativeBundleResult['diagnostics'] } | null>;
34
34
  /** What stx's native compiler answers (`@stacksjs/stx/native`). */
35
35
  declare interface NativeBundleResult {
36
36
  outFile?: string
@@ -52,4 +52,6 @@ declare type CompileNativeBundle = (options: {
52
52
  sharedRuntime?: boolean
53
53
  /** Messages a language at a time, beside the route files (stx 0.2.578 and later). */
54
54
  messages?: { dir: string, languages: string[] }
55
+ /** Web-storage keys shared with the Keychain: a web view's sign-in is the native screens' (stx 0.2.661 and later). */
56
+ shareStorage?: Record<string, string>
55
57
  }) => Promise<NativeBundleResult>;
@@ -1 +1 @@
1
- import{existsSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import{join,resolve}from"node:path";export function nativeScreenName(value){if(!value.endsWith(".stx"))return value;return value.replace(/\.stx$/,"").replace(/^resources\//,"").replace(/^\.?\//,"").replace(/[^\w-]+/g,"-").replace(/^-+|-+$/g,"")}export function nativeScreenRoutes(config){if(!config.nativeScreens)return;return Object.fromEntries(Object.entries(config.nativeScreens).map(([path,value])=>[path,nativeScreenName(value)]))}export function nativeScreenFiles(config,root,section="ios"){const dir=resolve(root,config.nativeScreensDir??"resources/native"),files={};for(const value of Object.values(config.nativeScreens??{})){const name=nativeScreenName(value);if(files[name])continue;const file=value.endsWith(".stx")?resolve(root,value):join(dir,`${value}.stx`);if(!existsSync(file))throw Error(`${section}.nativeScreens names the screen "${value}", 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??[]}}
1
+ import{existsSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import{join,resolve}from"node:path";export function nativeScreenName(value){if(!value.endsWith(".stx"))return value;return value.replace(/\.stx$/,"").replace(/^resources\//,"").replace(/^\.?\//,"").replace(/[^\w-]+/g,"-").replace(/^-+|-+$/g,"")}export function nativeScreenRoutes(config){if(!config.nativeScreens)return;return Object.fromEntries(Object.entries(config.nativeScreens).map(([path,value])=>[path,nativeScreenName(value)]))}export function nativeScreenFiles(config,root,section="ios"){const dir=resolve(root,config.nativeScreensDir??"resources/native"),files={};for(const value of Object.values(config.nativeScreens??{})){const name=nativeScreenName(value);if(files[name])continue;const file=value.endsWith(".stx")?resolve(root,value):join(dir,`${value}.stx`);if(!existsSync(file))throw Error(`${section}.nativeScreens names the screen "${value}", 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,share=config.shareStorage&&Object.keys(config.shareStorage).length?{shareStorage:{...config.shareStorage}}:{};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}:{},...share});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}:{},...share});return{outFile:result.outFile||outFile,diagnostics:result.diagnostics??[]}}
@@ -1,5 +1,5 @@
1
1
  import process from"node:process";import{randomUUID}from"node:crypto";import{existsSync,mkdirSync,renameSync,unlinkSync,writeFileSync}from"node:fs";import{connect}from"node:net";import{bold,cyan,dim,green}from"@stacksjs/cli";import{defaultsPath,projectPath,publicPath,storagePath}from"@stacksjs/path";import{seedCsrfPageResponse,validateDevCsrfRequest}from"./csrf";import{resolveComponentsLibraryRoot}from"./defaults-resources";import{shouldDelegateDashboardRequest}from"./dashboard-request-routing";import{resolveDashboardCraftExecutable}from"./dashboard-native";import{resolveDashboardDomain,resolveDashboardProxyStrategy}from"./dashboard-proxy";import{buildManifest,discoverModels,findAvailablePort,waitForServer}from"./dashboard-utils";import{runDashboardSupervisor}from"./dashboard-supervisor";import{ensureDiscoveredPackages}from"../discover-packages";import{resolveStxSource}from"../stx-source";if(process.env.STACKS_DASHBOARD_WORKER!=="1"){const exitCode=await runDashboardSupervisor(import.meta.path,process.argv.slice(2));process.exit(exitCode)}const verboseIdx=process.argv.indexOf("--verbose"),verbose=verboseIdx!==-1&&process.argv[verboseIdx+1]!=="false",startTime=Bun.nanoseconds(),{log:originalConsoleLog,warn:originalConsoleWarn}=console,bufferedLogs=[];console.log=(...args)=>{bufferedLogs.push(args.map(String).join(" "))};console.warn=(...args)=>{bufferedLogs.push(args.map(String).join(" "))};const dashboardPath=defaultsPath("views/dashboard"),userDashboardPath=projectPath("resources/views/dashboard"),preferredPort=Number(process.env.PORT_ADMIN)||3002,dashboardPort=await findAvailablePort(preferredPort),appUrl=process.env.APP_URL||"",dashboardDomain=resolveDashboardDomain(appUrl),sslBasePath=`${process.env.HOME}/.stacks/ssl`;function restoreConsole(){console.log=originalConsoleLog;console.warn=(...args)=>{const msg=args.map(String).join(" ");if(msg.includes("[STX] DOM API violation")||msg.includes("unsafe expression"))return;originalConsoleWarn(...args)}}async function startStxServer(){const{overridesReady}=await import("@stacksjs/config"),{autoImportsAreStale,generateAutoImportFiles,injectGlobalAutoImports}=await import("@stacksjs/server");await overridesReady;await ensureDiscoveredPackages();if(!existsSync(storagePath("framework/auto-imports/models.ts"))||autoImportsAreStale())await generateAutoImportFiles();await injectGlobalAutoImports();const serveSource=resolveStxSource({value:process.env.BUN_PLUGIN_STX_SRC});if(serveSource.kind==="missing")throw Error(`BUN_PLUGIN_STX_SRC points at ${serveSource.path}, which does not exist. Unset it to use the installed bun-plugin-stx.`);let serve;if(serveSource.kind==="override"){serve=(await import(serveSource.path)).serve;console.log(`[Dashboard] bun-plugin-stx from ${serveSource.path}`)}else serve=(await import("bun-plugin-stx/serve")).serve;const stxSource=resolveStxSource({value:process.env.STX_SRC});if(stxSource.kind==="missing")throw Error(`STX_SRC points at ${stxSource.path}, which does not exist. Unset it to use the installed @stacksjs/stx.`);let stxModule;if(stxSource.kind==="override"){stxModule=await import(stxSource.path);console.log(`[Dashboard] stx from ${stxSource.path}`)}const{listConfigFiles,readConfig,updateConfigKeys}=await import(defaultsPath("resources/functions/dashboard/config-io.ts")),localTsChartsRoot=process.env.TS_CHARTS_SRC,dependencyBundles=new Map;function resolveTsChartsSource(packageName){return[...localTsChartsRoot?[`${localTsChartsRoot}/packages/${packageName}/src/index.ts`]:[],projectPath(`node_modules/@ts-charts/${packageName}/src/index.ts`)].find(existsSync)||null}const tsChartsEntry=[...localTsChartsRoot?[`${localTsChartsRoot}/packages/ts-charts/src/index.ts`]:[],projectPath("node_modules/ts-charts/src/index.ts")].find(existsSync);async function buildDashboardDependency(entrypoint){const cached=dependencyBundles.get(entrypoint);if(cached)return cached;const build=(async()=>{const result=await Bun.build({entrypoints:[entrypoint],target:"browser",format:"esm",minify:!0,plugins:[{name:"dashboard-ts-charts-source",setup(builder){builder.onResolve({filter:/^@ts-charts\/[a-z0-9-]+$/},(args)=>{const packageName=args.path.slice(11),source=resolveTsChartsSource(packageName);if(source)return{path:source};return null})}}]});if(!result.success){const errors=result.logs.filter((log)=>log.level==="error").map((log)=>log.message).join(`
2
- `);throw Error(errors||`Failed to bundle ${entrypoint}`)}const output=result.outputs.find((file)=>file.path.endsWith(".js"));if(!output)throw Error(`No JavaScript output produced for ${entrypoint}`);return output.text()})();dependencyBundles.set(entrypoint,build);return build}function browserDependencyRoute(entrypoint){return async()=>{try{return new Response(await buildDashboardDependency(entrypoint),{headers:{"content-type":"text/javascript; charset=utf-8","cache-control":"no-cache"}})}catch(error){dependencyBundles.delete(entrypoint);return new Response(`throw new Error(${JSON.stringify(error.message)})`,{status:500,headers:{"content-type":"text/javascript; charset=utf-8"}})}}}let dashboardIconCss=null;async function buildDashboardIconCss(){const{generateCss}=await import("@stacksjs/stx"),dirs=[dashboardPath,userDashboardPath,defaultsPath("resources/components/Dashboard")],icons=new Set;for(const dir of dirs){if(!existsSync(dir))continue;const glob=new Bun.Glob("**/*.{stx,ts}");for await(const file of glob.scan({cwd:dir,absolute:!0})){const text=await Bun.file(file).text();for(const match of text.matchAll(/\bi-[a-z][a-z0-9]*(?:-[a-z0-9]+)+/g))icons.add(match[0])}}if(icons.size===0)return"";return generateCss(`<div class="${[...icons].join(" ")}"></div>`,process.cwd())}const configRoutes={"/__deps/charts.js":browserDependencyRoute(storagePath("framework/core/charts/src/index.ts")),"/__deps/ts-charts.js":browserDependencyRoute(tsChartsEntry||`${localTsChartsRoot}/packages/ts-charts/src/index.ts`),"/__deps/dashboard-icons.css":async()=>{if(!dashboardIconCss)dashboardIconCss=buildDashboardIconCss();return new Response(await dashboardIconCss,{headers:{"content-type":"text/css; charset=utf-8","cache-control":"no-cache"}})},"/api/config/list":async()=>{try{return Response.json({ok:!0,files:listConfigFiles()})}catch(e){return Response.json({ok:!1,error:e?.message},{status:500})}},"/api/config/read":async(req)=>{try{const name=new URL(req.url).searchParams.get("name")||"";if(!/^[\w-]+$/.test(name))return Response.json({ok:!1,error:"Invalid config name"},{status:400});const result=await readConfig(name);if(!result)return Response.json({ok:!1,error:"Not found"},{status:404});return Response.json({ok:!0,name,fields:result.fields})}catch(e){return Response.json({ok:!1,error:e?.message},{status:500})}},"/api/config/source":async(req)=>{try{const name=new URL(req.url).searchParams.get("name")||"";if(!/^[\w-]+$/.test(name))return Response.json({ok:!1,error:"Invalid config name"},{status:400});const result=await readConfig(name);if(!result)return Response.json({ok:!1,error:"Not found"},{status:404});return new Response(result.source,{headers:{"content-type":"text/plain; charset=utf-8"}})}catch(e){return Response.json({ok:!1,error:e?.message},{status:500})}},"/api/config/update":async(req)=>{if(req.method!=="POST")return Response.json({ok:!1,error:"Method not allowed"},{status:405});try{await validateDevCsrfRequest(req);const body=await req.json(),file=body.file?.replace(/\.ts$/,"");if(!file||!/^[\w-]+$/.test(file))return Response.json({ok:!1,error:"Invalid file"},{status:400});const updates=[];if(Array.isArray(body.updates))for(const u of body.updates){const k=u.key??u.path;if(typeof k==="string")updates.push({key:k,value:u.value})}else if(body.key)updates.push({key:body.key,value:body.value});if(updates.length===0)return Response.json({ok:!1,error:"No updates supplied"},{status:400});const invalid=updates.find((update)=>!/^[A-Za-z_$][\w$]*$/.test(update.key)||typeof update.value!=="string"&&typeof update.value!=="number"&&typeof update.value!=="boolean"||typeof update.value==="number"&&!Number.isFinite(update.value));if(invalid)return Response.json({ok:!1,error:`Invalid value for configuration key "${invalid.key}"`},{status:400});const typedUpdates=updates;try{await updateConfigKeys(file,typedUpdates)}catch(err){const error=err?.message||"Configuration values could not be updated.",results=typedUpdates.map((update)=>({key:update.key,ok:!1,error}));return Response.json({ok:!1,file,results,error},{status:422})}const results=typedUpdates.map((update)=>({key:update.key,ok:!0,newValue:update.value}));return Response.json({ok:!0,file,results})}catch(e){if(e?.status===403)return Response.json({ok:!1,error:"Forbidden",message:"CSRF token mismatch"},{status:403});return Response.json({ok:!1,error:e?.message},{status:500})}}},router=await import("@stacksjs/router");await router.loadRoutes(await router.appRouteRegistry());const stacksRoute=router.route;serve({root:projectPath("resources"),patterns:[userDashboardPath,dashboardPath],port:dashboardPort,componentsDir:defaultsPath("resources/components/Dashboard"),fallbackComponentsDir:resolveComponentsLibraryRoot(),layoutsDir:dashboardPath,partialsDir:dashboardPath,publicDir:publicPath(),renderCache:!0,renderCacheVary:"source",prewarmRenderCache:4,watchDirs:[projectPath("resources/functions"),defaultsPath("resources/functions")],quiet:!0,routes:configRoutes,onRequest:async(req)=>{const pathname=new URL(req.url).pathname;if(shouldDelegateDashboardRequest(pathname,req.method))return stacksRoute.handleRequest(req);return null},onResponse:seedCsrfPageResponse,auth:!1,...stxModule&&{stxModule}}).catch((err)=>{restoreConsole();console.error(`
2
+ `);throw Error(errors||`Failed to bundle ${entrypoint}`)}const output=result.outputs.find((file)=>file.path.endsWith(".js"));if(!output)throw Error(`No JavaScript output produced for ${entrypoint}`);return output.text()})();dependencyBundles.set(entrypoint,build);return build}function browserDependencyRoute(entrypoint){return async()=>{try{return new Response(await buildDashboardDependency(entrypoint),{headers:{"content-type":"text/javascript; charset=utf-8","cache-control":"no-cache"}})}catch(error){dependencyBundles.delete(entrypoint);return new Response(`throw new Error(${JSON.stringify(error.message)})`,{status:500,headers:{"content-type":"text/javascript; charset=utf-8"}})}}}let dashboardIconCss=null;async function buildDashboardIconCss(){const{generateCss}=await import("@stacksjs/stx"),dirs=[dashboardPath,userDashboardPath,defaultsPath("resources/components/Dashboard")],icons=new Set;for(const dir of dirs){if(!existsSync(dir))continue;const glob=new Bun.Glob("**/*.{stx,ts}");for await(const file of glob.scan({cwd:dir,absolute:!0})){const text=await Bun.file(file).text();for(const match of text.matchAll(/\bi-[a-z][a-z0-9]*(?:-[a-z0-9]+)+/g))icons.add(match[0])}}if(icons.size===0)return"";return generateCss(`<div class="${[...icons].join(" ")}"></div>`,process.cwd())}const configRoutes={"/__deps/charts.js":browserDependencyRoute(storagePath("framework/core/charts/src/index.ts")),"/__deps/ts-charts.js":browserDependencyRoute(tsChartsEntry||`${localTsChartsRoot}/packages/ts-charts/src/index.ts`),"/__deps/dashboard-icons.css":async()=>{if(!dashboardIconCss)dashboardIconCss=buildDashboardIconCss();return new Response(await dashboardIconCss,{headers:{"content-type":"text/css; charset=utf-8","cache-control":"no-cache"}})},"/api/config/list":async()=>{try{return Response.json({ok:!0,files:listConfigFiles()})}catch(e){return Response.json({ok:!1,error:e?.message},{status:500})}},"/api/config/read":async(req)=>{try{const name=new URL(req.url).searchParams.get("name")||"";if(!/^[\w-]+$/.test(name))return Response.json({ok:!1,error:"Invalid config name"},{status:400});const result=await readConfig(name);if(!result)return Response.json({ok:!1,error:"Not found"},{status:404});return Response.json({ok:!0,name,fields:result.fields})}catch(e){return Response.json({ok:!1,error:e?.message},{status:500})}},"/api/config/source":async(req)=>{try{const name=new URL(req.url).searchParams.get("name")||"";if(!/^[\w-]+$/.test(name))return Response.json({ok:!1,error:"Invalid config name"},{status:400});const result=await readConfig(name);if(!result)return Response.json({ok:!1,error:"Not found"},{status:404});return new Response(result.source,{headers:{"content-type":"text/plain; charset=utf-8"}})}catch(e){return Response.json({ok:!1,error:e?.message},{status:500})}},"/api/config/update":async(req)=>{if(req.method!=="POST")return Response.json({ok:!1,error:"Method not allowed"},{status:405});try{await validateDevCsrfRequest(req);const body=await req.json(),file=body.file?.replace(/\.ts$/,"");if(!file||!/^[\w-]+$/.test(file))return Response.json({ok:!1,error:"Invalid file"},{status:400});const updates=[];if(Array.isArray(body.updates))for(const u of body.updates){const k=u.key??u.path;if(typeof k==="string")updates.push({key:k,value:u.value})}else if(body.key)updates.push({key:body.key,value:body.value});if(updates.length===0)return Response.json({ok:!1,error:"No updates supplied"},{status:400});const invalid=updates.find((update)=>!/^[A-Za-z_$][\w$]*$/.test(update.key)||typeof update.value!=="string"&&typeof update.value!=="number"&&typeof update.value!=="boolean"||typeof update.value==="number"&&!Number.isFinite(update.value));if(invalid)return Response.json({ok:!1,error:`Invalid value for configuration key "${invalid.key}"`},{status:400});const typedUpdates=updates;try{await updateConfigKeys(file,typedUpdates)}catch(err){const error=err?.message||"Configuration values could not be updated.",results=typedUpdates.map((update)=>({key:update.key,ok:!1,error}));return Response.json({ok:!1,file,results,error},{status:422})}const results=typedUpdates.map((update)=>({key:update.key,ok:!0,newValue:update.value}));return Response.json({ok:!0,file,results})}catch(e){if(e?.status===403)return Response.json({ok:!1,error:"Forbidden",message:"CSRF token mismatch"},{status:403});return Response.json({ok:!1,error:e?.message},{status:500})}}},router=await import("@stacksjs/router");await router.loadRoutes(await router.appRouteRegistry());const stacksRoute=router.route;serve({root:projectPath("resources"),patterns:[userDashboardPath,dashboardPath],port:dashboardPort,componentsDir:defaultsPath("resources/components/Dashboard"),fallbackComponentsDir:resolveComponentsLibraryRoot(),layoutsDir:dashboardPath,partialsDir:dashboardPath,publicDir:publicPath(),renderCache:!0,renderCacheVary:"source",prewarmRenderCache:!1,watchDirs:[projectPath("resources/functions"),defaultsPath("resources/functions")],quiet:!0,routes:configRoutes,onRequest:async(req)=>{const pathname=new URL(req.url).pathname;if(shouldDelegateDashboardRequest(pathname,req.method))return stacksRoute.handleRequest(req);return null},onResponse:seedCsrfPageResponse,auth:!1,...stxModule&&{stxModule}}).catch((err)=>{restoreConsole();console.error(`
3
3
  Failed to start dashboard server: ${err?.message||err}
4
4
  `);process.exit(1)})}function portAnswers(port,timeoutMs=800){return new Promise((resolve)=>{const socket=connect({port,host:"127.0.0.1"}),finish=(answered)=>{try{socket.destroy()}catch{}resolve(answered)};socket.once("connect",()=>finish(!0));socket.once("error",()=>finish(!1));setTimeout(()=>finish(!1),timeoutMs).unref?.()})}async function registerWithRpxDaemon(domainToServe){const{writeEntry}=await import("@stacksjs/rpx");await writeEntry({id:`${domainToServe}-dashboard`,from:`localhost:${dashboardPort}`,to:domainToServe,cwd:process.cwd(),createdAt:new Date().toISOString(),pid:process.pid},void 0,verbose);return!0}async function startReverseProxy(){if(!dashboardDomain)return!1;try{let rpx;const loadRpx=async()=>rpx??=await import("@stacksjs/rpx"),strategy=await resolveDashboardProxyStrategy(dashboardDomain,{proxyManagedExternally:Boolean(process.env.STACKS_PROXY_MANAGED),isDaemonRunning:async()=>(await loadRpx()).isDaemonRunning(),httpsPortAnswers:()=>portAnswers(443)});if(strategy==="origin-only")return!1;if(strategy==="parent-managed")return!0;if(strategy==="daemon-route")return await registerWithRpxDaemon(dashboardDomain);await(await loadRpx()).startProxies({proxies:[{from:`localhost:${dashboardPort}`,to:dashboardDomain,cleanUrls:!1}],https:{basePath:sslBasePath,validityDays:825},regenerateUntrustedCerts:!1,verbose});return await portAnswers(443)}catch(error){if(verbose)originalConsoleLog(` ${dim(`Proxy: ${error}`)}`);return!1}}const[,discoveredModels]=await Promise.all([startStxServer(),discoverModels(projectPath("app/Models"),defaultsPath("app/Models"))]),{loadDashboardConfig}=await import(defaultsPath("resources/functions/dashboard/toggles.ts")),{feature:featureEnabled}=await import("@stacksjs/config"),{toggles:dashboardToggles,nav:dashboardNav}=await loadDashboardConfig(projectPath("config/dashboard.ts"),featureEnabled),manifestDir=storagePath("framework/runtime/dashboard"),manifestPath=storagePath("framework/runtime/dashboard/.discovered-models.json");mkdirSync(manifestDir,{recursive:!0});const manifestPayload={models:buildManifest(discoveredModels),sections:dashboardToggles,nav:dashboardNav},manifestTempPath=`${manifestPath}.${process.pid}.${randomUUID()}.tmp`;try{writeFileSync(manifestTempPath,JSON.stringify(manifestPayload,null,2),{flag:"wx"});renameSync(manifestTempPath,manifestPath)}finally{if(existsSync(manifestTempPath))unlinkSync(manifestTempPath)}const serverReady=await waitForServer(dashboardPort);restoreConsole();const proxyStarted=await startReverseProxy().catch((err)=>{if(verbose)console.warn("[Dashboard] Reverse proxy failed:",err);return!1}),dashboardHttpsUrl=dashboardDomain?`https://${dashboardDomain}`:null,dashboardLocalUrl=`http://localhost:${dashboardPort}`,initialUrl=dashboardHttpsUrl&&proxyStarted?`${dashboardHttpsUrl}/`:`${dashboardLocalUrl}/`,elapsedMs=(Bun.nanoseconds()-startTime)/1e6;console.log();console.log(` ${bold(cyan("stacks dashboard"))}`);console.log();if(dashboardHttpsUrl&&proxyStarted){console.log(` ${green("\u279C")} ${bold("Local")}: ${cyan(dashboardHttpsUrl)}`);console.log(` ${dim("\u279C")} ${dim("Origin")}: ${dim(dashboardLocalUrl)}`)}else{console.log(` ${green("\u279C")} ${bold("Local")}: ${cyan(dashboardLocalUrl)}`);if(dashboardHttpsUrl)console.log(` ${dim("\u279C")} ${dim("Pretty")}: ${dim(`${dashboardHttpsUrl} needs the rpx proxy - run \`buddy dev\``)}`)}console.log(` ${green("\u279C")} ${bold("Window")}: ${dim("Stacks Dashboard")} ${dim("1400\xD7900")}`);console.log(` ${green("\u279C")} ${bold("Models")}: ${dim(`${discoveredModels.length} discovered`)}`);if(dashboardPort!==preferredPort)console.log(` ${dim("\u279C")} ${dim(`Port ${preferredPort} in use, using ${dashboardPort}`)}`);if(!serverReady)console.log(` ${dim("\u26A0")} ${dim("Dev server may not be ready yet")}`);console.log();console.log(` ${dim(`ready in ${elapsedMs.toFixed(0)} ms`)}`);if(verbose){console.log();console.log(` ${dim("\u279C")} ${dim("URL")}: ${dim(initialUrl)}`);if(dashboardDomain){console.log(` ${dim("\u279C")} ${dim("SSL")}: ${dim(sslBasePath)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`)}if(bufferedLogs.length>0){console.log();for(const line of bufferedLogs)console.log(` ${dim(line)}`)}}console.log();let createApp,resolveCraftBinary;const nativeDisabled=process.env.STACKS_NO_NATIVE==="1",explicitCraftSdk=process.env.CRAFT_SDK_SRC;if(!nativeDisabled&&explicitCraftSdk){if(!existsSync(explicitCraftSdk))throw Error(`CRAFT_SDK_SRC not found: ${explicitCraftSdk}`);try{({createApp,resolveCraftBinary}=await import(explicitCraftSdk));if(!createApp||!resolveCraftBinary)throw Error("CRAFT_SDK_SRC must export createApp and resolveCraftBinary")}catch(error){throw Error(`Could not load CRAFT_SDK_SRC ${explicitCraftSdk}: ${error instanceof Error?error.message:String(error)}`)}}if(!nativeDisabled&&!explicitCraftSdk)try{({createApp,resolveCraftBinary}=await import("craft-native"))}catch{}const craftBinaryPath=createApp&&resolveCraftBinary?resolveDashboardCraftExecutable(resolveCraftBinary):void 0;if(createApp&&craftBinaryPath){const userIconPath=projectPath("resources/assets/images/app-icon.png"),defaultIconPath=defaultsPath("resources/assets/images/app-icon.png"),appIconPath=await Bun.file(userIconPath).exists()?userIconPath:await Bun.file(defaultIconPath).exists()?defaultIconPath:void 0,app=createApp({url:dashboardLocalUrl,quiet:!verbose,craftPath:craftBinaryPath,window:{title:"Stacks Dashboard",width:1400,height:900,titlebarHidden:!0,webSidebarMaterial:!0,webSidebarWidth:250,...appIconPath&&{icon:appIconPath}}});process.on("SIGINT",()=>{app.close();process.exit(0)});process.on("SIGTERM",()=>{app.close();process.exit(0)});try{await app.show();process.exit(0)}catch(error){const fallbackUrl=dashboardHttpsUrl||dashboardLocalUrl;console.log(` ${dim("Native Craft window failed:")} ${dim(error instanceof Error?error.message:String(error))}`);console.log(` ${dim("Dashboard available at:")} ${cyan(fallbackUrl)}
5
5
  `);await new Promise(()=>{})}}else{console.log(` ${dim(nativeDisabled?"Native window disabled by STACKS_NO_NATIVE=1. Open the URL above in a browser.":"Native window unavailable. Run pantry install craft, set CRAFT_BIN, or open the URL above in a browser.")}
@@ -1,2 +1,10 @@
1
1
  /** Whether `buddy lint` lints `file`, a path relative to the project root with `/` separators. */
2
2
  export declare function isLintablePath(file: string): boolean;
3
+ /** Whether the file's extension is one `buddy lint` reads at all. */
4
+ export declare function isLintableExtension(file: string): boolean;
5
+ /**
6
+ * Whether the file sits somewhere `buddy lint` never looks (dependencies,
7
+ * build output, caches). Separate from the extension test so a caller who
8
+ * names such a path is told it is the location, not the file type.
9
+ */
10
+ export declare function isIgnoredLocation(file: string): boolean;
@@ -1 +1 @@
1
- const lintableFile=/\.(?:ts|js|stx|json|md|yaml|yml)$/i,ignoredPath=/(?:^|\/)(?:node_modules|dist|pantry|storage\/framework\/cache|\.git|\.stx|\.stx-serve)(?:\/|$)/;export function isLintablePath(file){return lintableFile.test(file)&&!ignoredPath.test(file)}
1
+ const lintableFile=/\.(?:ts|js|stx|json|md|yaml|yml)$/i,ignoredPath=/(?:^|\/)(?:node_modules|dist|pantry|storage\/framework\/cache|\.git|\.stx|\.stx-serve)(?:\/|$)/;export function isLintablePath(file){return isLintableExtension(file)&&!isIgnoredLocation(file)}export function isLintableExtension(file){return lintableFile.test(file)}export function isIgnoredLocation(file){return ignoredPath.test(file)}
@@ -18,17 +18,30 @@
18
18
  * print "Linted" in 0.02s, having opened nothing, on a tree with problems.
19
19
  */
20
20
  export declare function lintableFiles(cwd: string): string[];
21
+ /**
22
+ * Narrow the project's lintable files to the paths a caller named.
23
+ *
24
+ * A path that matches nothing THROWS rather than falling back to the whole
25
+ * project. `buddy lint` and `buddy lint:fix` used to accept positional paths,
26
+ * discard them and run over everything, so naming one file rewrote 43: a
27
+ * caller who believes they scoped the change has to hear otherwise rather
28
+ * than read it out of `git status` (stacksjs/stacks#2953).
29
+ *
30
+ * Selection runs over {@link lintableFiles}, so .gitignore and the lintable
31
+ * extensions still decide, and naming a directory lints what is inside it.
32
+ */
33
+ export declare function selectLintableFiles(cwd: string, paths: readonly string[]): string[];
21
34
  /**
22
35
  * Lint (optionally auto-fix) the project's tracked source via pickier's SDK.
23
36
  * Returns `{ ok }` rather than exiting, so callers keep control of the process.
24
37
  * pickier auto-discovers its own config (pickier.config.ts, .config/pickier.ts,
25
38
  * …), so we don't pass one.
26
39
  */
27
- export declare function lintProject(options?: { cwd?: string, fix?: boolean, maxWarnings?: number }): Promise<{ ok: boolean }>;
40
+ export declare function lintProject(options?: { cwd?: string, fix?: boolean, maxWarnings?: number, paths?: readonly string[] }): Promise<{ ok: boolean }>;
28
41
  /** Auto-fix the project's code style — `lintProject` with fixing enabled. */
29
- export declare function lintFix(options?: { cwd?: string, maxWarnings?: number }): Promise<{ ok: boolean }>;
42
+ export declare function lintFix(options?: { cwd?: string, maxWarnings?: number, paths?: readonly string[] }): Promise<{ ok: boolean }>;
30
43
  /**
31
44
  * Format the project via pickier's SDK. `write` applies changes; `check`
32
45
  * verifies formatting without writing (fails if anything is unformatted).
33
46
  */
34
- export declare function formatProject(options?: { cwd?: string, write?: boolean, check?: boolean }): Promise<{ ok: boolean }>;
47
+ export declare function formatProject(options?: { cwd?: string, write?: boolean, check?: boolean, paths?: readonly string[] }): Promise<{ ok: boolean }>;
package/dist/lint/lint.js CHANGED
@@ -1 +1 @@
1
- import process from"node:process";import{log}from"@stacksjs/cli";import{spawnSync}from"node:child_process";import{existsSync}from"node:fs";import{join}from"node:path";import{runFormat,runLint}from"pickier";import{isLintablePath}from"./files";export function lintableFiles(cwd){const listed=spawnSync("git",["ls-files","-z","--cached","--others","--exclude-standard"],{cwd,encoding:"utf8",maxBuffer:268435456});if(listed.status===0)return listed.stdout.split("\x00").filter(isLintablePath);const notInstalled=listed.error?.code==="ENOENT",stderr=(listed.stderr??"").trim();if(notInstalled&&!existsSync(join(cwd,".git"))||/not a git repository/i.test(stderr))return walkLintableFiles(cwd);const reason=listed.error?.message||stderr||`exit code ${listed.status??listed.signal}`;throw Error(`Could not list the files to lint: \`git ls-files\` failed in ${cwd} (${reason})`)}function walkLintableFiles(cwd){return Array.from(new Bun.Glob("**/*").scanSync({cwd,onlyFiles:!0,dot:!0})).map((file)=>file.split("\\").join("/")).filter(isLintablePath).sort()}export async function lintProject(options={}){const cwd=options.cwd??process.cwd();log.info(options.fix?"Ensuring Code Style...":"Checking Code Style...");const maxWarnings=options.maxWarnings??9999;if(!Number.isInteger(maxWarnings)||maxWarnings<-1){await log.error("maxWarnings must be an integer of -1 or greater");return{ok:!1}}let files;try{files=lintableFiles(cwd)}catch(error){await log.error(error instanceof Error?error.message:String(error));return{ok:!1}}if(!files.length){log.success("Linted: no lintable files");return{ok:!0}}const ok=await runLint(files,{maxWarnings,fix:options.fix})===0;if(ok)log.success("Linted");return{ok}}export function lintFix(options={}){return lintProject({...options,fix:!0})}export async function formatProject(options={}){const cwd=options.cwd??process.cwd();let files;try{files=lintableFiles(cwd)}catch(error){await log.error(error instanceof Error?error.message:String(error));return{ok:!1}}if(!files.length)return{ok:!0};return{ok:await runFormat(files,{write:options.write,check:options.check})===0}}
1
+ import process from"node:process";import{log}from"@stacksjs/cli";import{spawnSync}from"node:child_process";import{existsSync,statSync}from"node:fs";import{join,relative,resolve}from"node:path";import{runFormat,runLint}from"pickier";import{isIgnoredLocation,isLintableExtension,isLintablePath}from"./files";export function lintableFiles(cwd){const listed=spawnSync("git",["ls-files","-z","--cached","--others","--exclude-standard"],{cwd,encoding:"utf8",maxBuffer:268435456});if(listed.status===0)return listed.stdout.split("\x00").filter(isLintablePath);const notInstalled=listed.error?.code==="ENOENT",stderr=(listed.stderr??"").trim();if(notInstalled&&!existsSync(join(cwd,".git"))||/not a git repository/i.test(stderr))return walkLintableFiles(cwd);const reason=listed.error?.message||stderr||`exit code ${listed.status??listed.signal}`;throw Error(`Could not list the files to lint: \`git ls-files\` failed in ${cwd} (${reason})`)}export function selectLintableFiles(cwd,paths){const all=lintableFiles(cwd),pool=new Set(all),selected=new Set;for(const named of paths){const rel=projectRelative(cwd,named);if(rel===null)throw Error(`Cannot lint ${named}: it is outside the project at ${cwd}`);if(rel===""){for(const file of all)selected.add(file);continue}if(pool.has(rel)){selected.add(rel);continue}const under=all.filter((file)=>file.startsWith(`${rel}/`));if(under.length===0)throw Error(whyNotLintable(cwd,named,rel));for(const file of under)selected.add(file)}return[...selected].sort()}function projectRelative(cwd,named){const rel=relative(cwd,resolve(cwd,named)).split("\\").join("/").replace(/\/+$/,"");if(rel===".."||rel.startsWith("../"))return null;return rel==="."?"":rel}function whyNotLintable(cwd,named,rel){const onDisk=join(cwd,rel);if(!existsSync(onDisk))return`Cannot lint ${named}: no such file or directory`;if(isIgnoredLocation(rel))return`Cannot lint ${named}: it is dependency, build or cache output, which buddy lint never reads`;if(statSync(onDisk).isDirectory())return`Cannot lint ${named}: it holds no files buddy lint handles`;if(!isLintableExtension(rel))return`Cannot lint ${named}: buddy lint handles .ts, .js, .stx, .json, .md, .yaml and .yml files`;return`Cannot lint ${named}: it is not one of the project's files, so .gitignore excludes it`}function walkLintableFiles(cwd){return Array.from(new Bun.Glob("**/*").scanSync({cwd,onlyFiles:!0,dot:!0})).map((file)=>file.split("\\").join("/")).filter(isLintablePath).sort()}export async function lintProject(options={}){const cwd=options.cwd??process.cwd();log.info(options.fix?"Ensuring Code Style...":"Checking Code Style...");const maxWarnings=options.maxWarnings??9999;if(!Number.isInteger(maxWarnings)||maxWarnings<-1){await log.error("maxWarnings must be an integer of -1 or greater");return{ok:!1}}let files;try{files=options.paths?.length?selectLintableFiles(cwd,options.paths):lintableFiles(cwd)}catch(error){await log.error(error instanceof Error?error.message:String(error));return{ok:!1}}if(!files.length){log.success("Linted: no lintable files");return{ok:!0}}const ok=await runLint(files,{maxWarnings,fix:options.fix})===0;if(ok)log.success("Linted");return{ok}}export function lintFix(options={}){return lintProject({...options,fix:!0})}export async function formatProject(options={}){const cwd=options.cwd??process.cwd();let files;try{files=options.paths?.length?selectLintableFiles(cwd,options.paths):lintableFiles(cwd)}catch(error){await log.error(error instanceof Error?error.message:String(error));return{ok:!1}}if(!files.length)return{ok:!0};return{ok:await runFormat(files,{write:options.write,check:options.check})===0}}
@@ -1 +1 @@
1
- import{runTestSuites}from"./runner";await runTestSuites(["feature"]);
1
+ import{namedTestPaths,runTestSuites}from"./runner";await runTestSuites(["feature"],void 0,namedTestPaths());
@@ -1 +1 @@
1
- import{runTestSuites}from"./runner";await runTestSuites([""],30000);
1
+ import{namedTestPaths,runTestSuites}from"./runner";await runTestSuites([""],30000,namedTestPaths());
@@ -1,2 +1,33 @@
1
1
  export declare function resolveTestSuiteFilters(project: string, suites: string[]): string[];
2
- export declare function runTestSuites(suites: string[], timeout?: number): Promise<void>;
2
+ /**
3
+ * The paths named on the command line, as `buddy test <paths>` passed them.
4
+ *
5
+ * They travel in the environment rather than argv because `buddyOptionArgs`
6
+ * renders an array as one comma-joined value, which cannot round-trip a path
7
+ * containing a comma. `runAction` merges `options.env` into the child process,
8
+ * which is how STACKS_DEV_SERVER already reaches an action.
9
+ */
10
+ export declare function namedTestPaths(): string[];
11
+ /**
12
+ * Test files under the paths a caller named, as `./`-prefixed paths relative
13
+ * to the project.
14
+ *
15
+ * A path matching no test file THROWS. `buddy test <path>` used to discard the
16
+ * path and run the whole suite, so it reported `0 fail` and exit 0 for a file
17
+ * it never opened: naming a test with 20 failures printed a green
18
+ * (stacksjs/stacks#2973). Falling back to the suite is exactly what turns a
19
+ * typo into a false green, so there is no fallback.
20
+ *
21
+ * The `./` prefix is not cosmetic. bunfig sets `[test] root = "tests"`, so a
22
+ * bare project-relative path matches nothing and `bun test` would report a
23
+ * clean run over no files.
24
+ */
25
+ export declare function resolveNamedTestFiles(project: string, paths: readonly string[]): string[];
26
+ /**
27
+ * Narrow named test files to one suite, for `test:unit` and `test:feature`.
28
+ *
29
+ * A path outside the suite is an error naming the command that does run it,
30
+ * rather than an empty run reported as a pass.
31
+ */
32
+ export declare function withinSuite(files: readonly string[], suite: string, paths: readonly string[]): string[];
33
+ export declare function runTestSuites(suites: string[], timeout?: number, paths?: readonly string[]): Promise<void>;
@@ -1 +1 @@
1
- import process from"node:process";import{existsSync,realpathSync}from"node:fs";import{join}from"node:path";import{log}from"@stacksjs/cli";import{projectPath}from"@stacksjs/path";const testFilePattern="**/*.{test,spec}.{js,jsx,ts,tsx,mjs,mts,cjs,cts}";export function resolveTestSuiteFilters(project,suites){const visitedDirectories=new Set;return suites.flatMap((suite)=>{const suiteDirectory=join(project,"tests",suite);if(!existsSync(suiteDirectory))return[];const canonicalDirectory=realpathSync(suiteDirectory);if(visitedDirectories.has(canonicalDirectory))return[];visitedDirectories.add(canonicalDirectory);return[...new Bun.Glob(testFilePattern).scanSync({cwd:suiteDirectory,onlyFiles:!0})].map((file)=>suite?`./tests/${suite}/${file}`:`./tests/${file}`)})}export async function runTestSuites(suites,timeout){const project=projectPath(),filters=resolveTestSuiteFilters(project,suites);if(filters.length===0){log.info("No matching tests found.");return}const args=["bun","test"];if(timeout)args.push("--timeout",String(timeout));args.push(...filters);const exitCode=await Bun.spawn(args,{cwd:project,stdio:["inherit","inherit","inherit"]}).exited;if(exitCode!==0){await log.error("Tests failed");process.exit(exitCode??1)}}
1
+ import process from"node:process";import{existsSync,realpathSync,statSync}from"node:fs";import{join,relative,resolve}from"node:path";import{log}from"@stacksjs/cli";import{projectPath}from"@stacksjs/path";const testFilePattern="**/*.{test,spec}.{js,jsx,ts,tsx,mjs,mts,cjs,cts}";export function resolveTestSuiteFilters(project,suites){const visitedDirectories=new Set;return suites.flatMap((suite)=>{const suiteDirectory=join(project,"tests",suite);if(!existsSync(suiteDirectory))return[];const canonicalDirectory=realpathSync(suiteDirectory);if(visitedDirectories.has(canonicalDirectory))return[];visitedDirectories.add(canonicalDirectory);return[...new Bun.Glob(testFilePattern).scanSync({cwd:suiteDirectory,onlyFiles:!0})].map((file)=>suite?`./tests/${suite}/${file}`:`./tests/${file}`)})}export function namedTestPaths(){const raw=process.env.STACKS_TEST_PATHS;if(!raw)return[];try{const parsed=JSON.parse(raw);return Array.isArray(parsed)?parsed.filter((path)=>typeof path==="string"):[]}catch{return[]}}export function resolveNamedTestFiles(project,paths){const found=new Set;for(const named of paths){const absolute=resolve(project,named),rel=relative(project,absolute).split("\\").join("/");if(rel===".."||rel.startsWith("../"))throw Error(`Cannot run ${named}: it is outside the project at ${project}`);if(!existsSync(absolute))throw Error(`Cannot run ${named}: no such file or directory`);if(statSync(absolute).isDirectory()){const under=[...new Bun.Glob(testFilePattern).scanSync({cwd:absolute,onlyFiles:!0})].map((file)=>`./${rel?`${rel}/`:""}${file.split("\\").join("/")}`);if(under.length===0)throw Error(`Cannot run ${named}: it holds no test files`);for(const file of under)found.add(file);continue}if(!new Bun.Glob(testFilePattern).match(rel))throw Error(`Cannot run ${named}: a test file is named *.test.ts or *.spec.ts (or .js, .tsx, .mts, ...)`);found.add(`./${rel}`)}return[...found].sort()}export function withinSuite(files,suite,paths){const prefix=`./tests/${suite}/`,inSuite=files.filter((file)=>file.startsWith(prefix));if(inSuite.length===0)throw Error(`No ${suite} tests under ${paths.join(" ")}. Run \`buddy test ${paths.join(" ")}\` for the tests that are there.`);return inSuite}export async function runTestSuites(suites,timeout,paths){const project=projectPath();let filters;if(paths?.length)try{const named=resolveNamedTestFiles(project,paths),suite=suites.find(Boolean);filters=suite?withinSuite(named,suite,paths):named}catch(error){await log.error(error instanceof Error?error.message:String(error));process.exit(1)}else filters=resolveTestSuiteFilters(project,suites);if(filters.length===0){log.info("No matching tests found.");return}const args=["bun","test"];if(timeout)args.push("--timeout",String(timeout));args.push(...filters);const exitCode=await Bun.spawn(args,{cwd:project,stdio:["inherit","inherit","inherit"]}).exited;if(exitCode!==0){await log.error("Tests failed");process.exit(exitCode??1)}}
package/dist/test/unit.js CHANGED
@@ -1 +1 @@
1
- import{runTestSuites}from"./runner";await runTestSuites(["unit"]);
1
+ import{namedTestPaths,runTestSuites}from"./runner";await runTestSuites(["unit"],void 0,namedTestPaths());
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.262",
5
+ "version": "0.75.264",
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.262",
70
- "@stacksjs/ai": "0.75.262",
71
- "@stacksjs/analytics": "0.75.262",
72
- "@stacksjs/api": "0.75.262",
73
- "@stacksjs/auth": "0.75.262",
74
- "@stacksjs/build": "0.75.262",
69
+ "@stacksjs/action-runner": "0.75.264",
70
+ "@stacksjs/ai": "0.75.264",
71
+ "@stacksjs/analytics": "0.75.264",
72
+ "@stacksjs/api": "0.75.264",
73
+ "@stacksjs/auth": "0.75.264",
74
+ "@stacksjs/build": "0.75.264",
75
75
  "@stacksjs/bumpx": "^0.2.25",
76
76
  "@stacksjs/bunpress": "^0.2.15",
77
- "@stacksjs/cli": "0.75.262",
78
- "@stacksjs/cloud": "0.75.262",
79
- "@stacksjs/cms": "0.75.262",
80
- "@stacksjs/components": "0.2.661",
81
- "@stacksjs/config": "0.75.262",
82
- "@stacksjs/database": "0.75.262",
83
- "@stacksjs/desktop-build": "0.75.262",
84
- "@stacksjs/dns": "0.75.262",
85
- "@stacksjs/enums": "0.75.262",
86
- "@stacksjs/env": "0.75.262",
87
- "@stacksjs/error-handling": "0.75.262",
88
- "@stacksjs/github": "0.75.262",
77
+ "@stacksjs/cli": "0.75.264",
78
+ "@stacksjs/cloud": "0.75.264",
79
+ "@stacksjs/cms": "0.75.264",
80
+ "@stacksjs/components": "0.2.663",
81
+ "@stacksjs/config": "0.75.264",
82
+ "@stacksjs/database": "0.75.264",
83
+ "@stacksjs/desktop-build": "0.75.264",
84
+ "@stacksjs/dns": "0.75.264",
85
+ "@stacksjs/enums": "0.75.264",
86
+ "@stacksjs/env": "0.75.264",
87
+ "@stacksjs/error-handling": "0.75.264",
88
+ "@stacksjs/github": "0.75.264",
89
89
  "@stacksjs/gitit": "^0.2.5",
90
- "@stacksjs/i18n": "0.75.262",
91
- "@stacksjs/image": "0.75.262",
92
- "@stacksjs/logging": "0.75.262",
90
+ "@stacksjs/i18n": "0.75.264",
91
+ "@stacksjs/image": "0.75.264",
92
+ "@stacksjs/logging": "0.75.264",
93
93
  "@stacksjs/logsmith": "^0.2.11",
94
- "@stacksjs/mobile": "0.75.262",
95
- "@stacksjs/orm": "0.75.262",
96
- "@stacksjs/path": "0.75.262",
97
- "@stacksjs/payments": "0.75.262",
98
- "@stacksjs/queue": "0.75.262",
99
- "@stacksjs/registry": "0.75.262",
100
- "@stacksjs/router": "0.75.262",
94
+ "@stacksjs/mobile": "0.75.264",
95
+ "@stacksjs/orm": "0.75.264",
96
+ "@stacksjs/path": "0.75.264",
97
+ "@stacksjs/payments": "0.75.264",
98
+ "@stacksjs/queue": "0.75.264",
99
+ "@stacksjs/registry": "0.75.264",
100
+ "@stacksjs/router": "0.75.264",
101
101
  "@stacksjs/rpx": "^0.11.54",
102
- "@stacksjs/scheduler": "0.75.262",
103
- "@stacksjs/search-engine": "0.75.262",
104
- "@stacksjs/security": "0.75.262",
105
- "@stacksjs/server": "0.75.262",
106
- "@stacksjs/sites": "0.75.262",
107
- "@stacksjs/storage": "0.75.262",
108
- "@stacksjs/strings": "0.75.262",
109
- "@stacksjs/stx": "0.2.661",
102
+ "@stacksjs/scheduler": "0.75.264",
103
+ "@stacksjs/search-engine": "0.75.264",
104
+ "@stacksjs/security": "0.75.264",
105
+ "@stacksjs/server": "0.75.264",
106
+ "@stacksjs/sites": "0.75.264",
107
+ "@stacksjs/storage": "0.75.264",
108
+ "@stacksjs/strings": "0.75.264",
109
+ "@stacksjs/stx": "0.2.663",
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.262",
114
- "@stacksjs/types": "0.75.262",
115
- "@stacksjs/utils": "0.75.262",
116
- "@stacksjs/whois": "0.75.262",
117
- "bun-plugin-stx": "0.2.661"
113
+ "@stacksjs/tunnel": "0.75.264",
114
+ "@stacksjs/types": "0.75.264",
115
+ "@stacksjs/utils": "0.75.264",
116
+ "@stacksjs/whois": "0.75.264",
117
+ "bun-plugin-stx": "0.2.663"
118
118
  },
119
119
  "peerDependencies": {
120
120
  "craft-native": ">=0.0.290",
@@ -129,7 +129,7 @@
129
129
  }
130
130
  },
131
131
  "devDependencies": {
132
- "@stacksjs/validation": "0.75.262",
132
+ "@stacksjs/validation": "0.75.264",
133
133
  "better-dx": "^0.2.24"
134
134
  }
135
135
  }