arcane-os 0.18.0 → 0.20.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.
- package/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/docs/architecture.md +18 -0
- package/docs/reference/cli.md +60 -33
- package/docs/reference/mail.md +65 -15
- package/docs/reference/pwa.md +16 -2
- package/docs/reference/sdk-api.md +36 -33
- package/docs/reviews/mail-server-purpose-review.md +48 -6
- package/package.json +1 -1
- package/src/cli/main.mjs +16 -17
- package/src/dev-server.mjs +10 -6
- package/src/mail-credentials.mjs +101 -632
- package/src/mail.mjs +17 -26
- package/src/packager/core.mjs +71 -26
- package/src/pwa.mjs +21 -18
- package/src/templates/workspace-template.mjs +1 -0
package/src/mail.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
|
3
3
|
import {
|
|
4
4
|
deleteMailCredential,
|
|
5
5
|
getMailCredentialStatus,
|
|
6
|
+
mailCredentialLocation,
|
|
6
7
|
readMailCredential,
|
|
7
8
|
setMailCredential
|
|
8
9
|
} from './mail-credentials.mjs';
|
|
@@ -54,13 +55,9 @@ function mailRecipientOptions(value,label){
|
|
|
54
55
|
function mailCredentialOptions(options){
|
|
55
56
|
return {
|
|
56
57
|
profile:options.profile,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
spawnImpl:options.spawnImpl,
|
|
61
|
-
runner:options.credentialRunner,
|
|
62
|
-
signal:options.signal,
|
|
63
|
-
timeoutMs:options.credentialTimeoutMs
|
|
58
|
+
cwd:options.cwd,
|
|
59
|
+
workspaceRoot:options.workspaceRoot,
|
|
60
|
+
signal:options.signal
|
|
64
61
|
};
|
|
65
62
|
}
|
|
66
63
|
|
|
@@ -92,24 +89,11 @@ async function deleteMailCredentialProfile(options){
|
|
|
92
89
|
|
|
93
90
|
async function sendMailFromReport(options){
|
|
94
91
|
const readReport=resolveMailCommandDependency(options,'readReport',null);
|
|
95
|
-
const readCredential=resolveMailCommandDependency(options,'readCredential',readMailCredential);
|
|
96
92
|
const send=resolveMailCommandDependency(options,'sendMail',sendResendMail);
|
|
97
93
|
throwIfAborted(options.signal);
|
|
98
94
|
const report=await readReport();
|
|
99
95
|
throwIfAborted(options.signal);
|
|
100
|
-
let apiKey=await
|
|
101
|
-
if(apiKey===null){
|
|
102
|
-
throw new ArcaneError(
|
|
103
|
-
ERROR_CODES.prerequisiteMissing,
|
|
104
|
-
`No Resend credential is configured for profile ${String(options.profile)}.`
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
if(!is.string(apiKey)||!apiKey){
|
|
108
|
-
throw new ArcaneError(
|
|
109
|
-
ERROR_CODES.operationFailed,
|
|
110
|
-
'The configured Resend credential could not be read.'
|
|
111
|
-
);
|
|
112
|
-
}
|
|
96
|
+
let apiKey=await readMailProviderKey(options);
|
|
113
97
|
try{
|
|
114
98
|
throwIfAborted(options.signal);
|
|
115
99
|
const result=await send({
|
|
@@ -141,15 +125,15 @@ async function sendMailFromReport(options){
|
|
|
141
125
|
}
|
|
142
126
|
}
|
|
143
127
|
|
|
144
|
-
async function
|
|
128
|
+
async function readMailProviderKey(options){
|
|
145
129
|
const readCredential=resolveMailCommandDependency(options,'readCredential',readMailCredential);
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
let apiKey=await readCredential(mailCredentialOptions(options));
|
|
130
|
+
const credentialOptions=mailCredentialOptions(options);
|
|
131
|
+
const apiKey=await readCredential(credentialOptions);
|
|
149
132
|
if(apiKey===null){
|
|
133
|
+
const location=mailCredentialLocation(credentialOptions);
|
|
150
134
|
throw new ArcaneError(
|
|
151
135
|
ERROR_CODES.prerequisiteMissing,
|
|
152
|
-
`
|
|
136
|
+
`Missing ${location.setting} in ${location.filePath}.`
|
|
153
137
|
);
|
|
154
138
|
}
|
|
155
139
|
if(!is.string(apiKey)||!apiKey){
|
|
@@ -158,6 +142,13 @@ async function serveMailGateway(options){
|
|
|
158
142
|
'The configured Resend credential could not be read.'
|
|
159
143
|
);
|
|
160
144
|
}
|
|
145
|
+
return apiKey;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function serveMailGateway(options){
|
|
149
|
+
const startServer=resolveMailCommandDependency(options,'startServer',startResendMailServer);
|
|
150
|
+
throwIfAborted(options.signal);
|
|
151
|
+
let apiKey=await readMailProviderKey(options);
|
|
161
152
|
try{
|
|
162
153
|
throwIfAborted(options.signal);
|
|
163
154
|
const recipientAllowlist=mailRecipientOptions(options.allowTo,'allowTo');
|
package/src/packager/core.mjs
CHANGED
|
@@ -424,6 +424,15 @@ function destinationJoin(root,relative){
|
|
|
424
424
|
return root==='.'?relative:`${root}/${relative}`;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
function appPackagePath(context, relative) {
|
|
428
|
+
return `apps/${context.appId}/${relative}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function packageResourceUrl(relative) {
|
|
432
|
+
const segments = relative.split('/');
|
|
433
|
+
return `./${segments.map(encodeURIComponent).join('/')}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
427
436
|
async function collectSelectedPath({
|
|
428
437
|
sourceRoot,
|
|
429
438
|
selected,
|
|
@@ -483,7 +492,7 @@ async function collectPackageRecords(context,{signal}={}){
|
|
|
483
492
|
await collectSelectedPath({
|
|
484
493
|
sourceRoot:context.appRoot,
|
|
485
494
|
selected,
|
|
486
|
-
destination:selected,
|
|
495
|
+
destination:appPackagePath(context, selected),
|
|
487
496
|
excludes:context.config.exclude,
|
|
488
497
|
reject:isAppSourceForbidden,
|
|
489
498
|
records,
|
|
@@ -512,23 +521,25 @@ async function collectPackageRecords(context,{signal}={}){
|
|
|
512
521
|
}
|
|
513
522
|
}
|
|
514
523
|
records.sort((left,right)=>compareText(left.destination,right.destination));
|
|
515
|
-
if(!records.some(record=>pathKey(record.destination)===pathKey(context.config.entry))){
|
|
524
|
+
if(!records.some(record=>pathKey(record.destination)===pathKey(appPackagePath(context, context.config.entry)))){
|
|
516
525
|
fail(`Package entry is missing from the selected files: ${context.config.entry}.`);
|
|
517
526
|
}
|
|
518
527
|
return records;
|
|
519
528
|
}
|
|
520
529
|
|
|
521
|
-
async function browserDocuments(records,entry){
|
|
530
|
+
async function browserDocuments(records,entry,appPrefix){
|
|
522
531
|
let entryDocument=null;
|
|
523
532
|
const documents=[];
|
|
524
533
|
for(const record of records){
|
|
525
534
|
const extension=path.posix.extname(record.destination).toLocaleLowerCase('en-US');
|
|
526
535
|
if(extension!=='.html'&&extension!=='.htm')continue;
|
|
536
|
+
const documentPath=record.destination.startsWith(appPrefix)
|
|
537
|
+
?record.destination.slice(appPrefix.length):record.destination;
|
|
527
538
|
const inspected=inspectImportMapHtml(await readFile(record.source,'utf8'),{
|
|
528
|
-
documentPath
|
|
539
|
+
documentPath
|
|
529
540
|
});
|
|
530
|
-
const document={path:record.destination,...copyJson(inspected)};
|
|
531
|
-
if(record.destination
|
|
541
|
+
const document={path:documentPath,packagePath:record.destination,...copyJson(inspected)};
|
|
542
|
+
if(record.destination===`${appPrefix}${entry}`){
|
|
532
543
|
entryDocument=document;
|
|
533
544
|
}else if(inspected.bases.length>0){
|
|
534
545
|
documents.push(document);
|
|
@@ -567,8 +578,8 @@ async function inspectContext(context,{signal}={}){
|
|
|
567
578
|
}),
|
|
568
579
|
...(context.config.adapter===undefined?{}:{adapter:context.config.adapter}),
|
|
569
580
|
descriptor:await optionalDescriptor(context),
|
|
570
|
-
browserDocuments:await browserDocuments(records,context.config.entry),
|
|
571
|
-
files:records.map(record=>record.destination),
|
|
581
|
+
browserDocuments:await browserDocuments(records,context.config.entry,appPackagePath(context,'')),
|
|
582
|
+
files:[...new Set(['index.html',...records.map(record=>record.destination)])].sort(compareText),
|
|
572
583
|
output:path.relative(context.workspaceRoot,context.outputRoot).split(path.sep).join('/')
|
|
573
584
|
};
|
|
574
585
|
}
|
|
@@ -611,6 +622,28 @@ async function copyRecords(records,stagingRoot,{signal,onEvent}={}){
|
|
|
611
622
|
}
|
|
612
623
|
}
|
|
613
624
|
|
|
625
|
+
async function writePackageLauncher(context, stagingRoot) {
|
|
626
|
+
function escapeHtml(value) {
|
|
627
|
+
return value.replaceAll('&', '&').replaceAll('"', '"')
|
|
628
|
+
.replaceAll('<', '<').replaceAll('>', '>');
|
|
629
|
+
}
|
|
630
|
+
const start = escapeHtml(packageResourceUrl(appPackagePath(context, context.config.entry)));
|
|
631
|
+
const title = escapeHtml(context.config.displayName);
|
|
632
|
+
const content = `<!doctype html>
|
|
633
|
+
<html lang="en">
|
|
634
|
+
<head>
|
|
635
|
+
<meta charset="utf-8">
|
|
636
|
+
<meta http-equiv="refresh" content="0; url=${start}">
|
|
637
|
+
<title>${title}</title>
|
|
638
|
+
</head>
|
|
639
|
+
<body>
|
|
640
|
+
<a href="${start}">Open ${title}</a>
|
|
641
|
+
</body>
|
|
642
|
+
</html>
|
|
643
|
+
`;
|
|
644
|
+
await writeFile(path.join(stagingRoot, 'index.html'), content, 'utf8');
|
|
645
|
+
}
|
|
646
|
+
|
|
614
647
|
async function listOutputFiles(root,{signal}={}){
|
|
615
648
|
const files=[];
|
|
616
649
|
async function visit(directory,relativeRoot=''){
|
|
@@ -652,6 +685,7 @@ function releaseManifest(context,files,pwaArtifacts){
|
|
|
652
685
|
displayName:context.config.displayName,
|
|
653
686
|
version:context.config.version,
|
|
654
687
|
entry:context.config.entry,
|
|
688
|
+
start:packageResourceUrl(appPackagePath(context, context.config.entry)),
|
|
655
689
|
strategy:context.config.strategy,
|
|
656
690
|
shared:[...context.config.shared],
|
|
657
691
|
...(pwaArtifacts?{pwa:{
|
|
@@ -695,6 +729,8 @@ async function replaceDirectory(stagingRoot,outputRoot){
|
|
|
695
729
|
async function packageWithContext(context,options={}){
|
|
696
730
|
const {signal,onEvent,browserPwa=true}=options;
|
|
697
731
|
const pwaEnabled=browserPwa&&context.config.pwa?.enabled===true;
|
|
732
|
+
const appPath=`apps/${context.appId}`;
|
|
733
|
+
const entryPath=appPackagePath(context,context.config.entry);
|
|
698
734
|
const inspected=await inspectContext(context,{signal});
|
|
699
735
|
if(options.dryRun){
|
|
700
736
|
return {
|
|
@@ -724,7 +760,14 @@ async function packageWithContext(context,options={}){
|
|
|
724
760
|
let promoted=false;
|
|
725
761
|
try{
|
|
726
762
|
const records=await collectPackageRecords(context,{signal});
|
|
727
|
-
|
|
763
|
+
async function copyBase() {
|
|
764
|
+
await copyRecords(records,stagingRoot,{signal,onEvent});
|
|
765
|
+
if (!records.some(function selectedRootIndex(record) {
|
|
766
|
+
return record.destination === 'index.html';
|
|
767
|
+
})) {
|
|
768
|
+
await writePackageLauncher(context, stagingRoot);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
728
771
|
const adapter=await loadAdapter(context);
|
|
729
772
|
if(adapter){
|
|
730
773
|
await adapter.buildArcanePackage({
|
|
@@ -745,21 +788,22 @@ async function packageWithContext(context,options={}){
|
|
|
745
788
|
const assetVersion=await readWorkspaceAssetVersion(context.workspaceRoot);
|
|
746
789
|
const version=pwaEnabled?null:assetVersion;
|
|
747
790
|
const inventory=new Set(files);
|
|
748
|
-
const offlineInventory=pwaEnabled?new Set(selectPwaFiles(files,context.config.pwa)):null;
|
|
791
|
+
const offlineInventory=pwaEnabled?new Set(selectPwaFiles(files,context.config.pwa,appPath)):null;
|
|
749
792
|
const offlineReferences=new Set();
|
|
750
|
-
const entryUrl=new URL(
|
|
793
|
+
const entryUrl=new URL(packageResourceUrl(entryPath),'http://arcane.invalid/');
|
|
751
794
|
const pwaDocumentSources=new Map();
|
|
752
795
|
if(pwaEnabled){
|
|
753
|
-
const documentPaths=new Set([
|
|
796
|
+
const documentPaths=new Set([entryPath]);
|
|
754
797
|
for(const selected of context.config.include){
|
|
755
|
-
|
|
798
|
+
const selectedPath=appPackagePath(context,selected);
|
|
799
|
+
if(/\.html?$/iu.test(selected)&&inventory.has(selectedPath))documentPaths.add(selectedPath);
|
|
756
800
|
}
|
|
757
801
|
for(const document of inspected.browserDocuments){
|
|
758
802
|
const appDocument=context.config.include.some(function includesAppDocument(selected){
|
|
759
803
|
return sameOrDescendant(document.path,selected);
|
|
760
804
|
});
|
|
761
|
-
if(appDocument&&document.managedMaps.length>0&&inventory.has(document.
|
|
762
|
-
documentPaths.add(document.
|
|
805
|
+
if(appDocument&&document.managedMaps.length>0&&inventory.has(document.packagePath)){
|
|
806
|
+
documentPaths.add(document.packagePath);
|
|
763
807
|
}
|
|
764
808
|
}
|
|
765
809
|
await Promise.all(
|
|
@@ -777,13 +821,13 @@ async function packageWithContext(context,options={}){
|
|
|
777
821
|
)
|
|
778
822
|
);
|
|
779
823
|
}
|
|
780
|
-
const entryDocument=pwaEnabled?pwaDocumentSources.get(
|
|
824
|
+
const entryDocument=pwaEnabled?pwaDocumentSources.get(entryPath).inspected
|
|
781
825
|
:inspected.browserDocuments.find(function matchingEntryDocument(document){
|
|
782
826
|
return document.path===context.config.entry;
|
|
783
827
|
});
|
|
784
828
|
const documentUrl=entryDocument?.bases[0]?.href
|
|
785
829
|
?new URL(entryDocument.bases[0].href,entryUrl):entryUrl;
|
|
786
|
-
const pending=[{file:
|
|
830
|
+
const pending=[{file:entryPath,documentUrl}];
|
|
787
831
|
for(const file of files){
|
|
788
832
|
if(/^arcane\/(?:modules|entities|components|css|sdk|dependencies)\//u.test(file)
|
|
789
833
|
&&/\.(?:m?js|html?|css)$/iu.test(file))pending.push({file,documentUrl});
|
|
@@ -792,15 +836,15 @@ async function packageWithContext(context,options={}){
|
|
|
792
836
|
}
|
|
793
837
|
}
|
|
794
838
|
for(const document of inspected.browserDocuments){
|
|
795
|
-
if(document.managedMaps.length>0&&!pwaDocumentSources.has(document.
|
|
796
|
-
const url=new URL(document.
|
|
797
|
-
pending.push({file:document.
|
|
839
|
+
if(document.managedMaps.length>0&&!pwaDocumentSources.has(document.packagePath)){
|
|
840
|
+
const url=new URL(packageResourceUrl(document.packagePath),entryUrl.origin);
|
|
841
|
+
pending.push({file:document.packagePath,documentUrl:document.bases[0]?.href
|
|
798
842
|
?new URL(document.bases[0].href,url):url});
|
|
799
843
|
}
|
|
800
844
|
}
|
|
801
845
|
for(const [file,document] of pwaDocumentSources){
|
|
802
|
-
if(file===
|
|
803
|
-
const url=new URL(file,entryUrl.origin);
|
|
846
|
+
if(file===entryPath)continue;
|
|
847
|
+
const url=new URL(packageResourceUrl(file),entryUrl.origin);
|
|
804
848
|
pending.push({file,documentUrl:document.inspected.bases[0]?.href
|
|
805
849
|
?new URL(document.inspected.bases[0].href,url):url});
|
|
806
850
|
}
|
|
@@ -832,7 +876,7 @@ async function packageWithContext(context,options={}){
|
|
|
832
876
|
if(!pwaEnabled&&!traversable)continue;
|
|
833
877
|
if(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url))continue;
|
|
834
878
|
try{
|
|
835
|
-
const ownerUrl=new URL(relative,entryUrl.origin);
|
|
879
|
+
const ownerUrl=new URL(packageResourceUrl(relative),entryUrl.origin);
|
|
836
880
|
const base=baseHref?new URL(baseHref,ownerUrl)
|
|
837
881
|
:baseKind==='document'||path.posix.basename(relative)==='arcane.importmap.json'
|
|
838
882
|
?current.documentUrl:ownerUrl;
|
|
@@ -854,7 +898,7 @@ async function packageWithContext(context,options={}){
|
|
|
854
898
|
if(files.some(file=>pathKey(file)===pathKey(RELEASE_MANIFEST_NAME))){
|
|
855
899
|
fail(`Package content must not author ${RELEASE_MANIFEST_NAME}.`);
|
|
856
900
|
}
|
|
857
|
-
if(!files.some(file=>pathKey(file)===pathKey(
|
|
901
|
+
if(!files.some(file=>pathKey(file)===pathKey(entryPath))){
|
|
858
902
|
fail(`Package output is missing its entry file: ${context.config.entry}.`);
|
|
859
903
|
}
|
|
860
904
|
const pwaArtifacts=pwaEnabled?createPwaArtifacts({
|
|
@@ -862,8 +906,9 @@ async function packageWithContext(context,options={}){
|
|
|
862
906
|
id:context.appId,
|
|
863
907
|
displayName:context.config.displayName,
|
|
864
908
|
version:context.config.version,
|
|
865
|
-
entry:
|
|
909
|
+
entry:packageResourceUrl(entryPath)
|
|
866
910
|
},
|
|
911
|
+
appPath,
|
|
867
912
|
sdkVersion:assetVersion,
|
|
868
913
|
pwa:context.config.pwa,
|
|
869
914
|
files,
|
|
@@ -878,7 +923,7 @@ async function packageWithContext(context,options={}){
|
|
|
878
923
|
files.push(artifact.path);
|
|
879
924
|
}
|
|
880
925
|
for(const [documentPath,document] of pwaDocumentSources){
|
|
881
|
-
const documentUrl=new URL(documentPath,entryUrl.origin);
|
|
926
|
+
const documentUrl=new URL(packageResourceUrl(documentPath),entryUrl.origin);
|
|
882
927
|
const outputBase=document.inspected.bases[0]?.href
|
|
883
928
|
?new URL(document.inspected.bases[0].href,documentUrl):documentUrl;
|
|
884
929
|
const outputDirectory=new URL('./',outputBase).pathname;
|
package/src/pwa.mjs
CHANGED
|
@@ -72,17 +72,20 @@ function selectedPath(file, selection) {
|
|
|
72
72
|
return file === selection || file.startsWith(prefix);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export function selectPwaFiles(files, pwa) {
|
|
75
|
+
export function selectPwaFiles(files, pwa, appPath = '') {
|
|
76
76
|
const config = normalizePwaConfig(pwa);
|
|
77
77
|
const include = config?.offline.include ?? [];
|
|
78
78
|
const exclude = config?.offline.exclude ?? [];
|
|
79
|
+
const appPrefix = appPath ? `${appPath}/` : '';
|
|
79
80
|
return files.filter(
|
|
80
81
|
function selectedOfflineFile(file) {
|
|
82
|
+
const relative = appPrefix && file.startsWith(appPrefix)
|
|
83
|
+
? file.slice(appPrefix.length) : file;
|
|
81
84
|
function includedPath(selection) {
|
|
82
|
-
return selectedPath(
|
|
85
|
+
return selectedPath(relative, selection);
|
|
83
86
|
}
|
|
84
87
|
function excludedPath(selection) {
|
|
85
|
-
return selectedPath(
|
|
88
|
+
return selectedPath(relative, selection);
|
|
86
89
|
}
|
|
87
90
|
return (include.length === 0 || include.some(includedPath))
|
|
88
91
|
&& !exclude.some(excludedPath);
|
|
@@ -199,6 +202,7 @@ export function createPwaArtifacts(
|
|
|
199
202
|
mode = 'release',
|
|
200
203
|
runtimeBase = './arcane/sdk/',
|
|
201
204
|
appBase,
|
|
205
|
+
appPath = '',
|
|
202
206
|
navigationAliases,
|
|
203
207
|
revision
|
|
204
208
|
} = {}
|
|
@@ -209,20 +213,19 @@ export function createPwaArtifacts(
|
|
|
209
213
|
throw new TypeError('PWA mode must be release or development.');
|
|
210
214
|
}
|
|
211
215
|
const entryUrl = new URL(app.entry, 'https://arcane.invalid/');
|
|
212
|
-
const applicationBase = appBase ?? (
|
|
213
|
-
? new URL('./', entryUrl).pathname : './');
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
);
|
|
216
|
+
const applicationBase = appBase ?? (appPath ? `./${appPath}/`
|
|
217
|
+
: mode === 'development' ? new URL('./', entryUrl).pathname : './');
|
|
218
|
+
// Relocating app files must not change an existing installed app's default identity.
|
|
219
|
+
const installationBase = appBase ?? (mode === 'development' ? applicationBase : './');
|
|
220
|
+
const manifest = {
|
|
221
|
+
id: installationBase,
|
|
222
|
+
name: app.displayName,
|
|
223
|
+
short_name: app.displayName,
|
|
224
|
+
start_url: manifestUrl(app.entry, appPath ? basePath : applicationBase),
|
|
225
|
+
scope: installationBase,
|
|
226
|
+
display: 'standalone',
|
|
227
|
+
...applicationManifest(config.manifest, applicationBase)
|
|
228
|
+
};
|
|
226
229
|
const generatedAssets = [
|
|
227
230
|
PWA_MANIFEST_NAME,
|
|
228
231
|
PWA_OFFLINE_MANIFEST_NAME,
|
|
@@ -232,7 +235,7 @@ export function createPwaArtifacts(
|
|
|
232
235
|
return resourceUrl(basePath, file);
|
|
233
236
|
}
|
|
234
237
|
);
|
|
235
|
-
const selectedFiles = selectPwaFiles(files, config);
|
|
238
|
+
const selectedFiles = selectPwaFiles(files, config, appPath);
|
|
236
239
|
const selectedAssets = [
|
|
237
240
|
...selectedFiles.map(
|
|
238
241
|
function packagedResource(file) {
|