Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion bin/jss.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import { Command } from 'commander';
import { createServer } from '../src/server.js';
import { loadConfig, saveConfig, printConfig, defaults } from '../src/config.js';
import { loadConfig, saveConfig, printConfig, defaults, parsePluginFlag } from '../src/config.js';
import { createInvite, listInvites, revokeInvite } from '../src/idp/invites.js';
import { findByUsername, updatePassword, deleteAccount } from '../src/idp/accounts.js';
import { setQuotaLimit, getQuotaInfo, reconcileQuota, formatBytes } from '../src/storage/quota.js';
Expand Down Expand Up @@ -157,6 +157,7 @@ program
.option('--mcp', 'Enable MCP (Model Context Protocol) server at /mcp — pod as a tool surface for agents (#490)')
.option('--no-mcp', 'Disable MCP server')
.option('-q, --quiet', 'Suppress log output')
.option('--plugin <module[@prefix]>', 'Mount an app plugin (repeatable; prefix must start with /). Appends to config-file plugins (#594)', (value, previous) => previous.concat([value]), [])
.option('--log-level <level>', 'Log level: error, warn, info, debug (default: info)')
.option('--print-config', 'Print configuration and exit')
.action(async (options) => {
Expand All @@ -169,6 +170,16 @@ program

const config = await loadConfig(options, options.config);

// --plugin entries APPEND to the config file's plugins rather than
// following the CLI-replaces-file rule — replacing would make -c plus
// one --plugin silently drop the file's declared apps (#594).
if (options.plugin?.length) {
config.plugins = [
...(Array.isArray(config.plugins) ? config.plugins : []),
...options.plugin.map(parsePluginFlag),
];
}

// Set DATA_ROOT env var so all modules use the same data directory
process.env.DATA_ROOT = path.resolve(config.root);

Expand Down
14 changes: 14 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,20 @@ plugins never attach their own `'upgrade'` listener. Return
A plugin that fails to import or activate fails `listen()` loudly rather
than booting a server silently missing an app.

The simple module + prefix case also works straight from the CLI, no
config file needed ([#594](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/594)):

```bash
jss start --root ./data --public \
--plugin './chat/plugin.js@/chat' \
--plugin '@scope/pkg/plugin.js@/app'
```

`--plugin` is repeatable; the prefix separator is the last `@` followed by
`/`, so scoped package names parse unambiguously. CLI entries **append** to
any `plugins` array from the config file (they don't replace it). Per-plugin
`config` objects and explicit `id`s remain config-file-only.

## Storage Quotas

Limit storage per pod to prevent abuse and manage resources:
Expand Down
23 changes: 23 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,29 @@ function loadEnvConfig() {
return config;
}

/**
* Parse one --plugin flag value: module[@prefix] (#594).
*
* The prefix separator is the LAST '@' whose remainder starts with '/',
* so scoped package specifiers parse unambiguously:
* '@scope/pkg/plugin.js@/app' -> { module: '@scope/pkg/plugin.js', prefix: '/app' }
* '@scope/pkg/plugin.js' -> { module: '@scope/pkg/plugin.js' }
* './chat/plugin.js@/chat' -> { module: './chat/plugin.js', prefix: '/chat' }
*
* Per-plugin config objects and explicit ids stay config-file territory;
* entries here go through the loader's usual validation untouched.
*/
export function parsePluginFlag(value) {
const str = String(value);
const at = str.lastIndexOf('@/');
if (at >= 0) {
// at === 0 yields module: '' ('@/app' has no module) — the loader's
// "each entry needs a module" beats a confusing import error.
return { module: str.slice(0, at), prefix: str.slice(at + 1) };
}
return { module: str };
}

/**
* Load configuration from a JSON file
*/
Expand Down
43 changes: 42 additions & 1 deletion test/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert';
import { loadConfig } from '../src/config.js';
import { loadConfig, parsePluginFlag } from '../src/config.js';

describe('config — env var boolean coercion', () => {
// Save/restore the env vars we touch so this test is hermetic.
Expand Down Expand Up @@ -190,3 +190,44 @@ describe('config — singleUserName default (#348)', () => {
}
});
});

describe('config — parsePluginFlag (#594)', () => {
it('parses module with prefix', () => {
assert.deepStrictEqual(parsePluginFlag('./chat/plugin.js@/chat'),
{ module: './chat/plugin.js', prefix: '/chat' });
});

it('parses bare module (no prefix)', () => {
assert.deepStrictEqual(parsePluginFlag('./chat/plugin.js'),
{ module: './chat/plugin.js' });
});

it('parses scoped package with prefix — scope @ is not the separator', () => {
assert.deepStrictEqual(parsePluginFlag('@scope/pkg/plugin.js@/app'),
{ module: '@scope/pkg/plugin.js', prefix: '/app' });
});

it('parses bare scoped package as module only', () => {
assert.deepStrictEqual(parsePluginFlag('@scope/pkg/plugin.js'),
{ module: '@scope/pkg/plugin.js' });
});

it('splits on the LAST @/ when several occur', () => {
assert.deepStrictEqual(parsePluginFlag('pkg@/weird/path.js@/mount'),
{ module: 'pkg@/weird/path.js', prefix: '/mount' });
});

it('leaves a prefix without leading slash to the loader to reject', () => {
// '@chat' has no '@/', so the whole value is the module — the import
// fails loudly rather than mounting somewhere unexpected.
assert.deepStrictEqual(parsePluginFlag('./p.js@chat'),
{ module: './p.js@chat' });
});

it('parses a missing module to module: "" for the loader to reject', () => {
// '@/app' splits at position 0 so the loader raises its clear
// "each entry needs a module" error instead of a confusing import failure.
assert.deepStrictEqual(parsePluginFlag('@/app'),
{ module: '', prefix: '/app' });
});
});