fix: update Next and harden the standalone runtime image (#201)
* fix: update Next and harden the standalone runtime image Upgrade Next and eslint-config-next to 16.4.1; build and run on the same pinned Alpine Node image, exclude pnpm cache from context, and omit build-only package managers from production. Co-Authored-By: Codex <noreply@openai.com> * test: make standalone staging checks portable to Windows Use platform paths for symlink comparison and the export-presence mock. Keep Unix permission-bit checks in an explicit Unix-only case; preserve file contents and relative-link behavior on Windows. Co-Authored-By: Codex <noreply@openai.com> * test: load the real developer view before DOM timing assertions The first developer-view test also fails on unchanged main under cold Windows transforms. Compile the real lazy component in setup while retaining its rendering, API assertions and original timeouts. All nine targeted tests pass. Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: James Maes <jmaes@dmdbrands.com> Co-authored-by: Codex <noreply@openai.com>
This commit was merged in pull request #201.
This commit is contained in:
7 files changed
+334
-299
No files matched your search
@@ -6,3 +6,5 @@ playwright-report
|
||||
coverage
|
||||
.env*
|
||||
*.log
|
||||
|
||||
.pnpm-store
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM registry-1.docker.io/library/node:22-bookworm-slim AS frontend
|
||||
FROM registry-1.docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS frontend
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
|
||||
@@ -9,8 +9,12 @@ ENV QQQ_BACKEND_URL=http://host.docker.internal:8000
|
||||
ENV NEXT_PUBLIC_MOCK_API=false
|
||||
RUN pnpm build
|
||||
|
||||
FROM registry-1.docker.io/library/node:22-bookworm-slim AS runtime
|
||||
FROM registry-1.docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS runtime
|
||||
LABEL org.opencontainers.image.source="https://github.com/QRun-IO/qqq-frontend-next"
|
||||
# Standalone production starts node directly; package managers are build-only.
|
||||
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /opt/yarn-* \
|
||||
&& rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \
|
||||
/usr/local/bin/pnpm /usr/local/bin/pnpx /usr/local/bin/yarn /usr/local/bin/yarnpkg
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0 PORT=3000
|
||||
COPY --from=frontend --chown=node:node /app/.next/standalone ./
|
||||
|
||||
+2
-2
@@ -57,7 +57,7 @@
|
||||
"dompurify": "^3.4.16",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^0.485.0",
|
||||
"next": "16.3.8",
|
||||
"next": "16.4.1",
|
||||
"rapidoc": "10.1.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -93,7 +93,7 @@
|
||||
"@vitest/ui": "^4.1.11",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-config-next": "16.3.6",
|
||||
"eslint-config-next": "16.4.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-jsdoc": "^62.7.1",
|
||||
"jest-axe": "^10.0.0",
|
||||
|
||||
Generated
+310
-292
File diff suppressed because it is too large.
Load diff
@@ -19,7 +19,7 @@
|
||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { QInstance } from '@/types'
|
||||
import { QContextProvider, useQContext } from '@/lib/context/q-context'
|
||||
@@ -113,6 +113,10 @@ function renderPage() {
|
||||
}
|
||||
|
||||
describe('RecordDeveloperViewPage', () => {
|
||||
// Transform the real lazy module before DOM waits begin. Cold Vitest transforms
|
||||
// on Windows can otherwise consume the first test's entire five-second wait.
|
||||
beforeAll(async () => { await import('@/components/developer/RecordDeveloperView') })
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getRecord).mockResolvedValue(record)
|
||||
vi.mocked(getRecordDeveloperData).mockResolvedValue({
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
/** @file Exercises the real variant lifecycle with only process and filesystem boundaries replaced. */
|
||||
import { EventEmitter } from 'node:events'
|
||||
import path from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
@@ -39,7 +40,7 @@ vi.mock('node:child_process', () => {
|
||||
// Existing stale export and build files cannot determine the browser's selected server.
|
||||
vi.mock('node:fs', async (original) => {
|
||||
const fs = {
|
||||
...await original<typeof import('node:fs')>(), existsSync: (file: string) => file.endsWith('next-dashboard/index.html') ? state.hasExport : true, mkdirSync: () => {}, rmSync: () => {},
|
||||
...await original<typeof import('node:fs')>(), existsSync: (file: string) => file.endsWith(path.join('next-dashboard', 'index.html')) ? state.hasExport : true, mkdirSync: () => {}, rmSync: () => {},
|
||||
readdirSync: () => ['Fixture.java'],
|
||||
readFileSync: (file: string) => file.endsWith('routes-manifest.json')
|
||||
? JSON.stringify({ rewrites: { beforeFiles: [], afterFiles: ['qqq', 'data', 'widget', 'metaData', 'download', 'processes', 'possibleValues', 'reports', 'manageSession', 'apis.json', 'api', 'qa'].map((prefix) => ({ source: `/${prefix}/:path*`, destination: `http://127.0.0.1:${state.wrongTarget ? '18765' : '18775'}/${prefix}/:path*` })), fallback: [] } })
|
||||
|
||||
@@ -49,8 +49,14 @@ it('keeps the variant build, static assets and entrypoint after the main build d
|
||||
expect(readFileSync(path.join(staged, 'public/icon.svg'), 'utf8')).toBe('<svg/>')
|
||||
expect(readFileSync(path.join(staged, 'qqq-server.mjs'), 'utf8')).toBe('container entrypoint')
|
||||
expect(readFileSync(path.join(staged, 'branding-assets.mjs'), 'utf8')).toBe('declared branding aliases')
|
||||
expect(readlinkSync(path.join(staged, 'node_modules/example'))).toBe('.pnpm/example')
|
||||
expect(path.normalize(readlinkSync(path.join(staged, 'node_modules/example')))).toBe(path.join('.pnpm', 'example'))
|
||||
expect(readFileSync(path.join(staged, 'node_modules/example/index.js'), 'utf8')).toBe('independent dependency')
|
||||
})
|
||||
|
||||
// Windows permissions are ACL-based; Node's mode bits do not represent this policy.
|
||||
it.skipIf(process.platform === 'win32')('restricts staged Unix file and directory permissions', () => {
|
||||
const { source, staged } = fixture()
|
||||
stageStandalone(staged, backend, source)
|
||||
expect(statSync(path.join(staged, 'server.js')).mode & 0o777).toBe(0o600)
|
||||
expect(statSync(path.join(staged, '.next/static')).mode & 0o777).toBe(0o700)
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user