arcane-os 0.55.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ import {
13
13
  import path from 'node:path';
14
14
  import {pathToFileURL} from 'node:url';
15
15
  import {appRelativeRoot,resolveAppRoot,resolvePackageOutputRoot} from '../app-layout.mjs';
16
+ import {TARGET_IDS} from '../constants.mjs';
16
17
  import {readInstalledSdkLayout} from '../sdk-runtime-layout.mjs';
17
18
  import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
18
19
  import {materializeNativeModules} from './native-modules.mjs';
@@ -320,7 +321,7 @@ function normalizeOptionalRecord(value,label){
320
321
  export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.join(appRelativeRoot(rootConfig,appId),APP_CONFIG_NAME)){
321
322
  assertOnlyKeys(value,new Set([
322
323
  'schemaVersion','id','displayName','version','entry','strategy','security',
323
- 'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory','documents'
324
+ 'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory','documents','nativeResources'
324
325
  ]),`${appId}/${APP_CONFIG_NAME}`);
325
326
  if(value.schemaVersion!==1)fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
326
327
  if(!is.string(value.id)||value.id!==appId||!APP_ID_PATTERN.test(value.id)){
@@ -335,6 +336,15 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
335
336
  );
336
337
  const include=validatePathList(value.include,`${appId}/${APP_CONFIG_NAME}.include`,{required:true});
337
338
  const exclude=validatePathList(value.exclude??[],`${appId}/${APP_CONFIG_NAME}.exclude`);
339
+ let nativeResources;
340
+ if(value.nativeResources!==undefined){
341
+ const label=`${appId}/${APP_CONFIG_NAME}.nativeResources`;
342
+ assertOnlyKeys(value.nativeResources,new Set(['include','exclude']),label);
343
+ nativeResources={
344
+ include:validatePathList(value.nativeResources.include,`${label}.include`),
345
+ exclude:validatePathList(value.nativeResources.exclude??[],`${label}.exclude`)
346
+ };
347
+ }
338
348
  if(include.some(allowed=>sameOrDescendant(APP_CONFIG_NAME,allowed))){
339
349
  fail(`${appId}/${APP_CONFIG_NAME}.include must not expose the authored package configuration.`);
340
350
  }
@@ -382,6 +392,7 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
382
392
  )}),
383
393
  include,
384
394
  exclude,
395
+ ...(nativeResources===undefined?{}:{nativeResources}),
385
396
  shared:[...value.shared],
386
397
  ...(adapter===undefined?{}:{adapter}),
387
398
  configPath
@@ -423,7 +434,8 @@ async function assertContainedRealPath(root,candidate,label){
423
434
  }
424
435
  }
425
436
 
426
- async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory}={}){
437
+ async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory,target='browser'}={}){
438
+ if(!TARGET_IDS.includes(target))fail(`Unsupported package target: ${String(target)}.`);
427
439
  const workspaceRoot=await realDirectory(normalizeWorkspaceRoot(requestedWorkspaceRoot),'Workspace root');
428
440
  const rootConfigPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
429
441
  const rootConfig=validateRootConfig(await readJson(rootConfigPath,ROOT_CONFIG_NAME),rootConfigPath);
@@ -436,14 +448,19 @@ async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory}={}){
436
448
  if(outputDirectory!==undefined){
437
449
  config.outputDirectory=normalizeRelativePath(outputDirectory,'package outputDirectory');
438
450
  }
451
+ const outputRoot=resolvePackageOutputRoot(workspaceRoot,rootConfig,config,{target,outputDirectory});
452
+ if(target!=='browser'&&config.nativeResources?.include.length>0){
453
+ config.outputDirectory=path.relative(workspaceRoot,outputRoot).split(path.sep).join('/');
454
+ }
439
455
  return {
440
456
  workspaceRoot,
441
457
  rootConfig,
442
458
  appsRoot,
443
459
  appRoot,
444
460
  appId,
461
+ target,
445
462
  config,
446
- outputRoot:resolvePackageOutputRoot(workspaceRoot,rootConfig,config)
463
+ outputRoot
447
464
  };
448
465
  }
449
466
 
@@ -494,6 +511,11 @@ async function assertPackageOutputLocation(context){
494
511
  for(const selected of context.config.include){
495
512
  assertSelectedInput(context.appRoot,selected,context.config.exclude,context.appId);
496
513
  }
514
+ if(context.target!=='browser'&&context.config.nativeResources){
515
+ for(const selected of context.config.nativeResources.include){
516
+ assertSelectedInput(context.appRoot,selected,context.config.nativeResources.exclude,`${context.appId} native`);
517
+ }
518
+ }
497
519
  for(const sharedId of context.config.shared){
498
520
  for(const route of context.rootConfig.sharedPayloads[sharedId]){
499
521
  const sourceRoot=path.resolve(context.workspaceRoot,route.source);
@@ -526,6 +548,7 @@ async function collectSelectedPath({
526
548
  reject,
527
549
  records,
528
550
  destinations,
551
+ coalescedRecords,
529
552
  signal,
530
553
  label,
531
554
  allowRoot=false
@@ -555,6 +578,7 @@ async function collectSelectedPath({
555
578
  reject,
556
579
  records,
557
580
  destinations,
581
+ coalescedRecords,
558
582
  signal,
559
583
  label
560
584
  });
@@ -567,9 +591,15 @@ async function collectSelectedPath({
567
591
  fail(`${label} overlaps the generated release manifest.`);
568
592
  }
569
593
  const key=pathKey(normalizedDestination);
570
- if(destinations.has(key))fail(`Package destination collision: ${normalizedDestination}.`);
594
+ if(destinations.has(key)){
595
+ const existing=coalescedRecords?.get(key);
596
+ if(existing?.source===absolute&&existing.destination===normalizedDestination)return;
597
+ fail(`Package destination collision: ${normalizedDestination}.`);
598
+ }
571
599
  destinations.add(key);
572
- records.push({source:absolute,destination:normalizedDestination});
600
+ const record={source:absolute,destination:normalizedDestination};
601
+ records.push(record);
602
+ coalescedRecords?.set(key,record);
573
603
  }
574
604
 
575
605
  async function collectPackageRecords(context,{signal}={}){
@@ -613,6 +643,29 @@ async function collectPackageRecords(context,{signal}={}){
613
643
  }
614
644
  }
615
645
  }
646
+ if(context.target!=='browser'&&context.config.nativeResources){
647
+ const nativeRecords=[];
648
+ const coalescedRecords=new Map(records.map(record=>[pathKey(record.destination),record]));
649
+ for(const selected of context.config.nativeResources.include){
650
+ await collectSelectedPath({
651
+ sourceRoot:context.appRoot,
652
+ selected,
653
+ destination:appPackagePath(context,selected),
654
+ excludes:context.config.nativeResources.exclude,
655
+ reject:isAppSourceForbidden,
656
+ records:nativeRecords,
657
+ destinations,
658
+ coalescedRecords,
659
+ signal,
660
+ label:`${context.appId} nativeResources`
661
+ });
662
+ }
663
+ for(const record of nativeRecords){
664
+ record.appRelativePath=path.relative(context.appRoot,record.source).split(path.sep).join('/');
665
+ record.nativeOnly=true;
666
+ records.push(record);
667
+ }
668
+ }
616
669
  records.sort((left,right)=>compareText(left.destination,right.destination));
617
670
  if(!records.some(record=>pathKey(record.destination)===pathKey(appPackagePath(context, context.config.entry)))){
618
671
  fail(`Package entry is missing from the selected files: ${context.config.entry}.`);
@@ -671,7 +724,7 @@ async function optionalDescriptor(context){
671
724
 
672
725
  async function inspectContext(context,{signal,records}={}){
673
726
  const selectedRecords=records??await collectPackageRecords(context,{signal});
674
- const documents=await browserDocuments(selectedRecords,context.config.entry,context.config.documents);
727
+ const documents=await browserDocuments(selectedRecords.filter(record=>!record.nativeOnly),context.config.entry,context.config.documents);
675
728
  return {
676
729
  appId:context.appId,
677
730
  displayName:context.config.displayName,
@@ -683,6 +736,7 @@ async function inspectContext(context,{signal,records}={}){
683
736
  ...(context.config.pwa===undefined?{}:{pwa:copyJson(context.config.pwa)}),
684
737
  include:[...context.config.include],
685
738
  exclude:[...context.config.exclude],
739
+ ...(context.config.nativeResources===undefined?{}:{nativeResources:copyJson(context.config.nativeResources)}),
686
740
  shared:[...context.config.shared],
687
741
  ...(context.config.security===undefined?{}:{security:copyJson(context.config.security)}),
688
742
  ...(context.config.localAIModelPolicy===undefined?{}:{
@@ -724,9 +778,9 @@ export async function discoverApps({workspaceRoot:requestedWorkspaceRoot}={}){
724
778
  return apps;
725
779
  }
726
780
 
727
- export async function inspectApp({workspaceRoot,appId,signal}={}){
781
+ export async function inspectApp({workspaceRoot,appId,signal,target,outputDirectory}={}){
728
782
  throwIfAborted(signal);
729
- const context=await loadContext(workspaceRoot,appId);
783
+ const context=await loadContext(workspaceRoot,appId,{target,outputDirectory});
730
784
  return inspectContext(context,{signal});
731
785
  }
732
786
 
@@ -846,7 +900,8 @@ async function replaceDirectory(stagingRoot,outputRoot){
846
900
  }
847
901
 
848
902
  async function packageWithContext(context,options={}){
849
- const {signal,onEvent,browserPwa=true,moduleFormat='import-map'}=options;
903
+ const {signal,onEvent,moduleFormat='import-map',browserPwa:requestedBrowserPwa=true}=options;
904
+ const browserPwa=context.target==='browser'&&requestedBrowserPwa;
850
905
  if(!['import-map','native'].includes(moduleFormat)){
851
906
  throw new TypeError('packageApp moduleFormat must be "import-map" or "native".');
852
907
  }
@@ -854,7 +909,9 @@ async function packageWithContext(context,options={}){
854
909
  const appPath=appRelativeRoot(context.rootConfig,context.appId);
855
910
  const entryPath=appPackagePath(context,context.config.entry);
856
911
  const records=await collectPackageRecords(context,{signal});
857
- const rootDocument = records.some(
912
+ const browserRecords=records.filter(record=>!record.nativeOnly);
913
+ const nativeRecords=records.filter(record=>record.nativeOnly);
914
+ const rootDocument = browserRecords.some(
858
915
  function selectedRootIndex(record) {
859
916
  return record.destination === 'index.html';
860
917
  }
@@ -901,7 +958,7 @@ async function packageWithContext(context,options={}){
901
958
  let promoted=false;
902
959
  try{
903
960
  async function copyBase() {
904
- await copyRecords(records,stagingRoot,{signal,onEvent});
961
+ await copyRecords(browserRecords,stagingRoot,{signal,onEvent});
905
962
  if (!rootDocument) {
906
963
  await writePackageLauncher(context, stagingRoot);
907
964
  }
@@ -913,6 +970,8 @@ async function packageWithContext(context,options={}){
913
970
  workspaceRoot:context.workspaceRoot,
914
971
  appRoot:context.appRoot,
915
972
  outputRoot:stagingRoot,
973
+ target:context.target,
974
+ browserPwa,
916
975
  copyBase,
917
976
  signal,
918
977
  onEvent
@@ -971,7 +1030,7 @@ async function packageWithContext(context,options={}){
971
1030
  stagingRoot,
972
1031
  files,
973
1032
  documents:inspected.browserDocuments.map(function documentPath(document){return document.packagePath;}),
974
- sharedFiles:records.filter(function sharedRecord(record){return record.appRelativePath===undefined;})
1033
+ sharedFiles:browserRecords.filter(function sharedRecord(record){return record.appRelativePath===undefined;})
975
1034
  .map(function sharedPath(record){return record.destination;}),
976
1035
  signal,
977
1036
  onEvent
@@ -1007,8 +1066,8 @@ async function packageWithContext(context,options={}){
1007
1066
  const documentUrl=entryDocument?.bases[0]?.href
1008
1067
  ?new URL(entryDocument.bases[0].href,entryUrl):entryUrl;
1009
1068
  const pending=[{file:entryPath,documentUrl}];
1010
- const sharedFiles=new Set(records.filter(record=>record.appRelativePath===undefined).map(record=>record.destination));
1011
- const applicationFiles=new Set(records.filter(function applicationRecord(record){
1069
+ const sharedFiles=new Set(browserRecords.filter(record=>record.appRelativePath===undefined).map(record=>record.destination));
1070
+ const applicationFiles=new Set(browserRecords.filter(function applicationRecord(record){
1012
1071
  return record.appRelativePath!==undefined;
1013
1072
  }).map(function applicationDestination(record){return record.destination;}));
1014
1073
  for(const file of files){
@@ -1132,6 +1191,17 @@ async function packageWithContext(context,options={}){
1132
1191
  }
1133
1192
  files.sort(compareText);
1134
1193
  }
1194
+ // Native resources bypass browser rewriting, module projection and PWA generation.
1195
+ // The app's browser selection remains authoritative wherever both select a file.
1196
+ const finalDestinations=new Set(files.map(pathKey));
1197
+ for(const record of nativeRecords){
1198
+ if(finalDestinations.has(pathKey(record.destination))){
1199
+ fail(`Native resource overlaps generated package content: ${record.destination}.`);
1200
+ }
1201
+ }
1202
+ await copyRecords(nativeRecords,stagingRoot,{signal,onEvent});
1203
+ files.push(...nativeRecords.map(record=>record.destination));
1204
+ files.sort(compareText);
1135
1205
  const manifest=releaseManifest(context,files,pwaArtifacts,rootDocument);
1136
1206
  await writeFile(
1137
1207
  path.join(stagingRoot,RELEASE_MANIFEST_NAME),
package/src/process.mjs CHANGED
@@ -33,8 +33,8 @@ function platformArguments(command,args,platform=process.platform){
33
33
  return [cliPath,...args];
34
34
  }
35
35
 
36
- function appendOutput(current,chunk){
37
- return current+chunk.toString('utf8');
36
+ function appendOutput(current,text){
37
+ return current+text;
38
38
  }
39
39
 
40
40
  async function emitLines(events,type,text){
@@ -45,9 +45,9 @@ async function emitLines(events,type,text){
45
45
  }
46
46
  }
47
47
 
48
- function deliverChunk(stream,events,type,chunk){
48
+ function deliverChunk(stream,events,type,text){
49
49
  stream.pause();
50
- return emitLines(events,type,chunk.toString('utf8')).catch(()=>{
50
+ return emitLines(events,type,text).catch(()=>{
51
51
  // The queue owns and propagates the first callback failure.
52
52
  }).finally(()=>{
53
53
  if(!stream.destroyed){
@@ -263,6 +263,10 @@ export async function runProcess(command,args=[],{
263
263
  return;
264
264
  }
265
265
 
266
+ // Each readable retains split UTF-8 sequences and flushes at EOF.
267
+ // Capture and events consume the same decoded text.
268
+ child.stdout.setEncoding('utf8');
269
+ child.stderr.setEncoding('utf8');
266
270
  child.stdout.on('data',chunk=>{
267
271
  stdout=appendOutput(stdout,chunk);
268
272
  ownDelivery(child.stdout,'process.stdout',chunk);
package/src/toolchain.mjs CHANGED
@@ -996,12 +996,14 @@ function pathsOverlap(left,right){
996
996
  async function packageNativeRelease(prepared,app,{
997
997
  workspaceOperationLease,
998
998
  selectedSdk,
999
+ target,
999
1000
  signal,
1000
1001
  onEvent
1001
1002
  }={}){
1002
1003
  const release=await packageApp({
1003
1004
  workspaceRoot:prepared.workspaceRoot,
1004
1005
  appId:app.appId,
1006
+ target,
1005
1007
  browserPwa:false,
1006
1008
  workspaceOperationLease,
1007
1009
  signal,
@@ -1065,7 +1067,7 @@ async function executePairedNativeBuild(options,adapter,{
1065
1067
  const packageOutputs=[];
1066
1068
  for(const app of [prepared.validation.app,...dependencyApps]){
1067
1069
  const releaseRoot=resolvePackageOutputRoot(
1068
- prepared.workspaceRoot,prepared.validation.config,app.manifest
1070
+ prepared.workspaceRoot,prepared.validation.config,app.manifest,{target}
1069
1071
  );
1070
1072
  if(pathsOverlap(releaseRoot,outputRoot)
1071
1073
  ||packageOutputs.some(existing=>pathsOverlap(existing,releaseRoot))){
@@ -1084,13 +1086,13 @@ async function executePairedNativeBuild(options,adapter,{
1084
1086
  descriptorSource:prepared.validation.app.descriptorSource,
1085
1087
  descriptorPath:prepared.validation.app.descriptorPath,
1086
1088
  validation:prepared.validation
1087
- },{workspaceOperationLease,selectedSdk,signal,onEvent});
1089
+ },{workspaceOperationLease,selectedSdk,target,signal,onEvent});
1088
1090
  const dependencyReleases=[];
1089
1091
  for(const dependency of dependencyApps){
1090
1092
  dependencyReleases.push(await packageNativeRelease(
1091
1093
  prepared,
1092
1094
  dependency,
1093
- {workspaceOperationLease,selectedSdk,signal,onEvent}
1095
+ {workspaceOperationLease,selectedSdk,target,signal,onEvent}
1094
1096
  ));
1095
1097
  }
1096
1098
  const toolchain=await adapter.prepare({