mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 09:33:19 +03:00
Unify error handling and propose actions to user on error
This commit is contained in:
parent
11acadfde9
commit
ef5bee43a1
@ -1,48 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import 'url-search-params-polyfill';
|
|
||||||
|
|
||||||
import { RouterProvider } from 'react-router-dom';
|
|
||||||
import { createAppRouter } from './dashboard/router'
|
|
||||||
import ErrorBoundary from './dashboard/error-boundary'
|
|
||||||
import * as api from './dashboard/api'
|
|
||||||
import * as timer from './dashboard/util/realtime-update-timer'
|
|
||||||
import { filtersBackwardsCompatibilityRedirect } from './dashboard/query';
|
|
||||||
import SiteContextProvider, { parseSiteFromDataset } from './dashboard/site-context';
|
|
||||||
import UserContextProvider from './dashboard/user-context'
|
|
||||||
import ThemeContextProvider from './dashboard/theme-context'
|
|
||||||
|
|
||||||
timer.start()
|
|
||||||
|
|
||||||
const container = document.getElementById('stats-react-container')
|
|
||||||
|
|
||||||
if (container) {
|
|
||||||
const site = parseSiteFromDataset(container.dataset)
|
|
||||||
|
|
||||||
const sharedLinkAuth = container.dataset.sharedLinkAuth
|
|
||||||
if (sharedLinkAuth) {
|
|
||||||
api.setSharedLinkAuth(sharedLinkAuth)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
filtersBackwardsCompatibilityRedirect(window.location, window.history)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error redirecting in a backwards compatible way', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = createAppRouter(site);
|
|
||||||
const app = (
|
|
||||||
<ErrorBoundary>
|
|
||||||
<ThemeContextProvider>
|
|
||||||
<SiteContextProvider site={site}>
|
|
||||||
<UserContextProvider role={container.dataset.currentUserRole} loggedIn={container.dataset.loggedIn === 'true'}>
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</UserContextProvider>
|
|
||||||
</SiteContextProvider>
|
|
||||||
</ThemeContextProvider>
|
|
||||||
</ErrorBoundary>
|
|
||||||
)
|
|
||||||
|
|
||||||
const root = createRoot(container)
|
|
||||||
root.render(app)
|
|
||||||
}
|
|
76
assets/js/dashboard.tsx
Normal file
76
assets/js/dashboard.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/** @format */
|
||||||
|
|
||||||
|
import React, { ReactNode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import 'url-search-params-polyfill'
|
||||||
|
|
||||||
|
import { RouterProvider } from 'react-router-dom'
|
||||||
|
import { createAppRouter } from './dashboard/router'
|
||||||
|
import ErrorBoundary from './dashboard/error/error-boundary'
|
||||||
|
import * as api from './dashboard/api'
|
||||||
|
import * as timer from './dashboard/util/realtime-update-timer'
|
||||||
|
import { filtersBackwardsCompatibilityRedirect } from './dashboard/query'
|
||||||
|
import SiteContextProvider, {
|
||||||
|
parseSiteFromDataset
|
||||||
|
} from './dashboard/site-context'
|
||||||
|
import UserContextProvider, { Role } from './dashboard/user-context'
|
||||||
|
import ThemeContextProvider from './dashboard/theme-context'
|
||||||
|
import {
|
||||||
|
GoBackToDashboard,
|
||||||
|
GoToSites,
|
||||||
|
SomethingWentWrongMessage
|
||||||
|
} from './dashboard/error/something-went-wrong'
|
||||||
|
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
const container = document.getElementById('stats-react-container')
|
||||||
|
|
||||||
|
if (container && container.dataset) {
|
||||||
|
let app: ReactNode
|
||||||
|
|
||||||
|
try {
|
||||||
|
const site = parseSiteFromDataset(container.dataset)
|
||||||
|
|
||||||
|
const sharedLinkAuth = container.dataset.sharedLinkAuth
|
||||||
|
|
||||||
|
if (sharedLinkAuth) {
|
||||||
|
api.setSharedLinkAuth(sharedLinkAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
filtersBackwardsCompatibilityRedirect(window.location, window.history)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error redirecting in a backwards compatible way', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = createAppRouter(site)
|
||||||
|
|
||||||
|
app = (
|
||||||
|
<ErrorBoundary
|
||||||
|
renderFallbackComponent={({ error }) => (
|
||||||
|
<SomethingWentWrongMessage
|
||||||
|
error={error}
|
||||||
|
callToAction={<GoBackToDashboard site={site} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ThemeContextProvider>
|
||||||
|
<SiteContextProvider site={site}>
|
||||||
|
<UserContextProvider
|
||||||
|
role={container.dataset.currentUserRole as Role}
|
||||||
|
loggedIn={container.dataset.loggedIn === 'true'}
|
||||||
|
>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</UserContextProvider>
|
||||||
|
</SiteContextProvider>
|
||||||
|
</ThemeContextProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading dashboard', err)
|
||||||
|
app = <SomethingWentWrongMessage error={err} callToAction={<GoToSites />} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = createRoot(container)
|
||||||
|
root.render(app)
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import RocketIcon from './stats/modals/rocket-icon'
|
|
||||||
|
|
||||||
export default class ErrorBoundary extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {error: null}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error) {
|
|
||||||
return {error: error}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.error) {
|
|
||||||
return (
|
|
||||||
<div className="text-center text-gray-900 dark:text-gray-100 mt-36">
|
|
||||||
<RocketIcon />
|
|
||||||
<div className="text-lg font-bold">Oops! Something went wrong</div>
|
|
||||||
<div className="text-lg">{this.state.error.name + ': ' + this.state.error.message}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
68
assets/js/dashboard/error/error-boundary.test.tsx
Normal file
68
assets/js/dashboard/error/error-boundary.test.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/** @format */
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import ErrorBoundary from './error-boundary'
|
||||||
|
|
||||||
|
const consoleErrorSpy = jest
|
||||||
|
.spyOn(global.console, 'error')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
|
||||||
|
const HappyPathUI = () => {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
if (count > 0) {
|
||||||
|
throw new Error('Anything')
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
data-testid="happy-path-ui"
|
||||||
|
onClick={() => {
|
||||||
|
setCount(1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Throw error
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorUI = ({ error }: { error?: unknown }) => {
|
||||||
|
return <div data-testid="error-ui">message: {(error as Error).message}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
it('shows only on error', async () => {
|
||||||
|
render(
|
||||||
|
<ErrorBoundary renderFallbackComponent={ErrorUI}>
|
||||||
|
<HappyPathUI />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
expect(screen.getByTestId('happy-path-ui')).toBeVisible()
|
||||||
|
expect(screen.queryByTestId('error-ui')).toBeNull()
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Throw error'))
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('happy-path-ui')).toBeNull()
|
||||||
|
expect(screen.getByTestId('error-ui')).toBeVisible()
|
||||||
|
|
||||||
|
expect(screen.getByText('message: Anything')).toBeVisible()
|
||||||
|
|
||||||
|
expect(consoleErrorSpy.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
expect.objectContaining({
|
||||||
|
detail: expect.objectContaining({ message: 'Anything' }),
|
||||||
|
type: 'unhandled exception'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
[
|
||||||
|
expect.objectContaining({
|
||||||
|
detail: expect.objectContaining({ message: 'Anything' }),
|
||||||
|
type: 'unhandled exception'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
[
|
||||||
|
expect.stringMatching(
|
||||||
|
'The above error occurred in the <HappyPathUI> component:'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
])
|
||||||
|
})
|
31
assets/js/dashboard/error/error-boundary.tsx
Normal file
31
assets/js/dashboard/error/error-boundary.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/** @format */
|
||||||
|
|
||||||
|
import React, { ReactNode, ReactElement } from 'react'
|
||||||
|
|
||||||
|
type ErrorBoundaryProps = {
|
||||||
|
children: ReactNode
|
||||||
|
renderFallbackComponent: (props: { error?: unknown }) => ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorBoundaryState = { error: null | unknown }
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends React.Component<
|
||||||
|
ErrorBoundaryProps,
|
||||||
|
ErrorBoundaryState
|
||||||
|
> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props)
|
||||||
|
this.state = { error: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: unknown) {
|
||||||
|
return { error }
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return this.props.renderFallbackComponent({ error: this.state.error })
|
||||||
|
}
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
27
assets/js/dashboard/error/something-went-wrong.test.tsx
Normal file
27
assets/js/dashboard/error/something-went-wrong.test.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/** @format */
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { GoToSites, SomethingWentWrongMessage } from './something-went-wrong'
|
||||||
|
|
||||||
|
it('handles unknown error', async () => {
|
||||||
|
render(<SomethingWentWrongMessage error={1} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Oops! Something went wrong.')).toBeVisible()
|
||||||
|
expect(screen.getByText('Unknown error')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles normal error', async () => {
|
||||||
|
render(<SomethingWentWrongMessage error={new Error('any message')} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Oops! Something went wrong.')).toBeVisible()
|
||||||
|
expect(screen.getByText('Error: any message')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows call to action if defined', async () => {
|
||||||
|
render(<SomethingWentWrongMessage error={1} callToAction={<GoToSites />} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Oops! Something went wrong.')).toBeVisible()
|
||||||
|
expect(screen.getByText('Try going back or')).toBeVisible()
|
||||||
|
expect(screen.getByRole('link', { name: 'go to your sites' })).toBeVisible()
|
||||||
|
})
|
70
assets/js/dashboard/error/something-went-wrong.tsx
Normal file
70
assets/js/dashboard/error/something-went-wrong.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/** @format */
|
||||||
|
|
||||||
|
import React, { ReactNode } from 'react'
|
||||||
|
import RocketIcon from '../stats/modals/rocket-icon'
|
||||||
|
import { useInRouterContext } from 'react-router-dom'
|
||||||
|
import { PlausibleSite } from '../site-context'
|
||||||
|
import { getRouterBasepath, rootRoute } from '../router'
|
||||||
|
import { AppNavigationLink } from '../navigation/use-app-navigate'
|
||||||
|
|
||||||
|
export function SomethingWentWrongMessage({
|
||||||
|
error,
|
||||||
|
callToAction = null
|
||||||
|
}: {
|
||||||
|
error: unknown
|
||||||
|
callToAction?: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="text-center text-gray-900 dark:text-gray-100 mt-36">
|
||||||
|
<RocketIcon />
|
||||||
|
<div className="text-lg">
|
||||||
|
<span className="font-bold">Oops! Something went wrong.</span>
|
||||||
|
{!!callToAction && ' '}
|
||||||
|
{callToAction}
|
||||||
|
</div>
|
||||||
|
<div className="text-md font-mono mt-2">
|
||||||
|
{error instanceof Error
|
||||||
|
? [error.name, error.message].join(': ')
|
||||||
|
: 'Unknown error'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkClass =
|
||||||
|
'hover:underline text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-600'
|
||||||
|
|
||||||
|
export function GoBackToDashboard({
|
||||||
|
site
|
||||||
|
}: {
|
||||||
|
site: Pick<PlausibleSite, 'domain' | 'shared'>
|
||||||
|
}) {
|
||||||
|
const canUseAppLink = useInRouterContext()
|
||||||
|
const linkText = 'go to dashboard'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<>Try going back or </>
|
||||||
|
{canUseAppLink ? (
|
||||||
|
<AppNavigationLink path={rootRoute.path} className={linkClass}>
|
||||||
|
{linkText}
|
||||||
|
</AppNavigationLink>
|
||||||
|
) : (
|
||||||
|
<a href={getRouterBasepath(site)} className={linkClass}>
|
||||||
|
{linkText}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GoToSites() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<>Try going back or </>
|
||||||
|
<a href={'/sites'} className={linkClass}>
|
||||||
|
{'go to your sites'}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,7 +1,13 @@
|
|||||||
/* @format */
|
/** @format */
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createBrowserRouter, Outlet } from 'react-router-dom'
|
import { createBrowserRouter, Outlet, useRouteError } from 'react-router-dom'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import { PlausibleSite, useSiteContext } from './site-context'
|
||||||
|
import {
|
||||||
|
GoBackToDashboard,
|
||||||
|
SomethingWentWrongMessage
|
||||||
|
} from './error/something-went-wrong'
|
||||||
import Dashboard from './index'
|
import Dashboard from './index'
|
||||||
import SourcesModal from './stats/modals/sources'
|
import SourcesModal from './stats/modals/sources'
|
||||||
import ReferrersDrilldownModal from './stats/modals/referrer-drilldown'
|
import ReferrersDrilldownModal from './stats/modals/referrer-drilldown'
|
||||||
@ -19,7 +25,6 @@ import PropsModal from './stats/modals/props'
|
|||||||
import ConversionsModal from './stats/modals/conversions'
|
import ConversionsModal from './stats/modals/conversions'
|
||||||
import FilterModal from './stats/modals/filter-modal'
|
import FilterModal from './stats/modals/filter-modal'
|
||||||
import QueryContextProvider from './query-context'
|
import QueryContextProvider from './query-context'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@ -156,19 +161,33 @@ export const filterRoute = {
|
|||||||
element: <FilterModal />
|
element: <FilterModal />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRouterBasepath(site) {
|
export function getRouterBasepath(
|
||||||
|
site: Pick<PlausibleSite, 'shared' | 'domain'>
|
||||||
|
): string {
|
||||||
const basepath = site.shared
|
const basepath = site.shared
|
||||||
? `/share/${encodeURIComponent(site.domain)}`
|
? `/share/${encodeURIComponent(site.domain)}`
|
||||||
: `/${encodeURIComponent(site.domain)}`
|
: `/${encodeURIComponent(site.domain)}`
|
||||||
return basepath
|
return basepath
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAppRouter(site) {
|
function RouteErrorElement() {
|
||||||
|
const site = useSiteContext()
|
||||||
|
const error = useRouteError()
|
||||||
|
return (
|
||||||
|
<SomethingWentWrongMessage
|
||||||
|
error={error}
|
||||||
|
callToAction={<GoBackToDashboard site={site} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAppRouter(site: PlausibleSite) {
|
||||||
const basepath = getRouterBasepath(site)
|
const basepath = getRouterBasepath(site)
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
...rootRoute,
|
...rootRoute,
|
||||||
|
errorElement: <RouteErrorElement />,
|
||||||
children: [
|
children: [
|
||||||
sourcesRoute,
|
sourcesRoute,
|
||||||
utmMediumsRoute,
|
utmMediumsRoute,
|
||||||
@ -199,6 +218,7 @@ export function createAppRouter(site) {
|
|||||||
{
|
{
|
||||||
basename: basepath,
|
basename: basepath,
|
||||||
future: {
|
future: {
|
||||||
|
// @ts-expect-error valid according to docs (https://reactrouter.com/en/main/routers/create-browser-router#optsfuture)
|
||||||
v7_prependBasename: true
|
v7_prependBasename: true
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
/* @format */
|
/** @format */
|
||||||
import React, { createContext, ReactNode, useContext } from 'react'
|
import React, { createContext, ReactNode, useContext } from 'react'
|
||||||
|
|
||||||
export function parseSiteFromDataset(dataset: Record<string, string>) {
|
export function parseSiteFromDataset(dataset: DOMStringMap): PlausibleSite {
|
||||||
const site = {
|
return {
|
||||||
domain: dataset.domain,
|
domain: dataset.domain!,
|
||||||
offset: dataset.offset,
|
offset: dataset.offset!,
|
||||||
hasGoals: dataset.hasGoals === 'true',
|
hasGoals: dataset.hasGoals === 'true',
|
||||||
hasProps: dataset.hasProps === 'true',
|
hasProps: dataset.hasProps === 'true',
|
||||||
funnelsAvailable: dataset.funnelsAvailable === 'true',
|
funnelsAvailable: dataset.funnelsAvailable === 'true',
|
||||||
@ -12,18 +12,17 @@ export function parseSiteFromDataset(dataset: Record<string, string>) {
|
|||||||
conversionsOptedOut: dataset.conversionsOptedOut === 'true',
|
conversionsOptedOut: dataset.conversionsOptedOut === 'true',
|
||||||
funnelsOptedOut: dataset.funnelsOptedOut === 'true',
|
funnelsOptedOut: dataset.funnelsOptedOut === 'true',
|
||||||
propsOptedOut: dataset.propsOptedOut === 'true',
|
propsOptedOut: dataset.propsOptedOut === 'true',
|
||||||
revenueGoals: JSON.parse(dataset.revenueGoals),
|
revenueGoals: JSON.parse(dataset.revenueGoals!),
|
||||||
funnels: JSON.parse(dataset.funnels),
|
funnels: JSON.parse(dataset.funnels!),
|
||||||
statsBegin: dataset.statsBegin,
|
statsBegin: dataset.statsBegin!,
|
||||||
nativeStatsBegin: dataset.nativeStatsBegin,
|
nativeStatsBegin: dataset.nativeStatsBegin!,
|
||||||
embedded: dataset.embedded,
|
embedded: dataset.embedded!,
|
||||||
background: dataset.background,
|
background: dataset.background!,
|
||||||
isDbip: dataset.isDbip === 'true',
|
isDbip: dataset.isDbip === 'true',
|
||||||
flags: JSON.parse(dataset.flags),
|
flags: JSON.parse(dataset.flags!),
|
||||||
validIntervalsByPeriod: JSON.parse(dataset.validIntervalsByPeriod),
|
validIntervalsByPeriod: JSON.parse(dataset.validIntervalsByPeriod!),
|
||||||
shared: !!dataset.sharedLinkAuth
|
shared: !!dataset.sharedLinkAuth
|
||||||
}
|
}
|
||||||
return site
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const siteContextDefaultValue = {
|
const siteContextDefaultValue = {
|
||||||
|
@ -5,7 +5,7 @@ import_config "prod.exs"
|
|||||||
config :esbuild,
|
config :esbuild,
|
||||||
default: [
|
default: [
|
||||||
args:
|
args:
|
||||||
~w(js/app.js js/dashboard.js js/embed.host.js js/embed.content.js --bundle --target=es2017 --loader:.js=jsx --outdir=../priv/static/js --define:BUILD_EXTRA=false),
|
~w(js/app.js js/dashboard.tsx js/embed.host.js js/embed.content.js --bundle --target=es2017 --loader:.js=jsx --outdir=../priv/static/js --define:BUILD_EXTRA=false),
|
||||||
cd: Path.expand("../assets", __DIR__),
|
cd: Path.expand("../assets", __DIR__),
|
||||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||||
]
|
]
|
||||||
|
@ -5,7 +5,7 @@ import_config "dev.exs"
|
|||||||
config :esbuild,
|
config :esbuild,
|
||||||
default: [
|
default: [
|
||||||
args:
|
args:
|
||||||
~w(js/app.js js/dashboard.js js/embed.host.js js/embed.content.js --bundle --target=es2017 --loader:.js=jsx --outdir=../priv/static/js --define:BUILD_EXTRA=false),
|
~w(js/app.js js/dashboard.tsx js/embed.host.js js/embed.content.js --bundle --target=es2017 --loader:.js=jsx --outdir=../priv/static/js --define:BUILD_EXTRA=false),
|
||||||
cd: Path.expand("../assets", __DIR__),
|
cd: Path.expand("../assets", __DIR__),
|
||||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||||
]
|
]
|
||||||
|
@ -20,7 +20,7 @@ config :esbuild,
|
|||||||
version: "0.17.11",
|
version: "0.17.11",
|
||||||
default: [
|
default: [
|
||||||
args:
|
args:
|
||||||
~w(js/app.js js/dashboard.js js/embed.host.js js/embed.content.js --bundle --target=es2017 --loader:.js=jsx --outdir=../priv/static/js --define:BUILD_EXTRA=true),
|
~w(js/app.js js/dashboard.tsx js/embed.host.js js/embed.content.js --bundle --target=es2017 --loader:.js=jsx --outdir=../priv/static/js --define:BUILD_EXTRA=true),
|
||||||
cd: Path.expand("../assets", __DIR__),
|
cd: Path.expand("../assets", __DIR__),
|
||||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user