fix: update api import paths & fix Dockerfile not copying frontend build

fixes issue #7 & #9

also remove agGrid dependency from package.json to reduce download size
This commit is contained in:
Jordan Knott 2020-07-19 17:25:58 -05:00
parent c3b1837589
commit 6f33cc5799
20 changed files with 67 additions and 125 deletions

View File

@ -7,9 +7,12 @@ RUN yarn build
FROM golang:1.14.5-alpine as backend FROM golang:1.14.5-alpine as backend
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
COPY . . COPY . .
COPY --from=frontend /usr/src/app/build . COPY --from=frontend /usr/src/app/build ./frontend/build
RUN go run cmd/mage/main.go backend:genFrontend backend:build RUN go run cmd/mage/main.go backend:genFrontend backend:genMigrations backend:build
FROM alpine:latest FROM alpine:latest
WORKDIR /root/ WORKDIR /root/

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"github.com/jordanknott/project-citadel/api/internal/commands" "github.com/jordanknott/project-citadel/internal/commands"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )

View File

@ -7,8 +7,8 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordan-wright/email" "github.com/jordan-wright/email"
"github.com/jordanknott/project-citadel/api/pg" "github.com/jordanknott/project-citadel/pg"
"github.com/jordanknott/project-citadel/api/router" "github.com/jordanknott/project-citadel/router"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"io/ioutil" "io/ioutil"

View File

@ -8,7 +8,7 @@ import (
"github.com/RichardKnop/machinery/v1/config" "github.com/RichardKnop/machinery/v1/config"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordanknott/project-citadel/api/pg" "github.com/jordanknott/project-citadel/pg"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )

View File

@ -11,11 +11,6 @@
"@fortawesome/free-regular-svg-icons": "^5.12.1", "@fortawesome/free-regular-svg-icons": "^5.12.1",
"@fortawesome/free-solid-svg-icons": "^5.12.1", "@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/react-fontawesome": "^0.1.8", "@fortawesome/react-fontawesome": "^0.1.8",
"@storybook/addon-backgrounds": "^5.3.17",
"@storybook/addon-docs": "^5.3.17",
"@storybook/addon-knobs": "^5.3.17",
"@storybook/addon-storysource": "^5.3.17",
"@storybook/addon-viewport": "^5.3.17",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
@ -34,9 +29,6 @@
"@types/react-router-dom": "^5.1.3", "@types/react-router-dom": "^5.1.3",
"@types/react-select": "^3.0.13", "@types/react-select": "^3.0.13",
"@types/styled-components": "^5.0.0", "@types/styled-components": "^5.0.0",
"@welldone-software/why-did-you-render": "^4.2.2",
"ag-grid-community": "^23.2.0",
"ag-grid-react": "^23.2.0",
"apollo-cache-inmemory": "^1.6.5", "apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8", "apollo-client": "^2.6.8",
"apollo-link": "^1.2.13", "apollo-link": "^1.2.13",
@ -103,6 +95,11 @@
"@graphql-codegen/typescript-react-apollo": "^1.13.2", "@graphql-codegen/typescript-react-apollo": "^1.13.2",
"@storybook/addon-actions": "^5.3.13", "@storybook/addon-actions": "^5.3.13",
"@storybook/addon-links": "^5.3.13", "@storybook/addon-links": "^5.3.13",
"@storybook/addon-backgrounds": "^5.3.17",
"@storybook/addon-docs": "^5.3.17",
"@storybook/addon-knobs": "^5.3.17",
"@storybook/addon-storysource": "^5.3.17",
"@storybook/addon-viewport": "^5.3.17",
"@storybook/addons": "^5.3.13", "@storybook/addons": "^5.3.13",
"@storybook/preset-create-react-app": "^1.5.2", "@storybook/preset-create-react-app": "^1.5.2",
"@storybook/react": "^5.3.13", "@storybook/react": "^5.3.13",

View File

@ -6,12 +6,9 @@ import Select from 'shared/components/Select';
import { User, Plus, Lock, Pencil, Trash } from 'shared/icons'; import { User, Plus, Lock, Pencil, Trash } from 'shared/icons';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import { RoleCode, useUpdateUserRoleMutation } from 'shared/generated/graphql'; import { RoleCode, useUpdateUserRoleMutation } from 'shared/generated/graphql';
import { AgGridReact } from 'ag-grid-react';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import Member from 'shared/components/Member'; import Member from 'shared/components/Member';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-material.css';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
export const RoleCheckmark = styled(Checkmark)` export const RoleCheckmark = styled(Checkmark)`
@ -58,7 +55,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
position: relative; position: relative;
text-decoration: none; text-decoration: none;
${props => ${(props) =>
props.disabled props.disabled
? css` ? css`
user-select: none; user-select: none;
@ -78,7 +75,7 @@ export const Content = styled.div`
export const CurrentPermission = styled.span` export const CurrentPermission = styled.span`
margin-left: 4px; margin-left: 4px;
color: rgba(${props => props.theme.colors.text.secondary}, 0.4); color: rgba(${(props) => props.theme.colors.text.secondary}, 0.4);
`; `;
export const Separator = styled.div` export const Separator = styled.div`
@ -89,13 +86,13 @@ export const Separator = styled.div`
export const WarningText = styled.span` export const WarningText = styled.span`
display: flex; display: flex;
color: rgba(${props => props.theme.colors.text.primary}, 0.4); color: rgba(${(props) => props.theme.colors.text.primary}, 0.4);
padding: 6px; padding: 6px;
`; `;
export const DeleteDescription = styled.div` export const DeleteDescription = styled.div`
font-size: 14px; font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary}); color: rgba(${(props) => props.theme.colors.text.primary});
`; `;
export const RemoveMemberButton = styled(Button)` export const RemoveMemberButton = styled(Button)`
@ -162,8 +159,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
<MiniProfileActions> <MiniProfileActions>
<MiniProfileActionWrapper> <MiniProfileActionWrapper>
{permissions {permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner') .filter((p) => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => ( .map((perm) => (
<MiniProfileActionItem <MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole} disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code} key={perm.code}
@ -214,9 +211,9 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
Choose a new user to take over ownership of this user's teams & projects. Choose a new user to take over ownership of this user's teams & projects.
</DeleteDescription> </DeleteDescription>
<UserSelect <UserSelect
onChange={v => setDeleteUser(v)} onChange={(v) => setDeleteUser(v)}
value={deleteUser} value={deleteUser}
options={users.map(u => ({ label: u.fullName, value: u.id }))} options={users.map((u) => ({ label: u.fullName, value: u.id }))}
/> />
</> </>
)} )}
@ -242,7 +239,11 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
Removing this user from the organzation will remove them from assigned tasks, projects, and teams. Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
</DeleteDescription> </DeleteDescription>
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription> <DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
<UserSelect onChange={() => {}} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} /> <UserSelect
onChange={() => {}}
value={null}
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
/>
<UserPassConfirmButton <UserPassConfirmButton
onClick={() => { onClick={() => {
// onDeleteUser(); // onDeleteUser();
@ -333,14 +334,14 @@ const MemberItemOption = styled(Button)`
`; `;
const MemberList = styled.div` const MemberList = styled.div`
border-top: 1px solid rgba(${props => props.theme.colors.border}); border-top: 1px solid rgba(${(props) => props.theme.colors.border});
`; `;
const MemberListItem = styled.div` const MemberListItem = styled.div`
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid rgba(${props => props.theme.colors.border}); border-bottom: 1px solid rgba(${(props) => props.theme.colors.border});
min-height: 40px; min-height: 40px;
padding: 12px 0 12px 40px; padding: 12px 0 12px 40px;
position: relative; position: relative;
@ -364,11 +365,11 @@ const MemberProfile = styled(TaskAssignee)`
`; `;
const MemberItemName = styled.p` const MemberItemName = styled.p`
color: rgba(${props => props.theme.colors.text.secondary}); color: rgba(${(props) => props.theme.colors.text.secondary});
`; `;
const MemberItemUsername = styled.p` const MemberItemUsername = styled.p`
color: rgba(${props => props.theme.colors.text.primary}); color: rgba(${(props) => props.theme.colors.text.primary});
`; `;
const MemberListHeader = styled.div` const MemberListHeader = styled.div`
@ -377,12 +378,12 @@ const MemberListHeader = styled.div`
`; `;
const ListTitle = styled.h3` const ListTitle = styled.h3`
font-size: 18px; font-size: 18px;
color: rgba(${props => props.theme.colors.text.secondary}); color: rgba(${(props) => props.theme.colors.text.secondary});
margin-bottom: 12px; margin-bottom: 12px;
`; `;
const ListDesc = styled.span` const ListDesc = styled.span`
font-size: 16px; font-size: 16px;
color: rgba(${props => props.theme.colors.text.primary}); color: rgba(${(props) => props.theme.colors.text.primary});
`; `;
const FilterSearch = styled(Input)` const FilterSearch = styled(Input)`
margin: 0; margin: 0;
@ -483,72 +484,13 @@ const ActionButtons = (params: any) => {
<ActionButton onClick={() => {}}> <ActionButton onClick={() => {}}>
<EditUserIcon width={16} height={16} /> <EditUserIcon width={16} height={16} />
</ActionButton> </ActionButton>
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}> <ActionButton onClick={($target) => params.onDeleteUser($target, params.value)}>
<DeleteUserIcon width={16} height={16} /> <DeleteUserIcon width={16} height={16} />
</ActionButton> </ActionButton>
</> </>
); );
}; };
type ListTableProps = {
users: Array<User>;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
};
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
const data = {
defaultColDef: {
resizable: true,
sortable: true,
},
columnDefs: [
{
minWidth: 55,
width: 55,
headerCheckboxSelection: true,
checkboxSelection: true,
},
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
{ minWidth: 225, headerName: 'Email', field: 'email' },
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
{
minWidth: 200,
headerName: 'Actions',
field: 'id',
cellRenderer: 'actionButtons',
cellRendererParams: {
onDeleteUser: (target: any, userID: any) => {
onDeleteUser(target, userID);
},
},
},
],
frameworkComponents: {
actionButtons: ActionButtons,
},
};
return (
<Root>
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
<AgGridReact
rowSelection="multiple"
defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs}
rowData={users.map(u => ({ ...u, roleName: 'member' }))}
frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => {
params.api.sizeColumnsToFit();
}}
onGridSizeChanged={params => {
params.api.sizeColumnsToFit();
}}
/>
</div>
</Root>
);
};
const Wrapper = styled.div` const Wrapper = styled.div`
background: #eff2f7; background: #eff2f7;
display: flex; display: flex;
@ -599,7 +541,7 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
width: 100%; width: 100%;
position: relative; position: relative;
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')}; color: ${(props) => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
&:hover { &:hover {
color: rgba(115, 103, 240); color: rgba(115, 103, 240);
} }
@ -620,7 +562,7 @@ const TabNavLine = styled.span<{ top: number }>`
width: 2px; width: 2px;
height: 48px; height: 48px;
transform: scaleX(1); transform: scaleX(1);
top: ${props => props.top}px; top: ${(props) => props.top}px;
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240)); background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
box-shadow: 0 0 8px 0 rgba(115, 103, 240); box-shadow: 0 0 8px 0 rgba(115, 103, 240);
@ -734,7 +676,7 @@ const Admin: React.FC<AdminProps> = ({
<ListActions> <ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" /> <FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
<InviteMemberButton <InviteMemberButton
onClick={$target => { onClick={($target) => {
onAddUser($target); onAddUser($target);
}} }}
> >
@ -744,7 +686,7 @@ const Admin: React.FC<AdminProps> = ({
</ListActions> </ListActions>
</MemberListHeader> </MemberListHeader>
<MemberList> <MemberList>
{users.map(member => { {users.map((member) => {
const projectTotal = member.owned.projects.length + member.member.projects.length; const projectTotal = member.owned.projects.length + member.member.projects.length;
return ( return (
<MemberListItem> <MemberListItem>
@ -757,7 +699,7 @@ const Admin: React.FC<AdminProps> = ({
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption> <MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
<MemberItemOption <MemberItemOption
variant="outline" variant="outline"
onClick={$target => { onClick={($target) => {
showPopup( showPopup(
$target, $target,
<TeamRoleManagerPopup <TeamRoleManagerPopup
@ -768,7 +710,7 @@ const Admin: React.FC<AdminProps> = ({
onUpdateUserPassword(user, password); onUpdateUserPassword(user, password);
}} }}
canChangeRole={(member.role && member.role.code !== 'owner') ?? false} canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
onChangeRole={roleCode => { onChangeRole={(roleCode) => {
updateUserRole({ variables: { userID: member.id, roleCode } }); updateUserRole({ variables: { userID: member.id, roleCode } });
}} }}
onDeleteUser={onDeleteUser} onDeleteUser={onDeleteUser}

2
go.mod
View File

@ -1,4 +1,4 @@
module github.com/jordanknott/project-citadel/api module github.com/jordanknott/project-citadel
go 1.13 go 1.13

View File

@ -29,7 +29,7 @@ omit_slice_element_pointers: true
# gqlgen will search for any type names in the schema in these go packages # gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them. # if they match it will use them, otherwise it will generate them.
autobind: autobind:
- "github.com/jordanknott/project-citadel/api/internal/db" - "github.com/jordanknott/project-citadel/internal/db"
# This section declares type mapping between the GraphQL and go type systems # This section declares type mapping between the GraphQL and go type systems
# #
@ -38,10 +38,10 @@ autobind:
# your liking # your liking
models: models:
ID: ID:
model: github.com/jordanknott/project-citadel/api/internal/graph.UUID model: github.com/jordanknott/project-citadel/internal/graph.UUID
Int: Int:
model: model:
- github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int
UUID: UUID:
model: github.com/jordanknott/project-citadel/api/internal/graph.UUID model: github.com/jordanknott/project-citadel/internal/graph.UUID

View File

@ -9,8 +9,8 @@ import (
_ "github.com/golang-migrate/migrate/v4/source/file" _ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/golang-migrate/migrate/v4/source/httpfs" "github.com/golang-migrate/migrate/v4/source/httpfs"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordanknott/project-citadel/api/internal/config" "github.com/jordanknott/project-citadel/internal/config"
"github.com/jordanknott/project-citadel/api/internal/migrations" "github.com/jordanknott/project-citadel/internal/migrations"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -7,8 +7,8 @@ import (
"time" "time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordanknott/project-citadel/api/internal/config" "github.com/jordanknott/project-citadel/internal/config"
"github.com/jordanknott/project-citadel/api/internal/route" "github.com/jordanknott/project-citadel/internal/route"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -14,7 +14,7 @@ import (
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/introspection" "github.com/99designs/gqlgen/graphql/introspection"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
gqlparser "github.com/vektah/gqlparser/v2" gqlparser "github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast" "github.com/vektah/gqlparser/v2/ast"
) )

View File

@ -12,9 +12,9 @@ import (
"github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/internal/auth" "github.com/jordanknott/project-citadel/internal/auth"
"github.com/jordanknott/project-citadel/api/internal/config" "github.com/jordanknott/project-citadel/internal/config"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
) )
// NewHandler returns a new graphql endpoint handler. // NewHandler returns a new graphql endpoint handler.

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
) )
func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*OwnedList, error) { func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*OwnedList, error) {

View File

@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
) )
type AddTaskLabelInput struct { type AddTaskLabelInput struct {

View File

@ -5,8 +5,8 @@ package graph
import ( import (
"sync" "sync"
"github.com/jordanknott/project-citadel/api/internal/config" "github.com/jordanknott/project-citadel/internal/config"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
) )
type Resolver struct { type Resolver struct {

View File

@ -11,7 +11,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"

View File

@ -8,8 +8,8 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/internal/auth" "github.com/jordanknott/project-citadel/internal/auth"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )

View File

@ -12,8 +12,8 @@ import (
"time" "time"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
"github.com/jordanknott/project-citadel/api/internal/frontend" "github.com/jordanknott/project-citadel/internal/frontend"
) )
func (h *CitadelHandler) Frontend(w http.ResponseWriter, r *http.Request) { func (h *CitadelHandler) Frontend(w http.ResponseWriter, r *http.Request) {

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/internal/auth" "github.com/jordanknott/project-citadel/internal/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -10,11 +10,11 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/jordanknott/project-citadel/api/internal/config" "github.com/jordanknott/project-citadel/internal/config"
"github.com/jordanknott/project-citadel/api/internal/db" "github.com/jordanknott/project-citadel/internal/db"
"github.com/jordanknott/project-citadel/api/internal/frontend" "github.com/jordanknott/project-citadel/internal/frontend"
"github.com/jordanknott/project-citadel/api/internal/graph" "github.com/jordanknott/project-citadel/internal/graph"
"github.com/jordanknott/project-citadel/api/internal/logger" "github.com/jordanknott/project-citadel/internal/logger"
"os" "os"
"path/filepath" "path/filepath"
) )