@fsn.dev/cli 0.0.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ashwin Mahadevan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,159 @@
1
+ import { app, BrowserWindow, protocol, net, session } from 'electron';
2
+ import { pathToFileURL } from 'node:url';
3
+ import path from 'node:path';
4
+
5
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
6
+ const SAME_SITE = { strict: 'strict', lax: 'lax', none: 'no_restriction' };
7
+
8
+ /** Apply one Set-Cookie header from a host response to Electron's cookie store. */
9
+ async function storeSetCookie(cookies, url, header) {
10
+ const [pair, ...attributes] = header.split(';');
11
+ const eq = pair.indexOf('=');
12
+ if (eq <= 0) return;
13
+ const cookie = { url, name: pair.slice(0, eq).trim(), value: pair.slice(eq + 1).trim() };
14
+ let expired = false;
15
+
16
+ for (const attribute of attributes) {
17
+ const [rawKey, ...rest] = attribute.split('=');
18
+ const key = rawKey.trim().toLowerCase();
19
+ const value = rest.join('=').trim();
20
+ if (key === 'path') cookie.path = value;
21
+ else if (key === 'domain') cookie.domain = value;
22
+ else if (key === 'secure') cookie.secure = true;
23
+ else if (key === 'httponly') cookie.httpOnly = true;
24
+ else if (key === 'samesite') cookie.sameSite = SAME_SITE[value.toLowerCase()] ?? 'unspecified';
25
+ else if (key === 'max-age') {
26
+ // Max-Age wins over Expires (RFC 6265 §5.3).
27
+ const seconds = Number(value);
28
+ if (seconds <= 0) expired = true;
29
+ else cookie.expirationDate = Date.now() / 1000 + seconds;
30
+ } else if (key === 'expires' && cookie.expirationDate === undefined) {
31
+ const time = Date.parse(value);
32
+ if (!Number.isNaN(time)) {
33
+ if (time <= Date.now()) expired = true;
34
+ else cookie.expirationDate = time / 1000;
35
+ }
36
+ }
37
+ }
38
+
39
+ try {
40
+ if (expired) {
41
+ const scope = new URL(url);
42
+ if (cookie.path) scope.pathname = cookie.path;
43
+ await cookies.remove(scope.href, cookie.name);
44
+ } else {
45
+ await cookies.set(cookie);
46
+ }
47
+ } catch (err) {
48
+ console.error(`[fsn] couldn't store cookie "${cookie.name}" from ${url}:`, err);
49
+ }
50
+ }
51
+
52
+ app.whenReady().then(async () => {
53
+ // fsn build stages everything under app/ alongside this file.
54
+ const appDir = path.join(app.getAppPath(), 'app');
55
+ const configPath = path.join(appDir, 'fsn.config.js');
56
+ const { default: config } = await import(pathToFileURL(configPath).href);
57
+
58
+ // Where hosts may write at runtime (the app bundle itself is read-only).
59
+ process.env.FSN_DATA_DIR = app.getPath('userData');
60
+
61
+ const hosts = new Map();
62
+ const hostFor = (hostname) =>
63
+ hostname.endsWith('.localhost') ? hosts.get(hostname.slice(0, -'.localhost'.length)) : undefined;
64
+
65
+ // Server code fetching its own origin (e.g. Next.js fetching the target of
66
+ // a server action's redirect()) runs in this process, where requests don't
67
+ // pass through protocol.handle. Send those to the host in-process as well.
68
+ // Installed before hosts load so frameworks that wrap fetch wrap this one.
69
+ const nodeFetch = globalThis.fetch;
70
+ globalThis.fetch = async function fetch(input, init) {
71
+ const href = typeof input === 'string' || input instanceof URL ? String(input) : input.url;
72
+ const url = new URL(href);
73
+ const host = hostFor(url.hostname);
74
+ if (!host) return nodeFetch(input, init);
75
+
76
+ const request = new Request(input, init);
77
+ // Hosts are only served over http; server code may assume https.
78
+ url.protocol = 'http:';
79
+ const response = await host.fetch(new Request(url, {
80
+ method: request.method,
81
+ headers: request.headers,
82
+ body: request.body,
83
+ duplex: 'half',
84
+ redirect: request.redirect,
85
+ signal: request.signal,
86
+ }));
87
+
88
+ const location = response.headers.get('location');
89
+ if (request.redirect !== 'follow' || !REDIRECT_STATUSES.has(response.status) || !location) {
90
+ return response;
91
+ }
92
+ // Follow redirects like fetch would; 303 (and 301/302 for POST) become GET.
93
+ const keepMethod = response.status === 307 || response.status === 308 ||
94
+ (request.method !== 'POST' && response.status !== 303);
95
+ return globalThis.fetch(new URL(location, url), keepMethod ? {
96
+ method: request.method, headers: request.headers, body: request.body, duplex: 'half',
97
+ } : { headers: request.headers });
98
+ };
99
+
100
+ // fsn build stages each host at app/<name>/, with a generated
101
+ // fsn.host.mjs that default-exports { fetch(request) => Response }.
102
+ for (const name of Object.keys(config.hosts)) {
103
+ const hostPath = path.join(appDir, name, 'fsn.host.mjs');
104
+ const { default: host } = await import(pathToFileURL(hostPath).href);
105
+ hosts.set(name, host);
106
+ }
107
+
108
+ // Requests to http://<name>.localhost go straight to that host in-process:
109
+ // no port, no socket, not reachable from any other process.
110
+ //
111
+ // Chromium hands intercepted requests over without the headers its network
112
+ // stack would add (Cookie, Origin, Referer), and ignores Set-Cookie on the
113
+ // responses. So cookies go through the session's cookie store here, and
114
+ // Origin/Referer are rebuilt from request.referrer, which Chromium sets and
115
+ // a page can only shorten, never forge.
116
+ const cookies = session.defaultSession.cookies;
117
+ protocol.handle('http', async (request) => {
118
+ const host = hostFor(new URL(request.url).hostname);
119
+ if (!host) return net.fetch(request, { bypassCustomProtocolHandlers: true });
120
+
121
+ const headers = new Headers(request.headers);
122
+ const jar = await cookies.get({ url: request.url });
123
+ if (jar.length > 0) headers.set('cookie', jar.map((c) => `${c.name}=${c.value}`).join('; '));
124
+ if (request.referrer && request.referrer !== 'about:client') {
125
+ headers.set('referer', request.referrer);
126
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
127
+ headers.set('origin', new URL(request.referrer).origin);
128
+ }
129
+ }
130
+
131
+ const response = await host.fetch(new Request(request.url, {
132
+ method: request.method,
133
+ headers,
134
+ body: request.body,
135
+ duplex: 'half',
136
+ redirect: 'manual',
137
+ signal: request.signal,
138
+ }));
139
+
140
+ for (const header of response.headers.getSetCookie()) {
141
+ await storeSetCookie(cookies, request.url, header);
142
+ }
143
+ return response;
144
+ });
145
+
146
+ for (const name of Object.keys(config.hosts)) {
147
+ const win = new BrowserWindow({
148
+ width: 1024,
149
+ height: 768,
150
+ title: name,
151
+ webPreferences: { sandbox: true },
152
+ });
153
+ win.loadURL(`http://${name}.localhost/`);
154
+ }
155
+
156
+ app.on('window-all-closed', () => {
157
+ if (process.platform !== 'darwin') app.quit();
158
+ });
159
+ });
@@ -0,0 +1,51 @@
1
+ // FSN cache handler for Next.js (`cacheHandler` in the runtime config).
2
+ //
3
+ // Next's default FileSystemCache writes ISR / revalidation results into
4
+ // `.next/`, which in a packaged app sits inside the read-only app.asar. This
5
+ // is the same cache with an overlay filesystem: writes go to a per-host,
6
+ // per-build directory under Electron's userData; reads look there first and
7
+ // fall back to the build-time files in `.next/`.
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const FileSystemCache = require('next/dist/server/lib/incremental-cache/file-system-cache').default;
12
+
13
+ const hostDir = __dirname;
14
+ const nextDir = path.join(hostDir, '.next');
15
+
16
+ function cacheDir() {
17
+ const dataDir = process.env.FSN_DATA_DIR;
18
+ if (!dataDir) return null;
19
+ // Namespace by build so an app update never serves the previous build's pages.
20
+ const buildId = fs.readFileSync(path.join(nextDir, 'BUILD_ID'), 'utf8').trim();
21
+ return path.join(dataDir, 'next-cache', path.basename(hostDir), buildId);
22
+ }
23
+
24
+ /** A CacheFs whose writes land in `overlayDir` and whose reads prefer it. */
25
+ function overlayFs(baseFs, overlayDir) {
26
+ const toOverlay = (file) => {
27
+ const rel = path.relative(nextDir, file);
28
+ return rel.startsWith('..') || path.isAbsolute(rel) ? null : path.join(overlayDir, rel);
29
+ };
30
+ const preferOverlay = (file) => {
31
+ const overlay = toOverlay(file);
32
+ return overlay && fs.existsSync(overlay) ? overlay : file;
33
+ };
34
+ return {
35
+ existsSync: (file) => fs.existsSync(preferOverlay(file)),
36
+ readFile: (file, ...rest) => baseFs.readFile(preferOverlay(file), ...rest),
37
+ readFileSync: (file, ...rest) => baseFs.readFileSync(preferOverlay(file), ...rest),
38
+ stat: (file) => baseFs.stat(preferOverlay(file)),
39
+ writeFile: (file, data) => baseFs.writeFile(toOverlay(file) ?? file, data),
40
+ mkdir: (dir) => baseFs.mkdir(toOverlay(dir) ?? dir),
41
+ };
42
+ }
43
+
44
+ class FsnCacheHandler extends FileSystemCache {
45
+ constructor(ctx) {
46
+ const dir = cacheDir();
47
+ super(dir && ctx.fs ? { ...ctx, fs: overlayFs(ctx.fs, dir) } : ctx);
48
+ }
49
+ }
50
+
51
+ module.exports = FsnCacheHandler;
@@ -0,0 +1,123 @@
1
+ // FSN host for Next.js, written by fsn build next to the app's .next/.
2
+ // Default export: { fetch(request: Request): Promise<Response> }.
3
+ //
4
+ // Next.js only exposes a Node-style (req, res) handler, so this wraps it in an
5
+ // http.Server that never listens and feeds it each Request over an in-memory
6
+ // socket pair. Node's own HTTP parser does the req/res plumbing.
7
+
8
+ import { readFileSync } from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import path from 'node:path';
11
+ import http from 'node:http';
12
+ import { Duplex, Readable } from 'node:stream';
13
+ import next from 'next';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+
17
+ // Boot from the config Next serialized at build time, the way the standalone
18
+ // server.js does: next()'s `conf` option is ignored by the router server it
19
+ // starts, but __NEXT_PRIVATE_STANDALONE_CONFIG isn't. next.config.* isn't
20
+ // loaded at runtime.
21
+ const { config } = JSON.parse(
22
+ readFileSync(path.join(__dirname, '.next', 'required-server-files.json'), 'utf8')
23
+ );
24
+ // fsn build stages a cache handler that writes ISR results to userData
25
+ // instead of the read-only app bundle.
26
+ config.cacheHandler = path.join(__dirname, 'fsn.cache-handler.cjs');
27
+
28
+ process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config);
29
+ const nextApp = next({ dev: false, dir: __dirname });
30
+ await nextApp.prepare();
31
+ // Next caches the loaded config per app directory; don't leak it to other hosts.
32
+ delete process.env.__NEXT_PRIVATE_STANDALONE_CONFIG;
33
+ const handle = nextApp.getRequestHandler();
34
+
35
+ const server = http.createServer((req, res) => handle(req, res));
36
+
37
+ /** A Duplex that passes for a net.Socket; writes go to its peer. */
38
+ class InProcessSocket extends Duplex {
39
+ constructor() {
40
+ super({ allowHalfOpen: true });
41
+ this.peer = null;
42
+ this.connecting = false;
43
+ this.pending = false;
44
+ this.encrypted = false;
45
+ this.remoteAddress = '127.0.0.1';
46
+ this.remotePort = 0;
47
+ this.localAddress = '127.0.0.1';
48
+ this.localPort = 0;
49
+ this.bytesRead = 0;
50
+ this.bytesWritten = 0;
51
+ }
52
+
53
+ _read() {}
54
+
55
+ _write(chunk, _encoding, callback) {
56
+ this.bytesWritten += chunk.length;
57
+ this.peer.push(chunk);
58
+ callback();
59
+ }
60
+
61
+ setKeepAlive() { return this; }
62
+ setNoDelay() { return this; }
63
+ setTimeout() { return this; }
64
+ ref() { return this; }
65
+ unref() { return this; }
66
+ address() { return { address: '127.0.0.1', family: 'IPv4', port: 0 }; }
67
+
68
+ destroySoon() {
69
+ if (this.writable) this.end();
70
+ if (this.writableFinished) this.destroy();
71
+ else this.once('finish', () => this.destroy());
72
+ }
73
+ }
74
+
75
+ function createSocketPair() {
76
+ const a = new InProcessSocket();
77
+ const b = new InProcessSocket();
78
+ a.peer = b;
79
+ b.peer = a;
80
+ a.on('finish', () => b.push(null));
81
+ b.on('finish', () => a.push(null));
82
+ return [a, b];
83
+ }
84
+
85
+ export default {
86
+ /** @param {Request} request */
87
+ fetch(request) {
88
+ const url = new URL(request.url);
89
+ return new Promise((resolve, reject) => {
90
+ const [serverSocket, clientSocket] = createSocketPair();
91
+ server.emit('connection', serverSocket);
92
+
93
+ const nodeRequest = http.request({
94
+ createConnection: (_options, callback) => {
95
+ process.nextTick(callback, null, clientSocket);
96
+ return clientSocket;
97
+ },
98
+ method: request.method,
99
+ path: url.pathname + url.search,
100
+ // A fetch Request has no Host header; without one Node sends
101
+ // `localhost`, and Next builds its own origin (e.g. for server action
102
+ // redirects) from it.
103
+ headers: { ...Object.fromEntries(request.headers), host: url.host },
104
+ }, (nodeResponse) => {
105
+ const headers = new Headers();
106
+ for (const [key, value] of Object.entries(nodeResponse.headers)) {
107
+ if (Array.isArray(value)) value.forEach((v) => headers.append(key, v));
108
+ else if (value != null) headers.set(key, value);
109
+ }
110
+ const status = nodeResponse.statusCode ?? 200;
111
+ // Response() rejects a body for HEAD and null-body statuses.
112
+ const hasBody = request.method !== 'HEAD' && ![101, 204, 205, 304].includes(status);
113
+ if (!hasBody) nodeResponse.resume();
114
+ resolve(new Response(hasBody ? Readable.toWeb(nodeResponse) : null, { status, headers }));
115
+ });
116
+
117
+ nodeRequest.on('error', reject);
118
+
119
+ if (request.body) Readable.fromWeb(request.body).pipe(nodeRequest);
120
+ else nodeRequest.end();
121
+ });
122
+ },
123
+ };
@@ -0,0 +1,4 @@
1
+ // FSN host for SvelteKit, written by fsn build next to the app's build/.
2
+ // build/ is emitted by @fsn.dev/adapter-sveltekit and default-exports
3
+ // { fetch(request: Request): Promise<Response> }.
4
+ export { default } from './build/index.js';
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { build } from './build.js';
3
+ import { init } from './init.js';
4
+ const FRAMEWORKS = new Set(['sveltekit', 'nextjs']);
5
+ const USAGE = `fsn — Full-Stack Native
6
+
7
+ Commands:
8
+ fsn init [--project <dir>] [--framework <sveltekit|nextjs>]
9
+ Scaffold a new FSN project.
10
+ --project defaults to "." (must be empty),
11
+ --framework defaults to nextjs.
12
+ fsn build Build the current project into a native app.
13
+
14
+ Docs: https://fsn.dev/cli
15
+ `;
16
+ try {
17
+ if (process.argv[2] === '--help' || process.argv[2] === '-h' || process.argv.length === 2) {
18
+ process.stdout.write(USAGE);
19
+ }
20
+ else if (process.argv[2] === 'build' && process.argv.length === 3) {
21
+ await build();
22
+ }
23
+ else if (process.argv[2] === 'init') {
24
+ let project = '.';
25
+ let framework = 'nextjs';
26
+ for (let i = 3; i < process.argv.length; i += 2) {
27
+ const flag = process.argv[i];
28
+ if (flag !== '--project' && flag !== '--framework') {
29
+ throw new Error(`Unknown argument "${flag}".\n\n${USAGE}`);
30
+ }
31
+ const value = process.argv[i + 1];
32
+ if (value === undefined) {
33
+ throw new Error(`Missing value for ${flag}.`);
34
+ }
35
+ if (flag === '--project')
36
+ project = value;
37
+ else
38
+ framework = value;
39
+ }
40
+ if (!FRAMEWORKS.has(framework)) {
41
+ throw new Error(`Unknown framework "${framework}". Expected one of: ${[...FRAMEWORKS].join(', ')}`);
42
+ }
43
+ await init(project, framework);
44
+ }
45
+ else {
46
+ process.stderr.write(USAGE);
47
+ process.exit(1);
48
+ }
49
+ }
50
+ catch (err) {
51
+ process.stderr.write('[fsn] error: ' + (err instanceof Error && err.stack ? err.stack : String(err)) + '\n');
52
+ process.exit(1);
53
+ }
54
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEpD,MAAM,KAAK,GAAG;;;;;;;;;;CAUb,CAAC;AAEF,IAAI,CAAC;IACH,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;SAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpE,MAAM,KAAK,EAAE,CAAC;IAChB,CAAC;SAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;QACtC,IAAI,OAAO,GAAG,GAAG,CAAC;QAClB,IAAI,SAAS,GAAG,QAAQ,CAAC;QACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAChD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;gBACnD,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,SAAS,KAAK,EAAE,CAAC,CAAC;YAC7D,CAAC;YACD,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAClC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,GAAG,CAAC,CAAC;YAChD,CAAC;YACD,IAAI,IAAI,KAAK,WAAW;gBAAE,OAAO,GAAG,KAAK,CAAC;;gBACrC,SAAS,GAAG,KAAK,CAAC;QACzB,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,sBAAsB,SAAS,uBAAuB,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtG,CAAC;QACD,MAAM,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjC,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAAC,OAAO,GAAG,EAAE,CAAC;IACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,GAAG,CAAC,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC7G,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Build a single named host in the current FSN project.
3
+ * Reads fsn.config.js to locate the host directory, installs dependencies
4
+ * if needed, then runs `pnpm build`.
5
+ *
6
+ * @param name Key in config.hosts, e.g. "sveltekit"
7
+ */
8
+ export declare function buildHost(name: string): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { log, exists, run } from './utils.js';
3
+ import { loadConfig } from './config.js';
4
+ /**
5
+ * Build a single named host in the current FSN project.
6
+ * Reads fsn.config.js to locate the host directory, installs dependencies
7
+ * if needed, then runs `pnpm build`.
8
+ *
9
+ * @param name Key in config.hosts, e.g. "sveltekit"
10
+ */
11
+ export async function buildHost(name) {
12
+ const cwd = process.cwd();
13
+ const config = await loadConfig(cwd);
14
+ const hostRelDir = config.hosts[name];
15
+ if (!hostRelDir) {
16
+ throw new Error(`Host "${name}" not found in fsn.config.js. Available: ${Object.keys(config.hosts).join(', ')}`);
17
+ }
18
+ const hostDir = resolve(cwd, hostRelDir);
19
+ log(`building host "${name}" in ${hostDir}`);
20
+ if (!await exists(join(hostDir, 'node_modules'))) {
21
+ await run('pnpm', ['install'], { cwd: hostDir });
22
+ }
23
+ await run('pnpm', ['build'], { cwd: hostDir });
24
+ log(`✓ host "${name}" built`);
25
+ }
26
+ //# sourceMappingURL=build-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-host.js","sourceRoot":"","sources":["../src/build-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,4CAA4C,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnH,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACzC,GAAG,CAAC,kBAAkB,IAAI,QAAQ,OAAO,EAAE,CAAC,CAAC;IAE7C,IAAI,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;QACjD,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAC/C,GAAG,CAAC,WAAW,IAAI,SAAS,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function build(): Promise<void>;
package/dist/build.js ADDED
@@ -0,0 +1,98 @@
1
+ import { mkdir, mkdtemp, writeFile, readFile, readdir, lstat, rm, cp, copyFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { log, exists, run, readAsset } from './utils.js';
5
+ import { loadConfig } from './config.js';
6
+ import { buildHost } from './build-host.js';
7
+ import { packageApp } from './package.js';
8
+ export async function build() {
9
+ const cwd = process.cwd();
10
+ const config = await loadConfig(cwd);
11
+ // Make sure every host has been built before staging them.
12
+ for (const name of Object.keys(config.hosts)) {
13
+ await buildHost(name);
14
+ }
15
+ const workdir = await mkdtemp(join(tmpdir(), 'fsn-build-'));
16
+ log(`workdir: ${workdir}`);
17
+ const appDir = join(workdir, 'app');
18
+ await mkdir(appDir, { recursive: true });
19
+ for (const [name, hostRelDir] of Object.entries(config.hosts)) {
20
+ const hostDir = resolve(cwd, hostRelDir);
21
+ const appHostDir = join(appDir, name);
22
+ await mkdir(appHostDir, { recursive: true });
23
+ const hostPkg = JSON.parse(await readFile(join(hostDir, 'package.json'), 'utf8'));
24
+ const hostDeps = { ...hostPkg.dependencies, ...hostPkg.devDependencies };
25
+ const isSvelteKit = '@sveltejs/kit' in hostDeps;
26
+ const isNextjs = 'next' in hostDeps;
27
+ if (!isSvelteKit && !isNextjs) {
28
+ throw new Error(`Cannot detect framework for host "${name}" at ${hostDir}: ` +
29
+ `expected @sveltejs/kit or next in its package.json.`);
30
+ }
31
+ // Production dependencies (only `dependencies`, flattened).
32
+ await deployProd(appHostDir, { filter: hostPkg.name, cwd });
33
+ if (isSvelteKit) {
34
+ // @fsn.dev/adapter-sveltekit's build/: the server bundled with its
35
+ // devDependencies, plus client assets and prerendered pages.
36
+ const buildDir = join(hostDir, 'build');
37
+ if (!await exists(join(buildDir, 'index.js'))) {
38
+ throw new Error(`Host "${name}" has no build/index.js. Is @fsn.dev/adapter-sveltekit ` +
39
+ `its SvelteKit adapter?`);
40
+ }
41
+ await cp(buildDir, join(appHostDir, 'build'), { recursive: true });
42
+ }
43
+ else {
44
+ await layerNextjsBuild(hostDir, appHostDir);
45
+ }
46
+ // The entry the main process loads: { fetch(request) => Response }.
47
+ // .mjs, since the host's package.json may not declare "type": "module".
48
+ const framework = isSvelteKit ? 'sveltekit' : 'nextjs';
49
+ await writeFile(join(appHostDir, 'fsn.host.mjs'), await readAsset(`fsn.host.${framework}.js`));
50
+ }
51
+ // Stage fsn.config.js so the main process can read hosts at runtime.
52
+ await copyFile(join(cwd, 'fsn.config.js'), join(appDir, 'fsn.config.js'));
53
+ // Write the Electron main, which routes window requests to each host.
54
+ await writeFile(join(workdir, 'main.js'), await readAsset('application.template.js'));
55
+ await packageApp({ workdir, cwd, main: 'main.js' });
56
+ }
57
+ /**
58
+ * Install a host package's production dependencies into `dest`.
59
+ *
60
+ * pnpm's isolated install leaves node_modules full of symlinks into .pnpm/ —
61
+ * those break require() once the tree is asar-bundled. `pnpm deploy` rewrites
62
+ * the tree with all prod deps fully inlined and devDeps stripped, using
63
+ * --node-linker=hoisted so node_modules is flat (no .pnpm subdir). It also
64
+ * copies the package's published files; build output is layered on after.
65
+ *
66
+ * @param options
67
+ * `filter` is the package name for pnpm --filter; `cwd` is a directory
68
+ * inside the pnpm workspace that contains it.
69
+ */
70
+ async function deployProd(dest, { filter, cwd }) {
71
+ // Non-legacy deploy needs a shared workspace lockfile (fsn init projects
72
+ // have one). Without one, fall back to --legacy — which newer pnpm (12)
73
+ // ignores when a lockfile exists, silently installing nothing.
74
+ const legacy = await exists(join(cwd, 'pnpm-lock.yaml')) ? [] : ['--legacy'];
75
+ await run('pnpm', ['--filter', filter, 'deploy', '--prod', ...legacy, '--config.node-linker=hoisted', dest], { cwd });
76
+ // Hoisted deploy leaves symlinks to sibling workspace packages in
77
+ // node_modules; once staged elsewhere they dangle and break packaging.
78
+ const nodeModules = join(dest, 'node_modules');
79
+ if (!await exists(nodeModules))
80
+ return;
81
+ for (const entry of await readdir(nodeModules)) {
82
+ const p = join(nodeModules, entry);
83
+ if ((await lstat(p)).isSymbolicLink() && !await exists(p))
84
+ await rm(p);
85
+ }
86
+ }
87
+ /**
88
+ * Copy a built Next.js app's build output into `dest`. The host boots from
89
+ * the config serialized in .next/, so no source or next.config.* is needed.
90
+ */
91
+ async function layerNextjsBuild(srcDir, dest) {
92
+ await cp(join(srcDir, '.next'), join(dest, '.next'), { recursive: true });
93
+ if (await exists(join(srcDir, 'public'))) {
94
+ await cp(join(srcDir, 'public'), join(dest, 'public'), { recursive: true });
95
+ }
96
+ await writeFile(join(dest, 'fsn.cache-handler.cjs'), await readAsset('fsn.cache-handler.nextjs.cjs'));
97
+ }
98
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Config } from './index.js';
2
+ /** Load and validate fsn.config.js from a project directory. */
3
+ export declare function loadConfig(cwd: string): Promise<Config>;
package/dist/config.js ADDED
@@ -0,0 +1,26 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { exists } from './utils.js';
4
+ /** Load and validate fsn.config.js from a project directory. */
5
+ export async function loadConfig(cwd) {
6
+ const configPath = join(cwd, 'fsn.config.js');
7
+ if (!await exists(configPath)) {
8
+ throw new Error(`fsn.config.js not found in ${cwd}`);
9
+ }
10
+ const mod = await import(pathToFileURL(configPath).href);
11
+ const config = mod.default || mod;
12
+ if (!config) {
13
+ throw new Error(`fsn.config.js must default-export a Config object`);
14
+ }
15
+ if (typeof config.hosts !== 'object' || config.hosts === null || Object.keys(config.hosts).length === 0) {
16
+ throw new Error(`fsn.config.js: hosts must be a non-empty object mapping names to package directories`);
17
+ }
18
+ for (const [name, hostPath] of Object.entries(config.hosts)) {
19
+ if (typeof hostPath !== 'string' || !await exists(join(resolve(cwd, hostPath), 'package.json'))) {
20
+ throw new Error(`fsn.config.js: hosts.${name} must be the path to a package directory ` +
21
+ `(e.g. './native'), but ${JSON.stringify(hostPath)} has no package.json.`);
22
+ }
23
+ }
24
+ return config;
25
+ }
26
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGpC,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAW;IAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC;IAClC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxG,MAAM,IAAI,KAAK,CAAC,sFAAsF,CAAC,CAAC;IAC1G,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5D,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;YAChG,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,2CAA2C;gBACvE,0BAA0B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,uBAAuB,CAC1E,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,14 @@
1
+ export type Config = {
2
+ /**
3
+ * Host name → path (relative to fsn.config.js) of a SvelteKit or Next.js
4
+ * package. Each host gets a window at `http://<host-name>.localhost/`.
5
+ */
6
+ hosts: Record<string, string>;
7
+ };
8
+ /**
9
+ * What `fsn build` generates for each host (app/<name>/fsn.host.mjs). The
10
+ * FSN main process passes it each request made to `http://<name>.localhost`.
11
+ */
12
+ export type Host = {
13
+ fetch(request: Request): Response | Promise<Response>;
14
+ };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function init(projectName: string, framework: string): Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,121 @@
1
+ import { readdir, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
2
+ import { resolve, join, basename } from 'node:path';
3
+ import { log, exists, run } from './utils.js';
4
+ export async function init(projectName, framework) {
5
+ const projectDir = resolve(projectName);
6
+ if (await exists(projectDir)) {
7
+ const entries = await readdir(projectDir);
8
+ if (entries.length > 0) {
9
+ throw new Error(`${projectDir} already exists and is not empty.`);
10
+ }
11
+ }
12
+ else {
13
+ await mkdir(projectDir, { recursive: true });
14
+ }
15
+ if (framework === 'sveltekit') {
16
+ await scaffoldSveltekit(projectDir);
17
+ }
18
+ else if (framework === 'nextjs') {
19
+ await scaffoldNextjs(projectDir);
20
+ }
21
+ await writeWorkspaceFiles(projectDir);
22
+ log(`done. Next:`);
23
+ if (projectDir !== process.cwd())
24
+ log(` cd ${projectName}`);
25
+ log(` pnpm install`);
26
+ log(` pnpm build`);
27
+ }
28
+ async function scaffoldSveltekit(projectDir) {
29
+ // sv create scaffolds into ./<dir>; we run it with cwd=projectDir so the
30
+ // skeleton lands in projectDir/native.
31
+ await run('pnpm', [
32
+ 'dlx',
33
+ 'sv@latest',
34
+ 'create',
35
+ 'native',
36
+ '--template', 'demo',
37
+ '--types', 'ts',
38
+ '--no-add-ons',
39
+ '--no-install',
40
+ '--no-dir-check',
41
+ '--no-download-check',
42
+ ], { cwd: projectDir });
43
+ // Swap the default adapter-auto for FSN's adapter, which builds the app
44
+ // into a host the Electron main process calls in-process — this is the
45
+ // whole point of FSN: ship fullstack frameworks, not static exports.
46
+ const nativeDir = join(projectDir, 'native');
47
+ // Newer sv templates put the kit config in vite.config.ts; older ones use
48
+ // svelte.config.js. Patch whichever imports adapter-auto.
49
+ let patched = false;
50
+ for (const file of ['svelte.config.js', 'vite.config.ts', 'vite.config.js']) {
51
+ const configPath = join(nativeDir, file);
52
+ if (!await exists(configPath))
53
+ continue;
54
+ const source = await readFile(configPath, 'utf8');
55
+ if (!source.includes(`'@sveltejs/adapter-auto'`))
56
+ continue;
57
+ await writeFile(configPath, source.replace(`'@sveltejs/adapter-auto'`, `'@fsn.dev/adapter-sveltekit'`));
58
+ patched = true;
59
+ }
60
+ if (!patched) {
61
+ throw new Error(`Couldn't find the @sveltejs/adapter-auto import in ${nativeDir} to replace.`);
62
+ }
63
+ const pkgPath = join(nativeDir, 'package.json');
64
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
65
+ delete pkg.devDependencies['@sveltejs/adapter-auto'];
66
+ pkg.devDependencies['@fsn.dev/adapter-sveltekit'] = '*';
67
+ await writeFile(pkgPath, JSON.stringify(pkg, null, '\t') + '\n');
68
+ await removeNestedWorkspace(nativeDir);
69
+ }
70
+ async function scaffoldNextjs(projectDir) {
71
+ // create-next-app doesn't accept a relative path with trailing components;
72
+ // running it with cwd=projectDir lands the project in projectDir/native.
73
+ await run('pnpm', [
74
+ 'dlx',
75
+ 'create-next-app@latest',
76
+ 'native',
77
+ '--yes',
78
+ '--ts',
79
+ '--eslint',
80
+ '--app',
81
+ '--src-dir',
82
+ '--no-tailwind',
83
+ '--import-alias', '@/*',
84
+ '--use-pnpm',
85
+ '--skip-install',
86
+ '--disable-git',
87
+ ], { cwd: projectDir });
88
+ const nativeDir = join(projectDir, 'native');
89
+ await removeNestedWorkspace(nativeDir);
90
+ }
91
+ /**
92
+ * create-next-app and sv both drop a pnpm-workspace.yaml inside the package
93
+ * to configure build scripts. Our parent workspace already controls those via
94
+ * `onlyBuiltDependencies`, and a nested workspace yaml confuses pnpm.
95
+ */
96
+ async function removeNestedWorkspace(nativeDir) {
97
+ const nestedWs = join(nativeDir, 'pnpm-workspace.yaml');
98
+ if (await exists(nestedWs))
99
+ await rm(nestedWs);
100
+ }
101
+ async function writeWorkspaceFiles(projectDir) {
102
+ await writeFile(join(projectDir, 'pnpm-workspace.yaml'),
103
+ // Both templates pull in sharp (Next.js directly, SvelteKit via
104
+ // enhanced-img), whose build script pnpm 10+ refuses to run unless it's
105
+ // explicitly approved.
106
+ `packages:\n - native\n\nonlyBuiltDependencies:\n - electron\n\nallowBuilds:\n sharp: true\n`);
107
+ await writeFile(join(projectDir, 'package.json'), JSON.stringify({
108
+ name: basename(projectDir),
109
+ private: true,
110
+ version: '0.0.0',
111
+ scripts: {
112
+ build: 'fsn build',
113
+ },
114
+ devDependencies: {
115
+ '@fsn.dev/cli': '*',
116
+ },
117
+ }, null, 2) + '\n');
118
+ await writeFile(join(projectDir, 'fsn.config.js'), `/** @type {import('@fsn.dev/cli').Config} */\nexport default {\n hosts: {\n native: './native',\n },\n};\n`);
119
+ await writeFile(join(projectDir, '.gitignore'), `node_modules\ndist\n.DS_Store\n*.log\n`);
120
+ }
121
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Turn a staged workdir (Electron main file + app/) into a packaged native app
3
+ * and copy it to <cwd>/dist.
4
+ *
5
+ * Writes the Electron app's package.json, installs Electron and
6
+ * @electron/packager, runs the packager for the current platform/arch, then
7
+ * replaces <cwd>/dist with the result. We use npm here (not pnpm) because the
8
+ * workdir is throwaway and npm doesn't gate Electron's post-install download.
9
+ *
10
+ * @param options.workdir Directory containing `main` and `app/`.
11
+ * @param options.cwd The user's project directory.
12
+ * @param options.main Filename of the Electron main process in workdir.
13
+ */
14
+ export declare function packageApp({ workdir, cwd, main }: {
15
+ workdir: string;
16
+ cwd: string;
17
+ main: string;
18
+ }): Promise<void>;
@@ -0,0 +1,63 @@
1
+ import { writeFile, readFile, rm, cp } from 'node:fs/promises';
2
+ import { join, basename } from 'node:path';
3
+ import { log, exists, run } from './utils.js';
4
+ function sanitizeAppName(name) {
5
+ return name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^-+|-+$/g, '') || 'app';
6
+ }
7
+ /**
8
+ * The Electron and @electron/packager versions to build with. They're pinned
9
+ * in this package's devDependencies (exact versions) so builds are
10
+ * reproducible and bumped like any other dependency.
11
+ */
12
+ async function pinnedVersions() {
13
+ const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
14
+ const result = {};
15
+ for (const name of ['electron', '@electron/packager']) {
16
+ const version = pkg.devDependencies?.[name];
17
+ if (typeof version !== 'string' || !/^\d+\.\d+\.\d+/.test(version)) {
18
+ throw new Error(`@fsn.dev/cli is missing a pinned version for ${name}.`);
19
+ }
20
+ result[name] = version;
21
+ }
22
+ return result;
23
+ }
24
+ /**
25
+ * Turn a staged workdir (Electron main file + app/) into a packaged native app
26
+ * and copy it to <cwd>/dist.
27
+ *
28
+ * Writes the Electron app's package.json, installs Electron and
29
+ * @electron/packager, runs the packager for the current platform/arch, then
30
+ * replaces <cwd>/dist with the result. We use npm here (not pnpm) because the
31
+ * workdir is throwaway and npm doesn't gate Electron's post-install download.
32
+ *
33
+ * @param options.workdir Directory containing `main` and `app/`.
34
+ * @param options.cwd The user's project directory.
35
+ * @param options.main Filename of the Electron main process in workdir.
36
+ */
37
+ export async function packageApp({ workdir, cwd, main }) {
38
+ const versions = await pinnedVersions();
39
+ const appName = sanitizeAppName(basename(cwd));
40
+ await writeFile(join(workdir, 'package.json'), JSON.stringify({ name: appName, version: '0.0.0', private: true, type: 'module', main }, null, 2) + '\n');
41
+ await run('npm', ['install', '--no-audit', '--no-fund', '--save-dev',
42
+ `electron@${versions.electron}`,
43
+ `@electron/packager@${versions['@electron/packager']}`,
44
+ ], { cwd: workdir });
45
+ const platform = process.platform === 'darwin' ? 'darwin'
46
+ : process.platform === 'win32' ? 'win32' : 'linux';
47
+ await run('npx', ['--no-install', '@electron/packager', '.', appName,
48
+ '--out', 'dist',
49
+ '--overwrite',
50
+ '--platform', platform,
51
+ '--arch', process.arch,
52
+ ], { cwd: workdir });
53
+ const projectDist = join(cwd, 'dist');
54
+ if (await exists(projectDist)) {
55
+ await rm(projectDist, { recursive: true, force: true });
56
+ }
57
+ // verbatimSymlinks: keep the .app's relative framework symlinks intact.
58
+ // Without it, fs.cp rewrites them to absolute paths into the temp workdir,
59
+ // which breaks the packaged app (e.g. Electron can't find icudtl.dat).
60
+ await cp(join(workdir, 'dist'), projectDist, { recursive: true, verbatimSymlinks: true });
61
+ log(`✓ Built native app at ${projectDist}`);
62
+ }
63
+ //# sourceMappingURL=package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.js","sourceRoot":"","sources":["../src/package.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export declare function log(...args: unknown[]): void;
2
+ export declare const exists: (p: string) => Promise<boolean>;
3
+ export declare function run(cmd: string, args: string[], options?: {
4
+ cwd?: string;
5
+ }): Promise<void>;
6
+ export declare function readAsset(name: string): Promise<string>;
package/dist/utils.js ADDED
@@ -0,0 +1,21 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ export function log(...args) {
4
+ process.stderr.write('[fsn] ' + args.join(' ') + '\n');
5
+ }
6
+ export const exists = (p) => access(p).then(() => true, () => false);
7
+ export async function run(cmd, args, options = {}) {
8
+ log('$', cmd, args.join(' '), options.cwd ? `(cwd=${options.cwd})` : '');
9
+ const child = spawn(cmd, args, { stdio: 'inherit', shell: false, ...options });
10
+ const code = await new Promise((resolve, reject) => {
11
+ child.on('close', resolve);
12
+ child.on('error', reject);
13
+ });
14
+ if (code !== 0) {
15
+ throw new Error(`${cmd} ${args.join(' ')} exited with code ${code}`);
16
+ }
17
+ }
18
+ export function readAsset(name) {
19
+ return readFile(new URL('../assets/' + name, import.meta.url), 'utf8');
20
+ }
21
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,UAAU,GAAG,CAAC,GAAG,IAAe;IACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,CAAS,EAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;AAE/F,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,GAAW,EAAE,IAAc,EAAE,UAA4B,EAAE;IACnF,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACzE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IAC/E,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAChE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC3B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IACH,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,qBAAqB,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,QAAQ,CAAC,IAAI,GAAG,CAAC,YAAY,GAAG,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;AACzE,CAAC"}
package/package.json CHANGED
@@ -1,14 +1,37 @@
1
1
  {
2
2
  "name": "@fsn.dev/cli",
3
- "version": "0.0.0",
3
+ "description": "FSN: Full-Stack Native. Use any full-stack framework to create a native app!",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/ashwin-mahadevan/fsn.git"
7
+ "url": "git+https://github.com/ashwin-mahadevan/fsn.git",
8
+ "directory": "packages/cli"
8
9
  },
9
- "author": {
10
- "name": "Ashwin Mahadevan",
11
- "email": "mahadevan@ashwinm.com",
12
- "url": "https://ashwinm.com"
10
+ "homepage": "https://fsn.dev",
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "version": "0.1.0",
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "bin": {
19
+ "fsn": "./dist/bin.js"
20
+ },
21
+ "files": [
22
+ "dist/",
23
+ "assets/"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.9.0",
30
+ "typescript": "^5.9.0",
31
+ "@electron/packager": "20.3.0",
32
+ "electron": "45.2.3"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc"
13
36
  }
14
37
  }
package/mise.toml DELETED
@@ -1,4 +0,0 @@
1
- [tools]
2
- node = "26"
3
- pnpm = "11"
4
-