arcane-os 0.54.1 → 0.55.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 +31 -0
- package/README.md +31 -26
- package/browser-runtime/core/client.mjs +491 -0
- package/browser-runtime/core/contracts.mjs +84 -0
- package/docs/architecture.md +31 -23
- package/docs/compatibility.md +10 -1
- package/docs/platform-targets.md +20 -11
- package/docs/reference/README.md +8 -6
- package/docs/reference/availability-and-normalization.md +4 -2
- package/docs/reference/behavioral-testing.md +7 -6
- package/docs/reference/cli.md +12 -4
- package/docs/reference/core-client.md +120 -0
- package/docs/reference/core-native-packaging.md +149 -0
- package/docs/reference/core-runtime.md +175 -0
- package/docs/reference/inventory/package-api.json +257 -5
- package/docs/reference/protocols.md +16 -1
- package/docs/reference/sdk-api.md +772 -15
- package/package.json +10 -3
- package/schemas/native-build-plan.schema.json +8 -0
- package/src/cli/main.mjs +8 -6
- package/src/core/host.mjs +23 -0
- package/src/core/runtime.mjs +333 -0
- package/src/core/stdio.mjs +209 -0
- package/src/core-classic-source.mjs +43 -0
- package/src/import-map.mjs +62 -0
- package/src/native/portable-layout.mjs +184 -0
- package/src/native/portable-provider.mjs +211 -0
- package/src/native-plan.mjs +10 -4
- package/src/native-provider-loader.mjs +15 -4
- package/src/packager/core.mjs +47 -18
- package/src/targets/index.mjs +3 -1
- package/src/toolchain.mjs +20 -5
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate the native host's classic script from the canonical ESM client.
|
|
5
|
+
* The two owned modules have declaration exports and two explicit imports;
|
|
6
|
+
* this projection is deliberately not a general JavaScript bundler.
|
|
7
|
+
*/
|
|
8
|
+
export async function createCoreClassicSource({eventOwnerModuleURL}={}){
|
|
9
|
+
if(typeof eventOwnerModuleURL!=='string'||!eventOwnerModuleURL){
|
|
10
|
+
throw new TypeError('The classic Core client needs the served SDK event-manager module URL.');
|
|
11
|
+
}
|
|
12
|
+
const [contracts,client]=await Promise.all([
|
|
13
|
+
readFile(new URL('../browser-runtime/core/contracts.mjs',import.meta.url),'utf8'),
|
|
14
|
+
readFile(new URL('../browser-runtime/core/client.mjs',import.meta.url),'utf8')
|
|
15
|
+
]);
|
|
16
|
+
const declarations=contracts.replace(/^export (?=(?:const|class|function) )/gmu,'');
|
|
17
|
+
const implementation=client
|
|
18
|
+
.replace("import {arcaneEvents} from '../event-manager.mjs';",'')
|
|
19
|
+
.replace("import {CORE_PROTOCOL,CORE_READY_EVENTS,CoreError,serializeCoreError} from './contracts.mjs';",'')
|
|
20
|
+
.replace(/^export (?=function )/gmu,'');
|
|
21
|
+
return `(function installArcaneCoreClassic(global){
|
|
22
|
+
'use strict';
|
|
23
|
+
const arcaneEvents=global.arcaneEvents??null;
|
|
24
|
+
${declarations}
|
|
25
|
+
${implementation}
|
|
26
|
+
if(global[CORE_CLIENT_KEY])return;
|
|
27
|
+
function loadSharedEventOwner(){
|
|
28
|
+
return import(${JSON.stringify(eventOwnerModuleURL)}).then(function sharedEventOwner(namespace){return namespace.arcaneEvents;});
|
|
29
|
+
}
|
|
30
|
+
// Document-created native injection precedes the page's managed import map.
|
|
31
|
+
// Only the module import waits for parsing; facade/RPC setup below is immediate.
|
|
32
|
+
const eventOwnerReady=arcaneEvents?Promise.resolve(arcaneEvents)
|
|
33
|
+
:global.document?.readyState==='loading'
|
|
34
|
+
?new Promise(function afterImportMap(resolve,reject){
|
|
35
|
+
global.document.addEventListener('DOMContentLoaded',function connectSharedEvents(){
|
|
36
|
+
loadSharedEventOwner().then(resolve,reject);
|
|
37
|
+
},{once:true});
|
|
38
|
+
})
|
|
39
|
+
:loadSharedEventOwner();
|
|
40
|
+
installCoreClient(global,{eventOwner:arcaneEvents,eventOwnerReady});
|
|
41
|
+
})(globalThis);
|
|
42
|
+
`;
|
|
43
|
+
}
|
package/src/import-map.mjs
CHANGED
|
@@ -42,6 +42,8 @@ const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
|
|
|
42
42
|
const SDK_BROWSER_SELF_IMPORTS=new Map([
|
|
43
43
|
['arcane-os/event-manager',SDK_BROWSER_ENTRY],
|
|
44
44
|
['arcane-os/websocket-client','sdk/websocket-client.mjs'],
|
|
45
|
+
['arcane-os/core/client','sdk/core/client.mjs'],
|
|
46
|
+
['arcane-os/core/contracts','sdk/core/contracts.mjs'],
|
|
45
47
|
['arcane-os/logging','sdk/logging.mjs'],
|
|
46
48
|
['arcane-os/browser-device','sdk/browser-device.mjs'],
|
|
47
49
|
['arcane-os/pwa','sdk/pwa.mjs'],
|
|
@@ -2087,6 +2089,66 @@ function scanHtmlStructure(html){
|
|
|
2087
2089
|
return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
|
|
2088
2090
|
}
|
|
2089
2091
|
|
|
2092
|
+
/** Remove generated PWA references from a selected output document without changing authored content. */
|
|
2093
|
+
export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {}) {
|
|
2094
|
+
if (!is.string(html)) throw new TypeError('PWA entry HTML must be a string.');
|
|
2095
|
+
const structure = scanHtmlStructure(html);
|
|
2096
|
+
const manifest = new URL(manifestUrl, documentUrl);
|
|
2097
|
+
const base = structure.bases.find(
|
|
2098
|
+
function baseWithHref(element) {
|
|
2099
|
+
return parseTagAttributes(element.open).has('href');
|
|
2100
|
+
}
|
|
2101
|
+
);
|
|
2102
|
+
const baseHref = base ? structuralAttribute(parseTagAttributes(base.open), 'href', 'base') : null;
|
|
2103
|
+
const resourceBase = baseHref ? new URL(baseHref, documentUrl) : documentUrl;
|
|
2104
|
+
const edits = [];
|
|
2105
|
+
for (const link of structure.links) {
|
|
2106
|
+
const attributes = parseTagAttributes(link.open);
|
|
2107
|
+
const href = htmlAttributeView(attributes.get('href') ?? '');
|
|
2108
|
+
const rel = htmlAttributeView(attributes.get('rel') ?? '');
|
|
2109
|
+
if (!href?.decoded || !rel) continue;
|
|
2110
|
+
let target;
|
|
2111
|
+
try {
|
|
2112
|
+
target = new URL(href.decoded, resourceBase);
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
// An unrelated non-URL reference stays under its authored owner.
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
if (target.origin !== manifest.origin || target.pathname !== manifest.pathname) continue;
|
|
2118
|
+
const tokens = [...rel.decoded.matchAll(/[^\t\n\f\r ]+/gu)];
|
|
2119
|
+
const manifestTokens = tokens.filter(
|
|
2120
|
+
function manifestRelationship(token) {
|
|
2121
|
+
return asciiLower(token[0]) === 'manifest';
|
|
2122
|
+
}
|
|
2123
|
+
);
|
|
2124
|
+
if (manifestTokens.length === 0) continue;
|
|
2125
|
+
if (manifestTokens.length === tokens.length) {
|
|
2126
|
+
edits.push(
|
|
2127
|
+
{start: link.start, end: link.end, value: ''}
|
|
2128
|
+
);
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
const position = attributes.positions.get('rel');
|
|
2132
|
+
for (const token of manifestTokens) {
|
|
2133
|
+
edits.push(
|
|
2134
|
+
{
|
|
2135
|
+
start: link.start + position.start + rel.positions[token.index],
|
|
2136
|
+
end: link.start + position.start + rel.positions[token.index + token[0].length],
|
|
2137
|
+
value: ''
|
|
2138
|
+
}
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
for (const script of structure.scripts) {
|
|
2143
|
+
if (parseTagAttributes(script.open).has('data-arcane-pwa')) {
|
|
2144
|
+
edits.push(
|
|
2145
|
+
{start: script.start, end: script.end, value: ''}
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return applyReferenceEdits(html, edits);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2090
2152
|
/** Update only active PWA entry references; registration never orders application startup. */
|
|
2091
2153
|
export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl}={}){
|
|
2092
2154
|
if(!is.string(html))throw new TypeError('PWA entry HTML must be a string.');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {mkdir, readFile, readdir, realpath, stat, writeFile} from 'node:fs/promises';
|
|
2
|
+
import {createRequire} from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import Is from 'strong-type';
|
|
6
|
+
import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
|
|
7
|
+
|
|
8
|
+
const is = new Is(false);
|
|
9
|
+
export const SDK_ROOT = fileURLToPath(new URL('../../', import.meta.url));
|
|
10
|
+
export const CORE_FILES = [
|
|
11
|
+
'src/core/host.mjs',
|
|
12
|
+
'src/core/runtime.mjs',
|
|
13
|
+
'src/core/stdio.mjs',
|
|
14
|
+
'src/event-manager.mjs',
|
|
15
|
+
'src/dom-event-instrumentation.mjs',
|
|
16
|
+
'browser-runtime/core/contracts.mjs',
|
|
17
|
+
'LICENSE'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function outputFile(root, relative) {
|
|
21
|
+
if (!is.string(relative) || !relative || path.isAbsolute(relative)) {
|
|
22
|
+
throw new ArcaneError(ERROR_CODES.usage, 'A selected output file needs a relative path.');
|
|
23
|
+
}
|
|
24
|
+
const destination = path.resolve(root, relative);
|
|
25
|
+
const within = path.relative(root, destination);
|
|
26
|
+
if (!within || within === '..' || within.startsWith(`..${path.sep}`) || path.isAbsolute(within)) {
|
|
27
|
+
throw new ArcaneError(ERROR_CODES.usage, `The selected file leaves its output directory: ${relative}.`);
|
|
28
|
+
}
|
|
29
|
+
return destination;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function writeOutput(root, relative, content, files, signal) {
|
|
33
|
+
throwIfAborted(signal);
|
|
34
|
+
const destination = outputFile(root, relative);
|
|
35
|
+
await mkdir(path.dirname(destination), {recursive: true});
|
|
36
|
+
await writeFile(destination, content, {flag: 'wx', signal});
|
|
37
|
+
files.push(relative.split(path.sep).join('/'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function copyRelease(selection, destination, root, files, signal) {
|
|
41
|
+
for (const relative of selection.release.files) {
|
|
42
|
+
throwIfAborted(signal);
|
|
43
|
+
// The supplied reader owns release selection. No text decoding, URL
|
|
44
|
+
// rewriting or line-ending conversion occurs at native assembly.
|
|
45
|
+
const content = is.function(selection.readReleaseFile)
|
|
46
|
+
? await selection.readReleaseFile(relative, {signal})
|
|
47
|
+
: await readFile(outputFile(selection.releaseRoot, relative), {signal});
|
|
48
|
+
outputFile(path.join(root, destination), relative);
|
|
49
|
+
await writeOutput(root, `${destination}/${relative}`, content, files, signal);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function copyPackageTree(source, relative, root, files, signal, preserved) {
|
|
54
|
+
throwIfAborted(signal);
|
|
55
|
+
const sourceRoot = await realpath(source);
|
|
56
|
+
const outputRelative = path.relative(sourceRoot, root);
|
|
57
|
+
if (!outputRelative || (!path.isAbsolute(outputRelative)
|
|
58
|
+
&& outputRelative !== '..' && !outputRelative.startsWith(`..${path.sep}`))) {
|
|
59
|
+
throw new ArcaneError(ERROR_CODES.usage, `Native output would copy itself from ${sourceRoot}.`);
|
|
60
|
+
}
|
|
61
|
+
const entries = await readdir(source, {withFileTypes: true});
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
throwIfAborted(signal);
|
|
64
|
+
const selected = path.join(source, entry.name);
|
|
65
|
+
const destination = `${relative}/${entry.name}`;
|
|
66
|
+
if (preserved.has(destination)) continue;
|
|
67
|
+
const info = await stat(selected);
|
|
68
|
+
if (info.isDirectory()) {
|
|
69
|
+
await copyPackageTree(selected, destination, root, files, signal, preserved);
|
|
70
|
+
} else if (info.isFile()) {
|
|
71
|
+
await writeOutput(root, destination, await readFile(selected, {signal}), files, signal);
|
|
72
|
+
} else {
|
|
73
|
+
throw new ArcaneError(ERROR_CODES.operationFailed, `Cannot copy the runtime package entry: ${selected}.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function copyCoreRuntime(root, files, signal, {
|
|
79
|
+
sourceRoot = SDK_ROOT, sdkDestination = 'node_modules/arcane-os'
|
|
80
|
+
} = {}) {
|
|
81
|
+
const metadata = JSON.parse(await readFile(path.join(sourceRoot, 'package.json'), 'utf8'));
|
|
82
|
+
// A direct browser projection may already occupy this package directory.
|
|
83
|
+
// Complete it from its selected installed package without replacing content.
|
|
84
|
+
const preserved = new Set(files);
|
|
85
|
+
for (const directory of ['bin', 'src', 'browser-runtime', 'runtime', 'schemas']) {
|
|
86
|
+
await copyPackageTree(path.join(sourceRoot, directory), `${sdkDestination}/${directory}`, root, files, signal, preserved);
|
|
87
|
+
}
|
|
88
|
+
for (const relative of ['package.json', 'LICENSE', 'COMMERCIAL-LICENSE.md', 'NOTICE', 'README.md', 'CHANGELOG.md']) {
|
|
89
|
+
const destination = `${sdkDestination}/${relative}`;
|
|
90
|
+
if (!preserved.has(destination)) {
|
|
91
|
+
await writeOutput(root, destination, await readFile(path.join(sourceRoot, relative), {signal}), files, signal);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Preserve each dependency's actual installed runtime resolution, including
|
|
96
|
+
// nested versions. Packaging reads published files; it executes no package.
|
|
97
|
+
const copied = new Map([[sdkDestination, await realpath(sourceRoot)]]);
|
|
98
|
+
async function includeDependency(name, from, parentDestination) {
|
|
99
|
+
throwIfAborted(signal);
|
|
100
|
+
const require = createRequire(path.join(from, 'package.json'));
|
|
101
|
+
let packageFile;
|
|
102
|
+
try {
|
|
103
|
+
packageFile = require.resolve(`${name}/package.json`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error;
|
|
106
|
+
let directory = path.dirname(require.resolve(name));
|
|
107
|
+
for (;;) {
|
|
108
|
+
const candidate = path.join(directory, 'package.json');
|
|
109
|
+
try {
|
|
110
|
+
const record = JSON.parse(await readFile(candidate, 'utf8'));
|
|
111
|
+
if (record.name === name) { packageFile = candidate; break; }
|
|
112
|
+
} catch (failure) {
|
|
113
|
+
if (failure.code !== 'ENOENT') throw failure;
|
|
114
|
+
}
|
|
115
|
+
const parent = path.dirname(directory);
|
|
116
|
+
if (parent === directory) throw error;
|
|
117
|
+
directory = parent;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const source = await realpath(path.dirname(packageFile));
|
|
121
|
+
// Reuse only a resolved ancestor package that Node can actually reach.
|
|
122
|
+
// This also terminates ordinary circular package dependencies.
|
|
123
|
+
let ancestor = parentDestination;
|
|
124
|
+
for (;;) {
|
|
125
|
+
const candidate = path.posix.join(ancestor, 'node_modules', name);
|
|
126
|
+
if (copied.has(candidate)) {
|
|
127
|
+
if (copied.get(candidate) === source) return;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
try { await stat(outputFile(root, `${candidate}/package.json`)); break; }
|
|
131
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
132
|
+
if (ancestor === '.') break;
|
|
133
|
+
ancestor = path.posix.dirname(ancestor);
|
|
134
|
+
}
|
|
135
|
+
const destination = `${parentDestination}/node_modules/${name}`;
|
|
136
|
+
const record = JSON.parse(await readFile(packageFile, 'utf8'));
|
|
137
|
+
let included = false;
|
|
138
|
+
try { await stat(outputFile(root, `${destination}/package.json`)); included = true; }
|
|
139
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
140
|
+
if (!included) await copyPackageTree(source, destination, root, files, signal, preserved);
|
|
141
|
+
copied.set(destination, source);
|
|
142
|
+
for (const dependency of Object.keys(record.dependencies ?? {})) {
|
|
143
|
+
await includeDependency(dependency, source, destination);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const dependency of Object.keys(metadata.dependencies ?? {})) {
|
|
147
|
+
await includeDependency(dependency, sourceRoot, sdkDestination);
|
|
148
|
+
}
|
|
149
|
+
return metadata;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function coreEntrySource(application, version, services) {
|
|
153
|
+
const imports = services.map(function serviceImport(service, index) {
|
|
154
|
+
const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
|
|
155
|
+
return `import createService${index} from ${JSON.stringify(specifier)};`;
|
|
156
|
+
});
|
|
157
|
+
const definitions = services.map(function serviceDefinition(service, index) {
|
|
158
|
+
return ` createService${index}(${JSON.stringify(service.options === undefined ? {} : service.options)})`;
|
|
159
|
+
});
|
|
160
|
+
return [
|
|
161
|
+
"import {startCoreHost} from 'arcane-os/core/host';",
|
|
162
|
+
...imports,
|
|
163
|
+
'',
|
|
164
|
+
'const host = startCoreHost({',
|
|
165
|
+
` application: ${JSON.stringify(application)},`,
|
|
166
|
+
` version: ${JSON.stringify(version)},`,
|
|
167
|
+
` services: [\n${definitions.join(',\n')}\n ]`,
|
|
168
|
+
'});',
|
|
169
|
+
'',
|
|
170
|
+
'function closeCore() { host.close(); }',
|
|
171
|
+
"process.on('SIGINT', closeCore);",
|
|
172
|
+
"process.on('SIGTERM', closeCore);",
|
|
173
|
+
'try {',
|
|
174
|
+
' await host.closed;',
|
|
175
|
+
'} catch (error) {',
|
|
176
|
+
" console.error('Arcane Core host failed:', error);",
|
|
177
|
+
' process.exitCode = 1;',
|
|
178
|
+
'} finally {',
|
|
179
|
+
" process.off('SIGINT', closeCore);",
|
|
180
|
+
" process.off('SIGTERM', closeCore);",
|
|
181
|
+
'}',
|
|
182
|
+
''
|
|
183
|
+
].join('\n');
|
|
184
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {mkdir, mkdtemp, readFile, realpath, rm, stat} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Is from 'strong-type';
|
|
4
|
+
import {createCoreClassicSource} from '../core-classic-source.mjs';
|
|
5
|
+
import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
|
|
6
|
+
import {SDK_ROOT, CORE_FILES, outputFile, writeOutput, copyRelease, copyCoreRuntime, coreEntrySource} from './portable-layout.mjs';
|
|
7
|
+
|
|
8
|
+
const is = new Is(false);
|
|
9
|
+
const PROTOCOL = 'arcane-native-builder/1';
|
|
10
|
+
const MANIFEST = 'arcane-native.json';
|
|
11
|
+
|
|
12
|
+
function portableTarget(value) {
|
|
13
|
+
if (value?.target !== 'portable') {
|
|
14
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable, 'This provider assembles portable Core payloads only.');
|
|
15
|
+
}
|
|
16
|
+
return {...value};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function emit(onEvent, event) {
|
|
20
|
+
if (is.function(onEvent)) await onEvent(event);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function contains(root, candidate) {
|
|
24
|
+
const relative = path.relative(root, candidate);
|
|
25
|
+
return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function futureRealPath(location) {
|
|
29
|
+
try { return await realpath(location); }
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code !== 'ENOENT') throw error;
|
|
32
|
+
const parent = path.dirname(location);
|
|
33
|
+
if (parent === location) throw error;
|
|
34
|
+
return path.join(await futureRealPath(parent), path.basename(location));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function outputDirectory(request) {
|
|
39
|
+
if (!is.string(request.outputRoot) || !request.outputRoot) {
|
|
40
|
+
throw new ArcaneError(ERROR_CODES.usage, 'Portable assembly requires an outputRoot.');
|
|
41
|
+
}
|
|
42
|
+
const output = await futureRealPath(path.resolve(request.outputRoot));
|
|
43
|
+
const protectedRoots = [SDK_ROOT, request.appReleaseRoot,
|
|
44
|
+
...(request.dependencies?.map(function dependencyRoot(item) { return item.releaseRoot; }) ?? []),
|
|
45
|
+
...(request.protectedRoots ?? [])];
|
|
46
|
+
for (const location of protectedRoots) {
|
|
47
|
+
if (!location) continue;
|
|
48
|
+
const root = await futureRealPath(path.resolve(location));
|
|
49
|
+
// SDK/release roots must never be replaced by generated output. A
|
|
50
|
+
// caller may select its ordinary project-local dist descendant.
|
|
51
|
+
if (contains(output, root) || (location !== SDK_ROOT && contains(root, output))) {
|
|
52
|
+
throw new ArcaneError(ERROR_CODES.usage, `Portable output overlaps selected source: ${location}.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await mkdir(output, {recursive: true});
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function serviceSelection(services, release) {
|
|
60
|
+
return services.map(function selectService(service) {
|
|
61
|
+
if (!is.string(service?.module) || !release.files.includes(service.module)) {
|
|
62
|
+
throw new ArcaneError(ERROR_CODES.prerequisiteMissing, `The selected release does not contain service module ${String(service?.module)}.`);
|
|
63
|
+
}
|
|
64
|
+
return {module: service.module, options: service.options === undefined ? {} : service.options};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function eventOwnerPath(release) {
|
|
69
|
+
const paths = release.files.filter(function sdkEventModule(relative) {
|
|
70
|
+
return relative === 'sdk/event-manager.mjs' || relative.endsWith('/sdk/event-manager.mjs')
|
|
71
|
+
|| /^node_modules\/(?:@[^/]+\/)?[^/]+\/browser-runtime\/event-manager\.mjs$/u.test(relative);
|
|
72
|
+
});
|
|
73
|
+
if (paths.length !== 1) {
|
|
74
|
+
throw new ArcaneError(ERROR_CODES.prerequisiteMissing,
|
|
75
|
+
'The portable app release must select one shared SDK event-manager module.',
|
|
76
|
+
{details: {paths}});
|
|
77
|
+
}
|
|
78
|
+
// Native hosts mount the complete app directory at one local URL origin.
|
|
79
|
+
// An origin-root URL keeps nested navigable documents on the same owner.
|
|
80
|
+
return '/' + paths[0].split('/').map(encodeURIComponent).join('/');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Package-owned assembly; never imports an OS checkout or application service. */
|
|
84
|
+
export function createPortableNativeProvider({services = []} = {}) {
|
|
85
|
+
return {
|
|
86
|
+
protocol: PROTOCOL,
|
|
87
|
+
async describe() {
|
|
88
|
+
return {
|
|
89
|
+
protocol: PROTOCOL,
|
|
90
|
+
id: 'arcane-sdk-portable',
|
|
91
|
+
targets: ['portable'],
|
|
92
|
+
executable: false,
|
|
93
|
+
serviceConfiguration: 'explicit release module factories'
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
async doctor({targetRequest, signal, onEvent} = {}) {
|
|
97
|
+
throwIfAborted(signal);
|
|
98
|
+
portableTarget(targetRequest);
|
|
99
|
+
const missing = [];
|
|
100
|
+
for (const relative of CORE_FILES) {
|
|
101
|
+
try { await stat(path.join(SDK_ROOT, relative)); }
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code !== 'ENOENT') throw error;
|
|
104
|
+
missing.push(relative);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const result = {ready: missing.length === 0, target: 'portable', sdkRoot: SDK_ROOT, missing, executable: false};
|
|
108
|
+
await emit(onEvent, {type: 'native.doctor.completed', ...result});
|
|
109
|
+
return result;
|
|
110
|
+
},
|
|
111
|
+
async prepare({targetRequest, signal, onEvent} = {}) {
|
|
112
|
+
throwIfAborted(signal);
|
|
113
|
+
const target = portableTarget(targetRequest);
|
|
114
|
+
const metadata = JSON.parse(await readFile(path.join(SDK_ROOT, 'package.json'), 'utf8'));
|
|
115
|
+
const result = {sdkRoot: SDK_ROOT, sdkVersion: metadata.version, target, executable: false};
|
|
116
|
+
await emit(onEvent, {type: 'native.prepared', target: 'portable', sdkVersion: metadata.version});
|
|
117
|
+
return result;
|
|
118
|
+
},
|
|
119
|
+
async build(request = {}) {
|
|
120
|
+
const {appDescriptor, release, signal, onEvent} = request;
|
|
121
|
+
throwIfAborted(signal);
|
|
122
|
+
const target = portableTarget(request.targetRequest);
|
|
123
|
+
const selectedServices = serviceSelection(services, release);
|
|
124
|
+
const eventOwnerModuleURL = eventOwnerPath(release);
|
|
125
|
+
const output = await outputDirectory(request);
|
|
126
|
+
throwIfAborted(signal);
|
|
127
|
+
// Unique assembly directories preserve earlier outputs and avoid
|
|
128
|
+
// overwriting an app's files or a concurrent packaging operation.
|
|
129
|
+
const root = await mkdtemp(path.join(output, 'arcane-portable-'));
|
|
130
|
+
const files = [];
|
|
131
|
+
try {
|
|
132
|
+
await emit(onEvent, {type: 'native.payload.started', appId: appDescriptor.id, outputRoot: root});
|
|
133
|
+
await copyRelease({...request, releaseRoot: request.appReleaseRoot}, 'app', root, files, signal);
|
|
134
|
+
const dependencies = [];
|
|
135
|
+
for (const [index, dependency] of (request.dependencies ?? []).entries()) {
|
|
136
|
+
const directory = `dependencies/${index}`;
|
|
137
|
+
await copyRelease(dependency, directory, root, files, signal);
|
|
138
|
+
dependencies.push({appId: dependency.appId, root: directory, release: dependency.release});
|
|
139
|
+
}
|
|
140
|
+
const sdk = await copyCoreRuntime(root, files, signal);
|
|
141
|
+
const selectedSdk = request.selectedSdk;
|
|
142
|
+
let appSdk;
|
|
143
|
+
if (selectedSdk && release.files.some(function selectedPackageFile(relative) {
|
|
144
|
+
return relative.startsWith(`${selectedSdk.packageSource}/`);
|
|
145
|
+
})) {
|
|
146
|
+
const destination = `app/${selectedSdk.packageSource}`;
|
|
147
|
+
let metadata;
|
|
148
|
+
if (release.files.includes(`${selectedSdk.packageSource}/package.json`)) {
|
|
149
|
+
metadata = JSON.parse(await readFile(outputFile(root, `${destination}/package.json`), 'utf8'));
|
|
150
|
+
} else {
|
|
151
|
+
metadata = await copyCoreRuntime(root, files, signal, {
|
|
152
|
+
sourceRoot: selectedSdk.packageRoot, sdkDestination: destination
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
appSdk = {name: metadata.name, version: metadata.version, root: destination};
|
|
156
|
+
}
|
|
157
|
+
await writeOutput(root, 'package.json', '{"type":"module"}\n', files, signal);
|
|
158
|
+
await writeOutput(root, 'runtime/arcane-core.mjs', coreEntrySource(appDescriptor, sdk.version, selectedServices), files, signal);
|
|
159
|
+
await writeOutput(root, 'runtime/arcane-api.js', await createCoreClassicSource({eventOwnerModuleURL}), files, signal);
|
|
160
|
+
const manifest = {
|
|
161
|
+
schemaVersion: 1,
|
|
162
|
+
kind: 'arcane-portable-native',
|
|
163
|
+
sdk: {name: sdk.name, version: sdk.version},
|
|
164
|
+
...(appSdk ? {appSdk} : {}),
|
|
165
|
+
app: appDescriptor,
|
|
166
|
+
target,
|
|
167
|
+
webRoot: 'app',
|
|
168
|
+
start: release.manifest?.app?.start ?? './index.html',
|
|
169
|
+
core: {entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices},
|
|
170
|
+
client: {source: 'runtime/arcane-api.js', injection: 'document-created', eventOwnerModuleURL},
|
|
171
|
+
dependencies,
|
|
172
|
+
minimumCoreVersion: request.minimumCoreVersion ?? null,
|
|
173
|
+
files: [...files, MANIFEST]
|
|
174
|
+
};
|
|
175
|
+
await writeOutput(root, MANIFEST, JSON.stringify(manifest, null, 2) + '\n', files, signal);
|
|
176
|
+
throwIfAborted(signal);
|
|
177
|
+
await emit(onEvent, {type: 'native.payload.completed', appId: appDescriptor.id, outputRoot: root});
|
|
178
|
+
throwIfAborted(signal);
|
|
179
|
+
return {app: appDescriptor, target: {...target, rootDir: root}, manifest};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
try { await rm(root, {recursive: true, force: true}); }
|
|
182
|
+
catch (cleanupError) {
|
|
183
|
+
throw new AggregateError([error, cleanupError], `Portable assembly failed; its incomplete output remains at ${root}.`);
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
async verify({artifact, targetRequest, signal, onEvent} = {}) {
|
|
189
|
+
throwIfAborted(signal);
|
|
190
|
+
portableTarget(targetRequest);
|
|
191
|
+
const root = artifact?.target?.rootDir;
|
|
192
|
+
if (!is.string(root) || !root) throw new ArcaneError(ERROR_CODES.usage, 'Select a portable artifact root.');
|
|
193
|
+
const manifest = JSON.parse(await readFile(path.join(root, MANIFEST), 'utf8'));
|
|
194
|
+
for (const relative of manifest.files) {
|
|
195
|
+
throwIfAborted(signal);
|
|
196
|
+
const info = await stat(outputFile(root, relative));
|
|
197
|
+
if (!info.isFile()) throw new ArcaneError(ERROR_CODES.operationFailed, `Portable file is unavailable: ${relative}.`);
|
|
198
|
+
}
|
|
199
|
+
const result = {target: 'portable', outputRoot: root, manifest, executable: false};
|
|
200
|
+
await emit(onEvent, {type: 'native.verified', target: 'portable', outputRoot: root});
|
|
201
|
+
return result;
|
|
202
|
+
},
|
|
203
|
+
async run() {
|
|
204
|
+
throw new ArcaneError(ERROR_CODES.nativeRunUnsupported,
|
|
205
|
+
'Portable is a Core/app payload, not a native executable. Select an implemented platform host to launch it.');
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const arcaneNativeBuilderProvider = createPortableNativeProvider();
|
|
211
|
+
export default arcaneNativeBuilderProvider;
|
package/src/native-plan.mjs
CHANGED
|
@@ -91,10 +91,13 @@ function selectedTargetRequest(value){
|
|
|
91
91
|
if(value.format!==definition.format){
|
|
92
92
|
fail(`Target ${value.target} requires format ${definition.format}.`,ERROR_CODES.targetUnavailable);
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
const nestedSigning=isObject(value.signing);
|
|
95
|
+
const signing=nestedSigning?value.signing.mode:value.signing;
|
|
96
|
+
const signingProfileId=(nestedSigning?value.signing.profileId:value.signingProfileId)??null;
|
|
97
|
+
if(signing!==definition.signingMode){
|
|
95
98
|
fail(`Target ${value.target} requires signing mode ${definition.signingMode}.`,ERROR_CODES.targetUnavailable);
|
|
96
99
|
}
|
|
97
|
-
if(
|
|
100
|
+
if(signingProfileId!==definition.signingProfileId){
|
|
98
101
|
fail(`Target ${value.target} has a different signing profile.`,ERROR_CODES.targetUnavailable);
|
|
99
102
|
}
|
|
100
103
|
return {
|
|
@@ -102,8 +105,8 @@ function selectedTargetRequest(value){
|
|
|
102
105
|
platform:value.platform,
|
|
103
106
|
architecture:value.architecture,
|
|
104
107
|
format:value.format,
|
|
105
|
-
signing
|
|
106
|
-
signingProfileId
|
|
108
|
+
signing,
|
|
109
|
+
signingProfileId
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -177,6 +180,7 @@ export async function createNativeBuildPlan({
|
|
|
177
180
|
appReleaseRoot,
|
|
178
181
|
release,
|
|
179
182
|
appDescriptor,
|
|
183
|
+
selectedSdk,
|
|
180
184
|
dependencyReleases,
|
|
181
185
|
minimumCoreVersion,
|
|
182
186
|
protectedRoots=[],
|
|
@@ -211,6 +215,7 @@ export async function createNativeBuildPlan({
|
|
|
211
215
|
toolchainRoot:selectedToolchainRoot,
|
|
212
216
|
toolchain:{...toolchain},
|
|
213
217
|
appDescriptor:descriptor,
|
|
218
|
+
...(selectedSdk===undefined?{}:{selectedSdk}),
|
|
214
219
|
application,
|
|
215
220
|
dependencies,
|
|
216
221
|
minimumCoreVersion:minimumCoreVersion??null,
|
|
@@ -244,6 +249,7 @@ export async function executeNativeBuildPlan(plan,{
|
|
|
244
249
|
toolchainRoot:plan.toolchainRoot,
|
|
245
250
|
toolchain:plan.toolchain,
|
|
246
251
|
appDescriptor:plan.appDescriptor,
|
|
252
|
+
...(plan.selectedSdk===undefined?{}:{selectedSdk:plan.selectedSdk}),
|
|
247
253
|
appReleaseRoot:plan.application.root,
|
|
248
254
|
release:plan.application.release,
|
|
249
255
|
readReleaseFile:plan.application.readFile,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
2
|
import {lstat,realpath} from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {pathToFileURL} from 'node:url';
|
|
4
|
+
import {fileURLToPath,pathToFileURL} from 'node:url';
|
|
5
5
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
6
6
|
import {validateNativeBuilder} from './native-plan.mjs';
|
|
7
7
|
|
|
@@ -106,7 +106,11 @@ async function resolveProviderLocation({
|
|
|
106
106
|
providerPath:canonicalProvider
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
canonicalRoot,
|
|
111
|
+
providerPath:canonicalProvider,
|
|
112
|
+
providerSource:'arcane-checkout'
|
|
113
|
+
};
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
export async function loadArcaneNativeProvider(options={}){
|
|
@@ -129,7 +133,13 @@ export async function loadArcaneNativeProvider(options={}){
|
|
|
129
133
|
target,
|
|
130
134
|
message:`Loading the Arcane ${String(target)} provider.`
|
|
131
135
|
});
|
|
132
|
-
const location=
|
|
136
|
+
const location=arcaneRoot===undefined&&target==='portable'
|
|
137
|
+
?{
|
|
138
|
+
canonicalRoot:path.resolve(fileURLToPath(new URL('../',import.meta.url))),
|
|
139
|
+
providerPath:fileURLToPath(new URL('./native/portable-provider.mjs',import.meta.url)),
|
|
140
|
+
providerSource:'sdk-package'
|
|
141
|
+
}
|
|
142
|
+
:await resolveProviderLocation({arcaneRoot,target,inspect,canonicalize});
|
|
133
143
|
throwIfAborted(signal);
|
|
134
144
|
let namespace;
|
|
135
145
|
try{
|
|
@@ -147,9 +157,10 @@ export async function loadArcaneNativeProvider(options={}){
|
|
|
147
157
|
);
|
|
148
158
|
throwIfAborted(signal);
|
|
149
159
|
const pairing={
|
|
150
|
-
arcaneRoot:location.canonicalRoot,
|
|
160
|
+
arcaneRoot:location.providerSource==='arcane-checkout'?location.canonicalRoot:null,
|
|
151
161
|
toolchainRoot:location.canonicalRoot,
|
|
152
162
|
providerPath:location.providerPath,
|
|
163
|
+
providerSource:location.providerSource,
|
|
153
164
|
nativeBuilder
|
|
154
165
|
};
|
|
155
166
|
await onEvent?.({
|