Skip to content

Commit 00f150b

Browse files
committed
new structure of monorepo
1 parent 5b1a638 commit 00f150b

File tree

14 files changed

+223
-433
lines changed

14 files changed

+223
-433
lines changed

readme.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ Options available in project folder:
6161

6262
## TODO
6363
- [x] Crossplatform for MacOs/Linux/Windows
64-
- [] Generate example api extension in fresh project
6564
- [] Support permissions migration
6665
- [] Support full db migration
6766
- [] Generate github/gitlab pipelines
68-
- [] Support all extension types
67+
- [x] Support all extension types

source/components/ActionsSelect.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@ interface Props {
1212
export const ActionsSelect: React.FC<Props> = (props) => {
1313
const { onSelection } = props;
1414
const title = useMemo(() => figlet.textSync("directus helper", "Small"), []);
15-
const hasProject = !!useSettings().settings.project;
15+
const { settings } = useSettings();
16+
const hasProject = !!settings?.project;
17+
const hasProductionTargets = !!settings.project?.targets?.length;
1618

1719
const items = [
1820
{ value: Actions.Migrate, label: "Migrate" },
1921
{ value: Actions.CopyToken, label: "Copy token for env" },
2022
!hasProject && { value: Actions.CreateProject, label: "Create project" },
2123
hasProject && { value: Actions.StartDev, label: "Start dev server" },
22-
hasProject && { value: Actions.BuildExtensions, label: "Build extensions" },
24+
hasProject &&
25+
hasProductionTargets && {
26+
value: Actions.BuildExtensions,
27+
label: "Build extensions",
28+
},
2329
hasProject && { value: Actions.ProjectSettings, label: "Project Settings" },
2430
{ value: Actions.Exit, label: "Exit" },
2531
].filter(Boolean) as Item[];

source/components/AppActionsForm.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import React, { useState } from "react";
21
import { Box, Text, useInput } from "ink";
3-
import { ActionsSelect } from "./ActionsSelect.js";
2+
import React, { useState } from "react";
43
import { Actions } from "../constants.js";
5-
import { DevServerDashboard } from "./DevServerDashboard/DevServerDashboard.js";
6-
import { BuildMode } from "../utils/devServer/devServerTasks/interface.js";
7-
import { MigratorDashboard } from "./MigratorDashboard/MigratorDashboard.js";
84
import { useBusy } from "../providers/BusyProvider.js";
5+
import { BuildMode } from "../utils/devServer/interfaces.js";
6+
import { ActionsSelect } from "./ActionsSelect.js";
97
import { CopyTokenScreen } from "./CopyTokenScreen.js";
10-
import { ProjectSettingsScreen } from "./SettingsScreen/SettingsScreen.js";
8+
import { DevServerDashboard } from "./DevServerDashboard/DevServerDashboard.js";
9+
import { MigratorDashboard } from "./MigratorDashboard/MigratorDashboard.js";
1110
import { ProjectCreationDashboard } from "./ProjectCreationDashboard/ProjectCreationDashboard.js";
11+
import { ProjectSettingsScreen } from "./SettingsScreen/SettingsScreen.js";
1212

1313
export const AppActionsForm: React.FC = () => {
1414
const [action, setAction] = useState<Actions>();

source/components/DevServerDashboard/DevServerDashboard.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
1-
import React, { useEffect, useRef, useState } from 'react';
2-
import { Box, Text } from 'ink';
3-
import { DevServer } from '../../utils/devServer/DevServer.js';
4-
import { StatusChangePayload } from '../../utils/devServer/interfaces.js';
5-
import { DashboardTask } from './DashboardTask.js';
6-
import { BuildMode } from '../../utils/devServer/devServerTasks/interface.js';
7-
import { DashboardBudge } from './DashboardBudge.js';
8-
import { DirectusStatusBudge } from './DirectusStatusBudge.js';
9-
import { useBusy } from '../../providers/BusyProvider.js';
1+
import React, { useEffect, useRef, useState } from "react";
2+
import { Box, Text } from "ink";
3+
import { DevServer } from "../../utils/devServer/DevServer.js";
4+
import {
5+
BuildMode,
6+
StatusChangePayload,
7+
} from "../../utils/devServer/interfaces.js";
8+
import { DashboardTask } from "./DashboardTask.js";
9+
import { DashboardBudge } from "./DashboardBudge.js";
10+
import { DirectusStatusBudge } from "./DirectusStatusBudge.js";
11+
import { useBusy } from "../../providers/BusyProvider.js";
1012

1113
interface Props {
1214
mode?: BuildMode;
1315
}
1416

15-
export const DevServerDashboard: React.FC<Props> = props => {
17+
export const DevServerDashboard: React.FC<Props> = (props) => {
1618
const devServer = useRef(new DevServer());
1719
const [statuses, setStatuses] = useState<Record<string, StatusChangePayload>>(
1820
{},
1921
);
2022
const busy = useBusy();
2123

24+
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
2225
useEffect(() => {
23-
devServer.current.onStatusChange(statuses => {
26+
devServer.current.onStatusChange((statuses) => {
2427
setStatuses({ ...statuses });
2528
});
2629

@@ -49,7 +52,7 @@ export const DevServerDashboard: React.FC<Props> = props => {
4952
>
5053
<Text color="green">Dev server</Text>
5154
<Box gap={3}>
52-
{Object.keys(statuses).map(name => (
55+
{Object.keys(statuses).map((name) => (
5356
<DashboardTask
5457
key={name}
5558
name={name}

source/components/ProjectCreationDashboard/createProject.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,8 @@ export const createProject = async (payload: ProjectCreationPayload) => {
2222
path.resolve(process.cwd(), "./.gitignore"),
2323
gitIgnoreFileTemplate(payload.devTarget),
2424
);
25-
const srcDir = path.resolve(process.cwd(), "src");
26-
await fs.mkdir(path.resolve(process.cwd(), payload.devTarget));
2725

28-
await fs.mkdir(path.resolve(srcDir, "api"), { recursive: true });
29-
// todo generate directus endpoint extension in api folder
30-
await fs.mkdir(path.resolve(srcDir, "hooks"), { recursive: true });
31-
await fs.mkdir(path.resolve(srcDir, "interfaces"), { recursive: true });
32-
await fs.mkdir(path.resolve(srcDir, "operations"), { recursive: true });
26+
await fs.mkdir(path.resolve(process.cwd(), payload.devTarget), {
27+
recursive: true,
28+
});
3329
};
Lines changed: 179 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,47 @@
1-
import chokidar from "chokidar";
1+
import { exec } from "node:child_process";
2+
import fs from "node:fs/promises";
23
import path from "node:path";
3-
import { HookBuildTask } from "./devServerTasks/hooksBuildTask.js";
4-
import { EndpointBuildTask } from "./devServerTasks/endpointBuildTask.js";
4+
import chokidar from "chokidar";
5+
import { safeTryPromise } from "../safeTry/safeTry.js";
6+
import { getSettings } from "../settingsUtils/settingsUtils.js";
57
import {
8+
BuildMode,
69
DevServerStatusChangeCallback,
10+
DevServerTaskStatus,
711
StatusChangePayload,
812
} from "./interfaces.js";
9-
import { BaseBuildTask } from "./devServerTasks/baseBuildTask.js";
10-
import { BuildMode } from "./devServerTasks/interface.js";
11-
import { InterfaceBuildTask } from "./devServerTasks/interfaceBuildTask.js";
12-
import { OperaionsBuildTask } from "./devServerTasks/operationsBuildTask.js";
13-
import { getSettings } from "../settingsUtils/settingsUtils.js";
1413

1514
export class DevServer {
16-
private readonly tasks: BaseBuildTask[] = [
17-
new HookBuildTask(),
18-
new EndpointBuildTask(),
19-
new InterfaceBuildTask(),
20-
new OperaionsBuildTask(),
21-
];
2215
private statuses: Record<string, StatusChangePayload> = {};
2316
private onStatusChangeCallback: DevServerStatusChangeCallback | undefined;
2417

2518
async start() {
19+
for (const folder of await this.getExtensionsFolders()) {
20+
chokidar
21+
.watch(folder, {
22+
ignored: ["**/node_modules/**/*", "**/.git/**/*", "**/dist/**/*"],
23+
ignoreInitial: true,
24+
})
25+
.on("all", () => {
26+
this.buildExtension(folder, BuildMode.Dev);
27+
});
28+
}
29+
}
30+
31+
private async getExtensionsFolders() {
2632
const settings = await getSettings();
2733
const projectSettings = settings.project;
2834

2935
if (!projectSettings) {
30-
return;
36+
return [];
3137
}
3238

33-
for (const task of this.tasks) {
34-
task.setStatusChangeCallback((status) => {
35-
this.statuses[task.name] = status;
36-
this.onStatusChangeCallback?.(this.statuses);
37-
});
39+
const srcDir = path.resolve(process.cwd(), projectSettings.src_dir);
40+
const extensions = await fs.readdir(srcDir);
3841

39-
chokidar
40-
.watch(
41-
path.resolve(process.cwd(), projectSettings.src_dir, task.trigger),
42-
{
43-
ignored: ["**/node_modules/**/*", "**/.git/**/*", "**/dist/**/*"],
44-
ignoreInitial: true,
45-
},
46-
)
47-
.on("all", (_, path) => {
48-
task.perform(path, BuildMode.Dev);
49-
});
50-
}
42+
return extensions.map((folderBaseName) =>
43+
path.resolve(srcDir, folderBaseName),
44+
);
5145
}
5246

5347
async buildProd() {
@@ -58,25 +52,166 @@ export class DevServer {
5852
return;
5953
}
6054

61-
for (const task of this.tasks) {
62-
const taskRoot = path.resolve(
63-
process.cwd(),
64-
projectSettings.src_dir,
65-
task.trigger,
66-
);
67-
this.appendStatusListener(task);
68-
task.perform(taskRoot, BuildMode.Prod);
55+
for (const folder of await this.getExtensionsFolders()) {
56+
this.buildExtension(folder, BuildMode.Prod);
6957
}
7058
}
7159

7260
onStatusChange(callback: DevServerStatusChangeCallback) {
7361
this.onStatusChangeCallback = callback;
7462
}
7563

76-
private appendStatusListener(task: BaseBuildTask) {
77-
task.setStatusChangeCallback((status) => {
78-
this.statuses[task.name] = status;
79-
this.onStatusChangeCallback?.(this.statuses);
64+
async buildExtension(extensionPath: string, mode: BuildMode) {
65+
const extensionName = path.basename(extensionPath);
66+
const status = this.statuses[extensionName];
67+
const settings = await getSettings();
68+
const targets =
69+
mode === BuildMode.Dev
70+
? settings.project?.dev_targets
71+
: settings.project?.targets;
72+
73+
if (!targets?.length) {
74+
return;
75+
}
76+
77+
const finishedBuildStatuses = [
78+
DevServerTaskStatus.Done,
79+
DevServerTaskStatus.Error,
80+
];
81+
const isPreviousBuildFinished =
82+
!status || finishedBuildStatuses.includes(status.status);
83+
84+
if (!isPreviousBuildFinished) {
85+
return;
86+
}
87+
88+
const hasNodeModules = await this.hasNodeModules(extensionPath);
89+
90+
if (!hasNodeModules) {
91+
await this.installPackages(extensionPath);
92+
}
93+
94+
this.setStatus(extensionName, {
95+
status: DevServerTaskStatus.Building,
96+
});
97+
98+
const [_, err] = await safeTryPromise(() =>
99+
this.run(this.getBuildCommand(mode), extensionPath, (data) => {
100+
this.setStatus(extensionName, {
101+
status: DevServerTaskStatus.Building,
102+
message: data,
103+
});
104+
}),
105+
);
106+
107+
if (err) {
108+
this.setStatus(extensionName, {
109+
status: DevServerTaskStatus.Error,
110+
message: (err as Error)?.message,
111+
});
112+
113+
return;
114+
}
115+
116+
const [, buildCopyError] = await safeTryPromise(() =>
117+
this.copyBuild(extensionPath, targets),
118+
);
119+
120+
if (buildCopyError) {
121+
this.setStatus(extensionName, {
122+
status: DevServerTaskStatus.Error,
123+
message: `Build copy error: ${(buildCopyError as Error).message}`,
124+
});
125+
126+
return;
127+
}
128+
129+
this.setStatus(extensionName, {
130+
status: DevServerTaskStatus.Done,
131+
});
132+
}
133+
134+
private async installPackages(root: string) {
135+
this.setStatus(root, { status: DevServerTaskStatus.InstallingPackages });
136+
137+
await this.run("npm ci", root, (data) => {
138+
this.setStatus(root, {
139+
status: DevServerTaskStatus.InstallingPackages,
140+
message: data,
141+
});
142+
});
143+
}
144+
145+
private async hasNodeModules(root: string) {
146+
const nodeModulesPath = path.resolve(root, "node_modules");
147+
148+
try {
149+
await fs.access(nodeModulesPath);
150+
return true;
151+
} catch {
152+
return false;
153+
}
154+
}
155+
156+
private setStatus(extension: string, payload: StatusChangePayload) {
157+
const name = path.basename(extension);
158+
this.statuses[name] = payload;
159+
this.onStatusChangeCallback?.(this.statuses);
160+
}
161+
162+
protected getBuildCommand(mode: BuildMode) {
163+
if (mode === BuildMode.Prod) {
164+
return "npm run build";
165+
}
166+
167+
return "npm run build:q";
168+
}
169+
170+
protected async copyBuild(extensionPath: string, targets: string[]) {
171+
const buildPath = path.resolve(extensionPath, "dist");
172+
const packageJsonPath = path.resolve(extensionPath, "package.json");
173+
174+
for (const target of targets) {
175+
const targetPath = path.resolve(
176+
process.cwd(),
177+
target,
178+
`directus-extension-${path.basename(extensionPath)}`,
179+
);
180+
181+
await fs.mkdir(path.resolve(targetPath, "dist"), { recursive: true });
182+
183+
await fs.cp(buildPath, path.resolve(targetPath, "dist"), {
184+
recursive: true,
185+
});
186+
await fs.copyFile(
187+
packageJsonPath,
188+
path.resolve(targetPath, "package.json"),
189+
);
190+
}
191+
}
192+
193+
private run(command: string, cwd: string, onData?: (data: string) => void) {
194+
return new Promise<void>((res, rej) => {
195+
const childProcess = exec(
196+
command,
197+
{
198+
cwd,
199+
},
200+
async (err: unknown) => {
201+
if (!err) {
202+
res();
203+
return;
204+
}
205+
206+
rej(err);
207+
},
208+
);
209+
210+
if (onData) {
211+
childProcess.stdout?.on("data", (data) => {
212+
onData(data.toString("utf-8"));
213+
});
214+
}
80215
});
81216
}
82217
}

0 commit comments

Comments
 (0)