Serve frontend components (#17798)

## Context
Ability to serve frontend component

See:
```typescript
curl -i 'http://localhost:3000/rest/front-components/35063b3f-bc4c-4358-8966-7762677802a3' \
--header 'Authorization: Bearer eyJhb...'
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/javascript
Date: Mon, 09 Feb 2026 10:28:17 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

// react-globals:react/jsx-runtime
var jsx = globalThis.jsx;
var jsxs = globalThis.jsxs;
var Fragment = globalThis.React.Fragment;

// src/front-components/test.tsx
var RemoteComponents = globalThis.RemoteComponents;
var Component = () => {
  return /* @__PURE__ */ jsxs(RemoteComponents.HtmlDiv, { style: { padding: "20px", fontFamily: "sans-serif" }, children: [
    /* @__PURE__ */ jsx(RemoteComponents.HtmlH1, { children: "My new component!" }),
    /* @__PURE__ */ jsx(RemoteComponents.HtmlP, { children: "This is your front component: test" })
  ] });
};
var test_default = globalThis.jsx(Component, {});
export {
  test_default as default
};
//# sourceMappingURL=test.mjs.map
```

readFile_v2 returns a Node.js Stream object (Readable). Here we are
using stream pipeline which connects the readable stream (file) to the
writable stream (HTTP response) which efficiently streams the file
content directly to the HTTP response without loading the entire file
into memory. (in chunks, handling backpressure and closing the
connection when the file is fully sent)
This commit is contained in:
Weiko
2026-02-09 15:52:14 +01:00
committed by GitHub
parent 617f634b6d
commit 96bc3594a3
5 changed files with 257 additions and 0 deletions
@@ -0,0 +1,79 @@
import {
Controller,
Get,
Param,
Res,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { pipeline } from 'stream/promises';
import { Response } from 'express';
import {
FileStorageException,
FileStorageExceptionCode,
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { FrontComponentRestApiExceptionFilter } from 'src/engine/metadata-modules/front-component/filters/front-component-rest-api-exception.filter';
import {
FrontComponentException,
FrontComponentExceptionCode,
} from 'src/engine/metadata-modules/front-component/front-component.exception';
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
@Controller('rest/front-components')
@UseGuards(WorkspaceAuthGuard)
@UseFilters(FrontComponentRestApiExceptionFilter)
export class FrontComponentController {
constructor(private readonly frontComponentService: FrontComponentService) {}
@Get(':frontComponentId')
@UseGuards(NoPermissionGuard)
async getBuiltJs(
@Res() res: Response,
@Param('frontComponentId') frontComponentId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
try {
const fileStream =
await this.frontComponentService.getBuiltComponentStream({
frontComponentId,
workspaceId: workspace.id,
});
res.setHeader('Content-Type', 'application/javascript');
await pipeline(fileStream, res);
} catch (error) {
// Mid-stream error: client already received partial data, nothing to do
if (res.headersSent) {
return;
}
if (
error instanceof FileStorageException &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
) {
throw new FrontComponentException(
'Front component built file not found',
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
);
}
if (error instanceof FrontComponentException) {
throw error;
}
throw new FrontComponentException(
`Error retrieving front component built file: ${error.message}`,
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_READY,
);
}
}
}