arcane-os 0.28.3 → 0.29.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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.29.0
4
+
5
+ - Add `legacyAppPaths: false` to `arcane-packager.json` for root applications
6
+ using `appsRoot: "."`. Import-map refresh, source development, and packaging
7
+ omit SDK-generated `apps/<id>` navigation aliases and legacy PWA worker and
8
+ inventory files. Omission or `true` preserves the existing behavior.
9
+ - Preserve application storage identity, explicit PWA manifest identity, direct
10
+ installed npm resource URLs, and selected authored files. The SDK does not
11
+ delete existing historical files. Applications choosing root-only output own
12
+ removal of those files and the resulting loss of old bookmark, launch, and
13
+ service-worker update URLs that depended on them.
14
+
15
+ ## 0.28.4
16
+
17
+ - Preserve installed component-relative browser-runtime and strong-type import
18
+ aliases in direct npm maps, including npm-alias workspaces. These aliases
19
+ resolve to the same installed modules as the direct paths, restoring browser
20
+ device-settings loading without copies or a second module instance.
21
+ - Serve explicitly included historical authored resources at their retained
22
+ paths in root-app development. Unselected navigation aliases and generated
23
+ PWA endpoints retain their existing behavior. Applications refresh managed
24
+ maps through the public SDK command after updating the dependency; no
25
+ authored SDK edits, saved-data migration, or dependency changes are required.
26
+
3
27
  ## 0.28.3
4
28
 
5
29
  - Add explicit `DBOPFS.removeEmptyTable(tableName)` for removing an existing
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.28.3` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.29.0` 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
 
@@ -228,6 +228,16 @@ portable runtime subpaths such as `arcane-os/preference-store` and
228
228
  modules. The result reports the complete map written to the selected
229
229
  application; no fixed entry count is a release contract.
230
230
 
231
+ For `appsRoot: "."`, the artifact is `modules/arcane.importmap.json` at the
232
+ application root. Set `"legacyAppPaths": false` in `arcane-packager.json` to
233
+ omit SDK-generated `apps/<id>/` navigation/PWA compatibility files and aliases.
234
+ The same workspace choice applies to `arcane dev` and `arcane package`; restart
235
+ an already running dev server after changing it. The default remains `true`.
236
+ Root PWA files, app/installation identity and selected authored files are
237
+ preserved. Existing files are never deleted by this option. See
238
+ [root-only generated output](pwa.md#root-only-generated-output) before changing
239
+ the URLs needed by previously installed apps.
240
+
231
241
  SDK `0.5.17` preserves the physical workspace route count and ordered include
232
242
  list. External and modern integrated routes require `components`, `css`,
233
243
  `dependencies`, `entities`, `img`, `modules`, and `sdk`; a physical workspace
@@ -532,6 +542,12 @@ selected SDK content directly from `node_modules`. Only the portable output
532
542
  receives copies; no workspace `arcane/` projection is required. Its runtime URLs,
533
543
  managed import-map targets, and PWA inventory destinations match source serving.
534
544
 
545
+ With `appsRoot: "."`, app files retain their root-relative layout. The optional
546
+ root-config `legacyAppPaths: false` omits SDK-generated compatibility files
547
+ beneath `apps/<id>/` from the planned and actual output. It does not omit
548
+ explicitly selected authored resources at those paths or change the default
549
+ packaged installation identity. Omission or `true` preserves existing behavior.
550
+
535
551
  ```text
536
552
  arcane package [--app <id>] [--dry-run]
537
553
  ```
@@ -180,6 +180,24 @@ query strings and fragments. It preserves authored files at those destinations.
180
180
  For a direct-installed root PWA it generates the static PWA records at the root,
181
181
  so normal static hosting needs no SDK request handler or runtime copy.
182
182
 
183
+ To select root-only generated output, set `"legacyAppPaths": false` beside
184
+ `"appsRoot": "."` in `arcane-packager.json`. This optional boolean defaults to
185
+ `true`; it has no effect on the `appsRoot: "apps"` layout. The shared import-map
186
+ refresh, source dev server, package inspection, dry run and package output then
187
+ omit SDK-generated `apps/<id>/` navigation pages, navigation aliases, and the
188
+ legacy PWA worker/offline inventory. Root PWA files and normal managed imports
189
+ remain available. Edit the root configuration before starting `arcane dev`;
190
+ restart an existing server after changing this workspace-level choice.
191
+
192
+ This option does not change the application ID, stored data, the existing
193
+ default installation ID or an authored `pwa.manifest.id`. It neither deletes
194
+ existing files nor removes authored resources from the app's include/exclude
195
+ selection. Explicitly included files under `apps/<id>/` still serve and package
196
+ as authored resources. Previously installed launch URLs and worker update URLs
197
+ need their old resources to remain available; retain the default compatibility
198
+ output when those URLs still need SDK support. See the
199
+ [root PWA compatibility boundary](pwa.md#root-only-generated-output).
200
+
183
201
  Direct-installed root maps expose `arcane-os/modules/<filename>` and
184
202
  `arcane-os/entities/<filename>` (including extensions), plus the existing
185
203
  focused lowercase package exports. They omit the old `arcane/*` and
@@ -71,6 +71,38 @@ introduced. Offline clients require a later successful connection to receive
71
71
  updated files; fixture coverage does not establish a particular installed app's
72
72
  actual browser lifecycle.
73
73
 
74
+ ### Root-only generated output
75
+
76
+ For a root application, add `"legacyAppPaths": false` to the workspace's
77
+ `arcane-packager.json`, alongside `"appsRoot": "."`. The default is `true`.
78
+ This selects root-only SDK-generated navigation and PWA output in import-map
79
+ refresh, `arcane dev`, package inspection/dry run, and browser packaging:
80
+
81
+ - The four root PWA files remain generated normally.
82
+ - The SDK generates no `apps/<id>/` navigation pages, legacy worker or legacy
83
+ offline inventory, and adds no legacy navigation aliases or dev redirects.
84
+ - The default source installation ID remains `/apps/<id>/`, the default
85
+ packaged installation ID remains `./`, and an explicit `manifest.id` remains
86
+ authoritative. An ID is an installation identifier, not a request to generate
87
+ a directory. App identity and saved application data remain unchanged.
88
+ - Existing files are left on disk. Explicitly selected authored old-path
89
+ resources remain in the normal source/package/offline inventory; this setting
90
+ is not a deletion or migration command.
91
+
92
+ Restart `arcane dev` after changing this workspace configuration. Omission or
93
+ `true` retains the compatibility behavior described above. The option has no
94
+ effect on apps whose configured `appsRoot` is `"apps"`.
95
+
96
+ An installed app may still launch an old `/apps/<id>/` URL, and an existing
97
+ worker registration may still update its old script/inventory URL. Disabling
98
+ generation does not redirect those installed clients, unregister their worker,
99
+ clear caches or guarantee their next update. Keep compatibility output enabled
100
+ while old URLs still require SDK support, or supply the required resources
101
+ through the application's own declared files and hosting policy. This SDK
102
+ option alone makes no claim about any existing installation's adoption.
103
+
104
+ ### Offline resource selection
105
+
74
106
  `offline.include` and `offline.exclude` select literal paths or directory
75
107
  prefixes from the selected emitted inventory. An omitted or empty include list
76
108
  selects that inventory; exclusions subtract from it. App files use app-relative
@@ -1041,6 +1041,15 @@ async function usevalidateAppConfig(...arguments_) {
1041
1041
 
1042
1042
  Validates one root packager mapping and its fixed shared-route boundaries.
1043
1043
 
1044
+ The schema-1 `arcane-packager.json` accepts the optional boolean
1045
+ `legacyAppPaths`, normalized to `true` when omitted. With `appsRoot: "."`,
1046
+ setting it to `false` omits generated legacy `apps/<id>/` navigation/PWA output
1047
+ across managed-map refresh, development serving, inspection and packaging.
1048
+ It leaves installation identity and authored file selection unchanged and has
1049
+ no effect with `appsRoot: "apps"`. See
1050
+ [standalone root applications](protocols.md#optional-standalone-root-application)
1051
+ for configuration, retained-file and existing-installation behavior.
1052
+
1044
1053
  ### Signature and result
1045
1054
 
1046
1055
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.28.3",
3
+ "version": "0.29.0",
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",
@@ -5,6 +5,12 @@ export function appRelativeRoot(config, appId) {
5
5
  return config.appsRoot === '.' ? '' : `apps/${appId}`;
6
6
  }
7
7
 
8
+ // Generated compatibility URLs are separate from installation and storage identity.
9
+ export function rootAppLegacyPath(config, appId) {
10
+ return config.appsRoot === '.' && config.legacyAppPaths !== false
11
+ ? `apps/${appId}` : undefined;
12
+ }
13
+
8
14
  export function resolveAppRoot(workspaceRoot, config, appId) {
9
15
  return path.resolve(workspaceRoot, appRelativeRoot(config, appId));
10
16
  }
@@ -6,7 +6,7 @@ import https from 'node:https';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import {resolveWorkspace} from './workspace.mjs';
9
- import {appRelativeRoot} from './app-layout.mjs';
9
+ import {appRelativeRoot,rootAppLegacyPath} from './app-layout.mjs';
10
10
  import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
11
11
  import {APP_DESCRIPTOR_NAME, projectPackageManifest} from './app-descriptor.mjs';
12
12
  import {APP_CONFIG_NAME, validateAppConfig} from './packager/core.mjs';
@@ -1047,10 +1047,11 @@ async function startOwnedDevServer({
1047
1047
  const pwaResourceUrls = new Set();
1048
1048
  function developmentPwaArtifacts(selectedRoutes, assets = [], version = assetVersion) {
1049
1049
  const rootApp = selectedRoutes.config.appsRoot === '.';
1050
+ const legacyAppPath = rootAppLegacyPath(selectedRoutes.config, selectedRoutes.appId);
1050
1051
  const appBase = applicationSourcePath(selectedRoutes.config,selectedRoutes.appId);
1051
1052
  const navigationAliases = {'/': selectedRoutes.startPath};
1052
- if (rootApp) {
1053
- const legacyBase = `/apps/${selectedRoutes.appId}`;
1053
+ if (legacyAppPath) {
1054
+ const legacyBase = `/${legacyAppPath}`;
1054
1055
  navigationAliases[legacyBase] = selectedRoutes.startPath;
1055
1056
  navigationAliases[`${legacyBase}/`] = selectedRoutes.startPath;
1056
1057
  for (const asset of [selectedRoutes.startPath,...assets]) {
@@ -1082,9 +1083,9 @@ async function startOwnedDevServer({
1082
1083
  basePath: '/',
1083
1084
  appBase,
1084
1085
  ...(rootApp ? {
1085
- installationId:`/apps/${routeSet.appId}/`,
1086
- legacyAppPath:`apps/${routeSet.appId}`
1086
+ installationId:`/apps/${routeSet.appId}/`
1087
1087
  } : {}),
1088
+ legacyAppPath,
1088
1089
  runtimeBase: selectedRoutes.browserRuntimeBase
1089
1090
  ?? selectedRoutes.installed?.browserRuntimeBase ?? '/arcane/sdk/',
1090
1091
  mode: 'development'
@@ -1176,7 +1177,7 @@ async function startOwnedDevServer({
1176
1177
  const target=parseRequestTarget(request.url);
1177
1178
  if(!target){deny(response,400,'Invalid request path.');return;}
1178
1179
  const {segments}=target;
1179
- const legacyAppRequest = mode === 'source' && routeSet.config.appsRoot === '.'
1180
+ const legacyAppRequest = mode === 'source' && rootAppLegacyPath(routeSet.config, routeSet.appId)
1180
1181
  && segments[0] === 'apps' && segments[1] === routeSet.appId;
1181
1182
  const legacyPwaRequest = legacyAppRequest && segments.length === 3
1182
1183
  && ['arcane-sw.js', 'arcane-offline.json'].includes(segments[2]);
@@ -1192,7 +1193,11 @@ async function startOwnedDevServer({
1192
1193
  const selectedRoutes = mode === 'source' && (appRequest || segments.length === 0 || generatedPwaPath)
1193
1194
  ? await refreshSourceRoutes() : currentSourceRoutes;
1194
1195
  const pwaEnabled = mode === 'source' ? selectedRoutes.app?.pwa?.enabled === true : routeSet.pwa;
1195
- if (legacyAppRequest && !(pwaEnabled && legacyPwaRequest)) {
1196
+ const authoredLegacyResource = legacyAppRequest
1197
+ && sourcePathAllowed(segments, selectedRoutes.app);
1198
+ if (legacyAppRequest
1199
+ && !(pwaEnabled && legacyPwaRequest)
1200
+ && !authoredLegacyResource) {
1196
1201
  const legacyPath = target.pathname.slice(`/apps/${routeSet.appId}`.length);
1197
1202
  const location = !legacyPath || legacyPath === '/' || legacyPath === '/index.html'
1198
1203
  ? selectedRoutes.startPath : legacyPath;
@@ -1470,12 +1470,19 @@ async function managedImportMapBuild(resolvedWorkspace,resolvedApp,signal,pwaEna
1470
1470
  return value.startsWith('./')
1471
1471
  ?`./${installedRuntimeTarget(value.slice(2),installed,{browser:true})}`:value;
1472
1472
  }
1473
+ function installedPhysicalCompatibilityUrl(value){
1474
+ if(!value.startsWith('./arcane/sdk/')
1475
+ &&!value.startsWith('./arcane/dependencies/strong-type/'))return null;
1476
+ return `./${installed.routes[0].destination}/${value.slice('./arcane/'.length)}`;
1477
+ }
1473
1478
  for(const [specifier,target] of Object.entries(built.imports)){
1474
1479
  if(rootApplication&&specifier.startsWith('arcane/'))continue;
1475
1480
  const selected=installedBrowserUrl(target);
1476
1481
  if(!rootApplication)imports[specifier]=selected;
1477
1482
  // Relative module imports and bare names must resolve to the same instance.
1478
1483
  imports[installedBrowserUrl(specifier)]=selected;
1484
+ const physicalCompatibilityUrl=installedPhysicalCompatibilityUrl(specifier);
1485
+ if(physicalCompatibilityUrl)imports[physicalCompatibilityUrl]=selected;
1479
1486
  }
1480
1487
  built.imports=imports;
1481
1488
  }
@@ -12,7 +12,7 @@ import {
12
12
  } from 'node:fs/promises';
13
13
  import path from 'node:path';
14
14
  import {pathToFileURL} from 'node:url';
15
- import {appRelativeRoot,resolveAppRoot,rootAppNavigation} from '../app-layout.mjs';
15
+ import {appRelativeRoot,resolveAppRoot,rootAppLegacyPath,rootAppNavigation} from '../app-layout.mjs';
16
16
  import {readInstalledSdkLayout} from '../sdk-runtime-layout.mjs';
17
17
  import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
18
18
  import {
@@ -275,11 +275,14 @@ function validateSharedRoute(route,label){
275
275
  }
276
276
 
277
277
  export function validateRootConfig(value,configPath=ROOT_CONFIG_NAME){
278
- assertOnlyKeys(value,new Set(['schemaVersion','appsRoot','distRoot','sharedPayloads']),ROOT_CONFIG_NAME);
278
+ assertOnlyKeys(value,new Set(['schemaVersion','appsRoot','distRoot','sharedPayloads','legacyAppPaths']),ROOT_CONFIG_NAME);
279
279
  if(value.schemaVersion!==1)fail(`${ROOT_CONFIG_NAME}.schemaVersion must be 1.`);
280
280
  if(!['apps','.'].includes(value.appsRoot)||value.distRoot!=='dist'){
281
281
  fail(`${ROOT_CONFIG_NAME} must bind appsRoot to "apps" or "." and distRoot to "dist".`);
282
282
  }
283
+ if (value.legacyAppPaths !== undefined && !is.boolean(value.legacyAppPaths)) {
284
+ fail(`${ROOT_CONFIG_NAME}.legacyAppPaths must be a boolean.`);
285
+ }
283
286
  if(!isPlainObject(value.sharedPayloads)){
284
287
  fail(`${ROOT_CONFIG_NAME}.sharedPayloads must be an object.`);
285
288
  }
@@ -293,7 +296,10 @@ export function validateRootConfig(value,configPath=ROOT_CONFIG_NAME){
293
296
  validateSharedRoute(route,`sharedPayloads.${id}[${index}]`)
294
297
  );
295
298
  }
296
- return {schemaVersion:1,appsRoot:value.appsRoot,distRoot:'dist',sharedPayloads,configPath};
299
+ return {
300
+ schemaVersion:1,appsRoot:value.appsRoot,distRoot:'dist',sharedPayloads,configPath,
301
+ legacyAppPaths:value.legacyAppPaths ?? true
302
+ };
297
303
  }
298
304
 
299
305
  function normalizeOptionalRecord(value,label){
@@ -575,7 +581,7 @@ async function optionalDescriptor(context){
575
581
  async function inspectContext(context,{signal}={}){
576
582
  const records=await collectPackageRecords(context,{signal});
577
583
  const documents=await browserDocuments(records,context.config.entry);
578
- const navigation=context.rootConfig.appsRoot==='.'?rootAppNavigation(
584
+ const navigation=rootAppLegacyPath(context.rootConfig,context.appId)?rootAppNavigation(
579
585
  context.appId,context.config.entry,documents.map(document=>document.path)
580
586
  ):[];
581
587
  for(const redirect of navigation){
@@ -757,11 +763,11 @@ async function replaceDirectory(stagingRoot,outputRoot){
757
763
  async function packageWithContext(context,options={}){
758
764
  const {signal,onEvent,browserPwa=true}=options;
759
765
  const pwaEnabled=browserPwa&&context.config.pwa?.enabled===true;
760
- const legacyAppPath=context.rootConfig.appsRoot==='.'?`apps/${context.appId}`:undefined;
766
+ const legacyAppPath=rootAppLegacyPath(context.rootConfig,context.appId);
761
767
  const appPath=appRelativeRoot(context.rootConfig,context.appId);
762
768
  const entryPath=appPackagePath(context,context.config.entry);
763
769
  const inspected=await inspectContext(context,{signal});
764
- const navigation=context.rootConfig.appsRoot==='.'?rootAppNavigation(
770
+ const navigation=legacyAppPath?rootAppNavigation(
765
771
  context.appId,context.config.entry,inspected.browserDocuments.map(document=>document.path)
766
772
  ):[];
767
773
  if(options.dryRun){
package/src/toolchain.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  import {loadArcaneIntegratedProvider} from './integrated-provider-loader.mjs';
13
13
  import {startDevServer} from './dev-server.mjs';
14
14
  import {applyPwaEntryReferences,generateImportMap,readApplicationTestImportMapContext} from './import-map.mjs';
15
- import {rootAppNavigation} from './app-layout.mjs';
15
+ import {rootAppLegacyPath,rootAppNavigation} from './app-layout.mjs';
16
16
  import {createPwaArtifacts} from './pwa.mjs';
17
17
  import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
18
18
  import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
@@ -327,7 +327,16 @@ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperat
327
327
  async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent}){
328
328
  const {workspaceRoot,appId}=prepared;
329
329
  const manifest=prepared.validation.app.manifest;
330
- const navigation=rootAppNavigation(appId,manifest.entry,inspected.browserDocuments.map(document=>document.path));
330
+ const legacyAppPath = rootAppLegacyPath(prepared.validation.config, appId);
331
+ const navigation = legacyAppPath ? rootAppNavigation(
332
+ appId,
333
+ manifest.entry,
334
+ inspected.browserDocuments.map(
335
+ function rootBrowserDocumentPath(document) {
336
+ return document.path;
337
+ }
338
+ )
339
+ ) : [];
331
340
  // These aliases belong to the SDK only after generation; retained app files stay authored.
332
341
  for(const redirect of navigation){
333
342
  throwIfAborted(signal);
@@ -343,9 +352,17 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
343
352
  const entry=`/${manifest.entry.split('/').map(encodeURIComponent).join('/')}`;
344
353
  const navigationAliases={
345
354
  '/':entry,
346
- [`/apps/${appId}`]:entry,
347
- [`/apps/${appId}/`]:entry,
348
- ...Object.fromEntries(navigation.map(redirect=>[`/${redirect.path}`,redirect.target]))
355
+ ...(legacyAppPath ? {
356
+ [`/${legacyAppPath}`]:entry,
357
+ [`/${legacyAppPath}/`]:entry,
358
+ ...Object.fromEntries(
359
+ navigation.map(
360
+ function rootNavigationAlias(redirect) {
361
+ return [`/${redirect.path}`,redirect.target];
362
+ }
363
+ )
364
+ )
365
+ } : {})
349
366
  };
350
367
  // The source host serves the installed files in place. There is no runtime projection.
351
368
  const files=[...new Set([
@@ -361,7 +378,7 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
361
378
  basePath:'/',
362
379
  appBase:'/',
363
380
  installationId:`/apps/${appId}/`,
364
- legacyAppPath:`apps/${appId}`,
381
+ legacyAppPath,
365
382
  runtimeBase:installed.browserRuntimeBase,
366
383
  mode:'development',
367
384
  navigationAliases