arcane-os 0.27.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,17 @@
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
+
3
15
  ## 0.27.0
4
16
 
5
17
  - Add optional standalone `appsRoot: "."` discovery and `new`/`init --apps-root .`.
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.27.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
 
@@ -59,6 +59,18 @@ authored `manifest.id` remains authoritative. Packaged default identity remains
59
59
  fragments and lead to the selected root document. No stored application data is
60
60
  rewritten by changing the layout.
61
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
+
62
74
  `offline.include` and `offline.exclude` select literal paths or directory
63
75
  prefixes from the selected emitted inventory. An omitted or empty include list
64
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.27.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",
@@ -1081,7 +1081,10 @@ async function startOwnedDevServer({
1081
1081
  navigationAliases,
1082
1082
  basePath: '/',
1083
1083
  appBase,
1084
- ...(rootApp ? {installationId:`/apps/${routeSet.appId}/`} : {}),
1084
+ ...(rootApp ? {
1085
+ installationId:`/apps/${routeSet.appId}/`,
1086
+ legacyAppPath:`apps/${routeSet.appId}`
1087
+ } : {}),
1085
1088
  runtimeBase: selectedRoutes.browserRuntimeBase
1086
1089
  ?? selectedRoutes.installed?.browserRuntimeBase ?? '/arcane/sdk/',
1087
1090
  mode: 'development'
@@ -1109,8 +1112,8 @@ async function startOwnedDevServer({
1109
1112
  pwaStates.set(selectedRoutes, state);
1110
1113
  if (selectedRoutes === currentSourceRoutes) currentPwaState = state;
1111
1114
  }
1112
- if (targetPath === '/arcane-sw.js'
1113
- || targetPath === '/arcane-offline.json') {
1115
+ if (targetPath.endsWith('/arcane-sw.js')
1116
+ || targetPath.endsWith('/arcane-offline.json')) {
1114
1117
  if (!state.inventoryTask) {
1115
1118
  // Timestamp selection before traversal, so an older snapshot
1116
1119
  // finishing later cannot acquire a newer HTTP modification date.
@@ -1173,10 +1176,13 @@ async function startOwnedDevServer({
1173
1176
  const target=parseRequestTarget(request.url);
1174
1177
  if(!target){deny(response,400,'Invalid request path.');return;}
1175
1178
  const {segments}=target;
1176
- const generatedPwaPath = ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs']
1177
- .includes(target.path);
1178
1179
  const legacyAppRequest = mode === 'source' && routeSet.config.appsRoot === '.'
1179
1180
  && segments[0] === 'apps' && segments[1] === routeSet.appId;
1181
+ const legacyPwaRequest = legacyAppRequest && segments.length === 3
1182
+ && ['arcane-sw.js', 'arcane-offline.json'].includes(segments[2]);
1183
+ const generatedPwaPath = legacyPwaRequest
1184
+ || ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs']
1185
+ .includes(target.path);
1180
1186
  const requestedMapping = currentSourceRoutes.mappings.find(function currentRequestMapping(route) {
1181
1187
  return route.prefix.every(function currentRequestSegment(segment,index) {
1182
1188
  return segments[index] === segment;
@@ -1186,7 +1192,7 @@ async function startOwnedDevServer({
1186
1192
  const selectedRoutes = mode === 'source' && (appRequest || segments.length === 0 || generatedPwaPath)
1187
1193
  ? await refreshSourceRoutes() : currentSourceRoutes;
1188
1194
  const pwaEnabled = mode === 'source' ? selectedRoutes.app?.pwa?.enabled === true : routeSet.pwa;
1189
- if (legacyAppRequest) {
1195
+ if (legacyAppRequest && !(pwaEnabled && legacyPwaRequest)) {
1190
1196
  const legacyPath = target.pathname.slice(`/apps/${routeSet.appId}`.length);
1191
1197
  const location = !legacyPath || legacyPath === '/' || legacyPath === '/index.html'
1192
1198
  ? selectedRoutes.startPath : legacyPath;
@@ -757,6 +757,7 @@ async function replaceDirectory(stagingRoot,outputRoot){
757
757
  async function packageWithContext(context,options={}){
758
758
  const {signal,onEvent,browserPwa=true}=options;
759
759
  const pwaEnabled=browserPwa&&context.config.pwa?.enabled===true;
760
+ const legacyAppPath=context.rootConfig.appsRoot==='.'?`apps/${context.appId}`:undefined;
760
761
  const appPath=appRelativeRoot(context.rootConfig,context.appId);
761
762
  const entryPath=appPackagePath(context,context.config.entry);
762
763
  const inspected=await inspectContext(context,{signal});
@@ -775,7 +776,11 @@ async function packageWithContext(context,options={}){
775
776
  PWA_MANIFEST_NAME,
776
777
  PWA_OFFLINE_MANIFEST_NAME,
777
778
  PWA_WORKER_NAME,
778
- PWA_BOOTSTRAP_NAME
779
+ PWA_BOOTSTRAP_NAME,
780
+ ...(legacyAppPath?[
781
+ `${legacyAppPath}/${PWA_WORKER_NAME}`,
782
+ `${legacyAppPath}/${PWA_OFFLINE_MANIFEST_NAME}`
783
+ ]:[])
779
784
  ]:[])
780
785
  ].sort(compareText)
781
786
  };
@@ -962,6 +967,7 @@ async function packageWithContext(context,options={}){
962
967
  entry:packageResourceUrl(entryPath)
963
968
  },
964
969
  appPath,
970
+ ...(legacyAppPath?{legacyAppPath}:{}),
965
971
  ...(installed?.direct?{runtimeBase:`.${installed.browserRuntimeBase}`} : {}),
966
972
  ...(navigationAliases?{navigationAliases}:{}),
967
973
  sdkVersion:assetVersion,
@@ -974,7 +980,9 @@ async function packageWithContext(context,options={}){
974
980
  if(inventory.has(artifact.path)){
975
981
  fail(`Package content overlaps generated PWA file: ${artifact.path}.`);
976
982
  }
977
- await writeFile(path.join(stagingRoot,artifact.path),artifact.content,'utf8');
983
+ const artifactPath=path.join(stagingRoot,...artifact.path.split('/'));
984
+ await mkdir(path.dirname(artifactPath),{recursive:true});
985
+ await writeFile(artifactPath,artifact.content,'utf8');
978
986
  files.push(artifact.path);
979
987
  }
980
988
  for(const [documentPath,document] of pwaDocumentSources){
package/src/pwa.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import Is from 'strong-type';
2
2
  import {randomUUID} from 'node:crypto';
3
+ import path from 'node:path';
3
4
  import {createPwaWorkerScript} from './pwa-worker.mjs';
4
5
  import {versionAssetUrl} from './import-map.mjs';
5
6
 
@@ -203,6 +204,7 @@ export function createPwaArtifacts(
203
204
  runtimeBase = './arcane/sdk/',
204
205
  appBase,
205
206
  installationId,
207
+ legacyAppPath,
206
208
  appPath = '',
207
209
  navigationAliases,
208
210
  revision
@@ -305,18 +307,60 @@ controller.ready.catch(
305
307
  const entryAssets = {manifest: PWA_MANIFEST_NAME, bootstrap: PWA_BOOTSTRAP_NAME};
306
308
  const manifestHref = resourceUrl(basePath, entryAssets.manifest);
307
309
  const bootstrapHref = resourceUrl(basePath, entryAssets.bootstrap);
310
+ const generatedFiles = [
311
+ {path: PWA_MANIFEST_NAME, content: json(manifest)},
312
+ {path: PWA_OFFLINE_MANIFEST_NAME, content: json(offlineManifest)},
313
+ {
314
+ path: PWA_WORKER_NAME,
315
+ content: createPwaWorkerScript(offlineManifest, `${runtimeBase}pwa.mjs`)
316
+ },
317
+ {path: PWA_BOOTSTRAP_NAME, content: bootstrap}
318
+ ];
319
+ if (legacyAppPath) {
320
+ const directory = legacyAppPath.endsWith('/') ? legacyAppPath : `${legacyAppPath}/`;
321
+ const priorDirectory = new URL(resourceUrl('./', directory), 'https://arcane.invalid/').pathname;
322
+ // Existing registrations continue updating their own script and inventory URLs.
323
+ function priorScopeUrl(value) {
324
+ if (value.startsWith('/') || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value)) return value;
325
+ const resolved = new URL(value, 'https://arcane.invalid/');
326
+ let relative = path.posix.relative(priorDirectory, resolved.pathname);
327
+ if (!relative) {
328
+ relative = resolved.pathname.endsWith('/')
329
+ ? './'
330
+ : `../${path.posix.basename(resolved.pathname)}`;
331
+ }
332
+ else {
333
+ if (!relative.startsWith('.')) relative = `./${relative}`;
334
+ if (resolved.pathname.endsWith('/') && !relative.endsWith('/')) relative += '/';
335
+ }
336
+ return `${relative}${resolved.search}${resolved.hash}`;
337
+ }
338
+ const legacyOfflineManifest = {
339
+ ...offlineManifest,
340
+ assets: [...new Set([
341
+ ...offlineManifest.assets.map(priorScopeUrl),
342
+ `./${PWA_OFFLINE_MANIFEST_NAME}`
343
+ ])],
344
+ navigationAliases: Object.fromEntries(
345
+ Object.entries(offlineManifest.navigationAliases).map(
346
+ function priorScopeNavigation([alias, target]) {
347
+ return [priorScopeUrl(alias), priorScopeUrl(target)];
348
+ }
349
+ )
350
+ )
351
+ };
352
+ generatedFiles.push(
353
+ {
354
+ path: `${directory}${PWA_WORKER_NAME}`,
355
+ content: createPwaWorkerScript(legacyOfflineManifest, priorScopeUrl(`${runtimeBase}pwa.mjs`))
356
+ },
357
+ {path: `${directory}${PWA_OFFLINE_MANIFEST_NAME}`, content: json(legacyOfflineManifest)}
358
+ );
359
+ }
308
360
  return {
309
361
  manifest,
310
362
  offlineManifest,
311
- files: [
312
- {path: PWA_MANIFEST_NAME, content: json(manifest)},
313
- {path: PWA_OFFLINE_MANIFEST_NAME, content: json(offlineManifest)},
314
- {
315
- path: PWA_WORKER_NAME,
316
- content: createPwaWorkerScript(offlineManifest, `${runtimeBase}pwa.mjs`)
317
- },
318
- {path: PWA_BOOTSTRAP_NAME, content: bootstrap}
319
- ],
363
+ files: generatedFiles,
320
364
  entryAssets,
321
365
  entryMarkup: `<link rel="manifest" href="${htmlAttribute(manifestHref)}">\n`
322
366
  + `<script type="module" async data-arcane-pwa src="${htmlAttribute(bootstrapHref)}"></script>\n`
package/src/toolchain.mjs CHANGED
@@ -361,6 +361,7 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
361
361
  basePath:'/',
362
362
  appBase:'/',
363
363
  installationId:`/apps/${appId}/`,
364
+ legacyAppPath:`apps/${appId}`,
364
365
  runtimeBase:installed.browserRuntimeBase,
365
366
  mode:'development',
366
367
  navigationAliases