arcane-os 0.5.17 → 0.5.18

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.
Files changed (183) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +9 -9
  3. package/bin/arcane-test.mjs +16 -13
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +41 -38
  5. package/browser-runtime/ai/browser-speech-providers.mjs +46 -43
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +127 -124
  7. package/browser-runtime/ai/browser-wasm.mjs +6 -3
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +26 -23
  9. package/browser-runtime/ai/model-controller.mjs +94 -91
  10. package/browser-runtime/ai/speech-worker-client.mjs +12 -9
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +56 -53
  12. package/browser-runtime/dependencies/strong-type/index.js +207 -82
  13. package/browser-runtime/dependencies/strong-type/package.json +15 -6
  14. package/browser-runtime/dom-event-instrumentation.mjs +24 -20
  15. package/browser-runtime/event-manager.mjs +111 -108
  16. package/browser-runtime/speech-text.mjs +5 -1
  17. package/docs/reference/ai/browser-speech.md +198 -5
  18. package/docs/reference/availability-and-normalization.md +40 -0
  19. package/docs/reference/inventory/package-api.json +1 -1
  20. package/docs/reference/inventory/runtime-components.json +1 -1
  21. package/docs/reference/inventory/runtime-modules.json +20 -24
  22. package/docs/reference/protocols.md +1 -1
  23. package/docs/reference/runtime-modules.md +125 -35
  24. package/docs/reference/sdk-api.md +23 -7
  25. package/examples/wasm-ai-demo/index.html +1 -0
  26. package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
  27. package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
  28. package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
  29. package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
  30. package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
  31. package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
  32. package/node_modules/strong-type/README.md +42 -24
  33. package/node_modules/strong-type/benchmark/core.js +324 -0
  34. package/node_modules/strong-type/index.js +207 -82
  35. package/node_modules/strong-type/package.json +14 -6
  36. package/package.json +3 -3
  37. package/runtime/arcane/components/app-bar.html +5 -2
  38. package/runtime/arcane/components/assistant-panel.html +5 -2
  39. package/runtime/arcane/components/chart.html +15 -12
  40. package/runtime/arcane/components/chat.html +117 -138
  41. package/runtime/arcane/components/dashboard-config.html +6 -3
  42. package/runtime/arcane/components/data-view.html +4 -1
  43. package/runtime/arcane/components/directory-picker.html +5 -2
  44. package/runtime/arcane/components/document-inspector.html +6 -3
  45. package/runtime/arcane/components/file-drop.html +5 -2
  46. package/runtime/arcane/components/file-inspector.html +9 -6
  47. package/runtime/arcane/components/file-manager.html +16 -13
  48. package/runtime/arcane/components/local-ai-status.html +5 -2
  49. package/runtime/arcane/components/markdown-document.html +14 -11
  50. package/runtime/arcane/components/markdown-editor.html +4 -1
  51. package/runtime/arcane/components/modal.html +4 -1
  52. package/runtime/arcane/components/output-panel.html +6 -3
  53. package/runtime/arcane/components/preferences-form.html +4 -1
  54. package/runtime/arcane/components/record-timeline.html +8 -5
  55. package/runtime/arcane/components/relationship-board.html +9 -6
  56. package/runtime/arcane/components/source-code-viewer.html +12 -9
  57. package/runtime/arcane/components/source-explanation.html +8 -5
  58. package/runtime/arcane/components/speech.html +24 -21
  59. package/runtime/arcane/components/summary-strip.html +4 -1
  60. package/runtime/arcane/components/task-progress.html +6 -3
  61. package/runtime/arcane/components/terminal-workspace.html +4 -1
  62. package/runtime/arcane/components/voice-transcription.html +11 -8
  63. package/runtime/arcane/components/web-navigator.html +5 -2
  64. package/runtime/arcane/entities/ApiModelRecord.js +5 -2
  65. package/runtime/arcane/entities/Calculation.js +5 -2
  66. package/runtime/arcane/entities/Chat.js +33 -33
  67. package/runtime/arcane/entities/CommunicationMessage.js +4 -1
  68. package/runtime/arcane/entities/CommunicationThread.js +4 -1
  69. package/runtime/arcane/entities/File.js +1 -1
  70. package/runtime/arcane/entities/Image.js +8 -5
  71. package/runtime/arcane/entities/IntentEnvelope.js +20 -17
  72. package/runtime/arcane/entities/Preference.js +9 -6
  73. package/runtime/arcane/entities/Theme.js +5 -2
  74. package/runtime/arcane/entities/User.js +11 -11
  75. package/runtime/arcane/entities/Weather.js +5 -2
  76. package/runtime/arcane/modules/AI.js +491 -165
  77. package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
  78. package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
  79. package/runtime/arcane/modules/AIProviderRuntime.js +88 -85
  80. package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
  81. package/runtime/arcane/modules/AIRuntimeState.js +29 -26
  82. package/runtime/arcane/modules/AnsiText.js +4 -1
  83. package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
  84. package/runtime/arcane/modules/AppDataScope.js +8 -5
  85. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
  86. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
  87. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
  88. package/runtime/arcane/modules/AsyncBoundary.js +13 -10
  89. package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
  90. package/runtime/arcane/modules/CalculatorEngine.js +5 -2
  91. package/runtime/arcane/modules/ChatRecords.js +18 -15
  92. package/runtime/arcane/modules/CommunicationAppController.js +9 -6
  93. package/runtime/arcane/modules/CommunicationHub.js +9 -6
  94. package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
  95. package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
  96. package/runtime/arcane/modules/ComponentContracts.js +56 -53
  97. package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
  98. package/runtime/arcane/modules/ConversationActionItems.js +15 -12
  99. package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
  100. package/runtime/arcane/modules/ConversationTimebox.js +28 -25
  101. package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
  102. package/runtime/arcane/modules/DBLS.js +7 -4
  103. package/runtime/arcane/modules/DBOPFS.js +7 -7
  104. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
  105. package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
  106. package/runtime/arcane/modules/DirectoryPicker.js +8 -5
  107. package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
  108. package/runtime/arcane/modules/DocumentNavigation.js +7 -4
  109. package/runtime/arcane/modules/Errors.js +28 -25
  110. package/runtime/arcane/modules/HTMLImport.js +7 -4
  111. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
  112. package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
  113. package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
  114. package/runtime/arcane/modules/MD.js +1 -1
  115. package/runtime/arcane/modules/Mail.js +46 -43
  116. package/runtime/arcane/modules/MailOutbox.mjs +48 -45
  117. package/runtime/arcane/modules/MailTransport.mjs +29 -26
  118. package/runtime/arcane/modules/MemoryRecords.js +7 -4
  119. package/runtime/arcane/modules/MessageAdvisory.js +6 -3
  120. package/runtime/arcane/modules/ModelDefinition.js +7 -4
  121. package/runtime/arcane/modules/Ollama.js +6 -3
  122. package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
  123. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
  124. package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
  125. package/runtime/arcane/modules/PreferenceStore.js +20 -17
  126. package/runtime/arcane/modules/PreparedSpeech.js +485 -0
  127. package/runtime/arcane/modules/Questionnaire.js +6 -3
  128. package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
  129. package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
  130. package/runtime/arcane/modules/RecordReviewStore.js +12 -9
  131. package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
  132. package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
  133. package/runtime/arcane/modules/ScreenCapture.js +20 -17
  134. package/runtime/arcane/modules/SpeechPlayback.js +29 -26
  135. package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
  136. package/runtime/arcane/modules/SystemAppearance.js +5 -2
  137. package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
  138. package/runtime/arcane/modules/TerminalClient.js +14 -11
  139. package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
  140. package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
  141. package/runtime/arcane/modules/ThemeManager.js +5 -2
  142. package/runtime/arcane/modules/TimeGuard.js +1 -1
  143. package/runtime/arcane/modules/ToolCallRouter.js +11 -8
  144. package/runtime/arcane/modules/WaitForComponent.js +19 -16
  145. package/runtime/arcane/modules/YouTubeMedia.js +4 -1
  146. package/runtime/strong-type/index.js +1276 -352
  147. package/runtime/strong-type/package.json +69 -45
  148. package/src/app-descriptor.mjs +17 -14
  149. package/src/application-tests.mjs +4 -1
  150. package/src/cli/main.mjs +8 -5
  151. package/src/constants.mjs +4 -1
  152. package/src/dev-server.mjs +8 -5
  153. package/src/doctor.mjs +5 -2
  154. package/src/dom-event-instrumentation.mjs +24 -20
  155. package/src/errors.mjs +7 -3
  156. package/src/event-manager.mjs +111 -108
  157. package/src/event-queue.mjs +8 -5
  158. package/src/events.mjs +12 -9
  159. package/src/import-map.mjs +35 -27
  160. package/src/installed-sdk-runtime.mjs +4 -1
  161. package/src/integrated-provider-loader.mjs +9 -6
  162. package/src/mail-credentials.mjs +16 -13
  163. package/src/mail-server.mjs +53 -50
  164. package/src/mail.mjs +18 -15
  165. package/src/native-plan.mjs +12 -9
  166. package/src/native-provider-loader.mjs +7 -4
  167. package/src/packager/core.mjs +18 -15
  168. package/src/process.mjs +6 -3
  169. package/src/release-bundle.mjs +14 -11
  170. package/src/runtime.mjs +4 -1
  171. package/src/scaffold.mjs +9 -6
  172. package/src/sdk-browser-runtime.mjs +4 -1
  173. package/src/source-server.mjs +17 -14
  174. package/src/targets/index.mjs +5 -2
  175. package/src/templates/workspace-template.mjs +13 -6
  176. package/src/testing-loader.mjs +7 -4
  177. package/src/testing.mjs +10 -7
  178. package/src/toolchain.mjs +13 -10
  179. package/src/update-check.mjs +9 -6
  180. package/src/workspace-operation-lock.mjs +14 -11
  181. package/src/workspace-runtime.mjs +4 -1
  182. package/src/workspace.mjs +17 -14
  183. package/runtime/arcane/modules/AIResponseLength.js +0 -32
@@ -1,3 +1,4 @@
1
+ import Is from 'strong-type';
1
2
  import {
2
3
  copyFile,
3
4
  lstat,
@@ -14,6 +15,8 @@ import {pathToFileURL} from 'node:url';
14
15
  import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
15
16
  import {inspectImportMapHtml,readWorkspaceAssetVersion,rewriteAssetReferences} from '../import-map.mjs';
16
17
 
18
+ const is = new Is(false);
19
+
17
20
  export const ROOT_CONFIG_NAME='arcane-packager.json';
18
21
  export const APP_CONFIG_NAME='arcane-package.json';
19
22
  export const RELEASE_MANIFEST_NAME='ARCANE_APP_RELEASE.json';
@@ -42,7 +45,7 @@ function throwIfAborted(signal){
42
45
  }
43
46
 
44
47
  async function emit(onEvent,event){
45
- if(typeof onEvent==='function')await onEvent(event);
48
+ if(is.function(onEvent))await onEvent(event);
46
49
  }
47
50
 
48
51
  function compareText(left,right){
@@ -52,7 +55,7 @@ function compareText(left,right){
52
55
  }
53
56
 
54
57
  function isPlainObject(value){
55
- return value!==null&&typeof value==='object'&&!Array.isArray(value);
58
+ return value!==null&&is.object(value)&&!is.array(value);
56
59
  }
57
60
 
58
61
  function copyJson(value){
@@ -67,14 +70,14 @@ function assertOnlyKeys(value,allowed,label){
67
70
  }
68
71
 
69
72
  function normalizeWorkspaceRoot(workspaceRoot){
70
- if(typeof workspaceRoot!=='string'||!workspaceRoot.trim()){
73
+ if(!is.string(workspaceRoot)||!workspaceRoot.trim()){
71
74
  fail('workspaceRoot must be a directory path.');
72
75
  }
73
76
  return path.resolve(workspaceRoot);
74
77
  }
75
78
 
76
79
  export function normalizeRelativePath(value,label='path'){
77
- if(typeof value!=='string'||!value||value.includes('\\')||TEXT_CONTROL_PATTERN.test(value)){
80
+ if(!is.string(value)||!value||value.includes('\\')||TEXT_CONTROL_PATTERN.test(value)){
78
81
  fail(`Unsafe ${label}: ${String(value)}`);
79
82
  }
80
83
  if(path.posix.isAbsolute(value)||/^[a-z]:/iu.test(value))fail(`Unsafe ${label}: ${value}`);
@@ -119,7 +122,7 @@ function isGlobLike(value){
119
122
  }
120
123
 
121
124
  function validatePathList(value,label,{required=false}={}){
122
- if(!Array.isArray(value)||(required&&value.length===0)){
125
+ if(!is.array(value)||(required&&value.length===0)){
123
126
  fail(`${label} must be ${required?'a non-empty':'an'} array of literal relative paths.`);
124
127
  }
125
128
  const normalized=value.map((entry,index)=>{
@@ -160,14 +163,14 @@ function isExcluded(relative,excludes){
160
163
  }
161
164
 
162
165
  function assertPresentationText(value,label){
163
- if(typeof value!=='string'||!value.trim()){
166
+ if(!is.string(value)||!value.trim()){
164
167
  fail(`${label} must be nonempty text.`);
165
168
  }
166
169
  return value;
167
170
  }
168
171
 
169
172
  export function parseSemver(value){
170
- if(typeof value!=='string')fail(`Invalid semantic version: ${String(value)}`);
173
+ if(!is.string(value))fail(`Invalid semantic version: ${String(value)}`);
171
174
  const match=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u.exec(value);
172
175
  if(!match)fail(`Invalid semantic version: ${value}`);
173
176
  const prerelease=match[4]?match[4].split('.'):[];
@@ -177,7 +180,7 @@ export function parseSemver(value){
177
180
  }
178
181
  }
179
182
  const numbers=match.slice(1,4).map(Number);
180
- if(numbers.some(number=>!Number.isSafeInteger(number))){
183
+ if(numbers.some(number=>!is.safeInteger(number))){
181
184
  fail(`Semantic version component exceeds JavaScript's safe integer range: ${value}`);
182
185
  }
183
186
  return {
@@ -204,7 +207,7 @@ export function incrementSemver(value,bump,preid='rc'){
204
207
  if(bump==='major')return formatSemver({major:current.major+1,minor:0,patch:0});
205
208
  if(bump==='minor')return formatSemver({major:current.major,minor:current.minor+1,patch:0});
206
209
  if(bump==='patch')return formatSemver({major:current.major,minor:current.minor,patch:current.patch+1});
207
- if(typeof preid!=='string'||!/^[0-9A-Za-z-]+$/u.test(preid)){
210
+ if(!is.string(preid)||!/^[0-9A-Za-z-]+$/u.test(preid)){
208
211
  fail(`Invalid prerelease identifier: ${String(preid)}`);
209
212
  }
210
213
  const next={major:current.major,minor:current.minor,patch:current.patch,prerelease:[]};
@@ -264,7 +267,7 @@ export function validateRootConfig(value,configPath=ROOT_CONFIG_NAME){
264
267
  const sharedPayloads={};
265
268
  for(const [id,routes] of Object.entries(value.sharedPayloads).sort(([left],[right])=>compareText(left,right))){
266
269
  if(!SAFE_SHARED_ID_PATTERN.test(id))fail(`Unsafe shared payload id: ${id}`);
267
- if(!Array.isArray(routes)||routes.length===0){
270
+ if(!is.array(routes)||routes.length===0){
268
271
  fail(`sharedPayloads.${id} must be a non-empty array.`);
269
272
  }
270
273
  sharedPayloads[id]=routes.map((route,index)=>
@@ -304,11 +307,11 @@ export function validateAppConfig(value,appId,rootConfig,configPath=`apps/${appI
304
307
  if(!['static','adapter'].includes(value.strategy)){
305
308
  fail(`${appId}/${APP_CONFIG_NAME}.strategy must be "static" or "adapter".`);
306
309
  }
307
- if(!Array.isArray(value.shared)||new Set(value.shared).size!==value.shared.length){
310
+ if(!is.array(value.shared)||new Set(value.shared).size!==value.shared.length){
308
311
  fail(`${appId}/${APP_CONFIG_NAME}.shared must be an array of unique shared payload ids.`);
309
312
  }
310
313
  for(const [index,id] of value.shared.entries()){
311
- if(typeof id!=='string'||!Object.hasOwn(rootConfig.sharedPayloads,id)){
314
+ if(!is.string(id)||!Object.hasOwn(rootConfig.sharedPayloads,id)){
312
315
  fail(`${appId}/${APP_CONFIG_NAME}.shared[${index}] references an unknown shared payload.`);
313
316
  }
314
317
  }
@@ -383,7 +386,7 @@ async function loadContext(requestedWorkspaceRoot,appId){
383
386
  const workspaceRoot=await realDirectory(normalizeWorkspaceRoot(requestedWorkspaceRoot),'Workspace root');
384
387
  const rootConfigPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
385
388
  const rootConfig=validateRootConfig(await readJson(rootConfigPath,ROOT_CONFIG_NAME),rootConfigPath);
386
- if(typeof appId!=='string'||!APP_ID_PATTERN.test(appId))fail(`Unsafe app id: ${String(appId)}`);
389
+ if(!is.string(appId)||!APP_ID_PATTERN.test(appId))fail(`Unsafe app id: ${String(appId)}`);
387
390
  const appsRoot=await realDirectory(path.join(workspaceRoot,rootConfig.appsRoot),'Apps root');
388
391
  const appRoot=resolveInside(appsRoot,appId,'app id');
389
392
  await assertContainedRealPath(appsRoot,appRoot,`apps/${appId}`);
@@ -616,7 +619,7 @@ async function loadAdapter(context){
616
619
  const adapterPath=resolveInside(context.appRoot,context.config.adapter,`${context.appId} adapter`);
617
620
  await assertContainedRealPath(context.appRoot,adapterPath,`${context.appId} adapter`);
618
621
  const module=await import(`${pathToFileURL(adapterPath).href}?source=${Date.now()}`);
619
- if(typeof module.buildArcanePackage!=='function'){
622
+ if(!is.function(module.buildArcanePackage)){
620
623
  fail(`${context.appId} adapter must export buildArcanePackage.`);
621
624
  }
622
625
  return module;
@@ -816,7 +819,7 @@ export async function verifyApp({workspaceRoot,appId,signal,onEvent}={}){
816
819
  const manifest=await readJson(path.join(outputRoot,RELEASE_MANIFEST_NAME),RELEASE_MANIFEST_NAME);
817
820
  if(!isPlainObject(manifest)||manifest.schemaVersion!==1||manifest.kind!=='arcane-app-release'
818
821
  ||manifest.packagerVersion!==PACKAGER_VERSION||manifest.app?.id!==appId
819
- ||manifest.app?.version!==context.config.version||!Array.isArray(manifest.files)){
822
+ ||manifest.app?.version!==context.config.version||!is.array(manifest.files)){
820
823
  fail(`${RELEASE_MANIFEST_NAME} is malformed.`);
821
824
  }
822
825
  const expected=manifest.files.map((file,index)=>normalizeRelativePath(
package/src/process.mjs CHANGED
@@ -1,8 +1,11 @@
1
+ import Is from 'strong-type';
1
2
  import {spawn} from 'node:child_process';
2
3
  import path from 'node:path';
3
4
  import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
4
5
  import {createEventQueue} from './event-queue.mjs';
5
6
 
7
+ const is = new Is(false);
8
+
6
9
  const DEFAULT_TERMINATION_GRACE_MS=1500;
7
10
 
8
11
  export function platformCommand(command,platform=process.platform){
@@ -58,7 +61,7 @@ function childIsRunning(child){
58
61
  }
59
62
 
60
63
  function childHasPid(child){
61
- return Number.isInteger(child?.pid)&&child.pid>0;
64
+ return is.integer(child?.pid)&&child.pid>0;
62
65
  }
63
66
 
64
67
  function terminateWindowsTree(child,{force=false}={}){
@@ -124,10 +127,10 @@ export async function runProcess(command,args=[],{
124
127
  input
125
128
  }={}){
126
129
  throwIfAborted(signal);
127
- if(!Array.isArray(args)||args.some(argument=>typeof argument!=='string')){
130
+ if(!is.array(args)||args.some(argument=>!is.string(argument))){
128
131
  throw new ArcaneError(ERROR_CODES.usage,'Process arguments must be a fixed array of strings.');
129
132
  }
130
- if(!Number.isInteger(terminationGraceMs)||terminationGraceMs<100||terminationGraceMs>30_000){
133
+ if(!is.integer(terminationGraceMs)||terminationGraceMs<100||terminationGraceMs>30_000){
131
134
  throw new ArcaneError(
132
135
  ERROR_CODES.usage,
133
136
  'terminationGraceMs must be an integer from 100 through 30000.'
@@ -1,3 +1,4 @@
1
+ import Is from 'strong-type';
1
2
  import {
2
3
  lstat,
3
4
  mkdir,
@@ -15,6 +16,8 @@ import {SDK_NAME,SDK_VERSION} from './constants.mjs';
15
16
  import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
16
17
  import {RELEASE_MANIFEST_NAME,PACKAGER_VERSION,parseSemver} from './packager/core.mjs';
17
18
 
19
+ const is = new Is(false);
20
+
18
21
  export const APP_BUNDLE_MANIFEST_NAME='ARCANE_APP_BUNDLE.json';
19
22
  export const APP_BUNDLE_DESCRIPTOR_NAME='arcane-app.json';
20
23
  export const APP_BUNDLE_RELEASE_PATH=`payload/${RELEASE_MANIFEST_NAME}`;
@@ -41,7 +44,7 @@ function fail(message,code='ARCANE_BUNDLE_INVALID',details){
41
44
  }
42
45
 
43
46
  async function emit(onEvent,event){
44
- if(typeof onEvent==='function')await onEvent(event);
47
+ if(is.function(onEvent))await onEvent(event);
45
48
  }
46
49
 
47
50
  function compareText(left,right){
@@ -51,7 +54,7 @@ function compareText(left,right){
51
54
  }
52
55
 
53
56
  function isPlainObject(value){
54
- return value!==null&&typeof value==='object'&&!Array.isArray(value);
57
+ return value!==null&&is.object(value)&&!is.array(value);
55
58
  }
56
59
 
57
60
  function copyJson(value){
@@ -63,7 +66,7 @@ function pathKey(value){
63
66
  }
64
67
 
65
68
  export function validateAppBundlePath(value,label='bundle path'){
66
- if(typeof value!=='string'||!value||value!==value.normalize('NFC')
69
+ if(!is.string(value)||!value||value!==value.normalize('NFC')
67
70
  ||value.includes('\\')||value.startsWith('/')||/^[a-z]:/iu.test(value)
68
71
  ||/[\u0000-\u001f\u007f]/u.test(value)){
69
72
  fail(`Unsafe ${label}: ${String(value)}.`);
@@ -100,7 +103,7 @@ function writeTextField(header,offset,length,value,label){
100
103
  }
101
104
 
102
105
  function writeOctalField(header,offset,length,value,label,{trailingSpace=false}={}){
103
- if(!Number.isSafeInteger(value)||value<0)fail(`${label} must be a nonnegative integer.`);
106
+ if(!is.safeInteger(value)||value<0)fail(`${label} must be a nonnegative integer.`);
104
107
  const terminal=trailingSpace?'\0 ':'\0';
105
108
  const digits=value.toString(8);
106
109
  const available=length-terminal.length;
@@ -184,8 +187,8 @@ function readJsonContent(content,label){
184
187
  function validateReleaseManifest(value){
185
188
  if(!isPlainObject(value)||value.schemaVersion!==1||value.kind!=='arcane-app-release'
186
189
  ||value.packagerVersion!==PACKAGER_VERSION||!isPlainObject(value.app)
187
- ||typeof value.app.id!=='string'||typeof value.app.version!=='string'
188
- ||!Array.isArray(value.files)){
190
+ ||!is.string(value.app.id)||!is.string(value.app.version)
191
+ ||!is.array(value.files)){
189
192
  fail(`${RELEASE_MANIFEST_NAME} is malformed.`);
190
193
  }
191
194
  parseSemver(value.app.version);
@@ -213,8 +216,8 @@ function bundleManifest(descriptor,release,files){
213
216
  }
214
217
 
215
218
  async function outputBoundary(outputPath,overwrite){
216
- if(typeof outputPath!=='string'||!outputPath.trim())fail('outputPath is required.');
217
- if(typeof overwrite!=='boolean')fail('overwrite must be a boolean.',ERROR_CODES.usage);
219
+ if(!is.string(outputPath)||!outputPath.trim())fail('outputPath is required.');
220
+ if(!is.boolean(overwrite))fail('overwrite must be a boolean.',ERROR_CODES.usage);
218
221
  const resolved=path.resolve(outputPath);
219
222
  await mkdir(path.dirname(resolved),{recursive:true});
220
223
  const parent=await realDirectory(path.dirname(resolved),'Bundle output directory');
@@ -301,7 +304,7 @@ function readOctalField(header,offset,length,label){
301
304
  const text=header.subarray(offset,offset+length).toString('ascii').replace(/[\0 ]+$/u,'');
302
305
  if(!/^[0-7]+$/u.test(text))fail(`${label} is not a valid ustar octal field.`);
303
306
  const value=Number.parseInt(text,8);
304
- if(!Number.isSafeInteger(value))fail(`${label} exceeds the supported integer range.`);
307
+ if(!is.safeInteger(value))fail(`${label} exceeds the supported integer range.`);
305
308
  return value;
306
309
  }
307
310
 
@@ -344,7 +347,7 @@ function requiredEntry(entries,entryPath){
344
347
 
345
348
  export async function verifyAppReleaseBundle({bundlePath,signal,onEvent}={}){
346
349
  throwIfAborted(signal);
347
- if(typeof bundlePath!=='string'||!bundlePath.trim())fail('bundlePath is required.');
350
+ if(!is.string(bundlePath)||!bundlePath.trim())fail('bundlePath is required.');
348
351
  const selected=path.resolve(bundlePath);
349
352
  const info=await lstat(selected);
350
353
  if(info.isSymbolicLink()||!info.isFile())fail('Bundle path must be a real file.');
@@ -360,7 +363,7 @@ export async function verifyAppReleaseBundle({bundlePath,signal,onEvent}={}){
360
363
  ||manifest.kind!==APP_BUNDLE_KIND||manifest.format!==APP_BUNDLE_FORMAT
361
364
  ||manifest.sdk?.name!==SDK_NAME||manifest.sdk?.version!==SDK_VERSION
362
365
  ||manifest.descriptor!==APP_BUNDLE_DESCRIPTOR_NAME
363
- ||manifest.release!==APP_BUNDLE_RELEASE_PATH||!Array.isArray(manifest.files)){
366
+ ||manifest.release!==APP_BUNDLE_RELEASE_PATH||!is.array(manifest.files)){
364
367
  fail(`${APP_BUNDLE_MANIFEST_NAME} is malformed.`);
365
368
  }
366
369
  const descriptor=copyJson(validateAppDescriptor(readJsonContent(
package/src/runtime.mjs CHANGED
@@ -1,8 +1,11 @@
1
+ import Is from 'strong-type';
1
2
  import {lstat,readFile,readdir,realpath} from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import {fileURLToPath} from 'node:url';
4
5
  import {SDK_VERSION} from './constants.mjs';
5
6
 
7
+ const is = new Is(false);
8
+
6
9
  const sdkRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
7
10
 
8
11
  function fail(message,code='ARCANE_RUNTIME_INVALID'){
@@ -25,7 +28,7 @@ function compareText(left,right){
25
28
  }
26
29
 
27
30
  function safeRelativePath(value){
28
- if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
31
+ if(!is.string(value)||!value||value.includes('\\')||value.includes('\0')
29
32
  ||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
30
33
  ||value==='.'||value.startsWith('../')||value.includes('/../')){
31
34
  fail(`SDK runtime contains an unsafe relative path: ${String(value)}.`);
package/src/scaffold.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import Is from 'strong-type';
1
2
  import {lstat,mkdir,readFile,readdir,writeFile} from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import {spawn} from 'node:child_process';
@@ -8,6 +9,8 @@ import {SDK_NAME,SDK_VERSION,workspaceTemplate} from './templates/workspace-temp
8
9
  import {inspectWorkspaceProfile,resolveSdkPackageDeclaration} from './workspace.mjs';
9
10
  import {parseSemver} from './packager/core.mjs';
10
11
 
12
+ const is = new Is(false);
13
+
11
14
  const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
12
15
  const DISPLAY_CONTROL_PATTERN=/[\x00-\x1f\x7f]/;
13
16
 
@@ -25,14 +28,14 @@ function throwIfAborted(signal){
25
28
  }
26
29
 
27
30
  async function emit(onEvent,event){
28
- if(typeof onEvent==='function')await onEvent(event);
31
+ if(is.function(onEvent))await onEvent(event);
29
32
  }
30
33
 
31
34
  function validateInputs(appId,displayName){
32
- if(typeof appId!=='string'||!APP_ID_PATTERN.test(appId)){
35
+ if(!is.string(appId)||!APP_ID_PATTERN.test(appId)){
33
36
  fail(`Invalid app id: ${String(appId)}. Use lowercase words separated by hyphens.`,'ARCANE_USAGE');
34
37
  }
35
- if(displayName!==undefined&&(typeof displayName!=='string'||displayName!==displayName.trim()
38
+ if(displayName!==undefined&&(!is.string(displayName)||displayName!==displayName.trim()
36
39
  ||!displayName||displayName.length>160||DISPLAY_CONTROL_PATTERN.test(displayName)
37
40
  ||/[<>]/.test(displayName))){
38
41
  fail('displayName must be plain trimmed text no longer than 160 characters.','ARCANE_USAGE');
@@ -162,7 +165,7 @@ async function readExistingPackage(workspaceRoot){
162
165
  let existing;
163
166
  try{existing=JSON.parse(source);}
164
167
  catch(error){fail(`Existing package.json is not valid JSON: ${error.message}.`);}
165
- if(!existing||typeof existing!=='object'||Array.isArray(existing))fail('Existing package.json must be a JSON object.');
168
+ if(!existing||!is.object(existing)||is.array(existing))fail('Existing package.json must be a JSON object.');
166
169
  return {exists:true,packagePath,source,document:existing};
167
170
  }
168
171
 
@@ -315,8 +318,8 @@ export async function createWorkspace({
315
318
  }){
316
319
  validateInputs(appId,displayName);
317
320
  validateScaffoldTarget(target);
318
- if(typeof targetPath!=='string'||!targetPath.trim())fail('targetPath is required.','ARCANE_USAGE');
319
- if(typeof initializeGit!=='boolean')fail('initializeGit must be a boolean.','ARCANE_USAGE');
321
+ if(!is.string(targetPath)||!targetPath.trim())fail('targetPath is required.','ARCANE_USAGE');
322
+ if(!is.boolean(initializeGit))fail('initializeGit must be a boolean.','ARCANE_USAGE');
320
323
  throwIfAborted(signal);
321
324
  const workspaceRoot=path.resolve(targetPath);
322
325
  await emit(onEvent,{type:'scaffold.started',mode:'create',workspaceRoot,appId,target});
@@ -1,8 +1,11 @@
1
+ import Is from 'strong-type';
1
2
  import {lstat,readFile,readdir,realpath} from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import {fileURLToPath} from 'node:url';
4
5
  import {SDK_VERSION} from './constants.mjs';
5
6
 
7
+ const is = new Is(false);
8
+
6
9
  const packageRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
7
10
  const defaultRoot=path.join(packageRoot,'browser-runtime');
8
11
 
@@ -26,7 +29,7 @@ function compareText(left,right){
26
29
  }
27
30
 
28
31
  function safeRelativePath(value){
29
- if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
32
+ if(!is.string(value)||!value||value.includes('\\')||value.includes('\0')
30
33
  ||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
31
34
  ||value==='.'||value.startsWith('../')||value.includes('/../')){
32
35
  fail(`SDK browser runtime contains an unsafe relative path: ${String(value)}.`);
@@ -1,3 +1,4 @@
1
+ import Is from 'strong-type';
1
2
  import {createReadStream} from 'node:fs';
2
3
  import {stat} from 'node:fs/promises';
3
4
  import http from 'node:http';
@@ -6,6 +7,8 @@ import path from 'node:path';
6
7
  import {pipeline} from 'node:stream/promises';
7
8
  import {fileURLToPath} from 'node:url';
8
9
 
10
+ const is = new Is(false);
11
+
9
12
  const MIME_TYPES = new Map([
10
13
  ['.css', 'text/css; charset=utf-8'],
11
14
  ['.gif', 'image/gif'],
@@ -61,7 +64,7 @@ function normalizeHost(value) {
61
64
 
62
65
  function normalizePort(value) {
63
66
  const port = Number(value ?? 0);
64
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
67
+ if (!is.integer(port) || port < 0 || port > 65535) {
65
68
  fail('Source server port must be an integer from 0 through 65535.');
66
69
  }
67
70
  return port;
@@ -113,7 +116,7 @@ function normalizeRoot(value, fieldName) {
113
116
  }
114
117
  return path.resolve(fileURLToPath(value));
115
118
  }
116
- if (typeof value !== 'string' || !value.trim()) {
119
+ if (!is.string(value) || !value.trim()) {
117
120
  fail(`${fieldName} must be a filesystem path or file URL.`);
118
121
  }
119
122
  return path.resolve(value);
@@ -123,7 +126,7 @@ function normalizeInclude(value, fieldName) {
123
126
  if (value === undefined) {
124
127
  return null;
125
128
  }
126
- if (!Array.isArray(value)) {
129
+ if (!is.array(value)) {
127
130
  fail(`${fieldName} must be an array when supplied.`);
128
131
  }
129
132
  return value.map(function normalizeEntry(entry, index) {
@@ -132,7 +135,7 @@ function normalizeInclude(value, fieldName) {
132
135
  }
133
136
 
134
137
  function normalizeMount(value, index) {
135
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
138
+ if (!value || !is.object(value) || is.array(value)) {
136
139
  fail(`mounts[${index}] must be an object.`);
137
140
  }
138
141
  if (value.urlPath === undefined) {
@@ -149,7 +152,7 @@ function normalizeMount(value, index) {
149
152
  }
150
153
 
151
154
  function normalizeMounts(value) {
152
- if (!Array.isArray(value) || value.length === 0) {
155
+ if (!is.array(value) || value.length === 0) {
153
156
  fail('Source server mounts must be a nonempty array.');
154
157
  }
155
158
  const mounts = value.map(normalizeMount);
@@ -280,7 +283,7 @@ function parseRange(value, size) {
280
283
  if (value === undefined) {
281
284
  return null;
282
285
  }
283
- if (typeof value !== 'string' || size === 0) {
286
+ if (!is.string(value) || size === 0) {
284
287
  return false;
285
288
  }
286
289
  const match = /^bytes=(\d*)-(\d*)$/u.exec(value.trim());
@@ -291,7 +294,7 @@ function parseRange(value, size) {
291
294
  let end;
292
295
  if (!match[1]) {
293
296
  const suffixLength = Number(match[2]);
294
- if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0) {
297
+ if (!is.safeInteger(suffixLength) || suffixLength <= 0) {
295
298
  return false;
296
299
  }
297
300
  start = Math.max(0, size - suffixLength);
@@ -300,7 +303,7 @@ function parseRange(value, size) {
300
303
  else {
301
304
  start = Number(match[1]);
302
305
  end = match[2] ? Number(match[2]) : size - 1;
303
- if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end)
306
+ if (!is.safeInteger(start) || !is.safeInteger(end)
304
307
  || start < 0 || end < start || start >= size) {
305
308
  return false;
306
309
  }
@@ -344,7 +347,7 @@ async function streamFile(request, response, filePath, information, settings) {
344
347
  }
345
348
 
346
349
  async function publishEvent(onEvent, type, detail) {
347
- if (typeof onEvent !== 'function') {
350
+ if (!is.function(onEvent)) {
348
351
  return;
349
352
  }
350
353
  try {
@@ -419,7 +422,7 @@ function createServer(settings) {
419
422
  if (settings.tls === undefined || settings.tls === null || settings.tls === false) {
420
423
  return http.createServer(listener);
421
424
  }
422
- if (typeof settings.tls !== 'object' || Array.isArray(settings.tls)) {
425
+ if (!is.object(settings.tls) || is.array(settings.tls)) {
423
426
  fail('Source server tls must be a Node HTTPS options object when supplied.');
424
427
  }
425
428
  return https.createServer(settings.tls, listener);
@@ -480,11 +483,11 @@ function closeFunction(server) {
480
483
  }
481
484
 
482
485
  export async function startSourceExampleServer(options = {}) {
483
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
486
+ if (!options || !is.object(options) || is.array(options)) {
484
487
  fail('Source server options must be an object.');
485
488
  }
486
- if (options.signal && (typeof options.signal.addEventListener !== 'function'
487
- || typeof options.signal.removeEventListener !== 'function')) {
489
+ if (options.signal && (!is.function(options.signal.addEventListener)
490
+ || !is.function(options.signal.removeEventListener))) {
488
491
  fail('Source server signal must be an AbortSignal when supplied.');
489
492
  }
490
493
  throwIfAborted(options.signal);
@@ -544,7 +547,7 @@ export async function startSourceExampleServer(options = {}) {
544
547
  serverErrorListener = await listen(server, settings.host, settings.port,
545
548
  reportOperationalError);
546
549
  const address = server.address();
547
- if (!address || typeof address === 'string') {
550
+ if (!address || is.string(address)) {
548
551
  await new Promise(function closeUnknownAddress(resolve) {
549
552
  server.close(resolve);
550
553
  });
@@ -1,3 +1,4 @@
1
+ import Is from 'strong-type';
1
2
  import path from 'node:path';
2
3
  import {packageApp,verifyApp} from '../packager/core.mjs';
3
4
  import {startDevServer} from '../dev-server.mjs';
@@ -9,6 +10,8 @@ import {
9
10
  validateNativeBuilder
10
11
  } from '../native-plan.mjs';
11
12
 
13
+ const is = new Is(false);
14
+
12
15
  const DEFINITIONS=[
13
16
  {
14
17
  id:'browser',displayName:'Browser/static',status:'available',
@@ -188,7 +191,7 @@ function selectedNativeRequest(targetId,targetRequest){
188
191
  }
189
192
 
190
193
  function selectedArtifact(artifact){
191
- if(!artifact||typeof artifact!=='object'||Array.isArray(artifact)){
194
+ if(!artifact||!is.object(artifact)||is.array(artifact)){
192
195
  throw new ArcaneError(ERROR_CODES.usage,'A native artifact is required.');
193
196
  }
194
197
  return artifact;
@@ -202,7 +205,7 @@ export function createNativeTargetAdapter({targetId,nativeBuilder}={}){
202
205
  async function requireProviderTarget(){
203
206
  providerDescriptionPromise??=Promise.resolve(provider.describe()).then(description=>{
204
207
  if(!description||description.protocol!=='arcane-native-builder/1'
205
- ||!Array.isArray(description.targets)||!description.targets.includes(targetId)){
208
+ ||!is.array(description.targets)||!description.targets.includes(targetId)){
206
209
  throw new ArcaneError(
207
210
  ERROR_CODES.targetUnavailable,
208
211
  `The selected native provider does not support target ${targetId}.`
@@ -1,3 +1,4 @@
1
+ import Is from 'strong-type';
1
2
  import {
2
3
  ARCANE_PROTOCOL,
3
4
  CLI_EVENT_PROTOCOL,
@@ -6,6 +7,8 @@ import {
6
7
  TARGET_ADAPTER_PROTOCOL
7
8
  } from '../constants.mjs';
8
9
 
10
+ const is = new Is(false);
11
+
9
12
  const NPM_PACKAGE_NAME_PATTERN=/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
10
13
  const LOCAL_TARBALL_PATTERN=/^file:.+\.tgz$/iu;
11
14
 
@@ -34,9 +37,9 @@ export function createWorkspaceLockDocument({
34
37
  packageVersion=SDK_VERSION,
35
38
  packageSource=`node_modules/${dependencyName}`
36
39
  }={}){
37
- if(packageName!==SDK_NAME||typeof packageVersion!=='string'
40
+ if(packageName!==SDK_NAME||!is.string(packageVersion)
38
41
  ||!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u.test(packageVersion)
39
- ||typeof dependencyName!=='string'||dependencyName.length>214
42
+ ||!is.string(dependencyName)||dependencyName.length>214
40
43
  ||!NPM_PACKAGE_NAME_PATTERN.test(dependencyName)
41
44
  ||packageSource!==`node_modules/${dependencyName}`){
42
45
  throw new Error('Invalid workspace SDK lock authority.');
@@ -68,12 +71,12 @@ export function workspaceTemplate({
68
71
  }){
69
72
  const canonicalSpecifier=sdkDependencyName===SDK_NAME
70
73
  &&(sdkDependencySpecifier===SDK_VERSION
71
- ||(typeof sdkDependencySpecifier==='string'
74
+ ||(is.string(sdkDependencySpecifier)
72
75
  &&LOCAL_TARBALL_PATTERN.test(sdkDependencySpecifier)
73
76
  &&!/[\x00-\x1f\x7f]/.test(sdkDependencySpecifier)));
74
77
  const aliasSpecifier=sdkDependencyName!==SDK_NAME
75
78
  &&sdkDependencySpecifier===`npm:${SDK_NAME}@${SDK_VERSION}`;
76
- if(typeof sdkDependencyName!=='string'||sdkDependencyName.length>214
79
+ if(!is.string(sdkDependencyName)||sdkDependencyName.length>214
77
80
  ||!NPM_PACKAGE_NAME_PATTERN.test(sdkDependencyName)
78
81
  ||sdkPackageSource!==`node_modules/${sdkDependencyName}`
79
82
  ||(!canonicalSpecifier&&!aliasSpecifier)){
@@ -359,12 +362,16 @@ ${bootstrapMarkup}</head>
359
362
  ?'arcane/ThemeBootstrap':'../../../arcane/modules/ThemeBootstrap.js';
360
363
  const appDataSpecifier=namedImports
361
364
  ?'arcane/AppDataScope':'../../../arcane/modules/AppDataScope.js';
362
- files.set(`apps/${appId}/modules/App.js`,`import arcaneThemeReady from '${themeSpecifier}';
365
+ const strongTypeSpecifier=namedImports
366
+ ?'strong-type':'../../../arcane/dependencies/strong-type/index.js';
367
+ files.set(`apps/${appId}/modules/App.js`,`import Is from '${strongTypeSpecifier}';
368
+ import arcaneThemeReady from '${themeSpecifier}';
363
369
  import {
364
370
  resolveApplicationId,
365
371
  resolveApplicationLocalStorageKey
366
372
  } from '${appDataSpecifier}';
367
373
 
374
+ const is = new Is(false);
368
375
  const appName=${JSON.stringify(name)};
369
376
  const action=document.querySelector('#app-action');
370
377
  const status=document.querySelector('#app-status');
@@ -377,7 +384,7 @@ const countKey=resolveApplicationLocalStorageKey('hello-count',{applicationId:ap
377
384
  function loadHelloCount(){
378
385
  try{
379
386
  const value=Number(globalThis.localStorage?.getItem(countKey)??0);
380
- return Number.isSafeInteger(value)&&value>=0?value:0;
387
+ return is.safeInteger(value)&&value>=0?value:0;
381
388
  }catch{
382
389
  return 0;
383
390
  }
@@ -1,7 +1,10 @@
1
+ import Is from 'strong-type';
1
2
  import {lstatSync,realpathSync} from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import {fileURLToPath} from 'node:url';
4
5
 
6
+ const is = new Is(false);
7
+
5
8
  const TESTING_SPECIFIER='arcane-os/testing';
6
9
  const TESTING_URL=new URL('./testing.mjs',import.meta.url).href;
7
10
  const RUNTIME_STRONG_TYPE_SPECIFIER='../../node_modules/strong-type/index.js';
@@ -25,7 +28,7 @@ function urlLikeSpecifier(specifier){
25
28
  }
26
29
 
27
30
  function safeManagedTarget(target){
28
- return typeof target==='string'
31
+ return is.string(target)
29
32
  &&target.startsWith('./')
30
33
  &&target.length>2
31
34
  &&!/[\\%?#\u0000-\u001f\u007f]/u.test(target)
@@ -115,14 +118,14 @@ export function initialize({managedImportMap}={}){
115
118
  const imports=managedImportMap.imports;
116
119
  if(managedImportMap.protocol!==MANAGED_IMPORT_MAP_PROTOCOL
117
120
  ||!MANAGED_IMPORT_MAP_BOUNDARIES.has(managedImportMap.boundary)
118
- ||imports===null||typeof imports!=='object'||Array.isArray(imports)){
121
+ ||imports===null||!is.object(imports)||is.array(imports)){
119
122
  loaderFailure('Managed test import-map loader data is malformed.');
120
123
  }
121
124
  const boundary=physicalDirectory(managedImportMap.baseURL);
122
125
  const exact=new Map();
123
126
  const urls=new Map();
124
127
  for(const [specifier,target] of Object.entries(imports)){
125
- if(typeof specifier!=='string'||specifier===''||!safeManagedTarget(target)){
128
+ if(!is.string(specifier)||specifier===''||!safeManagedTarget(target)){
126
129
  loaderFailure(`Managed test import-map entry is invalid: ${String(specifier)}.`);
127
130
  }
128
131
  const targetURL=physicalManagedTarget(
@@ -149,7 +152,7 @@ export function initialize({managedImportMap}={}){
149
152
  function managedResolution(specifier,context){
150
153
  if(managedImports===null)return null;
151
154
  if(managedImports.has(specifier))return managedImports.get(specifier);
152
- if(urlLikeSpecifier(specifier)&&typeof context.parentURL==='string'){
155
+ if(urlLikeSpecifier(specifier)&&is.string(context.parentURL)){
153
156
  let requested;
154
157
  try{requested=new URL(specifier,context.parentURL).href;}
155
158
  catch{requested=null;}