@objectstack/cli 2.0.6 → 3.0.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/.turbo/turbo-build.log +10 -6
- package/CHANGELOG.md +30 -0
- package/README.md +88 -3
- package/dist/bin.js +691 -425
- package/dist/chunk-CSHQEILI.js +246 -0
- package/dist/chunk-Q74JNWKD.js +248 -0
- package/dist/config-A7BN6UIT.js +11 -0
- package/dist/config-UN34WBHT.js +10 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +641 -382
- package/package.json +9 -9
- package/src/bin.ts +18 -1
- package/src/commands/generate.ts +181 -3
- package/src/commands/plugin.ts +372 -0
- package/src/index.ts +2 -0
- package/src/utils/plugin-commands.ts +163 -0
- package/test/commands.test.ts +6 -0
- package/test/plugin-commands.test.ts +162 -0
- package/test/plugin.test.ts +173 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CLI Command Contribution resolved from a plugin manifest.
|
|
9
|
+
*/
|
|
10
|
+
interface ResolvedCommandContribution {
|
|
11
|
+
/** CLI command name */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Brief description */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Module path to import */
|
|
16
|
+
module?: string;
|
|
17
|
+
/** Source plugin package name */
|
|
18
|
+
pluginName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Discover CLI command contributions from installed plugins.
|
|
23
|
+
*
|
|
24
|
+
* Scans the project's `objectstack.config.ts` for plugins that declare
|
|
25
|
+
* `contributes.commands` in their manifest, then dynamically imports
|
|
26
|
+
* those plugin modules to register Commander.js commands.
|
|
27
|
+
*
|
|
28
|
+
* @param program - The root Commander.js program to register commands on
|
|
29
|
+
*/
|
|
30
|
+
export async function loadPluginCommands(program: Command): Promise<void> {
|
|
31
|
+
let config: Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const loaded = await loadConfig();
|
|
35
|
+
config = loaded.config;
|
|
36
|
+
} catch {
|
|
37
|
+
// No config file found — nothing to load
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const plugins: unknown[] = [
|
|
42
|
+
...((config.plugins as unknown[] | undefined) || []),
|
|
43
|
+
...((config.devPlugins as unknown[] | undefined) || []),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Collect command contributions from plugin manifests
|
|
47
|
+
const contributions: ResolvedCommandContribution[] = [];
|
|
48
|
+
|
|
49
|
+
for (const plugin of plugins) {
|
|
50
|
+
if (!plugin || typeof plugin !== 'object') continue;
|
|
51
|
+
const p = plugin as Record<string, unknown>;
|
|
52
|
+
|
|
53
|
+
const manifest = p.manifest as Record<string, unknown> | undefined;
|
|
54
|
+
const contributes = (manifest?.contributes ?? p.contributes) as Record<string, unknown> | undefined;
|
|
55
|
+
if (!contributes) continue;
|
|
56
|
+
|
|
57
|
+
const commands = contributes.commands as Array<Record<string, unknown>> | undefined;
|
|
58
|
+
if (!Array.isArray(commands)) continue;
|
|
59
|
+
|
|
60
|
+
const pluginName = resolvePluginName(p);
|
|
61
|
+
|
|
62
|
+
for (const cmd of commands) {
|
|
63
|
+
if (!cmd || typeof cmd.name !== 'string') continue;
|
|
64
|
+
contributions.push({
|
|
65
|
+
name: cmd.name,
|
|
66
|
+
description: typeof cmd.description === 'string' ? cmd.description : undefined,
|
|
67
|
+
module: typeof cmd.module === 'string' ? cmd.module : undefined,
|
|
68
|
+
pluginName,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (contributions.length === 0) return;
|
|
74
|
+
|
|
75
|
+
// Load and register each contributed command
|
|
76
|
+
for (const contribution of contributions) {
|
|
77
|
+
try {
|
|
78
|
+
const commands = await importPluginCommands(contribution);
|
|
79
|
+
for (const cmd of commands) {
|
|
80
|
+
program.addCommand(cmd);
|
|
81
|
+
}
|
|
82
|
+
} catch (error: unknown) {
|
|
83
|
+
// Log warning but don't crash — plugin commands are optional
|
|
84
|
+
if (process.env.DEBUG) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
console.error(
|
|
87
|
+
chalk.yellow(` ⚠ Failed to load CLI command '${contribution.name}' from plugin '${contribution.pluginName}': ${message}`)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Import Commander.js commands from a plugin module.
|
|
96
|
+
*
|
|
97
|
+
* The module must export commands in one of these forms:
|
|
98
|
+
* - `export const commands: Command[]`
|
|
99
|
+
* - `export default Command`
|
|
100
|
+
* - `export default Command[]`
|
|
101
|
+
*/
|
|
102
|
+
async function importPluginCommands(
|
|
103
|
+
contribution: ResolvedCommandContribution
|
|
104
|
+
): Promise<Command[]> {
|
|
105
|
+
// Resolve the module specifier
|
|
106
|
+
const moduleId = contribution.module
|
|
107
|
+
? `${contribution.pluginName}/${contribution.module.replace(/^\.\//, '')}`
|
|
108
|
+
: contribution.pluginName;
|
|
109
|
+
|
|
110
|
+
const mod = await import(moduleId);
|
|
111
|
+
|
|
112
|
+
// Form 1: Named export `commands`
|
|
113
|
+
if (Array.isArray(mod.commands)) {
|
|
114
|
+
return mod.commands.filter(isCommandInstance);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Form 2: Default export (single or array)
|
|
118
|
+
const defaultExport = mod.default;
|
|
119
|
+
if (defaultExport) {
|
|
120
|
+
if (Array.isArray(defaultExport)) {
|
|
121
|
+
return defaultExport.filter(isCommandInstance);
|
|
122
|
+
}
|
|
123
|
+
if (isCommandInstance(defaultExport)) {
|
|
124
|
+
return [defaultExport];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fallback: search for any Command instances in module exports
|
|
129
|
+
const commands: Command[] = [];
|
|
130
|
+
for (const key of Object.keys(mod)) {
|
|
131
|
+
if (isCommandInstance(mod[key])) {
|
|
132
|
+
commands.push(mod[key]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return commands;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a value is a Commander.js Command instance.
|
|
141
|
+
* Uses duck-typing to avoid import dependency issues.
|
|
142
|
+
*/
|
|
143
|
+
function isCommandInstance(value: unknown): value is Command {
|
|
144
|
+
if (value === null || typeof value !== 'object') return false;
|
|
145
|
+
const obj = value as Record<string, unknown>;
|
|
146
|
+
return (
|
|
147
|
+
typeof obj.name === 'function' &&
|
|
148
|
+
typeof obj.description === 'function' &&
|
|
149
|
+
typeof obj.action === 'function' &&
|
|
150
|
+
typeof obj.parse === 'function'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a human-readable name from a plugin object.
|
|
156
|
+
*/
|
|
157
|
+
function resolvePluginName(plugin: Record<string, unknown>): string {
|
|
158
|
+
if (typeof plugin.name === 'string') return plugin.name;
|
|
159
|
+
const manifest = plugin.manifest as Record<string, unknown> | undefined;
|
|
160
|
+
if (manifest && typeof manifest.name === 'string') return manifest.name;
|
|
161
|
+
if (plugin.constructor && plugin.constructor.name !== 'Object') return plugin.constructor.name;
|
|
162
|
+
return 'unknown';
|
|
163
|
+
}
|
package/test/commands.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { validateCommand } from '../src/commands/validate';
|
|
|
9
9
|
import { initCommand } from '../src/commands/init';
|
|
10
10
|
import { infoCommand } from '../src/commands/info';
|
|
11
11
|
import { generateCommand } from '../src/commands/generate';
|
|
12
|
+
import { pluginCommand } from '../src/commands/plugin';
|
|
12
13
|
|
|
13
14
|
describe('CLI Commands', () => {
|
|
14
15
|
it('should have compile command', () => {
|
|
@@ -61,4 +62,9 @@ describe('CLI Commands', () => {
|
|
|
61
62
|
expect(generateCommand.alias()).toBe('g');
|
|
62
63
|
expect(generateCommand.description()).toContain('Generate');
|
|
63
64
|
});
|
|
65
|
+
|
|
66
|
+
it('should have plugin command with subcommands', () => {
|
|
67
|
+
expect(pluginCommand.name()).toBe('plugin');
|
|
68
|
+
expect(pluginCommand.description()).toContain('plugin');
|
|
69
|
+
});
|
|
64
70
|
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { loadPluginCommands } from '../src/utils/plugin-commands';
|
|
4
|
+
|
|
5
|
+
// Mock the config loader
|
|
6
|
+
vi.mock('../src/utils/config.js', () => ({
|
|
7
|
+
loadConfig: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { loadConfig } from '../src/utils/config';
|
|
11
|
+
|
|
12
|
+
const mockedLoadConfig = vi.mocked(loadConfig);
|
|
13
|
+
|
|
14
|
+
describe('loadPluginCommands', () => {
|
|
15
|
+
let program: Command;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
program = new Command();
|
|
19
|
+
program.name('objectstack');
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should do nothing when no config file exists', async () => {
|
|
24
|
+
mockedLoadConfig.mockRejectedValue(new Error('No config found'));
|
|
25
|
+
|
|
26
|
+
await loadPluginCommands(program);
|
|
27
|
+
expect(program.commands).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should do nothing when no plugins have command contributions', async () => {
|
|
31
|
+
mockedLoadConfig.mockResolvedValue({
|
|
32
|
+
config: {
|
|
33
|
+
plugins: [
|
|
34
|
+
{ name: 'simple-plugin' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
absolutePath: '/test/objectstack.config.ts',
|
|
38
|
+
duration: 10,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await loadPluginCommands(program);
|
|
42
|
+
expect(program.commands).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should do nothing when plugins array is empty', async () => {
|
|
46
|
+
mockedLoadConfig.mockResolvedValue({
|
|
47
|
+
config: {
|
|
48
|
+
plugins: [],
|
|
49
|
+
},
|
|
50
|
+
absolutePath: '/test/objectstack.config.ts',
|
|
51
|
+
duration: 10,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await loadPluginCommands(program);
|
|
55
|
+
expect(program.commands).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should do nothing when config has no plugins key', async () => {
|
|
59
|
+
mockedLoadConfig.mockResolvedValue({
|
|
60
|
+
config: {},
|
|
61
|
+
absolutePath: '/test/objectstack.config.ts',
|
|
62
|
+
duration: 10,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await loadPluginCommands(program);
|
|
66
|
+
expect(program.commands).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should detect command contributions from plugin manifest', async () => {
|
|
70
|
+
// This test verifies that the contribution detection logic works,
|
|
71
|
+
// even though the dynamic import will fail (no real module to load)
|
|
72
|
+
mockedLoadConfig.mockResolvedValue({
|
|
73
|
+
config: {
|
|
74
|
+
plugins: [
|
|
75
|
+
{
|
|
76
|
+
name: '@acme/plugin-marketplace',
|
|
77
|
+
manifest: {
|
|
78
|
+
contributes: {
|
|
79
|
+
commands: [
|
|
80
|
+
{
|
|
81
|
+
name: 'marketplace',
|
|
82
|
+
description: 'Manage marketplace apps',
|
|
83
|
+
module: './cli',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
absolutePath: '/test/objectstack.config.ts',
|
|
92
|
+
duration: 10,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// loadPluginCommands will try to import the module and fail silently
|
|
96
|
+
// (since no real module exists), but it should not throw
|
|
97
|
+
await loadPluginCommands(program);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should detect contributions from top-level contributes field', async () => {
|
|
101
|
+
mockedLoadConfig.mockResolvedValue({
|
|
102
|
+
config: {
|
|
103
|
+
plugins: [
|
|
104
|
+
{
|
|
105
|
+
name: '@acme/plugin-deploy',
|
|
106
|
+
contributes: {
|
|
107
|
+
commands: [
|
|
108
|
+
{
|
|
109
|
+
name: 'deploy',
|
|
110
|
+
description: 'Deploy to cloud',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
absolutePath: '/test/objectstack.config.ts',
|
|
118
|
+
duration: 10,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Should not throw even if import fails
|
|
122
|
+
await loadPluginCommands(program);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should skip non-object plugins', async () => {
|
|
126
|
+
mockedLoadConfig.mockResolvedValue({
|
|
127
|
+
config: {
|
|
128
|
+
plugins: [
|
|
129
|
+
'some-string-plugin',
|
|
130
|
+
null,
|
|
131
|
+
undefined,
|
|
132
|
+
42,
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
absolutePath: '/test/objectstack.config.ts',
|
|
136
|
+
duration: 10,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await loadPluginCommands(program);
|
|
140
|
+
expect(program.commands).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle plugins with non-array commands gracefully', async () => {
|
|
144
|
+
mockedLoadConfig.mockResolvedValue({
|
|
145
|
+
config: {
|
|
146
|
+
plugins: [
|
|
147
|
+
{
|
|
148
|
+
name: '@acme/bad-plugin',
|
|
149
|
+
contributes: {
|
|
150
|
+
commands: 'not-an-array',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
absolutePath: '/test/objectstack.config.ts',
|
|
156
|
+
duration: 10,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await loadPluginCommands(program);
|
|
160
|
+
expect(program.commands).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { pluginCommand } from '../src/commands/plugin';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
describe('Plugin Command', () => {
|
|
8
|
+
it('should have plugin command with subcommands', () => {
|
|
9
|
+
expect(pluginCommand.name()).toBe('plugin');
|
|
10
|
+
expect(pluginCommand.description()).toContain('plugin');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should have list subcommand with alias', () => {
|
|
14
|
+
const list = pluginCommand.commands.find(c => c.name() === 'list');
|
|
15
|
+
expect(list).toBeDefined();
|
|
16
|
+
expect(list!.alias()).toBe('ls');
|
|
17
|
+
expect(list!.description()).toContain('List');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should have info subcommand', () => {
|
|
21
|
+
const info = pluginCommand.commands.find(c => c.name() === 'info');
|
|
22
|
+
expect(info).toBeDefined();
|
|
23
|
+
expect(info!.description()).toContain('information');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should have add subcommand', () => {
|
|
27
|
+
const add = pluginCommand.commands.find(c => c.name() === 'add');
|
|
28
|
+
expect(add).toBeDefined();
|
|
29
|
+
expect(add!.description()).toContain('Add');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should have remove subcommand with alias', () => {
|
|
33
|
+
const remove = pluginCommand.commands.find(c => c.name() === 'remove');
|
|
34
|
+
expect(remove).toBeDefined();
|
|
35
|
+
expect(remove!.alias()).toBe('rm');
|
|
36
|
+
expect(remove!.description()).toContain('Remove');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('Plugin Config Manipulation', () => {
|
|
41
|
+
let tmpDir: string;
|
|
42
|
+
let configPath: string;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'os-plugin-test-'));
|
|
46
|
+
configPath = path.join(tmpDir, 'objectstack.config.ts');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('addPluginToConfig (via file manipulation)', () => {
|
|
54
|
+
it('should add plugin import and entry to config with existing plugins array', () => {
|
|
55
|
+
fs.writeFileSync(configPath, `import { defineStack } from '@objectstack/spec';
|
|
56
|
+
|
|
57
|
+
export default defineStack({
|
|
58
|
+
manifest: {
|
|
59
|
+
name: 'test-app',
|
|
60
|
+
version: '1.0.0',
|
|
61
|
+
},
|
|
62
|
+
plugins: [
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
`);
|
|
66
|
+
|
|
67
|
+
// Simulate what the add command does
|
|
68
|
+
let content = fs.readFileSync(configPath, 'utf-8');
|
|
69
|
+
const packageName = '@objectstack/plugin-auth';
|
|
70
|
+
const varName = 'authPlugin';
|
|
71
|
+
const importLine = `import ${varName} from '${packageName}';\n`;
|
|
72
|
+
|
|
73
|
+
// Add import after last import
|
|
74
|
+
const importRegex = /^import .+$/gm;
|
|
75
|
+
let lastImportEnd = 0;
|
|
76
|
+
let match: RegExpExecArray | null;
|
|
77
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
78
|
+
lastImportEnd = match.index + match[0].length;
|
|
79
|
+
}
|
|
80
|
+
content = content.slice(0, lastImportEnd) + '\n' + importLine + content.slice(lastImportEnd);
|
|
81
|
+
|
|
82
|
+
// Add to plugins array
|
|
83
|
+
content = content.replace(
|
|
84
|
+
/(plugins\s*:\s*\[)/,
|
|
85
|
+
`$1\n ${varName},`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(configPath, content);
|
|
89
|
+
|
|
90
|
+
const result = fs.readFileSync(configPath, 'utf-8');
|
|
91
|
+
expect(result).toContain("import authPlugin from '@objectstack/plugin-auth'");
|
|
92
|
+
expect(result).toContain('authPlugin,');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should add plugin to config without existing plugins array', () => {
|
|
96
|
+
fs.writeFileSync(configPath, `import { defineStack } from '@objectstack/spec';
|
|
97
|
+
|
|
98
|
+
export default defineStack({
|
|
99
|
+
manifest: {
|
|
100
|
+
name: 'test-app',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
`);
|
|
105
|
+
|
|
106
|
+
let content = fs.readFileSync(configPath, 'utf-8');
|
|
107
|
+
const packageName = '@objectstack/plugin-security';
|
|
108
|
+
const varName = 'securityPlugin';
|
|
109
|
+
const importLine = `import ${varName} from '${packageName}';\n`;
|
|
110
|
+
|
|
111
|
+
// Add import
|
|
112
|
+
const importRegex = /^import .+$/gm;
|
|
113
|
+
let lastImportEnd = 0;
|
|
114
|
+
let match: RegExpExecArray | null;
|
|
115
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
116
|
+
lastImportEnd = match.index + match[0].length;
|
|
117
|
+
}
|
|
118
|
+
content = content.slice(0, lastImportEnd) + '\n' + importLine + content.slice(lastImportEnd);
|
|
119
|
+
|
|
120
|
+
// Add plugins array
|
|
121
|
+
if (!/plugins\s*:\s*\[/.test(content)) {
|
|
122
|
+
content = content.replace(
|
|
123
|
+
/(defineStack\(\{[\s\S]*?)(}\s*\))/,
|
|
124
|
+
`$1 plugins: [\n ${varName},\n ],\n$2`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fs.writeFileSync(configPath, content);
|
|
129
|
+
|
|
130
|
+
const result = fs.readFileSync(configPath, 'utf-8');
|
|
131
|
+
expect(result).toContain("import securityPlugin from '@objectstack/plugin-security'");
|
|
132
|
+
expect(result).toContain('plugins: [');
|
|
133
|
+
expect(result).toContain('securityPlugin,');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('removePluginFromConfig (via file manipulation)', () => {
|
|
138
|
+
it('should remove plugin import and entry from config', () => {
|
|
139
|
+
fs.writeFileSync(configPath, `import { defineStack } from '@objectstack/spec';
|
|
140
|
+
import authPlugin from '@objectstack/plugin-auth';
|
|
141
|
+
|
|
142
|
+
export default defineStack({
|
|
143
|
+
manifest: {
|
|
144
|
+
name: 'test-app',
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
},
|
|
147
|
+
plugins: [
|
|
148
|
+
authPlugin,
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
let content = fs.readFileSync(configPath, 'utf-8');
|
|
154
|
+
const packageName = '@objectstack/plugin-auth';
|
|
155
|
+
|
|
156
|
+
// Remove import
|
|
157
|
+
const importRegex = new RegExp(`^import .+['"]${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"].*$\\n?`, 'gm');
|
|
158
|
+
content = content.replace(importRegex, '');
|
|
159
|
+
|
|
160
|
+
// Remove from plugins array
|
|
161
|
+
content = content.replace(/\s*authPlugin,?\n?/g, '\n');
|
|
162
|
+
|
|
163
|
+
fs.writeFileSync(configPath, content);
|
|
164
|
+
|
|
165
|
+
const result = fs.readFileSync(configPath, 'utf-8');
|
|
166
|
+
expect(result).not.toContain('@objectstack/plugin-auth');
|
|
167
|
+
expect(result).not.toContain('authPlugin');
|
|
168
|
+
// The rest of the config should remain intact
|
|
169
|
+
expect(result).toContain("import { defineStack } from '@objectstack/spec'");
|
|
170
|
+
expect(result).toContain('manifest');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|