Skip to content

Commit e428881

Browse files
authored
feat: enable SSR on Amplify Hosting (#34)
* fix: update frontend build configuration and enable server-side rendering * feat: SSR mode * fix: eslint error
1 parent 3e1e86b commit e428881

26 files changed

+1119
-1064
lines changed

amplify.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ backend:
99

1010
frontend:
1111
phases:
12+
preBuild:
13+
commands:
14+
- "npm ci --cache .npm --prefer-offline"
1215
build:
1316
commands:
14-
- npm run build
17+
- "npm run build"
1518
artifacts:
16-
baseDirectory: build/client
19+
baseDirectory: .amplify-hosting
1720
files:
1821
- "**/*"
1922
cache:

app/components/account-card.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ interface AccountCardProps {
66
onClick?: () => void;
77
}
88

9-
type Account = Schema["Account"]["type"];
9+
type Account = Pick<
10+
Schema["Account"]["type"],
11+
"id" | "name" | "photo" | "organizationLine" | "residence"
12+
>;
1013

1114
export function AccountCard({ account, onClick }: AccountCardProps) {
1215
return (

app/components/account-form.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ import { MultiSelect } from "./ui/multi-select";
88
export interface AccountFormProps {
99
error?: Error | null;
1010
onCancel: () => void;
11-
account?: Schema["Account"]["type"]; // 既存のアカウント情報(編集時)
12-
projects?: Schema["Project"]["type"][]; // 利用可能なプロジェクトリスト
13-
projectAssignments?: Schema["ProjectAssignment"]["type"][]; // 現在のプロジェクトアサインメント
11+
account?: Pick<
12+
Schema["Account"]["type"],
13+
"id" | "name" | "email" | "photo" | "organizationLine" | "residence"
14+
>; // 既存のアカウント情報(編集時)
15+
projects?: Pick<Schema["Project"]["type"], "id" | "name">[]; // 利用可能なプロジェクトリスト
16+
projectAssignments?: Pick<
17+
Schema["ProjectAssignment"]["type"],
18+
"projectId" | "startDate" | "endDate"
19+
>[]; // 現在のプロジェクトアサインメント
1420
}
1521

1622
export function AccountForm({
@@ -143,8 +149,11 @@ export function AccountForm({
143149
}
144150

145151
interface ProjectAssignmentSelectorProps {
146-
projects: Schema["Project"]["type"][];
147-
initialAssignments: Schema["ProjectAssignment"]["type"][];
152+
projects: Pick<Schema["Project"]["type"], "id" | "name">[];
153+
initialAssignments: Pick<
154+
Schema["ProjectAssignment"]["type"],
155+
"projectId" | "startDate" | "endDate"
156+
>[];
148157
}
149158

150159
interface Assignment {

app/components/project-technology-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface ProjectTechnologyFormProps {
77
onCancel: () => void;
88
projectTechnology?: {
99
name: string;
10-
description?: string;
10+
description?: string | null;
1111
};
1212
}
1313

@@ -60,7 +60,7 @@ export function ProjectTechnologyForm({
6060
id="description"
6161
name="description"
6262
className="flex h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
63-
defaultValue={projectTechnology?.description}
63+
defaultValue={projectTechnology?.description ?? undefined}
6464
/>
6565
</div>
6666

app/lib/account.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import type { AmplifyServer } from "aws-amplify/adapter-core";
12
import type { Schema } from "../../amplify/data/resource";
2-
import { client } from "./amplify-client";
3+
import { client } from "./amplify-ssr-client";
34

45
export async function updateProjectAssignments({
6+
contextSpec,
57
account,
68
projectAssignments,
79
}: {
10+
contextSpec: AmplifyServer.ContextSpec;
811
account: Pick<Schema["Account"]["type"], "id"> & {
912
assignments?:
1013
| Pick<
@@ -27,7 +30,7 @@ export async function updateProjectAssignments({
2730
);
2831

2932
for (const assignmentToRemove of assignmentsToRemove) {
30-
await client.models.ProjectAssignment.delete({
33+
await client.models.ProjectAssignment.delete(contextSpec, {
3134
id: assignmentToRemove.id,
3235
});
3336
}
@@ -46,14 +49,14 @@ export async function updateProjectAssignments({
4649
existingAssignment.startDate !== pa.startDate ||
4750
existingAssignment.endDate !== pa.endDate
4851
) {
49-
await client.models.ProjectAssignment.update({
52+
await client.models.ProjectAssignment.update(contextSpec, {
5053
id: existingAssignment.id,
5154
startDate: pa.startDate,
5255
endDate: pa.endDate || undefined,
5356
});
5457
}
5558
} else {
56-
await client.models.ProjectAssignment.create({
59+
await client.models.ProjectAssignment.create(contextSpec, {
5760
accountId: account.id,
5861
projectId: pa.projectId,
5962
startDate: pa.startDate,

app/lib/amplify-ssr-client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Schema } from "amplify/data/resource";
2+
import { parseAmplifyConfig } from "aws-amplify/utils";
3+
import { generateClient } from "aws-amplify/api/server";
4+
import config from "../../amplify_outputs.json";
5+
6+
const amplifyConfig = parseAmplifyConfig(config);
7+
export const client = generateClient<Schema>({
8+
config: amplifyConfig,
9+
});

app/lib/amplifyServerUtils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createServerRunner } from "amplify-adapter-react-router";
2+
import outputs from "../../amplify_outputs.json";
3+
4+
export const { runWithAmplifyServerContext } = createServerRunner({
5+
config: outputs,
6+
});

app/lib/project.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { Schema } from "amplify/data/resource";
2-
import { client } from "./amplify-client";
2+
import { client } from "./amplify-ssr-client";
3+
import type { AmplifyServer } from "aws-amplify/adapter-core";
34

45
export async function updateProjectTechnologyLinks({
6+
contextSpec,
57
project,
68
projectTechnologyIds,
79
}: {
10+
contextSpec: AmplifyServer.ContextSpec;
811
project: Pick<Schema["Project"]["type"], "id"> & {
912
technologies: Pick<
1013
Schema["ProjectTechnologyLink"]["type"],
@@ -21,7 +24,7 @@ export async function updateProjectTechnologyLinks({
2124
(link) => !projectTechnologyIds.includes(link.technologyId),
2225
) ?? [];
2326
for (const linkToRemove of linksToRemove) {
24-
await client.models.ProjectTechnologyLink.delete({
27+
await client.models.ProjectTechnologyLink.delete(contextSpec, {
2528
id: linkToRemove.id,
2629
});
2730
}
@@ -30,7 +33,7 @@ export async function updateProjectTechnologyLinks({
3033
(id) => !currentTechIds.includes(id),
3134
);
3235
for (const techId of techToAdd) {
33-
await client.models.ProjectTechnologyLink.create({
36+
await client.models.ProjectTechnologyLink.create(contextSpec, {
3437
projectId: project.id,
3538
technologyId: techId,
3639
});

app/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useEffect } from "react";
1717
import { ToasterProvider } from "~/components/toaster-provider";
1818
import config from "../amplify_outputs.json";
1919

20-
Amplify.configure(config);
20+
Amplify.configure(config, { ssr: true });
2121

2222
export const links: Route.LinksFunction = () => [
2323
{ rel: "preconnect", href: "https://fonts.googleapis.com" },

app/routes/accounts.tsx

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// No need to import React with modern JSX transform
22
import { Button } from "../components/ui/button";
33
import { AccountCard } from "../components/account-card";
4-
import { client } from "../lib/amplify-client";
4+
import { client } from "../lib/amplify-ssr-client";
55
import { useNavigate } from "react-router";
66
import { parse } from "csv-parse/browser/esm";
77
import { CSVImportDialog } from "../components/csv-import-dialog";
88
import type { Route } from "./+types/accounts";
9+
import { runWithAmplifyServerContext } from "~/lib/amplifyServerUtils";
910

1011
export function meta() {
1112
return [
@@ -14,7 +15,7 @@ export function meta() {
1415
];
1516
}
1617

17-
export async function clientAction({ request }: { request: Request }) {
18+
export async function action({ request }: { request: Request }) {
1819
const formData = await request.formData();
1920
const csvFile = formData.get("csvFile") as File;
2021

@@ -66,27 +67,33 @@ export async function clientAction({ request }: { request: Request }) {
6667
continue;
6768
}
6869

69-
try {
70-
const { errors } = await client.models.Account.create({
71-
name: record.name,
72-
email: record.email,
73-
photo: record.photo || undefined,
74-
organizationLine: record.organizationLine,
75-
residence: record.residence,
76-
});
77-
78-
if (errors) {
79-
results.errors.push(
80-
`行 ${i + 1}: ${errors.map((err) => err.message).join(", ")}`,
81-
);
82-
} else {
83-
results.success++;
84-
}
85-
} catch (error) {
86-
results.errors.push(
87-
`行 ${i + 1}: ${error instanceof Error ? error.message : "不明なエラー"}`,
88-
);
89-
}
70+
const responseHeaders = new Headers();
71+
await runWithAmplifyServerContext({
72+
serverContext: { request, responseHeaders },
73+
operation: async (contextSpec) => {
74+
try {
75+
const { errors } = await client.models.Account.create(contextSpec, {
76+
name: record.name,
77+
email: record.email,
78+
photo: record.photo || undefined,
79+
organizationLine: record.organizationLine,
80+
residence: record.residence,
81+
});
82+
83+
if (errors) {
84+
results.errors.push(
85+
`行 ${i + 1}: ${errors.map((err) => err.message).join(", ")}`,
86+
);
87+
} else {
88+
results.success++;
89+
}
90+
} catch (error) {
91+
results.errors.push(
92+
`行 ${i + 1}: ${error instanceof Error ? error.message : "不明なエラー"}`,
93+
);
94+
}
95+
},
96+
});
9097
}
9198

9299
return {
@@ -99,17 +106,23 @@ export async function clientAction({ request }: { request: Request }) {
99106
}
100107
}
101108

102-
export async function clientLoader() {
103-
try {
104-
const { data } = await client.models.Account.list();
105-
return { accounts: data };
106-
} catch (err) {
107-
console.error("Error fetching accounts:", err);
108-
return {
109-
accounts: [],
110-
error: err instanceof Error ? err.message : "Unknown error occurred",
111-
};
112-
}
109+
export async function loader({ request }: Route.LoaderArgs) {
110+
const responseHeaders = new Headers();
111+
return runWithAmplifyServerContext({
112+
serverContext: { request, responseHeaders },
113+
operation: async (contextSpec) => {
114+
try {
115+
const { data } = await client.models.Account.list(contextSpec);
116+
return { accounts: data };
117+
} catch (err) {
118+
console.error("Error fetching accounts:", err);
119+
return {
120+
accounts: [],
121+
error: err instanceof Error ? err.message : "Unknown error occurred",
122+
};
123+
}
124+
},
125+
});
113126
}
114127

115128
export default function Accounts({

0 commit comments

Comments
 (0)