@creatorem/cli 0.0.1 → 1.0.4
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/package.json +26 -5
- package/src/cli.tsx +141 -0
- package/src/commands/create-dashboard.tsx +455 -0
- package/src/commands/create-mobile.tsx +555 -0
- package/src/commands/create.tsx +1119 -0
- package/src/commands/generate-migration.mjs +17 -66
- package/src/commands/generate-migration.tsx +46 -0
- package/src/commands/generate-schemas.mjs +2 -2
- package/src/commands/generate-schemas.tsx +36 -0
- package/src/dashboard-features/ai/index.ts +102 -0
- package/src/dashboard-features/analytics/index.ts +31 -0
- package/src/dashboard-features/billing/index.ts +349 -0
- package/src/dashboard-features/content-type/index.ts +64 -0
- package/src/dashboard-features/email-templates/index.ts +17 -0
- package/src/dashboard-features/emailer/index.ts +27 -0
- package/src/dashboard-features/index.ts +28 -0
- package/src/dashboard-features/keybindings/index.ts +52 -0
- package/src/dashboard-features/manager.ts +349 -0
- package/src/dashboard-features/monitoring/index.ts +16 -0
- package/src/dashboard-features/notification/index.ts +40 -0
- package/src/dashboard-features/onboarding/index.ts +65 -0
- package/src/dashboard-features/organization/index.ts +38 -0
- package/src/dashboard-features/types.ts +41 -0
- package/src/mobile-features/index.ts +12 -0
- package/src/mobile-features/manager.ts +1 -0
- package/src/mobile-features/notification/index.ts +41 -0
- package/src/mobile-features/onboarding/index.ts +35 -0
- package/src/mobile-features/organization/index.ts +38 -0
- package/src/mobile-features/types.ts +1 -0
- package/src/shims/signal-exit.js +9 -0
- package/src/ui/app.tsx +68 -0
- package/src/ui/multi-select.tsx +106 -0
- package/src/utils/ast.ts +422 -0
- package/src/utils/env-template.ts +635 -0
- package/tests/test-cli-features.sh +81 -0
- package/tests/test-cli-mobile.sh +65 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +21 -0
- package/bin/cli.mjs +0 -40
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { FeatureRemover } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const MobileOrganizationFeature: FeatureRemover = {
|
|
4
|
+
key: 'organization',
|
|
5
|
+
cliUI: {
|
|
6
|
+
title: 'Organization',
|
|
7
|
+
features: [
|
|
8
|
+
'Database implementation',
|
|
9
|
+
'Role management',
|
|
10
|
+
'Member management',
|
|
11
|
+
'Built React logic (useOrganization, <OrganizationSwitcher />, ...)'
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
dependenciesToRemove: ['@kit/organization'],
|
|
15
|
+
// Removes `import useOrgFilters from '@kit/organization/native/use-filters'`
|
|
16
|
+
// and the `useOrgFilters()` call from hooks/use-filters.ts
|
|
17
|
+
i18nNamespacePrefix: 'p_org',
|
|
18
|
+
useFilters: 'useOrgFilters',
|
|
19
|
+
// Also removes initOrgServerFilters() from apps/api/lib/init-server-filters.ts
|
|
20
|
+
// when create-mobile provisions the fallback API app.
|
|
21
|
+
serverFilter: 'initOrgServerFilters',
|
|
22
|
+
router: {
|
|
23
|
+
importName: 'organizationRouter',
|
|
24
|
+
importPath: '@kit/organization/router',
|
|
25
|
+
},
|
|
26
|
+
repo: {
|
|
27
|
+
filesToDelete: [
|
|
28
|
+
'supabase/schemas/030-organization-enums.sql',
|
|
29
|
+
'supabase/schemas/031-kit-org.sql',
|
|
30
|
+
'supabase/schemas/032-organization.sql',
|
|
31
|
+
'supabase/schemas/033-organization-roles.sql',
|
|
32
|
+
'supabase/schemas/034-organization-members.sql',
|
|
33
|
+
'supabase/schemas/035-organization-invitations.sql',
|
|
34
|
+
'supabase/schemas/036-organization-settings.sql',
|
|
35
|
+
'supabase/schemas/037-organization-notifications.sql',
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { FeatureRemover } from '../dashboard-features/types.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Bypass the alias by importing from the specific file in node_modules
|
|
2
|
+
// We use a relative path that we hope resolves correctly from dist/
|
|
3
|
+
// But since we are bundling, we need the path relative to THIS file (src/shims/signal-exit.js)
|
|
4
|
+
// AND we need to make sure esbuild doesn't treat it as the aliased package.
|
|
5
|
+
// Using a relative path to node_modules/signal-exit/dist/mjs/index.js usually works.
|
|
6
|
+
import { onExit, load, unload } from '../../node_modules/signal-exit/dist/mjs/index.js';
|
|
7
|
+
|
|
8
|
+
export default onExit;
|
|
9
|
+
export { onExit, load, unload };
|
package/src/ui/app.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, Box } from 'ink';
|
|
3
|
+
import GenerateMigration from '../commands/generate-migration.js';
|
|
4
|
+
import GenerateSchemas from '../commands/generate-schemas.js';
|
|
5
|
+
import CreateDashboard from '../commands/create-dashboard.js';
|
|
6
|
+
import CreateMobile from '../commands/create-mobile.js';
|
|
7
|
+
import Create from '../commands/create.js';
|
|
8
|
+
|
|
9
|
+
interface AppProps {
|
|
10
|
+
command?: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
flags?: Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const App: React.FC<AppProps> = ({ command, args = [], flags = {} }) => {
|
|
16
|
+
if (!command) {
|
|
17
|
+
return (
|
|
18
|
+
<Box flexDirection="column">
|
|
19
|
+
<Text>Please specify a command.</Text>
|
|
20
|
+
<Text>Example: creatorem create-dashboard</Text>
|
|
21
|
+
</Box>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The original code had a 'help' command check.
|
|
26
|
+
// Assuming the new instruction implies 'help' is now handled differently or not needed here.
|
|
27
|
+
// If 'help' is still a command, it would need its own 'if' block.
|
|
28
|
+
// For now, I'll keep the original 'help' output if command is 'help'
|
|
29
|
+
// and the new output if command is undefined.
|
|
30
|
+
|
|
31
|
+
if (command === 'help') {
|
|
32
|
+
return (
|
|
33
|
+
<Box flexDirection="column" padding={1}>
|
|
34
|
+
<Text color="green">Creatorem CLI</Text>
|
|
35
|
+
<Text>Run <Text color="cyan">creatorem --help</Text> to see available commands.</Text>
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (command === 'generate-migration') {
|
|
41
|
+
return <GenerateMigration args={args} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (command === 'generate-schemas') {
|
|
45
|
+
return <GenerateSchemas args={args} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (command === 'create-dashboard') {
|
|
49
|
+
return <CreateDashboard args={args} flags={flags} />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (command === 'create-mobile') {
|
|
53
|
+
return <CreateMobile args={args} flags={flags} />;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (command === 'create') {
|
|
57
|
+
return <Create args={args} flags={flags} />;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Box flexDirection="column" padding={1}>
|
|
62
|
+
<Text color="red">Unknown command: {command}</Text>
|
|
63
|
+
<Text>Run <Text color="cyan">creatorem --help</Text> to see available commands.</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default App;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
|
|
4
|
+
interface Item {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
features?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MultiSelectProps {
|
|
13
|
+
items: Item[];
|
|
14
|
+
initialSelectedValues?: string[];
|
|
15
|
+
onSubmit: (selected: string[]) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|
19
|
+
items,
|
|
20
|
+
initialSelectedValues = [],
|
|
21
|
+
onSubmit,
|
|
22
|
+
}) => {
|
|
23
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
24
|
+
const [selectedValues, setSelectedValues] = useState<string[]>(initialSelectedValues);
|
|
25
|
+
// Ref-based guard: prevents double-submit when terminal sends \r\n as two keypress events
|
|
26
|
+
const submittedRef = useRef(false);
|
|
27
|
+
|
|
28
|
+
useInput((input, key) => {
|
|
29
|
+
if (key.upArrow) {
|
|
30
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : items.length - 1));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (key.downArrow) {
|
|
34
|
+
setSelectedIndex(prev => (prev < items.length - 1 ? prev + 1 : 0));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (input === ' ') {
|
|
38
|
+
const currentItem = items[selectedIndex];
|
|
39
|
+
if (selectedValues.includes(currentItem.value)) {
|
|
40
|
+
setSelectedValues(prev => prev.filter(v => v !== currentItem.value));
|
|
41
|
+
} else {
|
|
42
|
+
setSelectedValues(prev => [...prev, currentItem.value]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key.return) {
|
|
47
|
+
if (submittedRef.current) return;
|
|
48
|
+
submittedRef.current = true;
|
|
49
|
+
onSubmit(selectedValues);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Box flexDirection="column">
|
|
55
|
+
{items.map((item, index) => {
|
|
56
|
+
const isSelected = selectedValues.includes(item.value);
|
|
57
|
+
const isFocused = index === selectedIndex;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Box key={item.value} flexDirection="column">
|
|
61
|
+
<Box>
|
|
62
|
+
<Text color={isFocused ? 'cyan' : undefined}>
|
|
63
|
+
{isFocused ? '> ' : ' '}
|
|
64
|
+
</Text>
|
|
65
|
+
<Text color={isSelected ? 'green' : undefined}>
|
|
66
|
+
[{isSelected ? 'x' : ' '}]
|
|
67
|
+
</Text>
|
|
68
|
+
{item.title ? (
|
|
69
|
+
<Text bold>{item.title}</Text>
|
|
70
|
+
) : (
|
|
71
|
+
<Text>{item.label}</Text>
|
|
72
|
+
)}
|
|
73
|
+
</Box>
|
|
74
|
+
{isFocused && item.description ? (
|
|
75
|
+
<Box paddingLeft={4}>
|
|
76
|
+
<Text color="gray">{item.description}</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
) : null}
|
|
79
|
+
{isFocused && item.features && item.features.length > 0 ? (
|
|
80
|
+
<Box paddingLeft={4} flexDirection="column">
|
|
81
|
+
{item.features.map(feature => (
|
|
82
|
+
<Text key={`${item.value}-${feature}`} color="gray">
|
|
83
|
+
- {feature}
|
|
84
|
+
</Text>
|
|
85
|
+
))}
|
|
86
|
+
</Box>
|
|
87
|
+
) : null}
|
|
88
|
+
</Box>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
<Box marginTop={1}>
|
|
92
|
+
{selectedValues.length === 0 ? (
|
|
93
|
+
<Text color="gray">
|
|
94
|
+
(Press space to select, enter to submit)
|
|
95
|
+
</Text>
|
|
96
|
+
) : (
|
|
97
|
+
<Text color="green">
|
|
98
|
+
(Press space to toggle, enter to submit)
|
|
99
|
+
</Text>
|
|
100
|
+
)}
|
|
101
|
+
</Box>
|
|
102
|
+
</Box>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default MultiSelect;
|
package/src/utils/ast.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export const createProject = () => {
|
|
6
|
+
return new Project({
|
|
7
|
+
skipAddingFilesFromTsConfig: true,
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const loadFile = (project: Project, filePath: string) => {
|
|
12
|
+
return project.addSourceFileAtPath(filePath);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const removeImport = (sourceFile: any, moduleSpecifier: string) => {
|
|
16
|
+
const importDec = sourceFile.getImportDeclaration((i: any) =>
|
|
17
|
+
i.getModuleSpecifierValue() === moduleSpecifier
|
|
18
|
+
);
|
|
19
|
+
if (importDec) {
|
|
20
|
+
importDec.remove();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const removeInterfaceProperty = (sourceFile: any, interfaceName: string, propertyName: string) => {
|
|
25
|
+
// 1. Try finding top-level interface
|
|
26
|
+
let intf = sourceFile.getInterface(interfaceName);
|
|
27
|
+
|
|
28
|
+
// 2. If not found, try finding inside modules (declare module '...')
|
|
29
|
+
if (!intf) {
|
|
30
|
+
const modules = sourceFile.getModules();
|
|
31
|
+
for (const module of modules) {
|
|
32
|
+
intf = module.getInterface(interfaceName);
|
|
33
|
+
if (intf) break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (intf) {
|
|
38
|
+
// Check if property is nested (e.g. 'resources.p_org')
|
|
39
|
+
// This is a bit complex if we want generic nested support.
|
|
40
|
+
// For the specific case of i18next resources, the interface property 'resources' is an object type literal.
|
|
41
|
+
// The user wants to remove properties FROM that object type literal.
|
|
42
|
+
|
|
43
|
+
// Strategy:
|
|
44
|
+
// If interface has property 'resources', and we want to remove 'p_org' FROM 'resources'...
|
|
45
|
+
// The current usage in organization/index.ts is: removeInterfaceProperty(sourceFile, 'resources', 'p_org');
|
|
46
|
+
// This implies the function expects 'resources' to be the interface name? NO.
|
|
47
|
+
// In i18next.d.ts: interface CustomTypeOptions { resources: { ... } }
|
|
48
|
+
|
|
49
|
+
// Wait, the usage in organization/index.ts is:
|
|
50
|
+
// removeInterfaceProperty(sourceFile, 'resources', 'p_org');
|
|
51
|
+
// valid interface name is 'CustomTypeOptions'.
|
|
52
|
+
// 'resources' is a PROPERTY of 'CustomTypeOptions', which has a type literal value.
|
|
53
|
+
|
|
54
|
+
// So I should fix the usage in organization/index.ts OR update this helper to handle this specific structure.
|
|
55
|
+
// Given the helper signature (interfaceName, propertyName), passing 'resources' as interfaceName is WRONG effectively.
|
|
56
|
+
// But wait, maybe I can make this helper smarter or create a new one.
|
|
57
|
+
|
|
58
|
+
// Let's implement a 'removePropertyFromInterfaceProperty' helper?
|
|
59
|
+
// Or update this one to strict usage.
|
|
60
|
+
|
|
61
|
+
// Let's stick to the current plan: Update `ast.ts` to generally support finding interfaces in modules.
|
|
62
|
+
// AND add a new helper `removeNestedInterfaceProperty`?
|
|
63
|
+
|
|
64
|
+
// Actually, looking at `i18next.d.ts`, `resources` is a property of `CustomTypeOptions`.
|
|
65
|
+
// The value of `resources` is `{ p_org: ... }`.
|
|
66
|
+
|
|
67
|
+
const prop = intf.getProperty(propertyName);
|
|
68
|
+
if (prop) {
|
|
69
|
+
prop.remove();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const removePropertyFromInterfaceType = (sourceFile: any, interfaceName: string, parentPropertyName: string, childPropertyName: string) => {
|
|
75
|
+
// 1. Try finding top-level interface
|
|
76
|
+
let intf = sourceFile.getInterface(interfaceName);
|
|
77
|
+
|
|
78
|
+
// 2. If not found, try finding inside modules (declare module '...')
|
|
79
|
+
if (!intf) {
|
|
80
|
+
const modules = sourceFile.getModules();
|
|
81
|
+
for (const module of modules) {
|
|
82
|
+
intf = module.getInterface(interfaceName);
|
|
83
|
+
if (intf) break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (intf) {
|
|
88
|
+
const parentProp = intf.getProperty(parentPropertyName);
|
|
89
|
+
if (parentProp) {
|
|
90
|
+
// In ts-morph, getTypeNode() returns a TypeNode.
|
|
91
|
+
// We need to check if it's a TypeLiteralNode to use getMember.
|
|
92
|
+
const typeNode = parentProp.getTypeNode();
|
|
93
|
+
if (typeNode && typeNode.getKind() === SyntaxKind.TypeLiteral) {
|
|
94
|
+
const typeLiteral = typeNode.asKind(SyntaxKind.TypeLiteral);
|
|
95
|
+
if (typeLiteral) {
|
|
96
|
+
// Get all properties and check names to handle quotes (e.g. 'p_org-settings')
|
|
97
|
+
const properties = typeLiteral.getProperties();
|
|
98
|
+
for (const prop of properties) {
|
|
99
|
+
// prop.getName() usually returns the name without quotes if possible, or with?
|
|
100
|
+
// Let's check both or normalize.
|
|
101
|
+
const name = prop.getName();
|
|
102
|
+
// Remove quotes from name if present for comparison
|
|
103
|
+
const normalizedName = name.replace(/['"]/g, '');
|
|
104
|
+
const normalizedTarget = childPropertyName.replace(/['"]/g, '');
|
|
105
|
+
|
|
106
|
+
if (normalizedName === normalizedTarget) {
|
|
107
|
+
prop.remove();
|
|
108
|
+
// Don't break if we want to remove multiple?
|
|
109
|
+
// But here we target one property.
|
|
110
|
+
// However, if we mutate the array while iterating...
|
|
111
|
+
// ts-morph handles removal safely usually, but let's break to be safe as we found the match.
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const removeHookCall = (sourceFile: any, hookName: string) => {
|
|
122
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
123
|
+
for (const call of callExpressions) {
|
|
124
|
+
const expression = call.getExpression();
|
|
125
|
+
if (expression.getText() === hookName) {
|
|
126
|
+
// Check if it's a standalone statement (ExpressionStatement)
|
|
127
|
+
const stmt = call.getParentIfKind(SyntaxKind.ExpressionStatement);
|
|
128
|
+
if (stmt) {
|
|
129
|
+
stmt.remove();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const removeObjectFromArray = (sourceFile: any, variableName: string, propertyKey: string, propertyValueIdentifier: string) => {
|
|
136
|
+
const variable = sourceFile.getVariableDeclaration(variableName);
|
|
137
|
+
if (variable) {
|
|
138
|
+
const initializer = variable.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
|
|
139
|
+
if (initializer) {
|
|
140
|
+
const elements = initializer.getElements();
|
|
141
|
+
for (const element of elements) {
|
|
142
|
+
if (element.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
143
|
+
const obj = element;
|
|
144
|
+
const prop = obj.getProperty(propertyKey);
|
|
145
|
+
|
|
146
|
+
if (prop && prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
147
|
+
const assignment = prop;
|
|
148
|
+
// Check if the initializer text matches our target (e.g. 'organizationProxy')
|
|
149
|
+
if (assignment.getInitializer()?.getText() === propertyValueIdentifier) {
|
|
150
|
+
initializer.removeElement(element);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ... existing exports ...
|
|
161
|
+
|
|
162
|
+
export const unwrapDefaultExport = (sourceFile: any, wrapperName: string) => {
|
|
163
|
+
const exportAssignments = sourceFile.getExportAssignments();
|
|
164
|
+
const defaultExport = exportAssignments.find((e: any) => !e.isExportEquals());
|
|
165
|
+
|
|
166
|
+
if (defaultExport) {
|
|
167
|
+
const expression = defaultExport.getExpression();
|
|
168
|
+
|
|
169
|
+
if (expression.getKind() === SyntaxKind.CallExpression) {
|
|
170
|
+
const callExpr = expression;
|
|
171
|
+
// Need to cast or assume it has getExpression
|
|
172
|
+
const exprName = callExpr.getExpression().getText();
|
|
173
|
+
|
|
174
|
+
if (exprName === wrapperName) {
|
|
175
|
+
const args = callExpr.getArguments();
|
|
176
|
+
|
|
177
|
+
if (args.length > 0) {
|
|
178
|
+
const firstArg = args[0];
|
|
179
|
+
defaultExport.setExpression(firstArg.getText());
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const removeI18nNamespace = (sourceFile: any, namespacePrefix: string) => {
|
|
187
|
+
// 1. Remove properties from resources interface
|
|
188
|
+
// Try finding top-level interface
|
|
189
|
+
let intf = sourceFile.getInterface('CustomTypeOptions');
|
|
190
|
+
if (!intf) {
|
|
191
|
+
const modules = sourceFile.getModules();
|
|
192
|
+
for (const module of modules) {
|
|
193
|
+
intf = module.getInterface('CustomTypeOptions');
|
|
194
|
+
if (intf) break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (intf) {
|
|
199
|
+
const resourcesProp = intf.getProperty('resources');
|
|
200
|
+
if (resourcesProp) {
|
|
201
|
+
const typeNode = resourcesProp.getTypeNode();
|
|
202
|
+
if (typeNode && typeNode.getKind() === SyntaxKind.TypeLiteral) {
|
|
203
|
+
const typeLiteral = typeNode.asKind(SyntaxKind.TypeLiteral);
|
|
204
|
+
if (typeLiteral) {
|
|
205
|
+
const properties = typeLiteral.getProperties();
|
|
206
|
+
for (const prop of properties) {
|
|
207
|
+
const name = prop.getName();
|
|
208
|
+
const normalizedName = name.replace(/['"]/g, '');
|
|
209
|
+
|
|
210
|
+
// Check if name matches prefix (e.g. 'p_org' matches prefix 'p_org')
|
|
211
|
+
if (normalizedName === namespacePrefix || normalizedName.startsWith(`${namespacePrefix}-`)) {
|
|
212
|
+
prop.remove();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 2. Remove imports that import files starting with the prefix in the locales folder
|
|
221
|
+
// This assumes a specific structure: .../locales/en/${prefix}*.json
|
|
222
|
+
// Or just check if the import module specifier contains the prefix in the filename part?
|
|
223
|
+
// Let's be a bit specific to avoid false positives.
|
|
224
|
+
// The user's files are like `../../../kit/organization/src/i18n/locales/en/p_org.json`
|
|
225
|
+
|
|
226
|
+
// Strategy: iterate imports, get module specifier.
|
|
227
|
+
// If it ends with `/${namespacePrefix}.json` or `/${namespacePrefix}-*.json`, remove it.
|
|
228
|
+
|
|
229
|
+
const imports = sourceFile.getImportDeclarations();
|
|
230
|
+
for (const importDec of imports) {
|
|
231
|
+
const moduleSpecifier = importDec.getModuleSpecifierValue();
|
|
232
|
+
const baseName = path.basename(moduleSpecifier);
|
|
233
|
+
|
|
234
|
+
if (baseName === `${namespacePrefix}.json` || baseName.startsWith(`${namespacePrefix}-`)) {
|
|
235
|
+
importDec.remove();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const removeInternalPackages = (sourceFile: any, packagesToRemove: string[]) => {
|
|
241
|
+
const variableDeclaration = sourceFile.getVariableDeclaration('INTERNAL_PACKAGES');
|
|
242
|
+
if (variableDeclaration) {
|
|
243
|
+
const initializer = variableDeclaration.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
|
|
244
|
+
if (initializer) {
|
|
245
|
+
const elements = initializer.getElements();
|
|
246
|
+
for (const element of elements) {
|
|
247
|
+
const text = element.getText().replace(/['"]/g, '');
|
|
248
|
+
if (packagesToRemove.includes(text)) {
|
|
249
|
+
initializer.removeElement(element);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const removeInlineJSX = (sourceFile: any, jsxTagName: string) => {
|
|
257
|
+
// Determine which kind of JSX element we are looking for
|
|
258
|
+
// 1. Self closing: <TagName /> (JsxSelfClosingElement)
|
|
259
|
+
// 2. Opening/Closing: <TagName>...</TagName> (JsxElement)
|
|
260
|
+
|
|
261
|
+
// Find all JSX elements
|
|
262
|
+
// We can search for both JsxSelfClosingElement and JsxElement
|
|
263
|
+
|
|
264
|
+
const selfClosingElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement);
|
|
265
|
+
|
|
266
|
+
for (const element of selfClosingElements) {
|
|
267
|
+
const tagName = element.getTagNameNode().getText();
|
|
268
|
+
|
|
269
|
+
if (tagName === jsxTagName) {
|
|
270
|
+
if (typeof element.remove === 'function') {
|
|
271
|
+
element.remove();
|
|
272
|
+
} else if (typeof element.replaceWithText === 'function') {
|
|
273
|
+
element.replaceWithText('');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement);
|
|
279
|
+
|
|
280
|
+
for (const element of jsxElements) {
|
|
281
|
+
if (element.getOpeningElement().getTagNameNode().getText() === jsxTagName) {
|
|
282
|
+
if (typeof element.remove === 'function') {
|
|
283
|
+
element.remove();
|
|
284
|
+
} else if (typeof element.replaceWithText === 'function') {
|
|
285
|
+
element.replaceWithText('');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export const removePropertyAssignment = (sourceFile: any, propertyName: string) => {
|
|
292
|
+
// Find all property assignments with the matching name
|
|
293
|
+
const assignments = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
|
|
294
|
+
for (const assignment of assignments) {
|
|
295
|
+
if (assignment.getName() === propertyName) {
|
|
296
|
+
if (typeof assignment.remove === 'function') {
|
|
297
|
+
assignment.remove();
|
|
298
|
+
} else if (typeof assignment.replaceWithText === 'function') {
|
|
299
|
+
assignment.replaceWithText('');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Also handle ShorthandPropertyAssignment (e.g. { keybindingsModel })
|
|
305
|
+
const shorthands = sourceFile.getDescendantsOfKind(SyntaxKind.ShorthandPropertyAssignment);
|
|
306
|
+
for (const shorthand of shorthands) {
|
|
307
|
+
if (shorthand.getName() === propertyName) {
|
|
308
|
+
if (typeof shorthand.remove === 'function') {
|
|
309
|
+
shorthand.remove();
|
|
310
|
+
} else if (typeof shorthand.replaceWithText === 'function') {
|
|
311
|
+
shorthand.replaceWithText('');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const getRootIdentifierText = (expression: any): string | null => {
|
|
318
|
+
if (!expression) return null;
|
|
319
|
+
|
|
320
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
321
|
+
return expression.getText();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (expression.getKind() === SyntaxKind.CallExpression) {
|
|
325
|
+
return getRootIdentifierText(expression.getExpression());
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
329
|
+
return getRootIdentifierText(expression.getExpression());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (expression.getKind() === SyntaxKind.ElementAccessExpression) {
|
|
333
|
+
return getRootIdentifierText(expression.getExpression());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (expression.getKind() === SyntaxKind.ParenthesizedExpression) {
|
|
337
|
+
return getRootIdentifierText(expression.getExpression());
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return null;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export const removeEnvExtendsByModuleSpecifiers = (sourceFile: any, moduleSpecifiers: string[]) => {
|
|
344
|
+
if (!moduleSpecifiers || moduleSpecifiers.length === 0) return;
|
|
345
|
+
|
|
346
|
+
const moduleSet = new Set(moduleSpecifiers);
|
|
347
|
+
const identifiersToRemove = new Set<string>();
|
|
348
|
+
|
|
349
|
+
const matchesAnySpecifier = (importPath: string): boolean => {
|
|
350
|
+
if (moduleSet.has(importPath)) return true;
|
|
351
|
+
for (const spec of moduleSet) {
|
|
352
|
+
if (importPath.startsWith(spec + '/')) return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const imports = sourceFile.getImportDeclarations();
|
|
358
|
+
for (const importDec of imports) {
|
|
359
|
+
const moduleSpecifier = importDec.getModuleSpecifierValue();
|
|
360
|
+
if (!matchesAnySpecifier(moduleSpecifier)) continue;
|
|
361
|
+
|
|
362
|
+
const defaultImport = importDec.getDefaultImport();
|
|
363
|
+
if (defaultImport) {
|
|
364
|
+
identifiersToRemove.add(defaultImport.getText());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const namespaceImport = importDec.getNamespaceImport();
|
|
368
|
+
if (namespaceImport) {
|
|
369
|
+
identifiersToRemove.add(namespaceImport.getText());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const namedImport of importDec.getNamedImports()) {
|
|
373
|
+
identifiersToRemove.add(namedImport.getNameNode().getText());
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
importDec.remove();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (identifiersToRemove.size === 0) return;
|
|
380
|
+
|
|
381
|
+
const properties = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
|
|
382
|
+
for (const property of properties) {
|
|
383
|
+
if (property.getName() !== 'extends') continue;
|
|
384
|
+
|
|
385
|
+
const initializer = property.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
|
|
386
|
+
if (!initializer) continue;
|
|
387
|
+
|
|
388
|
+
const elements = initializer.getElements();
|
|
389
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
390
|
+
const element = elements[i];
|
|
391
|
+
const rootIdentifier = getRootIdentifierText(element);
|
|
392
|
+
if (rootIdentifier && identifiersToRemove.has(rootIdentifier)) {
|
|
393
|
+
initializer.removeElement(i);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Removes a SpreadAssignment from a named object literal (const or export const).
|
|
401
|
+
* Matches `...routerName` or `...routerName(...)` (call expression spread).
|
|
402
|
+
*
|
|
403
|
+
* @param sourceFile ts-morph SourceFile
|
|
404
|
+
* @param objectName Name of the variable whose initializer contains the spread (e.g. 'appRouter' body)
|
|
405
|
+
* @param routerName Root identifier to match (e.g. 'aiRouter', 'getKeybindingsRouter')
|
|
406
|
+
*/
|
|
407
|
+
export const removeSpreadFromObject = (sourceFile: any, objectName: string, routerName: string) => {
|
|
408
|
+
const spreads = sourceFile.getDescendantsOfKind(SyntaxKind.SpreadAssignment);
|
|
409
|
+
for (const spread of spreads) {
|
|
410
|
+
const expr = spread.getExpression();
|
|
411
|
+
// Resolve root identifier: handles both `...aiRouter` and `...getKeybindingsRouter(args)`
|
|
412
|
+
let rootName: string;
|
|
413
|
+
if (expr.getKind() === SyntaxKind.CallExpression) {
|
|
414
|
+
rootName = (expr as any).getExpression().getText();
|
|
415
|
+
} else {
|
|
416
|
+
rootName = expr.getText();
|
|
417
|
+
}
|
|
418
|
+
if (rootName === routerName) {
|
|
419
|
+
spread.remove();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|