arcane-os 0.26.0 → 0.27.1

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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.27.1
4
+
5
+ - Preserve the previous `apps/<id>/arcane-sw.js` and `arcane-offline.json`
6
+ endpoints when generating a root-layout PWA. Both use the canonical worker
7
+ and current root inventory, including navigation aliases and installed npm
8
+ resource paths; portable output also retains deployment-relative URLs.
9
+ - Serve those previous worker and inventory endpoints directly during SDK
10
+ development instead of redirecting worker-script requests. Retain ordinary
11
+ browser update/activation, caches, application identity and saved data.
12
+ - Keep root layout optional and existing multi-app and Node server behavior
13
+ unchanged. Installed-application execution remains consumer-owned.
14
+
15
+ ## 0.27.0
16
+
17
+ - Add optional standalone `appsRoot: "."` discovery and `new`/`init --apps-root .`.
18
+ Preserve declared app IDs, existing multi-app/integrated layouts, nested pages,
19
+ and the selected native/portable package interface.
20
+ - Serve and package direct installed SDK URLs under `/node_modules/<dependency>/`.
21
+ Managed bare and relative imports select the same module instances, including
22
+ npm aliases; dynamic component resources resolve from their own installed URL.
23
+ - Generate root static PWA files and prior `/apps/<id>/` navigation pages through
24
+ the existing `arcane import-map` operation. Preserve authored installation IDs,
25
+ source/package defaults, query strings, fragments, and complete app content.
26
+ Ordinary static hosts need no additional SDK server or copied runtime.
27
+ - Keep existing physical and virtual browser routes, Node mail APIs, listener
28
+ configuration, and server-consumer entrypoints unchanged.
29
+
3
30
  ## 0.26.0
4
31
 
5
32
  - Add explicit installed-package browser runtime routes. Source development,
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.26.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.27.1` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -27,9 +27,14 @@ External browser apps can use the installed npm package directly, without a
27
27
  generated workspace `arcane/` directory or `arcane.lock.json`. Select the
28
28
  [four installed-package routes](docs/reference/protocols.md#installed-package-browser-routes)
29
29
  in `arcane-packager.json`: development, managed import maps, and PWA resources
30
- read the installed SDK while keeping `/arcane/...` browser URLs. Portable app
31
- packages still contain their selected runtime. Existing materialized workspaces
32
- and the scaffold's current layout remain supported. Node services continue to
30
+ read the installed SDK at real `/node_modules/arcane-os/...` browser URLs when
31
+ each destination equals its source. An alias uses its actual installed folder.
32
+ Select `appsRoot: "."` for a standalone root app, or scaffold one with
33
+ `arcane new my-app --apps-root .`. After npm installation, `arcane import-map`
34
+ refreshes managed maps and the root app's static PWA/navigation files for ordinary
35
+ static hosting. Portable app packages still contain their selected runtime.
36
+ Existing `apps/<id>`, virtual `/arcane` routes, and materialized workspaces remain
37
+ supported. Node services continue to
33
38
  use the installed CLI or public imports such as `arcane-os/mail`.
34
39
 
35
40
  The [mail gateway](docs/reference/mail.md) serves HTTPS with HTTP/2 on port 4433
@@ -273,8 +273,8 @@ export function mountPwaInstallPrompt({appName = ''} = {}) {
273
273
  if (!globalThis.document) return null;
274
274
  // Both modules may start independently; saved theme loading is not a barrier.
275
275
  await Promise.all([
276
- import(new URL('../modules/HTMLImport.js', import.meta.url).href),
277
- import(new URL('../modules/ThemeBootstrap.js', import.meta.url).href)
276
+ import('arcane/HTMLImport'),
277
+ import('arcane/ThemeBootstrap')
278
278
  ]);
279
279
  if (owner.state.status === 'disposed') return null;
280
280
  if (!document.body) {
@@ -295,7 +295,7 @@ export function mountPwaInstallPrompt({appName = ''} = {}) {
295
295
  host.hidden = true;
296
296
  host.dataset.appName = String(appName);
297
297
  host.dataset.arcanePwaInstall = '';
298
- host.setAttribute('href', new URL('../components/pwa-install.html', import.meta.url).href);
298
+ host.setAttribute('href', new URL('../components/pwa-install.html', import.meta.resolve('arcane/HTMLImport')).href);
299
299
  return new Promise(function waitForInstallComponent(resolve, reject) {
300
300
  const observer = new MutationObserver(function observeRemovedInstallComponent() {
301
301
  if (!host.isConnected) cancelMount();
@@ -87,10 +87,11 @@ app-scoped toolchain operation unchanged.
87
87
  An external workspace can select `installed-v1` routes directly from its
88
88
  `node_modules/arcane-os` installation, without a workspace `arcane/` tree or
89
89
  `arcane.lock.json`. The SDK reads the installed package version and maps its
90
- runtime, browser runtime, runtime dependency, and licenses to the established
91
- browser destinations. Existing `physical-v1` workspaces and the explicit
92
- materializer remain supported; scaffolding retains its existing physical
93
- layout. See [installed-package routes](reference/protocols.md#installed-package-browser-routes)
90
+ runtime, browser runtime, runtime dependency, and licenses to the configured
91
+ browser destinations. Direct destinations are the real npm paths. Existing
92
+ `physical-v1` workspaces, virtual installed routes, and the explicit materializer
93
+ remain supported. Root scaffolding selects direct npm routes; the existing
94
+ multi-app scaffold retains its physical layout. See [installed-package routes](reference/protocols.md#installed-package-browser-routes)
94
95
  for the configuration and npm-alias form.
95
96
 
96
97
  An Arcane OS checkout is an integrated SDK consumer, not the owner
@@ -245,14 +246,24 @@ apps synthesize that descriptor from their schema-1 package plus the current
245
246
  native registry during migration.
246
247
 
247
248
  An external app can use four `installed-v1` shared routes in
248
- `arcane-packager.json`: the installed SDK's `runtime/arcane` maps to `/arcane`,
249
- `browser-runtime` to `/arcane/sdk`, `runtime/strong-type` to
250
- `/arcane/dependencies/strong-type`, and its three license files to
251
- `/licenses/arcane-os`. Source serving and import-map generation read those
249
+ `arcane-packager.json`. With each destination equal to its source, browser URLs
250
+ point directly into `/node_modules/arcane-os/runtime/arcane`,
251
+ `/node_modules/arcane-os/browser-runtime`, and
252
+ `/node_modules/arcane-os/runtime/strong-type`; the three package license files
253
+ remain in `/node_modules/arcane-os`. An alias uses its actual installed folder.
254
+ Source serving and import-map generation read those
252
255
  installed files without creating a workspace runtime copy. Distribution copies
253
256
  the selected routes completely inside the portable artifact, preserving the
254
257
  same URLs. Existing physical `arcane/` routes retain their behavior.
255
258
 
259
+ `appsRoot: "."` places one standalone application at the workspace root while
260
+ retaining its declared ID and app-relative entries. The existing `apps` option
261
+ keeps multi-app and integrated layouts. Root static hosting consumes the same
262
+ npm URLs, managed maps, generated PWA files, and compatibility navigation pages
263
+ as SDK development. TWiN's ordinary static host needs no additional server;
264
+ separate Node API hosts such as Stripe keep their existing server and SDK mail
265
+ imports. Application location does not change their listeners or configuration.
266
+
256
267
  Release schema 1 and builder identity `arcane-app-packager-v1` remain unchanged
257
268
  because current Arcane native consumers treat them as public contracts. Native
258
269
  builders consume the schema-2 descriptor as a separate build input while v1
@@ -109,7 +109,7 @@ Creates one repository-shaped external application workspace and the selected
109
109
  app. It never creates more than one app or silently installs a global SDK.
110
110
 
111
111
  ```text
112
- arcane new <id> [--path <directory>] [--display-name <name>] [--target <target>] [--git]
112
+ arcane new <id> [--path <directory>] [--apps-root apps|.] [--display-name <name>] [--target <target>] [--git]
113
113
  ```
114
114
 
115
115
  ### Options and result
@@ -120,6 +120,12 @@ directory as a repository. Native target scaffolds also retain `browser` and
120
120
  include the required icon. The result reports the workspace, app, descriptor,
121
121
  target, and created paths.
122
122
 
123
+ `--apps-root .` creates a standalone root application using its installed npm
124
+ SDK directly. The default `--apps-root apps` preserves `apps/<id>`. Root setup
125
+ does not install dependencies or copy a runtime: run `npm install`, then
126
+ `npm run import-map`. Until installation, its result reports the import map as
127
+ pending with reason `sdk-install-required`.
128
+
123
129
  ### Example
124
130
 
125
131
  ```bash
@@ -135,7 +141,7 @@ initialization writes only the selected `apps/<id>/` boundary and does not add
135
141
  an SDK dependency to the Arcane OS repository.
136
142
 
137
143
  ```text
138
- arcane init [id] [--workspace <directory>] [--app <id>] [--display-name <name>] [--target <target>]
144
+ arcane init [id] [--workspace <directory>] [--app <id>] [--apps-root apps|.] [--display-name <name>] [--target <target>]
139
145
  ```
140
146
 
141
147
  ### Errors and safety
@@ -145,6 +151,10 @@ incompatible workspace fail rather than being overwritten. Initialization is
145
151
  idempotent only for files whose existing content satisfies the scaffold
146
152
  contract.
147
153
 
154
+ `--apps-root .` selects root setup for a standalone workspace. Omission retains
155
+ the configured layout, or `apps` for a new configuration. `init` never moves an
156
+ existing application or converts the integrated Arcane OS layout.
157
+
148
158
  ### Example
149
159
 
150
160
  ```bash
@@ -318,13 +328,14 @@ workspace/runtime routes. It defaults to HTTPS on localhost; `--public` enables
318
328
  from other devices on the network, using HTTPS by default.
319
329
 
320
330
  For an external workspace, the server exposes the selected SDK routes alongside
321
- the application. `installed-v1` serves directly from the installed package
322
- under `/arcane`, `/arcane/sdk`, and `/arcane/dependencies/strong-type`, without
323
- creating a workspace runtime directory. Existing `physical-v1` workspaces serve
331
+ the application. Direct `installed-v1` routes serve the real
332
+ `/node_modules/<dependency>/...` URLs without creating a workspace runtime
333
+ directory. Earlier virtual `/arcane` installed routes remain supported.
334
+ Existing `physical-v1` workspaces serve
324
335
  their projected `arcane/` root. Integrated workspaces retain their configured
325
336
  physical routes.
326
- The explicit live-source SDK mapping remains unchanged and does not replace the
327
- installed projection.
337
+ An explicit live-source SDK mapping follows the selected browser destinations
338
+ while reading canonical SDK source; it never replaces installed files.
328
339
 
329
340
  ```text
330
341
  arcane dev [--app <id>] [--public] [--http | --https] [--cert <file> --key <file>] [--host <address>] [--port 8000] [--http-port 0]
@@ -107,31 +107,31 @@ needed:
107
107
  ```json
108
108
  {
109
109
  "schemaVersion": 1,
110
- "appsRoot": "apps",
110
+ "appsRoot": ".",
111
111
  "distRoot": "dist",
112
112
  "sharedPayloads": {
113
113
  "browser-runtime": [
114
114
  {
115
115
  "source": "node_modules/arcane-os/runtime/arcane",
116
- "destination": "arcane",
116
+ "destination": "node_modules/arcane-os/runtime/arcane",
117
117
  "include": ["components", "css", "entities", "img", "modules"],
118
118
  "exclude": []
119
119
  },
120
120
  {
121
121
  "source": "node_modules/arcane-os/browser-runtime",
122
- "destination": "arcane/sdk",
122
+ "destination": "node_modules/arcane-os/browser-runtime",
123
123
  "include": ["."],
124
124
  "exclude": []
125
125
  },
126
126
  {
127
127
  "source": "node_modules/arcane-os/runtime/strong-type",
128
- "destination": "arcane/dependencies/strong-type",
128
+ "destination": "node_modules/arcane-os/runtime/strong-type",
129
129
  "include": ["."],
130
130
  "exclude": []
131
131
  },
132
132
  {
133
133
  "source": "node_modules/arcane-os",
134
- "destination": "licenses/arcane-os",
134
+ "destination": "node_modules/arcane-os",
135
135
  "include": ["LICENSE", "COMMERCIAL-LICENSE.md", "NOTICE"],
136
136
  "exclude": []
137
137
  }
@@ -142,22 +142,44 @@ needed:
142
142
 
143
143
  The first include list also accepts a final `security` entry. An npm alias such
144
144
  as `arcane-sdk` substitutes `node_modules/arcane-sdk` for the package prefix in
145
- all four source paths. Destinations remain unchanged. Package selection and SDK
145
+ all four source and destination paths. Package selection and SDK
146
146
  version come from the installed dependency, not a generated runtime lock.
147
147
 
148
148
  `arcane import-map`, `arcane dev`, packaging, and generated PWA inventories use
149
- the same logical destinations. Source serving and map generation read installed
149
+ the same actual npm destinations. For example, `arcane/HTMLImport` resolves to
150
+ `./node_modules/arcane-os/runtime/arcane/modules/HTMLImport.js`, and
151
+ `arcane-os/event-manager` resolves to
152
+ `./node_modules/arcane-os/browser-runtime/event-manager.mjs`. A normal static
153
+ server can serve those files directly. Source serving and map generation read installed
150
154
  files without materializing them into the workspace. A selected portable
151
155
  package copies its complete selected resources inside the artifact, preserving
152
156
  the same browser URLs and saved managed import maps.
153
157
 
154
158
  Existing `physical-v1` configurations whose first source is `arcane` remain
155
159
  supported. The explicit materializer below still refreshes those projections,
156
- and scaffolding retains its existing physical layout. Selecting `installed-v1`
160
+ and the default multi-app scaffold retains its existing physical layout. The
161
+ earlier installed routes with virtual `/arcane` destinations also remain
162
+ supported. Selecting `installed-v1`
157
163
  does not delete any preexisting workspace files. The separate host-document
158
164
  `generateDocumentImportMaps()` API continues to accept an already materialized
159
165
  runtime; its input contract is unchanged.
160
166
 
167
+ ### Optional standalone root application
168
+
169
+ `appsRoot: "."` selects one application whose `arcane-app.json`,
170
+ `arcane-package.json`, entry, and app-owned files occupy the workspace root.
171
+ The declared application ID remains unchanged. `appsRoot: "apps"` continues
172
+ to discover `apps/<id>` and supports the existing integrated and multi-app
173
+ layouts. Entries and include/exclude paths remain relative to the application.
174
+
175
+ Root HTML uses `<base href="./">`; nested navigable documents use their actual
176
+ depth back to the workspace. Managed bare imports remain the public interface;
177
+ their targets follow the selected npm routes. `arcane import-map` also writes
178
+ root-app navigation pages for the previous `/apps/<id>/` links, preserving
179
+ query strings and fragments. It preserves authored files at those destinations.
180
+ For a direct-installed root PWA it generates the static PWA records at the root,
181
+ so normal static hosting needs no SDK request handler or runtime copy.
182
+
161
183
  ## Installed SDK runtime materialization
162
184
 
163
185
  `materializeInstalledSdkRuntime()` is the Node entrypoint for refreshing one
@@ -47,6 +47,30 @@ explicit `id` and `scope` values, resolve against `./apps/<id>/`; an icon such
47
47
  as `img/library.png` therefore remains beneath the app directory. Absolute URL
48
48
  fields retain their authored destination.
49
49
 
50
+ For `appsRoot: "."`, application URLs stay at the deployment root. Direct npm
51
+ routes keep runtime URLs under `/node_modules/<dependency>/...`. The public
52
+ `arcane import-map` operation generates static PWA files beside the root entry
53
+ and adds their links to the selected navigable documents. This works with an
54
+ ordinary static host; it does not copy the installed runtime into `arcane/`.
55
+ The source root defaults its installation ID to `/apps/<id>/` to preserve the
56
+ previous source identity while its start URL and scope move to the root. An
57
+ authored `manifest.id` remains authoritative. Packaged default identity remains
58
+ `./`. Existing `/apps/<id>/` navigation aliases retain query strings and
59
+ fragments and lead to the selected root document. No stored application data is
60
+ rewritten by changing the layout.
61
+
62
+ Root PWA generation also retains `apps/<id>/arcane-sw.js` and
63
+ `apps/<id>/arcane-offline.json`. These are ordinary generated files served at
64
+ their original URLs, including on a static host. The same canonical worker and
65
+ current inventory use root resource/navigation URLs, rebased for the previous
66
+ scope when deployed beneath a directory. An existing worker's inventory refresh
67
+ can learn the new navigation destinations; the browser can update the worker at
68
+ its retained script URL through normal update and activation. No cache deletion,
69
+ forced activation, user-data migration or application-owned server handler is
70
+ introduced. Offline clients require a later successful connection to receive
71
+ updated files; fixture coverage does not establish a particular installed app's
72
+ actual browser lifecycle.
73
+
50
74
  `offline.include` and `offline.exclude` select literal paths or directory
51
75
  prefixes from the selected emitted inventory. An omitted or empty include list
52
76
  selects that inventory; exclusions subtract from it. App files use app-relative
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -84,7 +84,7 @@
84
84
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
85
85
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
86
86
  "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
87
- "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
87
+ "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
88
88
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
89
89
  "check": "node tools/check-source.mjs && npm test",
90
90
  "check:release": "node tools/check-source.mjs --package-only && npm run test:release",
@@ -16,6 +16,7 @@
16
16
  const {arcaneLogging}=await import('arcane-os/logging');
17
17
 
18
18
  const host=this;
19
+ const componentHref=new URL(host.getAttribute('href'),document.baseURI);
19
20
  const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
20
21
  const {
21
22
  effectiveDashboardVisibility,
@@ -232,7 +233,7 @@
232
233
  const items=document.createElement('section');
233
234
 
234
235
  styles.rel='stylesheet';
235
- styles.href=new URL('./arcane/css/dashboard-config.css?v=2',document.baseURI).href;
236
+ styles.href=new URL('../css/dashboard-config.css?v=2',componentHref).href;
236
237
  heading.dataset.dashboardConfigHeading='';
237
238
  message.dataset.dashboardConfigDescription='';
238
239
  empty.className='dashboard-config-empty';
@@ -20,6 +20,7 @@
20
20
  const {arcaneLogging}=await import('arcane-os/logging');
21
21
 
22
22
  const host=this;
23
+ const componentHref=new URL(host.getAttribute('href'),document.baseURI);
23
24
  const openButton=host.shadowRoot.querySelector('#openData');
24
25
  const dataModal=host.shadowRoot.querySelector('#dataModal');
25
26
  const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
@@ -89,7 +90,7 @@
89
90
  fileManager.dataset.layout=host.dataset.layout||'files';
90
91
  fileManager.setAttribute(
91
92
  'href',
92
- './arcane/components/file-manager.html?v=18'
93
+ new URL('./file-manager.html?v=18',componentHref).href
93
94
  );
94
95
 
95
96
  content.append(heading,fileManager);
@@ -352,6 +352,7 @@
352
352
  //importing Document at the end of the file to avoid blocking;
353
353
 
354
354
  const host=this;
355
+ const componentHref=new URL(host.getAttribute('href'),document.baseURI);
355
356
  const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
356
357
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
357
358
  const events=createArcaneEventSource(host,{source:'arcane.component.file-manager',eventTypes:['file-manager-action','file-manager-select','file-manager-open','file-manager-ready']});
@@ -830,8 +831,8 @@
830
831
  icon.className='tree-entry-icon';
831
832
  icon.alt='';
832
833
  icon.src=entry.kind==='directory'
833
- ?new URL('./arcane/img/folder.svg',document.baseURI).href
834
- :new URL('./arcane/img/doc.svg',document.baseURI).href;
834
+ ?new URL('../img/folder.svg',componentHref).href
835
+ :new URL('../img/doc.svg',componentHref).href;
835
836
  name.className='tree-entry-name';
836
837
  name.innerText=entry.name;
837
838
  count.className='tree-file-count';
@@ -1409,7 +1410,7 @@
1409
1410
  deleteButton.innerText='Delete File';
1410
1411
  deleteIcon.alt='';
1411
1412
  deleteIcon.className='action-icon';
1412
- deleteIcon.src=new URL('./arcane/img/trash.svg',document.baseURI).href;
1413
+ deleteIcon.src=new URL('../img/trash.svg',componentHref).href;
1413
1414
  deleteButton.prepend(deleteIcon);
1414
1415
  deleteButton.addEventListener(
1415
1416
  'click',
@@ -1615,7 +1616,7 @@
1615
1616
 
1616
1617
  fileIcon.alt='';
1617
1618
  fileIcon.className='icon file-icon';
1618
- fileIcon.src=new URL('./arcane/img/doc.svg',document.baseURI).href;
1619
+ fileIcon.src=new URL('../img/doc.svg',componentHref).href;
1619
1620
  fileDetails.className='file-details';
1620
1621
  fileLabel.className='file-name';
1621
1622
  fileLabel.innerText=fileName;
@@ -1638,7 +1639,7 @@
1638
1639
 
1639
1640
  deleteIcon.alt='';
1640
1641
  deleteIcon.className='action-icon';
1641
- deleteIcon.src=new URL('./arcane/img/trash.svg',document.baseURI).href;
1642
+ deleteIcon.src=new URL('../img/trash.svg',componentHref).href;
1642
1643
 
1643
1644
  openButton.addEventListener(
1644
1645
  'click',
@@ -1938,7 +1939,7 @@
1938
1939
  directory.dataset.directory=tableName;
1939
1940
  folder.className='file folder';
1940
1941
  folderIcon.className='icon';
1941
- folderIcon.src=new URL('./arcane/img/folder.svg',document.baseURI).href;
1942
+ folderIcon.src=new URL('../img/folder.svg',componentHref).href;
1942
1943
  folderName.innerText=tableName;
1943
1944
  fileCount.className='file-count';
1944
1945
  fileCount.innerText=`${files.length} ${files.length===1?'file':'files'}`;
@@ -1,4 +1,5 @@
1
1
  import Is from 'strong-type';
2
+ import {resolveAppRoot} from './app-layout.mjs';
2
3
  import {isDeepStrictEqual} from 'node:util';
3
4
  import {readFile, writeFile} from 'node:fs/promises';
4
5
  import path from 'node:path';
@@ -427,7 +428,8 @@ async function readJsonFile(filePath,label,{optional=false}={}){
427
428
 
428
429
  export async function refreshAppPackageProjection({workspaceRoot, appId, signal, onEvent}) {
429
430
  throwIfAborted(signal);
430
- const appRoot = path.join(workspaceRoot, 'apps', appId);
431
+ const rootConfig=await readJsonFile(path.join(workspaceRoot,'arcane-packager.json'),'arcane-packager.json');
432
+ const appRoot = resolveAppRoot(workspaceRoot,rootConfig,appId);
431
433
  const descriptorPath = path.join(appRoot, APP_DESCRIPTOR_NAME);
432
434
  const authored = await readJsonFile(
433
435
  descriptorPath,
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+
3
+ // The app identity is independent of whether its files occupy a workspace root.
4
+ export function appRelativeRoot(config, appId) {
5
+ return config.appsRoot === '.' ? '' : `apps/${appId}`;
6
+ }
7
+
8
+ export function resolveAppRoot(workspaceRoot, config, appId) {
9
+ return path.resolve(workspaceRoot, appRelativeRoot(config, appId));
10
+ }
11
+
12
+ export function appBaseHref(workspaceRoot, appRoot, document = 'index.html') {
13
+ const directory = path.dirname(path.resolve(appRoot, document));
14
+ const relative = path.relative(directory, workspaceRoot).split(path.sep).join('/');
15
+ return relative ? `${relative}/` : './';
16
+ }
17
+
18
+ export function rootAppNavigation(appId, entry, documents = []) {
19
+ const redirects=new Map();
20
+ for(const document of documents)redirects.set(`apps/${appId}/${document}`,document);
21
+ redirects.set(`apps/${appId}/index.html`,entry);
22
+ return [...redirects].map(([file,target])=>{
23
+ const relative=path.posix.relative(path.posix.dirname(file),target);
24
+ const destination=JSON.stringify(relative).replaceAll('<','\\u003c');
25
+ return {
26
+ path:file,target:`/${target}`,
27
+ content:'<!doctype html>\n<!-- Arcane root application navigation -->\n'
28
+ +'<meta charset="utf-8">\n<title>Opening application</title>\n'
29
+ +`<script>const target=new URL(${destination},location.href);target.search=location.search;target.hash=location.hash;location.replace(target.href);</script>\n`
30
+ };
31
+ });
32
+ }
@@ -124,7 +124,9 @@ async function selectedTestFiles({workspaceRoot,workspaceMode,appRoot,signal}){
124
124
  if(workspaceMode==='external'){
125
125
  await collectOptionalTests(path.join(workspaceRoot,'test'),workspaceRoot,files,signal);
126
126
  }
127
- if(appRoot)await collectOptionalTests(path.join(appRoot,'test'),appRoot,files,signal);
127
+ if(appRoot&&(workspaceMode!=='external'||!samePath(workspaceRoot,appRoot))){
128
+ await collectOptionalTests(path.join(appRoot,'test'),appRoot,files,signal);
129
+ }
128
130
  return [...files].sort();
129
131
  }
130
132
 
package/src/cli/main.mjs CHANGED
@@ -14,6 +14,7 @@ const VALUE_OPTIONS=new Set([
14
14
  'display-name',
15
15
  'workspace',
16
16
  'app',
17
+ 'apps-root',
17
18
  'sdk-runtime-source',
18
19
  'arcane-root',
19
20
  'host',
@@ -60,8 +61,8 @@ const MAX_NODE_TIMER_DELAY_MS=2_147_483_647;
60
61
  export const HELP_TEXT=`Arcane OS application SDK ${SDK_VERSION}
61
62
 
62
63
  Usage:
63
- ${CLI_NAME} new <id> [--path <directory>] [--display-name <name>] [--target <target>] [--git]
64
- ${CLI_NAME} init [id] [--workspace <directory>] [--display-name <name>] [--target <target>]
64
+ ${CLI_NAME} new <id> [--path <directory>] [--apps-root apps|.] [--display-name <name>] [--target <target>] [--git]
65
+ ${CLI_NAME} init [id] [--workspace <directory>] [--apps-root apps|.] [--display-name <name>] [--target <target>]
65
66
  ${CLI_NAME} upgrade [--workspace <directory>] [--app <id>]
66
67
  ${CLI_NAME} doctor [--workspace <directory>] [--arcane-root <directory>]
67
68
  ${CLI_NAME} import-map [--workspace <directory>] [--app <id>]
@@ -421,15 +422,6 @@ function readScope(value){
421
422
  return scope;
422
423
  }
423
424
 
424
- function inferredAppId(workspaceRoot){
425
- const id=path.basename(workspaceRoot)
426
- .normalize('NFKD')
427
- .toLowerCase()
428
- .replace(/[^a-z0-9]+/gu,'-')
429
- .replace(/^-+|-+$/gu,'');
430
- return id||'arcane-app';
431
- }
432
-
433
425
  function noExtraPositionals(command,positionals,expected=0){
434
426
  if(positionals.length>expected){
435
427
  usage(`Unexpected argument for ${command}.`);
@@ -503,6 +495,9 @@ function operationOptions(command,parsed,cwd){
503
495
  if(command!=='mail'&&flags.has('report-stdin')){
504
496
  usage('--report-stdin is supported only by mail send.');
505
497
  }
498
+ if(values['apps-root']!==undefined&&!['new','init'].includes(command)){
499
+ usage('--apps-root is supported only by new and init.');
500
+ }
506
501
 
507
502
  if(command==='new'){
508
503
  const appId=positionals[0];
@@ -511,6 +506,7 @@ function operationOptions(command,parsed,cwd){
511
506
  return {
512
507
  targetPath:path.resolve(cwd,values.path??appId),
513
508
  appId,
509
+ appsRoot:values['apps-root']??'apps',
514
510
  displayName:values['display-name'],
515
511
  target:values.target??'browser',
516
512
  initializeGit:flags.has('git')
@@ -520,7 +516,8 @@ function operationOptions(command,parsed,cwd){
520
516
  noExtraPositionals(command,positionals,1);
521
517
  return {
522
518
  workspaceRoot,
523
- appId:positionals[0]??values.app??inferredAppId(workspaceRoot),
519
+ appId:positionals[0]??values.app,
520
+ appsRoot:values['apps-root'],
524
521
  displayName:values['display-name'],
525
522
  target:values.target??'browser'
526
523
  };