Compare commits
4 commits
main
...
feat/i18n-
Author | SHA1 | Date | |
---|---|---|---|
|
a4015a22c4 | ||
|
7678ef33a0 | ||
|
9f3f110268 | ||
|
18223d9cde |
173 changed files with 1794 additions and 897 deletions
8
.Dockerfile
Normal file
8
.Dockerfile
Normal file
|
@ -0,0 +1,8 @@
|
|||
FROM gitpod/workspace-node
|
||||
|
||||
# Install latest pnpm
|
||||
RUN curl -fsSL https://get.pnpm.io/install.sh | SHELL=`which bash` bash -
|
||||
|
||||
# Install deno in gitpod
|
||||
RUN curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||
RUN /home/gitpod/.deno/bin/deno completions bash > /home/gitpod/.bashrc.d/90-deno && echo 'export DENO_INSTALL="/home/gitpod/.deno"' >> /home/gitpod/.bashrc.d/90-deno && echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> /home/gitpod/.bashrc.d/90-deno
|
5
.changeset/cuddly-vans-reply.md
Normal file
5
.changeset/cuddly-vans-reply.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/markdown-remark': patch
|
||||
---
|
||||
|
||||
Remove `is:raw` from remark Shiki plugin
|
25
.changeset/eleven-buttons-wash.md
Normal file
25
.changeset/eleven-buttons-wash.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
'@astrojs/cloudflare': patch
|
||||
'@astrojs/partytown': patch
|
||||
'@astrojs/alpinejs': patch
|
||||
'@astrojs/prefetch': patch
|
||||
'@astrojs/tailwind': patch
|
||||
'@astrojs/markdoc': patch
|
||||
'@astrojs/sitemap': patch
|
||||
'@astrojs/underscore-redirects': patch
|
||||
'@astrojs/preact': patch
|
||||
'@astrojs/svelte': patch
|
||||
'@astrojs/vercel': patch
|
||||
'@astrojs/react': patch
|
||||
'@astrojs/solid-js': patch
|
||||
'@astrojs/node': patch
|
||||
'@astrojs/lit': patch
|
||||
'@astrojs/mdx': patch
|
||||
'@astrojs/vue': patch
|
||||
'@astrojs/internal-helpers': patch
|
||||
'@astrojs/markdown-remark': patch
|
||||
'@astrojs/telemetry': patch
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Add provenance statement when publishing the library from CI
|
5
.changeset/fair-otters-worry.md
Normal file
5
.changeset/fair-otters-worry.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Improve error message when user attempts to render a dynamic component reference
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fixed an issue where the transitions router did not work within framework components.
|
5
.changeset/green-crabs-breathe.md
Normal file
5
.changeset/green-crabs-breathe.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/telemetry': patch
|
||||
---
|
||||
|
||||
Removed an unnecessary dependency.
|
5
.changeset/red-masks-drop.md
Normal file
5
.changeset/red-masks-drop.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fix `tsconfig.json` update causing the server to crash
|
5
.changeset/tasty-meals-buy.md
Normal file
5
.changeset/tasty-meals-buy.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Remove unused CSS output files when inlined
|
5
.changeset/thick-cups-knock.md
Normal file
5
.changeset/thick-cups-knock.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Update link for Netlify SSR
|
5
.changeset/thirty-ravens-fly.md
Normal file
5
.changeset/thirty-ravens-fly.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Node-based adapters now create less server-side javascript
|
5
.changeset/tricky-otters-cross.md
Normal file
5
.changeset/tricky-otters-cross.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/partytown': patch
|
||||
---
|
||||
|
||||
Expose types for TypeScript users
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'@astrojs/cloudflare': patch
|
||||
---
|
||||
|
||||
fixes `AdvancedRuntime` & `DirectoryRuntime` types to work woth Cloudflare caches
|
5
.changeset/witty-fishes-heal.md
Normal file
5
.changeset/witty-fishes-heal.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Improve `astro info` copy to clipboard compatability
|
|
@ -6,6 +6,10 @@ RUN npm install -g @playwright/test
|
|||
# Install latest pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install deno
|
||||
ENV DENO_INSTALL=/usr/local
|
||||
RUN curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& curl -sSL https://dl.google.com/linux/direct/google-chrome-stable_current_$(dpkg --print-architecture).deb -o /tmp/chrome.deb \
|
||||
&& apt-get -y install /tmp/chrome.deb
|
||||
|
|
34
.devcontainer/deno/devcontainer.json
Normal file
34
.devcontainer/deno/devcontainer.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "Deno",
|
||||
"build": {
|
||||
"dockerfile": "../examples.deno.Dockerfile"
|
||||
},
|
||||
|
||||
"workspaceFolder": "/workspaces/astro/examples/deno",
|
||||
|
||||
"portsAttributes": {
|
||||
"4321": {
|
||||
"label": "Application",
|
||||
"onAutoForward": "openPreview"
|
||||
}
|
||||
},
|
||||
|
||||
"forwardPorts": [4321],
|
||||
|
||||
"postCreateCommand": "pnpm install && cd /workspaces/astro && pnpm run build",
|
||||
|
||||
"waitFor": "postCreateCommand",
|
||||
|
||||
"postAttachCommand": {
|
||||
"Server": "pnpm start --host"
|
||||
},
|
||||
|
||||
"customizations": {
|
||||
"codespaces": {
|
||||
"openFiles": ["src/pages/index.astro"]
|
||||
},
|
||||
"vscode": {
|
||||
"extensions": ["astro-build.astro-vscode", "esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
10
.devcontainer/examples.deno.Dockerfile
Normal file
10
.devcontainer/examples.deno.Dockerfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
FROM mcr.microsoft.com/devcontainers/javascript-node:0-18
|
||||
|
||||
# Install latest pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install deno
|
||||
ENV DENO_INSTALL=/usr/local
|
||||
RUN curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||
|
||||
COPY example-welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
@ -133,6 +133,11 @@ jobs:
|
|||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Use Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.35.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
|
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
@ -22,9 +22,6 @@ jobs:
|
|||
name: Changelog PR or Release
|
||||
if: ${{ github.repository_owner == 'withastro' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
|
7
.github/workflows/snapshot-release.yml
vendored
7
.github/workflows/snapshot-release.yml
vendored
|
@ -19,11 +19,6 @@ jobs:
|
|||
name: Create a snapshot release of a pull request
|
||||
if: ${{ github.repository_owner == 'withastro' && github.event.issue.pull_request && startsWith(github.event.comment.body, '!preview') }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Check if user has admin access (only admins can publish snapshot releases)."
|
||||
uses: "lannonbr/repo-permission-check-action@2.0.0"
|
||||
|
@ -86,8 +81,6 @@ jobs:
|
|||
id: publish
|
||||
run: |
|
||||
pnpm run release --tag next--${{ steps.getSnapshotName.outputs.result }} > publish.output.txt 2>&1
|
||||
echo "Release complete"
|
||||
cat publish.output.txt
|
||||
echo ::set-output name=result::`cat publish.output.txt`
|
||||
env:
|
||||
# Needs access to publish to npm
|
||||
|
|
24
.github/workflows/test-hosts.yml
vendored
24
.github/workflows/test-hosts.yml
vendored
|
@ -11,6 +11,8 @@ env:
|
|||
VERCEL_ORG_ID: ${{ secrets.VERCEL_TEST_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_TEST_PROJECT_ID }}
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_TEST_SITE_ID }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TEST_AUTH_TOKEN }}
|
||||
FORCE_COLOR: true
|
||||
|
||||
jobs:
|
||||
|
@ -32,20 +34,22 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Astro
|
||||
run: pnpm turbo build --filter astro --filter @astrojs/vercel
|
||||
|
||||
- name: Build test project
|
||||
- name: Install Hosts CLIs
|
||||
run: pnpm install --global netlify-cli vercel
|
||||
|
||||
- name: Deploy Vercel
|
||||
working-directory: ./packages/integrations/vercel/test/hosted/hosted-astro-project
|
||||
run:
|
||||
pnpm run build
|
||||
|
||||
- name: Deploy to Vercel
|
||||
working-directory: ./packages/integrations/vercel/test/hosted/hosted-astro-project
|
||||
run:
|
||||
pnpm dlx vercel --prod --prebuilt
|
||||
vercel --prod --prebuilt
|
||||
|
||||
- name: Test
|
||||
- name: Deploy Netlify
|
||||
working-directory: ./packages/integrations/netlify/test/hosted/hosted-astro-project
|
||||
run:
|
||||
pnpm run build
|
||||
netlify deploy --prod
|
||||
|
||||
- name: Test both hosts
|
||||
run:
|
||||
pnpm run test:e2e:hosts
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
---
|
||||
image:
|
||||
file: .Dockerfile
|
||||
# Commands to start on workspace startup
|
||||
tasks:
|
||||
- before: |
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
# Convert context URL to an array
|
||||
mapfile -t CONTEXT_URL_ITEMS < <(echo "$GITPOD_WORKSPACE_CONTEXT_URL" | tr '/' '\n')
|
||||
|
||||
# Install latest pnpm
|
||||
curl -fsSL https://get.pnpm.io/install.sh | SHELL=`which bash` bash -
|
||||
|
||||
# Check if Gitpod started from a specific example directory in the repository
|
||||
if [ "${CONTEXT_URL_ITEMS[7]}" = "examples" ]; then
|
||||
EXAMPLE_PROJECT=${CONTEXT_URL_ITEMS[8]}
|
||||
|
|
|
@ -52,6 +52,8 @@ Join us on [Discord](https://astro.build/chat) to meet other maintainers. We'll
|
|||
| [@astrojs/svelte](packages/integrations/svelte) | [![astro version](https://img.shields.io/npm/v/@astrojs/svelte.svg?label=%20)](packages/integrations/svelte/CHANGELOG.md) |
|
||||
| [@astrojs/vue](packages/integrations/vue) | [![astro version](https://img.shields.io/npm/v/@astrojs/vue.svg?label=%20)](packages/integrations/vue/CHANGELOG.md) |
|
||||
| [@astrojs/lit](packages/integrations/lit) | [![astro version](https://img.shields.io/npm/v/@astrojs/lit.svg?label=%20)](packages/integrations/lit/CHANGELOG.md) |
|
||||
| [@astrojs/deno](packages/integrations/deno) | [![astro version](https://img.shields.io/npm/v/@astrojs/deno.svg?label=%20)](packages/integrations/deno/CHANGELOG.md) |
|
||||
| [@astrojs/netlify](packages/integrations/netlify) | [![astro version](https://img.shields.io/npm/v/@astrojs/netlify.svg?label=%20)](packages/integrations/netlify/CHANGELOG.md) |
|
||||
| [@astrojs/node](packages/integrations/node) | [![astro version](https://img.shields.io/npm/v/@astrojs/node.svg?label=%20)](packages/integrations/node/CHANGELOG.md) |
|
||||
| [@astrojs/vercel](packages/integrations/vercel) | [![astro version](https://img.shields.io/npm/v/@astrojs/vercel.svg?label=%20)](packages/integrations/vercel/CHANGELOG.md) |
|
||||
| [@astrojs/cloudflare](packages/integrations/cloudflare) | [![astro version](https://img.shields.io/npm/v/@astrojs/cloudflare.svg?label=%20)](packages/integrations/cloudflare/CHANGELOG.md) |
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^3.2.4"
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^1.1.1",
|
||||
"@astrojs/mdx": "^1.1.0",
|
||||
"@astrojs/rss": "^3.0.0",
|
||||
"@astrojs/sitemap": "^3.0.1",
|
||||
"astro": "^3.2.4"
|
||||
"@astrojs/sitemap": "^3.0.0",
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
],
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"astro": "^3.2.4"
|
||||
"astro": "^3.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "^2.0.0-beta.0"
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/alpinejs": "^0.3.1",
|
||||
"@astrojs/alpinejs": "^0.3.0",
|
||||
"@types/alpinejs": "^3.7.2",
|
||||
"alpinejs": "^3.12.3",
|
||||
"astro": "^3.2.4"
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/lit": "^3.0.1",
|
||||
"@astrojs/lit": "^3.0.0",
|
||||
"@webcomponents/template-shadowroot": "^0.2.1",
|
||||
"astro": "^3.2.4",
|
||||
"astro": "^3.2.2",
|
||||
"lit": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,12 +11,12 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^3.0.1",
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@astrojs/solid-js": "^3.0.2",
|
||||
"@astrojs/svelte": "^4.0.3",
|
||||
"@astrojs/vue": "^3.0.1",
|
||||
"astro": "^3.2.4",
|
||||
"@astrojs/preact": "^3.0.0",
|
||||
"@astrojs/react": "^3.0.2",
|
||||
"@astrojs/solid-js": "^3.0.1",
|
||||
"@astrojs/svelte": "^4.0.2",
|
||||
"@astrojs/vue": "^3.0.0",
|
||||
"astro": "^3.2.2",
|
||||
"preact": "^10.17.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^3.0.1",
|
||||
"@astrojs/preact": "^3.0.0",
|
||||
"@preact/signals": "^1.2.1",
|
||||
"astro": "^3.2.4",
|
||||
"astro": "^3.2.2",
|
||||
"preact": "^10.17.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@astrojs/react": "^3.0.2",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"astro": "^3.2.4",
|
||||
"astro": "^3.2.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/solid-js": "^3.0.2",
|
||||
"astro": "^3.2.4",
|
||||
"@astrojs/solid-js": "^3.0.1",
|
||||
"astro": "^3.2.2",
|
||||
"solid-js": "^1.7.11"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/svelte": "^4.0.3",
|
||||
"astro": "^3.2.4",
|
||||
"@astrojs/svelte": "^4.0.2",
|
||||
"astro": "^3.2.2",
|
||||
"svelte": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/vue": "^3.0.1",
|
||||
"astro": "^3.2.4",
|
||||
"@astrojs/vue": "^3.0.0",
|
||||
"astro": "^3.2.2",
|
||||
"vue": "^3.3.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^6.0.3",
|
||||
"astro": "^3.2.4"
|
||||
"@astrojs/node": "^6.0.2",
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ export default function createIntegration(): AstroIntegration {
|
|||
// See the @astrojs/react integration for an example
|
||||
// https://github.com/withastro/astro/blob/main/packages/integrations/react/src/index.ts
|
||||
},
|
||||
'astro:build:setup': ({ pages, updateConfig }) => {
|
||||
// See the @astrojs/lit integration for an example
|
||||
// https://github.com/withastro/astro/blob/main/packages/integrations/lit/src/index.ts
|
||||
'astro:build:setup': ({ config, updateConfig }) => {
|
||||
// See the @astrojs/netlify integration for an example
|
||||
// https://github.com/withastro/astro/blob/main/packages/integrations/netlify/src/integration-functions.ts
|
||||
},
|
||||
'astro:build:done': ({ dir, routes }) => {
|
||||
// See the @astrojs/partytown integration for an example
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
],
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"astro": "^3.2.4"
|
||||
"astro": "^3.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "^2.0.0-beta.0"
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
"server": "node dist/server/entry.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^6.0.3",
|
||||
"astro": "^3.2.4",
|
||||
"@astrojs/node": "^6.0.2",
|
||||
"astro": "^3.2.2",
|
||||
"html-minifier": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^3.2.4"
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^3.2.4"
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^3.2.4"
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
"server": "node dist/server/entry.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^6.0.3",
|
||||
"@astrojs/svelte": "^4.0.3",
|
||||
"astro": "^3.2.4",
|
||||
"@astrojs/node": "^6.0.2",
|
||||
"@astrojs/svelte": "^4.0.2",
|
||||
"astro": "^3.2.2",
|
||||
"svelte": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/tailwind": "^5.0.2",
|
||||
"@astrojs/node": "^6.0.3",
|
||||
"astro": "^3.2.4"
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"@astrojs/node": "^6.0.2",
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/markdoc": "^0.5.2",
|
||||
"astro": "^3.2.4"
|
||||
"@astrojs/markdoc": "^0.5.1",
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/markdown-remark": "^3.2.1",
|
||||
"astro": "^3.2.4",
|
||||
"@astrojs/markdown-remark": "^3.2.0",
|
||||
"astro": "^3.2.2",
|
||||
"hast-util-select": "^5.0.5",
|
||||
"rehype-autolink-headings": "^6.1.1",
|
||||
"rehype-slug": "^5.1.0",
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^3.2.4"
|
||||
"astro": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^1.1.1",
|
||||
"@astrojs/preact": "^3.0.1",
|
||||
"astro": "^3.2.4",
|
||||
"@astrojs/mdx": "^1.1.0",
|
||||
"@astrojs/preact": "^3.0.0",
|
||||
"astro": "^3.2.2",
|
||||
"preact": "^10.17.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^3.0.1",
|
||||
"@astrojs/preact": "^3.0.0",
|
||||
"@nanostores/preact": "^0.5.0",
|
||||
"astro": "^3.2.4",
|
||||
"astro": "^3.2.2",
|
||||
"nanostores": "^0.9.3",
|
||||
"preact": "^10.17.1"
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^1.1.1",
|
||||
"@astrojs/tailwind": "^5.0.2",
|
||||
"@astrojs/mdx": "^1.1.0",
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"@types/canvas-confetti": "^1.6.0",
|
||||
"astro": "^3.2.4",
|
||||
"astro": "^3.2.2",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"postcss": "^8.4.28",
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^3.2.4",
|
||||
"astro": "^3.2.2",
|
||||
"vite-plugin-pwa": "0.16.4",
|
||||
"workbox-window": "^7.0.0"
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^3.2.4",
|
||||
"astro": "^3.2.2",
|
||||
"vitest": "^0.34.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,5 @@
|
|||
# astro
|
||||
|
||||
## 3.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8638](https://github.com/withastro/astro/pull/8638) [`160d1cd75`](https://github.com/withastro/astro/commit/160d1cd755e70af1d8ec294d01dd2cb32d60db50) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - The `@astrojs/tailwind` integration now creates a `tailwind.config.mjs` file by default
|
||||
|
||||
- [#8767](https://github.com/withastro/astro/pull/8767) [`30de32436`](https://github.com/withastro/astro/commit/30de324361bc261956eb9fc08fe60a82ff602a9b) Thanks [@martrapp](https://github.com/martrapp)! - Revert fix #8472
|
||||
|
||||
[#8472](https://github.com/withastro/astro/pull/8472) caused some style files from previous pages to not be cleanly deleted on view transitions. For a discussion of a future fix for the original issue [#8144](https://github.com/withastro/astro/issues/8114) see [#8745](https://github.com/withastro/astro/pull/8745).
|
||||
|
||||
- [#8741](https://github.com/withastro/astro/pull/8741) [`c4a7ec425`](https://github.com/withastro/astro/commit/c4a7ec4255e7acb9555cb8bb74ea13c5fbb2ac17) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixed an issue on Windows where lowercase drive letters in current working directory led to missing scripts and styles.
|
||||
|
||||
- [#8772](https://github.com/withastro/astro/pull/8772) [`c24f70d91`](https://github.com/withastro/astro/commit/c24f70d91601dd3a6b5a84f04d61824e775e9b44) Thanks [@martrapp](https://github.com/martrapp)! - Fix flickering during view transitions
|
||||
|
||||
- [#8754](https://github.com/withastro/astro/pull/8754) [`93b092266`](https://github.com/withastro/astro/commit/93b092266febfad16a48575f8eee12d5910bf071) Thanks [@bluwy](https://github.com/bluwy)! - Make CSS chunk names less confusing
|
||||
|
||||
- [#8776](https://github.com/withastro/astro/pull/8776) [`29cdfa024`](https://github.com/withastro/astro/commit/29cdfa024886dd581cb207586f7dfec6966bdd4e) Thanks [@martrapp](https://github.com/martrapp)! - Fix transition attributes on islands
|
||||
|
||||
- [#8773](https://github.com/withastro/astro/pull/8773) [`eaed844ea`](https://github.com/withastro/astro/commit/eaed844ea8f2f52e0c9caa40bb3ec7377e10595f) Thanks [@sumimakito](https://github.com/sumimakito)! - Fix an issue where HTML attributes do not render if getHTMLAttributes in an image service returns a Promise
|
||||
|
||||
## 3.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8737](https://github.com/withastro/astro/pull/8737) [`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c) Thanks [@ematipico](https://github.com/ematipico)! - Add provenance statement when publishing the library from CI
|
||||
|
||||
- [#8747](https://github.com/withastro/astro/pull/8747) [`d78806dfe`](https://github.com/withastro/astro/commit/d78806dfe0301ea7ffe6c7c1f783bd415ac7cda9) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improve error message when user attempts to render a dynamic component reference
|
||||
|
||||
- [#8736](https://github.com/withastro/astro/pull/8736) [`d1c75fe15`](https://github.com/withastro/astro/commit/d1c75fe158839699c59728cf3a83888e8c72a459) Thanks [@bluwy](https://github.com/bluwy)! - Fix `tsconfig.json` update causing the server to crash
|
||||
|
||||
- [#8743](https://github.com/withastro/astro/pull/8743) [`aa265d730`](https://github.com/withastro/astro/commit/aa265d73024422967c1b1c68ad268c419c6c798f) Thanks [@bluwy](https://github.com/bluwy)! - Remove unused CSS output files when inlined
|
||||
|
||||
- [#8700](https://github.com/withastro/astro/pull/8700) [`78adbc443`](https://github.com/withastro/astro/commit/78adbc4433208458291e36713909762e148e1e5d) Thanks [@jacobthesheep](https://github.com/jacobthesheep)! - Update link for Netlify SSR
|
||||
|
||||
- [#8729](https://github.com/withastro/astro/pull/8729) [`21e0757ea`](https://github.com/withastro/astro/commit/21e0757ea22a57d344c934045ca19db93b684436) Thanks [@lilnasy](https://github.com/lilnasy)! - Node-based adapters now create less server-side javascript
|
||||
|
||||
- [#8730](https://github.com/withastro/astro/pull/8730) [`357270f2a`](https://github.com/withastro/astro/commit/357270f2a3d0bf2aa634ba7e52e9d17618eff4a7) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improve `astro info` copy to clipboard compatability
|
||||
|
||||
- Updated dependencies [[`21f482657`](https://github.com/withastro/astro/commit/21f4826576c2c812a1604e18717799da3470decd), [`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c), [`21e0757ea`](https://github.com/withastro/astro/commit/21e0757ea22a57d344c934045ca19db93b684436)]:
|
||||
- @astrojs/markdown-remark@3.2.1
|
||||
- @astrojs/internal-helpers@0.2.1
|
||||
- @astrojs/telemetry@3.0.3
|
||||
|
||||
## 3.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
@ -32,13 +32,6 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
// windows drive letters can sometimes be lowercase, which vite cannot process
|
||||
if (process.platform === 'win32') {
|
||||
const cwd = process.cwd();
|
||||
const correctedCwd = cwd.slice(0, 1).toUpperCase() + cwd.slice(1);
|
||||
if (correctedCwd !== cwd) process.chdir(correctedCwd);
|
||||
}
|
||||
|
||||
return import('./dist/cli/index.js')
|
||||
.then(({ cli }) => cli(process.argv))
|
||||
.catch((error) => {
|
||||
|
|
10
packages/astro/client.d.ts
vendored
10
packages/astro/client.d.ts
vendored
|
@ -127,6 +127,16 @@ declare module 'astro:transitions/client' {
|
|||
export const navigate: TransitionRouterModule['navigate'];
|
||||
}
|
||||
|
||||
declare module 'astro:i18n' {
|
||||
type I18nModule = typeof import('./dist/i18n/index.js');
|
||||
|
||||
// TODO: documentation
|
||||
export const getI18nBaseUrl: (locale: string) => string;
|
||||
|
||||
// TODO: documentation
|
||||
export const getLocalesBaseUrl: () => string[];
|
||||
}
|
||||
|
||||
declare module 'astro:middleware' {
|
||||
export * from 'astro/middleware/namespace';
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import React from 'react';
|
||||
import { navigate } from "astro:transitions/client";
|
||||
export default function ClickToNavigate({ to, id }) {
|
||||
return <button id={id} onClick={() => navigate(to)}>Navigate to `{to}`</button>;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<ViewTransitions/>
|
||||
</head>
|
||||
<body>
|
||||
<p>Local transitions</p>
|
||||
<slot/>
|
||||
<script>
|
||||
document.addEventListener("astro:after-swap", () => {
|
||||
document.querySelector("p").addEventListener("transitionstart", () => {
|
||||
console.info("transitionstart");
|
||||
});
|
||||
document.documentElement.setAttribute("class", "blue");
|
||||
});
|
||||
document.dispatchEvent(new Event("astro:after-swap"));
|
||||
</script>
|
||||
</body>
|
||||
<style>
|
||||
p {
|
||||
transition: background-color 1s;
|
||||
}
|
||||
p {
|
||||
background-color: #0ee;
|
||||
color: red;
|
||||
}
|
||||
.blue p {
|
||||
background-color: #ee0;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
</html>
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
import ClickToNavigate from "../components/ClickToNavigate.jsx"
|
||||
import { ViewTransitions } from "astro:transitions";
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<ViewTransitions />
|
||||
</head>
|
||||
<body>
|
||||
<ClickToNavigate id="react-client-load-navigate-button" to="/two" client:load/>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
import ListenerLayout from '../components/listener-layout.astro';
|
||||
---
|
||||
<ListenerLayout>
|
||||
<a id="totwo" href="/listener-two">Go to listener two</a>
|
||||
</ListenerLayout>
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
import ListenerLayout from '../components/listener-layout.astro';
|
||||
---
|
||||
<ListenerLayout>
|
||||
<a id="toone" href="/listener-one">Go to listener one</a>
|
||||
</ListenerLayout>
|
|
@ -230,28 +230,6 @@ test.describe('View Transitions', () => {
|
|||
await expect(h, 'imported CSS updated').toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
});
|
||||
|
||||
test('No page rendering during swap()', async ({ page, astro }) => {
|
||||
let transitions = 0;
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'info' && msg.text() === 'transitionstart') ++transitions;
|
||||
});
|
||||
|
||||
// Go to page 1
|
||||
await page.goto(astro.resolveUrl('/listener-one'));
|
||||
let p = page.locator('#totwo');
|
||||
await expect(p, 'should have content').toHaveText('Go to listener two');
|
||||
// on load a CSS transition is started triggered by a class on the html element
|
||||
expect(transitions).toEqual(1);
|
||||
|
||||
// go to page 2
|
||||
await page.click('#totwo');
|
||||
p = page.locator('#toone');
|
||||
await expect(p, 'should have content').toHaveText('Go to listener one');
|
||||
// swap() resets that class, the after-swap listener sets it again.
|
||||
// the temporarily missing class must not trigger page rendering
|
||||
expect(transitions).toEqual(1);
|
||||
});
|
||||
|
||||
test('click hash links does not do navigation', async ({ page, astro }) => {
|
||||
// Go to page 1
|
||||
await page.goto(astro.resolveUrl('/one'));
|
||||
|
@ -670,7 +648,7 @@ test.describe('View Transitions', () => {
|
|||
expect(loads.length, 'There should be 2 page loads').toEqual(2);
|
||||
});
|
||||
|
||||
test.skip('client:only styles are retained on transition', async ({ page, astro }) => {
|
||||
test('client:only styles are retained on transition', async ({ page, astro }) => {
|
||||
const totalExpectedStyles = 7;
|
||||
|
||||
// Go to page 1
|
||||
|
@ -753,21 +731,6 @@ test.describe('View Transitions', () => {
|
|||
await expect(p, 'should have content').toHaveText('Page 1');
|
||||
});
|
||||
|
||||
test('Use the client side router in framework components', async ({ page, astro }) => {
|
||||
await page.goto(astro.resolveUrl('/client-load'));
|
||||
|
||||
// the button is set to naviagte() to /two
|
||||
const button = page.locator('#react-client-load-navigate-button');
|
||||
|
||||
await expect(button, 'should have content').toHaveText('Navigate to `/two`');
|
||||
|
||||
await button.click();
|
||||
|
||||
const p = page.locator('#two');
|
||||
|
||||
await expect(p, 'should have content').toHaveText('Page 2');
|
||||
});
|
||||
|
||||
test('body inline scripts do not re-execute on navigation', async ({ page, astro }) => {
|
||||
const errors = [];
|
||||
page.addListener('pageerror', (err) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "astro",
|
||||
"version": "3.2.4",
|
||||
"version": "3.2.2",
|
||||
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
|
||||
"type": "module",
|
||||
"author": "withastro",
|
||||
|
@ -78,7 +78,8 @@
|
|||
"default": "./dist/core/middleware/namespace.js"
|
||||
},
|
||||
"./transitions": "./dist/transitions/index.js",
|
||||
"./transitions/router": "./dist/transitions/router.js"
|
||||
"./transitions/router": "./dist/transitions/router.js",
|
||||
"./i18n": "./dist/i18n/index.js"
|
||||
},
|
||||
"imports": {
|
||||
"#astro/*": "./dist/*.js"
|
||||
|
|
|
@ -1330,6 +1330,84 @@ export interface AstroUserConfig {
|
|||
* ```
|
||||
*/
|
||||
optimizeHoistedScript?: boolean;
|
||||
|
||||
// TODO review with docs team before merging to `main`
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n
|
||||
* @type {object}
|
||||
* @version 3.*.*
|
||||
* @type {object}
|
||||
* @description
|
||||
*
|
||||
* Allows to configure the beaviour of the i18n routing
|
||||
*/
|
||||
i18n?: {
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.defaultLocale
|
||||
* @type {string}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* The default locale of your website/application
|
||||
*/
|
||||
defaultLocale: string;
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.locales
|
||||
* @type {string[]}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* A list of locales supported by the website.
|
||||
*/
|
||||
locales: string[];
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.fallback
|
||||
* @type {Record<string, string[]>}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* The fallback system of the locales. By default, the fallback system affect the **content only**, and it doesn't
|
||||
* do any redirects.
|
||||
*
|
||||
* This means that when attempting to navigate to a page that hasn't been translated, Astro will pull the content
|
||||
* from the page of the default locale and render it. No redirects will happen.
|
||||
*/
|
||||
fallback?: Record<string, string[]>;
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.fallbackControl
|
||||
* @type {"none" | "render" | "redirect"}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* Controls the fallback system of the internationalisation:
|
||||
* - `none`: Astro will do nothing and will return `404` if a translated page isn't translated;
|
||||
* - `redirect`: Astro will do a redirect to the fallback language if the translated page returns a `404`;
|
||||
* - `render`: currently unsupported by Astro
|
||||
*
|
||||
*/
|
||||
fallbackControl: 'none' | 'render' | 'redirect';
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.detectBrowserLanguage
|
||||
* @type {boolean}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* Whether Astro should detect the language of the browser - usually using the `Accept-Language` header. This is a feature
|
||||
* that should be supported by the adapter. If detected, the adapter can decide to redirect the user to the localised version of the website.
|
||||
*
|
||||
* When set to `true`, you should make sure that the adapter you're using is able to provide this feature to you.
|
||||
*/
|
||||
detectBrowserLanguage: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1790,6 +1868,11 @@ export type AstroFeatureMap = {
|
|||
* The adapter can emit static assets
|
||||
*/
|
||||
assets?: AstroAssetsFeature;
|
||||
|
||||
/**
|
||||
* List of features that orbit around the i18n routing
|
||||
*/
|
||||
i18n?: AstroInternationalisationFeature;
|
||||
};
|
||||
|
||||
export interface AstroAssetsFeature {
|
||||
|
@ -1804,6 +1887,13 @@ export interface AstroAssetsFeature {
|
|||
isSquooshCompatible?: boolean;
|
||||
}
|
||||
|
||||
export interface AstroInternationalisationFeature {
|
||||
/**
|
||||
* Wether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header.
|
||||
*/
|
||||
detectBrowserLanguage?: SupportsKind;
|
||||
}
|
||||
|
||||
export interface AstroAdapter {
|
||||
name: string;
|
||||
serverEntrypoint?: string;
|
||||
|
@ -2103,7 +2193,13 @@ export interface AstroPluginOptions {
|
|||
logger: Logger;
|
||||
}
|
||||
|
||||
export type RouteType = 'page' | 'endpoint' | 'redirect';
|
||||
/**
|
||||
* - page: a route that lives in the file system, usually an Astro component
|
||||
* - endpoint: a route that lives in the file system, usually a JS file that exposes endpoints methods
|
||||
* - redirect: a route points to another route that lives in the file system
|
||||
* - fallback: a route that doesn't exist in the file system that needs to be handled with other means, usually the middleware
|
||||
*/
|
||||
export type RouteType = 'page' | 'endpoint' | 'redirect' | 'fallback';
|
||||
|
||||
export interface RoutePart {
|
||||
content: string;
|
||||
|
|
|
@ -111,7 +111,7 @@ export async function getImage(
|
|||
src: imageURL,
|
||||
attributes:
|
||||
service.getHTMLAttributes !== undefined
|
||||
? await service.getHTMLAttributes(validatedOptions, imageConfig)
|
||||
? service.getHTMLAttributes(validatedOptions, imageConfig)
|
||||
: {},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ const ALIASES = new Map([
|
|||
]);
|
||||
const ASTRO_CONFIG_STUB = `import { defineConfig } from 'astro/config';\n\nexport default defineConfig({});`;
|
||||
const TAILWIND_CONFIG_STUB = `/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
|
@ -74,6 +74,7 @@ const OFFICIAL_ADAPTER_TO_IMPORT_MAP: Record<string, string> = {
|
|||
vercel: '@astrojs/vercel/serverless',
|
||||
cloudflare: '@astrojs/cloudflare',
|
||||
node: '@astrojs/node',
|
||||
deno: '@astrojs/deno',
|
||||
};
|
||||
|
||||
// Users might lack access to the global npm registry, this function
|
||||
|
@ -159,7 +160,7 @@ export async function add(names: string[], { flags }: AddOptions) {
|
|||
'./tailwind.config.mjs',
|
||||
'./tailwind.config.js',
|
||||
],
|
||||
defaultConfigFile: './tailwind.config.mjs',
|
||||
defaultConfigFile: './tailwind.config.cjs',
|
||||
defaultConfigContent: TAILWIND_CONFIG_STUB,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ type RawContentEvent = { name: ChokidarEvent; entry: string };
|
|||
type ContentEvent = { name: ChokidarEvent; entry: URL };
|
||||
|
||||
type DataEntryMetadata = Record<string, never>;
|
||||
type ContentEntryMetadata = { slug: string, path: string };
|
||||
type ContentEntryMetadata = { slug: string };
|
||||
type CollectionEntryMap = {
|
||||
[collection: string]:
|
||||
| {
|
||||
|
@ -276,7 +276,7 @@ export async function createContentTypesGenerator({
|
|||
if (!(entryKey in collectionEntryMap[collectionKey].entries)) {
|
||||
collectionEntryMap[collectionKey] = {
|
||||
type: 'content',
|
||||
entries: { ...collectionInfo.entries, [entryKey]: { slug: addedSlug, path: event.entry.toString() } },
|
||||
entries: { ...collectionInfo.entries, [entryKey]: { slug: addedSlug } },
|
||||
};
|
||||
}
|
||||
return { shouldGenerateTypes: true };
|
||||
|
@ -453,15 +453,7 @@ async function writeContentFiles({
|
|||
)}] }`;
|
||||
|
||||
const slugType = JSON.stringify(entryMetadata.slug);
|
||||
contentTypesStr += [
|
||||
`${entryKey}: {`,
|
||||
` id: ${entryKey};`,
|
||||
` slug: ${slugType};`,
|
||||
` path: ${JSON.stringify(entryMetadata.path)};`,
|
||||
` body: string;`,
|
||||
` collection: ${collectionKey};`,
|
||||
` data: ${dataType}`,
|
||||
`} & ${renderType};`].join("\n");
|
||||
contentTypesStr += `${entryKey}: {\n id: ${entryKey};\n slug: ${slugType};\n body: string;\n collection: ${collectionKey};\n data: ${dataType}\n} & ${renderType};\n`;
|
||||
}
|
||||
contentTypesStr += `};\n`;
|
||||
break;
|
||||
|
|
|
@ -25,6 +25,7 @@ export function getOutFolder(
|
|||
switch (routeType) {
|
||||
case 'endpoint':
|
||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||
case 'fallback':
|
||||
case 'page':
|
||||
case 'redirect':
|
||||
switch (astroConfig.build.format) {
|
||||
|
@ -52,6 +53,7 @@ export function getOutFile(
|
|||
case 'endpoint':
|
||||
return new URL(npath.basename(pathname), outFolder);
|
||||
case 'page':
|
||||
case 'fallback':
|
||||
case 'redirect':
|
||||
switch (astroConfig.build.format) {
|
||||
case 'directory': {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { GetModuleInfo, ModuleInfo } from 'rollup';
|
||||
import type { GetModuleInfo } from 'rollup';
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import npath from 'node:path';
|
||||
|
@ -6,29 +6,20 @@ import type { AstroSettings } from '../../@types/astro.js';
|
|||
import { viteID } from '../util.js';
|
||||
import { getTopLevelPages } from './graph.js';
|
||||
|
||||
// These pages could be used as base names for the chunk hashed name, but they are confusing
|
||||
// and should be avoided it possible
|
||||
const confusingBaseNames = ['404', '500'];
|
||||
|
||||
// The short name for when the hash can be included
|
||||
// We could get rid of this and only use the createSlugger implementation, but this creates
|
||||
// slightly prettier names.
|
||||
export function shortHashedName(id: string, ctx: { getModuleInfo: GetModuleInfo }): string {
|
||||
const parents = Array.from(getTopLevelPages(id, ctx));
|
||||
return createNameHash(
|
||||
getFirstParentId(parents),
|
||||
parents.map(([page]) => page.id)
|
||||
);
|
||||
}
|
||||
const firstParentId = parents[0]?.[0].id;
|
||||
const firstParentName = firstParentId ? npath.parse(firstParentId).name : 'index';
|
||||
|
||||
export function createNameHash(baseId: string | undefined, hashIds: string[]): string {
|
||||
const baseName = baseId ? prettifyBaseName(npath.parse(baseId).name) : 'index';
|
||||
const hash = crypto.createHash('sha256');
|
||||
for (const id of hashIds) {
|
||||
hash.update(id, 'utf-8');
|
||||
for (const [page] of parents) {
|
||||
hash.update(page.id, 'utf-8');
|
||||
}
|
||||
const h = hash.digest('hex').slice(0, 8);
|
||||
const proposedName = baseName + '.' + h;
|
||||
const proposedName = firstParentName + '.' + h;
|
||||
return proposedName;
|
||||
}
|
||||
|
||||
|
@ -43,7 +34,7 @@ export function createSlugger(settings: AstroSettings) {
|
|||
.map(([page]) => page.id)
|
||||
.sort()
|
||||
.join('-');
|
||||
const firstParentId = getFirstParentId(parents) || indexPage;
|
||||
const firstParentId = parents[0]?.[0].id || indexPage;
|
||||
|
||||
// Use the last two segments, for ex /docs/index
|
||||
let dir = firstParentId;
|
||||
|
@ -54,7 +45,7 @@ export function createSlugger(settings: AstroSettings) {
|
|||
break;
|
||||
}
|
||||
|
||||
const name = prettifyBaseName(npath.parse(npath.basename(dir)).name);
|
||||
const name = npath.parse(npath.basename(dir)).name;
|
||||
key = key.length ? name + sep + key : name;
|
||||
dir = npath.dirname(dir);
|
||||
i++;
|
||||
|
@ -85,32 +76,3 @@ export function createSlugger(settings: AstroSettings) {
|
|||
return name;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first parent id from `parents` where its name is not confusing.
|
||||
* Returns undefined if there's no parents.
|
||||
*/
|
||||
function getFirstParentId(parents: [ModuleInfo, number, number][]) {
|
||||
for (const parent of parents) {
|
||||
const id = parent[0].id;
|
||||
const baseName = npath.parse(id).name;
|
||||
if (!confusingBaseNames.includes(baseName)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
// If all parents are confusing, just use the first one. Or if there's no
|
||||
// parents, this will return undefined.
|
||||
return parents[0]?.[0].id;
|
||||
}
|
||||
|
||||
const charsToReplaceRe = /[.\[\]]/g;
|
||||
const underscoresRe = /_+/g;
|
||||
/**
|
||||
* Prettify base names so they're easier to read:
|
||||
* - index -> index
|
||||
* - [slug] -> _slug_
|
||||
* - [...spread] -> _spread_
|
||||
*/
|
||||
function prettifyBaseName(str: string) {
|
||||
return str.replace(charsToReplaceRe, '_').replace(underscoresRe, '_');
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import { fileURLToPath } from 'node:url';
|
|||
import type { OutputAsset, OutputChunk } from 'rollup';
|
||||
import type { BufferEncoding } from 'vfile';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroSettings,
|
||||
ComponentInstance,
|
||||
GetStaticPathsItem,
|
||||
|
@ -58,7 +57,7 @@ import type {
|
|||
StaticBuildOptions,
|
||||
StylesheetAsset,
|
||||
} from './types.js';
|
||||
import { getTimeStat } from './util.js';
|
||||
import { getTimeStat, shouldAppendForwardSlash } from './util.js';
|
||||
|
||||
function createEntryURL(filePath: string, outFolder: URL) {
|
||||
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
||||
|
@ -431,26 +430,6 @@ interface GeneratePathOptions {
|
|||
mod: ComponentInstance;
|
||||
}
|
||||
|
||||
function shouldAppendForwardSlash(
|
||||
trailingSlash: AstroConfig['trailingSlash'],
|
||||
buildFormat: AstroConfig['build']['format']
|
||||
): boolean {
|
||||
switch (trailingSlash) {
|
||||
case 'always':
|
||||
return true;
|
||||
case 'never':
|
||||
return false;
|
||||
case 'ignore': {
|
||||
switch (buildFormat) {
|
||||
case 'directory':
|
||||
return true;
|
||||
case 'file':
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
||||
const trailingSlash = opts.settings.config.trailingSlash;
|
||||
const buildFormat = opts.settings.config.build.format;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import * as crypto from 'node:crypto';
|
||||
import * as npath from 'node:path';
|
||||
import type { GetModuleInfo } from 'rollup';
|
||||
import { type ResolvedConfig, type Plugin as VitePlugin } from 'vite';
|
||||
import { isBuildableCSSRequest } from '../../../vite-plugin-astro-server/util.js';
|
||||
|
@ -91,7 +93,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
|
|||
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
|
||||
// Split delayed assets to separate modules
|
||||
// so they can be injected where needed
|
||||
const chunkId = assetName.createNameHash(id, [id]);
|
||||
const chunkId = createNameHash(id, [id]);
|
||||
internals.cssModuleToChunkIdMap.set(id, chunkId);
|
||||
return chunkId;
|
||||
}
|
||||
|
@ -270,6 +272,17 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
|
|||
|
||||
/***** UTILITY FUNCTIONS *****/
|
||||
|
||||
function createNameHash(baseId: string, hashIds: string[]): string {
|
||||
const baseName = baseId ? npath.parse(baseId).name : 'index';
|
||||
const hash = crypto.createHash('sha256');
|
||||
for (const id of hashIds) {
|
||||
hash.update(id, 'utf-8');
|
||||
}
|
||||
const h = hash.digest('hex').slice(0, 8);
|
||||
const proposedName = baseName + '.' + h;
|
||||
return proposedName;
|
||||
}
|
||||
|
||||
function* getParentClientOnlys(
|
||||
id: string,
|
||||
ctx: { getModuleInfo: GetModuleInfo },
|
||||
|
|
|
@ -1,4 +1,29 @@
|
|||
import type { AstroConfig } from '../../@types/astro.js';
|
||||
|
||||
export function getTimeStat(timeStart: number, timeEnd: number) {
|
||||
const buildTime = timeEnd - timeStart;
|
||||
return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the Astro configuration, it tells if a slash should be appended or not
|
||||
*/
|
||||
export function shouldAppendForwardSlash(
|
||||
trailingSlash: AstroConfig['trailingSlash'],
|
||||
buildFormat: AstroConfig['build']['format']
|
||||
): boolean {
|
||||
switch (trailingSlash) {
|
||||
case 'always':
|
||||
return true;
|
||||
case 'never':
|
||||
return false;
|
||||
case 'ignore': {
|
||||
switch (buildFormat) {
|
||||
case 'directory':
|
||||
return true;
|
||||
case 'file':
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -271,6 +271,48 @@ export const AstroConfigSchema = z.object({
|
|||
.boolean()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
|
||||
i18n: z.optional(
|
||||
z
|
||||
.object({
|
||||
defaultLocale: z.string(),
|
||||
locales: z.string().array(),
|
||||
fallback: z.record(z.string(), z.string().array()).optional().default({}),
|
||||
detectBrowserLanguage: z.boolean().optional().default(false),
|
||||
// TODO: properly add default when the feature goes of experimental
|
||||
fallbackControl: z.enum(['none', 'redirect', 'render']).optional().default('none'),
|
||||
})
|
||||
.optional()
|
||||
.superRefine((i18n, ctx) => {
|
||||
if (i18n) {
|
||||
const { defaultLocale, locales, fallback } = i18n;
|
||||
if (!locales.includes(defaultLocale)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `The default locale \`${defaultLocale}\` is not present in the \`i18n.locales\` array.`,
|
||||
});
|
||||
}
|
||||
if (fallback) {
|
||||
for (const [fallbackKey, fallbackArray] of Object.entries(fallback)) {
|
||||
if (!locales.includes(fallbackKey)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `The locale \`${fallbackKey}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
|
||||
});
|
||||
}
|
||||
|
||||
for (const fallbackArrayKey of fallbackArray) {
|
||||
if (!locales.includes(fallbackArrayKey)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `The locale \`${fallbackArrayKey}\` value in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
})
|
||||
.strict(
|
||||
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
|
||||
|
|
|
@ -29,6 +29,7 @@ import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
|||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
|
||||
import { joinPaths } from './path.js';
|
||||
import astroInternalization from '../i18n/vite-plugin-i18n.js';
|
||||
|
||||
interface CreateViteOptions {
|
||||
settings: AstroSettings;
|
||||
|
@ -134,6 +135,7 @@ export async function createVite(
|
|||
vitePluginSSRManifest(),
|
||||
astroAssetsPlugin({ settings, logger, mode }),
|
||||
astroTransitions(),
|
||||
!!settings.config.experimental.i18n && astroInternalization({ settings, logger }),
|
||||
],
|
||||
publicDir: fileURLToPath(settings.config.publicDir),
|
||||
root: fileURLToPath(settings.config.root),
|
||||
|
|
|
@ -1243,5 +1243,21 @@ export const UnsupportedConfigTransformError = {
|
|||
hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
|
||||
} satisfies ErrorData;
|
||||
|
||||
export const MissingLocale = {
|
||||
name: 'MissingLocaleError',
|
||||
title: 'The provided locale does not exist.',
|
||||
message: (locale: string, locales: string[]) => {
|
||||
return `The locale \`${locale}\` does not exist in the configured locales. Available locales: ${locales.join(
|
||||
', '
|
||||
)}.`;
|
||||
},
|
||||
} satisfies ErrorData;
|
||||
|
||||
export const CantRenderPage = {
|
||||
name: 'CantRenderPage',
|
||||
title: "Astro can't render the route.",
|
||||
message: "You tried to render a route that isn't a redirect and doesn't have any component.",
|
||||
} satisfies ErrorData;
|
||||
|
||||
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip
|
||||
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { MiddlewareResponseHandler, Params } from '../../@types/astro.js';
|
||||
import type { MiddlewareEndpointHandler, Params } from '../../@types/astro.js';
|
||||
import { createAPIContext } from '../endpoint/index.js';
|
||||
import { sequence } from './sequence.js';
|
||||
|
||||
function defineMiddleware(fn: MiddlewareResponseHandler) {
|
||||
function defineMiddleware(fn: MiddlewareEndpointHandler) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { APIContext, MiddlewareResponseHandler } from '../../@types/astro.js';
|
||||
import type { APIContext, MiddlewareEndpointHandler } from '../../@types/astro.js';
|
||||
import { defineMiddleware } from './index.js';
|
||||
|
||||
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
|
||||
|
@ -6,10 +6,10 @@ import { defineMiddleware } from './index.js';
|
|||
*
|
||||
* It accepts one or more middleware handlers and makes sure that they are run in sequence.
|
||||
*/
|
||||
export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareResponseHandler {
|
||||
export function sequence(...handlers: MiddlewareEndpointHandler[]): MiddlewareEndpointHandler {
|
||||
const length = handlers.length;
|
||||
if (!length) {
|
||||
const handler: MiddlewareResponseHandler = defineMiddleware((context, next) => {
|
||||
const handler: MiddlewareEndpointHandler = defineMiddleware((context, next) => {
|
||||
return next();
|
||||
});
|
||||
return handler;
|
||||
|
|
|
@ -67,7 +67,7 @@ export class Pipeline {
|
|||
*/
|
||||
async renderRoute(
|
||||
renderContext: RenderContext,
|
||||
componentInstance: ComponentInstance
|
||||
componentInstance: ComponentInstance | undefined
|
||||
): Promise<Response> {
|
||||
const result = await this.#tryRenderRoute(
|
||||
renderContext,
|
||||
|
@ -100,7 +100,8 @@ export class Pipeline {
|
|||
async #tryRenderRoute<MiddlewareReturnType = Response>(
|
||||
renderContext: Readonly<RenderContext>,
|
||||
env: Readonly<Environment>,
|
||||
mod: Readonly<ComponentInstance>,
|
||||
mod: Readonly<ComponentInstance> | undefined,
|
||||
|
||||
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||
): Promise<Response> {
|
||||
const apiContext = createAPIContext({
|
||||
|
@ -113,6 +114,7 @@ export class Pipeline {
|
|||
|
||||
switch (renderContext.route.type) {
|
||||
case 'page':
|
||||
case 'fallback':
|
||||
case 'redirect': {
|
||||
if (onRequest) {
|
||||
return await callMiddleware<Response>(
|
||||
|
@ -138,13 +140,7 @@ export class Pipeline {
|
|||
}
|
||||
}
|
||||
case 'endpoint': {
|
||||
const result = await callEndpoint(
|
||||
mod as any as EndpointHandler,
|
||||
env,
|
||||
renderContext,
|
||||
onRequest
|
||||
);
|
||||
return result;
|
||||
return await callEndpoint(mod as any as EndpointHandler, env, renderContext, onRequest);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||
|
|
|
@ -9,6 +9,10 @@ export function routeIsRedirect(route: RouteData | undefined): route is Redirect
|
|||
return route?.type === 'redirect';
|
||||
}
|
||||
|
||||
export function routeIsFallback(route: RouteData | undefined): route is RedirectRouteData {
|
||||
return route?.type === 'fallback';
|
||||
}
|
||||
|
||||
export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): string {
|
||||
const routeData = redirectRoute.redirectRoute;
|
||||
const route = redirectRoute.redirect;
|
||||
|
|
|
@ -34,7 +34,7 @@ export type CreateRenderContextArgs = Partial<
|
|||
> & {
|
||||
route: RouteData;
|
||||
request: RenderContext['request'];
|
||||
mod: ComponentInstance;
|
||||
mod: ComponentInstance | undefined;
|
||||
env: Environment;
|
||||
};
|
||||
|
||||
|
|
|
@ -13,9 +13,12 @@ import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../
|
|||
import type { RenderContext } from './context.js';
|
||||
import type { Environment } from './environment.js';
|
||||
import { createResult } from './result.js';
|
||||
import { AstroError } from '../errors/index.js';
|
||||
import { CantRenderPage } from '../errors/errors-data.js';
|
||||
import { routeIsFallback } from '../redirects/helpers.js';
|
||||
|
||||
export type RenderPage = {
|
||||
mod: ComponentInstance;
|
||||
mod: ComponentInstance | undefined;
|
||||
renderContext: RenderContext;
|
||||
env: Environment;
|
||||
cookies: AstroCookies;
|
||||
|
@ -29,6 +32,11 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag
|
|||
location: redirectRouteGenerate(renderContext.route, renderContext.params),
|
||||
},
|
||||
});
|
||||
// TODO: check this one
|
||||
} else if (routeIsFallback(renderContext.route)) {
|
||||
return new Response(null);
|
||||
} else if (!mod) {
|
||||
throw new AstroError(CantRenderPage);
|
||||
}
|
||||
|
||||
// Validate the page component before rendering the page
|
||||
|
|
|
@ -4,9 +4,10 @@ import type { Logger } from '../logger/core.js';
|
|||
import { routeIsRedirect } from '../redirects/index.js';
|
||||
import { getParams } from '../routing/params.js';
|
||||
import { RouteCache, callGetStaticPaths, findPathItemByKey } from './route-cache.js';
|
||||
import { routeIsFallback } from '../redirects/helpers.js';
|
||||
|
||||
interface GetParamsAndPropsOptions {
|
||||
mod: ComponentInstance;
|
||||
mod: ComponentInstance | undefined;
|
||||
route?: RouteData | undefined;
|
||||
routeCache: RouteCache;
|
||||
pathname: string;
|
||||
|
@ -26,11 +27,13 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
|
|||
// This is a dynamic route, start getting the params
|
||||
const params = getRouteParams(route, pathname) ?? {};
|
||||
|
||||
if (routeIsRedirect(route)) {
|
||||
if (routeIsRedirect(route) || routeIsFallback(route)) {
|
||||
return [params, {}];
|
||||
}
|
||||
|
||||
validatePrerenderEndpointCollision(route, mod, params);
|
||||
if (mod) {
|
||||
validatePrerenderEndpointCollision(route, mod, params);
|
||||
}
|
||||
|
||||
// During build, the route cache should already be populated.
|
||||
// During development, the route cache is filled on-demand and may be empty.
|
||||
|
|
|
@ -16,7 +16,7 @@ import { validateDynamicRouteModule, validateGetStaticPathsResult } from '../rou
|
|||
import { generatePaginateFunction } from './paginate.js';
|
||||
|
||||
interface CallGetStaticPathsOptions {
|
||||
mod: ComponentInstance;
|
||||
mod: ComponentInstance | undefined;
|
||||
route: RouteData;
|
||||
routeCache: RouteCache;
|
||||
logger: Logger;
|
||||
|
@ -33,7 +33,9 @@ export async function callGetStaticPaths({
|
|||
const cached = routeCache.get(route);
|
||||
if (cached?.staticPaths) return cached.staticPaths;
|
||||
|
||||
validateDynamicRouteModule(mod, { ssr, route });
|
||||
if (mod) {
|
||||
validateDynamicRouteModule(mod, { ssr, route });
|
||||
}
|
||||
|
||||
// No static paths in SSR mode. Return an empty RouteCacheEntry.
|
||||
if (ssr && !route.prerender) {
|
||||
|
@ -42,22 +44,26 @@ export async function callGetStaticPaths({
|
|||
return entry;
|
||||
}
|
||||
|
||||
let staticPaths: GetStaticPathsResult = [];
|
||||
// Add a check here to make TypeScript happy.
|
||||
// This is already checked in validateDynamicRouteModule().
|
||||
if (!mod.getStaticPaths) {
|
||||
throw new Error('Unexpected Error.');
|
||||
}
|
||||
if (mod) {
|
||||
if (!mod.getStaticPaths) {
|
||||
throw new Error('Unexpected Error.');
|
||||
}
|
||||
|
||||
// Calculate your static paths.
|
||||
let staticPaths: GetStaticPathsResult = [];
|
||||
staticPaths = await mod.getStaticPaths({
|
||||
// Q: Why the cast?
|
||||
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
|
||||
paginate: generatePaginateFunction(route) as PaginateFunction,
|
||||
rss() {
|
||||
throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
|
||||
},
|
||||
});
|
||||
if (mod) {
|
||||
// Calculate your static paths.
|
||||
staticPaths = await mod.getStaticPaths({
|
||||
// Q: Why the cast?
|
||||
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
|
||||
paginate: generatePaginateFunction(route) as PaginateFunction,
|
||||
rss() {
|
||||
throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
validateGetStaticPathsResult(staticPaths, logger, route);
|
||||
|
||||
|
|
58
packages/astro/src/i18n/index.ts
Normal file
58
packages/astro/src/i18n/index.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { AstroError } from '../core/errors/index.js';
|
||||
import { MissingLocale } from '../core/errors/errors-data.js';
|
||||
import type { AstroConfig } from '../@types/astro.js';
|
||||
import { shouldAppendForwardSlash } from '../core/build/util.js';
|
||||
|
||||
type GetI18nBaseUrl = {
|
||||
locale: string;
|
||||
base: string;
|
||||
locales: string[];
|
||||
trailingSlash: AstroConfig['trailingSlash'];
|
||||
format: AstroConfig['build']['format'];
|
||||
};
|
||||
/**
|
||||
* The base URL
|
||||
*/
|
||||
export function getI18nBaseUrl({ locale, base, locales, trailingSlash, format }: GetI18nBaseUrl) {
|
||||
if (!locales.includes(locale)) {
|
||||
throw new AstroError({
|
||||
...MissingLocale,
|
||||
message: MissingLocale.message(locale, locales),
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedLocale = normalizeLocale(locale);
|
||||
if (shouldAppendForwardSlash(trailingSlash, format)) {
|
||||
return `${base}${normalizedLocale}/`;
|
||||
} else {
|
||||
return `${base}/${normalizedLocale}`;
|
||||
}
|
||||
}
|
||||
|
||||
type GetLocalesBaseUrl = {
|
||||
base: string;
|
||||
locales: string[];
|
||||
trailingSlash: AstroConfig['trailingSlash'];
|
||||
format: AstroConfig['build']['format'];
|
||||
};
|
||||
|
||||
export function getLocalesBaseUrl({ base, locales, trailingSlash, format }: GetLocalesBaseUrl) {
|
||||
return locales.map((locale) => {
|
||||
const normalizedLocale = normalizeLocale(locale);
|
||||
if (shouldAppendForwardSlash(trailingSlash, format)) {
|
||||
return `${base}${normalizedLocale}/`;
|
||||
} else {
|
||||
return `${base}/${normalizedLocale}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Given a locale, this function:
|
||||
* - replaces the `_` with a `-`;
|
||||
* - transforms all letters to be lower case;
|
||||
*/
|
||||
function normalizeLocale(locale: string): string {
|
||||
return locale.replaceAll('_', '-').toLowerCase();
|
||||
}
|
38
packages/astro/src/i18n/middleware.ts
Normal file
38
packages/astro/src/i18n/middleware.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import type { AstroConfig, MiddlewareEndpointHandler } from '../@types/astro.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
|
||||
export function createI18nMiddleware(
|
||||
config: Readonly<AstroConfig>,
|
||||
logger: Logger
|
||||
): MiddlewareEndpointHandler | undefined {
|
||||
const i18n = config.experimental?.i18n;
|
||||
if (!i18n) {
|
||||
return undefined;
|
||||
}
|
||||
const fallbackKeys = Object.keys(i18n.fallback);
|
||||
const locales = i18n.locales;
|
||||
|
||||
logger.debug('i18n', 'Successfully created middleware');
|
||||
return async (context, next) => {
|
||||
if (fallbackKeys.length <= 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const response = await next();
|
||||
if (i18n.fallbackControl === 'redirect' && response instanceof Response) {
|
||||
const url = context.url;
|
||||
const separators = url.pathname.split('/');
|
||||
|
||||
const urlLocale = separators.find((s) => locales.includes(s));
|
||||
|
||||
if (urlLocale && fallbackKeys.includes(urlLocale)) {
|
||||
// TODO: correctly handle chain of fallback
|
||||
const fallbackLocale = i18n.fallback[urlLocale][0];
|
||||
const newPathname = url.pathname.replace(`/${urlLocale}`, `/${fallbackLocale}`);
|
||||
return context.redirect(newPathname);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
39
packages/astro/src/i18n/vite-plugin-i18n.ts
Normal file
39
packages/astro/src/i18n/vite-plugin-i18n.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import * as vite from 'vite';
|
||||
import type { AstroSettings } from '../@types/astro.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
|
||||
const virtualModuleId = 'astro:i18n';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
type AstroInternalization = {
|
||||
settings: AstroSettings;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
export default function astroInternalization({ settings }: AstroInternalization): vite.Plugin {
|
||||
return {
|
||||
name: 'astro:i18n',
|
||||
async resolveId(id) {
|
||||
if (id === virtualModuleId) {
|
||||
return resolvedVirtualModuleId;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
return `
|
||||
import { getI18nBaseUrl as getI18nBaseUrlInternal, getLocalesBaseUrl as _getLocalesBaseUrl } from "astro/i18n";
|
||||
|
||||
const defaultLocale = ${JSON.stringify(settings.config.experimental.i18n!.defaultLocale)};
|
||||
const locales = ${JSON.stringify(settings.config.experimental.i18n!.locales)};
|
||||
const fallback = ${JSON.stringify(settings.config.experimental.i18n!.fallback)};
|
||||
const base = ${JSON.stringify(settings.config.base)};
|
||||
const trailingSlash = ${JSON.stringify(settings.config.trailingSlash)};
|
||||
const format = ${JSON.stringify(settings.config.build.format)};
|
||||
|
||||
export const getI18nBaseUrl = (locale) => getI18nBaseUrlInternal({ locale, base, locales, trailingSlash, format });
|
||||
export const getLocalesBaseUrl = () => _getLocalesBaseUrl({ base, locales, trailingSlash, format });
|
||||
`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -23,6 +23,9 @@ const ALL_UNSUPPORTED: Required<AstroFeatureMap> = {
|
|||
staticOutput: UNSUPPORTED,
|
||||
hybridOutput: UNSUPPORTED,
|
||||
assets: UNSUPPORTED_ASSETS_FEATURE,
|
||||
i18n: {
|
||||
detectBrowserLanguage: UNSUPPORTED,
|
||||
},
|
||||
};
|
||||
|
||||
type ValidationResult = {
|
||||
|
|
|
@ -15,13 +15,10 @@ export interface HydrationMetadata {
|
|||
componentExport: { value: string };
|
||||
}
|
||||
|
||||
type Props = Record<string | number | symbol, any>;
|
||||
|
||||
interface ExtractedProps {
|
||||
isPage: boolean;
|
||||
hydration: HydrationMetadata | null;
|
||||
props: Props;
|
||||
propsWithoutTransitionAttributes: Props;
|
||||
props: Record<string | number | symbol, any>;
|
||||
}
|
||||
|
||||
const transitionDirectivesToCopyOnIsland = Object.freeze([
|
||||
|
@ -32,14 +29,13 @@ const transitionDirectivesToCopyOnIsland = Object.freeze([
|
|||
// Used to extract the directives, aka `client:load` information about a component.
|
||||
// Finds these special props and removes them from what gets passed into the component.
|
||||
export function extractDirectives(
|
||||
inputProps: Props,
|
||||
inputProps: Record<string | number | symbol, any>,
|
||||
clientDirectives: SSRResult['clientDirectives']
|
||||
): ExtractedProps {
|
||||
let extracted: ExtractedProps = {
|
||||
isPage: false,
|
||||
hydration: null,
|
||||
props: {},
|
||||
propsWithoutTransitionAttributes: {},
|
||||
};
|
||||
for (const [key, value] of Object.entries(inputProps)) {
|
||||
if (key.startsWith('server:')) {
|
||||
|
@ -100,14 +96,10 @@ export function extractDirectives(
|
|||
}
|
||||
} else {
|
||||
extracted.props[key] = value;
|
||||
if (!transitionDirectivesToCopyOnIsland.includes(key)) {
|
||||
extracted.propsWithoutTransitionAttributes[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const sym of Object.getOwnPropertySymbols(inputProps)) {
|
||||
extracted.props[sym] = inputProps[sym];
|
||||
extracted.propsWithoutTransitionAttributes[sym] = inputProps[sym];
|
||||
}
|
||||
|
||||
return extracted;
|
||||
|
|
|
@ -92,10 +92,7 @@ async function renderFrameworkComponent(
|
|||
displayName,
|
||||
};
|
||||
|
||||
const { hydration, isPage, props, propsWithoutTransitionAttributes } = extractDirectives(
|
||||
_props,
|
||||
clientDirectives
|
||||
);
|
||||
const { hydration, isPage, props } = extractDirectives(_props, clientDirectives);
|
||||
let html = '';
|
||||
let attrs: Record<string, string> | undefined = undefined;
|
||||
|
||||
|
@ -220,7 +217,7 @@ async function renderFrameworkComponent(
|
|||
({ html, attrs } = await renderer.ssr.renderToStaticMarkup.call(
|
||||
{ result },
|
||||
Component,
|
||||
propsWithoutTransitionAttributes,
|
||||
props,
|
||||
children,
|
||||
metadata
|
||||
));
|
||||
|
@ -245,7 +242,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
|||
({ html, attrs } = await renderer.ssr.renderToStaticMarkup.call(
|
||||
{ result },
|
||||
Component,
|
||||
propsWithoutTransitionAttributes,
|
||||
props,
|
||||
children,
|
||||
metadata
|
||||
));
|
||||
|
|
|
@ -13,14 +13,9 @@ type Events = 'astro:page-load' | 'astro:after-swap';
|
|||
// only update history entries that are managed by us
|
||||
// leave other entries alone and do not accidently add state.
|
||||
const persistState = (state: State) => history.state && history.replaceState(state, '');
|
||||
|
||||
const inBrowser = import.meta.env.SSR === false;
|
||||
|
||||
export const supportsViewTransitions = inBrowser && !!document.startViewTransition;
|
||||
|
||||
export const supportsViewTransitions = !!document.startViewTransition;
|
||||
export const transitionEnabledOnThisPage = () =>
|
||||
inBrowser && !!document.querySelector('[name="astro-view-transitions-enabled"]');
|
||||
|
||||
!!document.querySelector('[name="astro-view-transitions-enabled"]');
|
||||
const samePage = (otherLocation: URL) =>
|
||||
location.pathname === otherLocation.pathname && location.search === otherLocation.search;
|
||||
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
|
||||
|
@ -45,27 +40,26 @@ const announce = () => {
|
|||
60
|
||||
);
|
||||
};
|
||||
|
||||
const PERSIST_ATTR = 'data-astro-transition-persist';
|
||||
|
||||
let parser: DOMParser;
|
||||
const parser = new DOMParser();
|
||||
// explained at its usage
|
||||
let noopEl: HTMLDivElement;
|
||||
if (import.meta.env.DEV) {
|
||||
noopEl = document.createElement('div');
|
||||
}
|
||||
|
||||
// The History API does not tell you if navigation is forward or back, so
|
||||
// you can figure it using an index. On pushState the index is incremented so you
|
||||
// can use that to determine popstate if going forward or back.
|
||||
let currentHistoryIndex = 0;
|
||||
|
||||
if (inBrowser) {
|
||||
if (history.state) {
|
||||
// we reloaded a page with history state
|
||||
// (e.g. history navigation from non-transition page or browser reload)
|
||||
currentHistoryIndex = history.state.index;
|
||||
scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
|
||||
} else if (transitionEnabledOnThisPage()) {
|
||||
history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, '');
|
||||
}
|
||||
if (history.state) {
|
||||
// we reloaded a page with history state
|
||||
// (e.g. history navigation from non-transition page or browser reload)
|
||||
currentHistoryIndex = history.state.index;
|
||||
scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
|
||||
} else if (transitionEnabledOnThisPage()) {
|
||||
history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, '');
|
||||
}
|
||||
|
||||
const throttle = (cb: (...args: any[]) => any, delay: number) => {
|
||||
let wait = false;
|
||||
// During the waiting time additional events are lost.
|
||||
|
@ -157,24 +151,18 @@ function isInfinite(animation: Animation) {
|
|||
|
||||
const updateHistoryAndScrollPosition = (toLocation: URL, replace: boolean, intraPage: boolean) => {
|
||||
const fresh = !samePage(toLocation);
|
||||
let scrolledToTop = false;
|
||||
if (toLocation.href !== location.href) {
|
||||
if (replace) {
|
||||
history.replaceState({ ...history.state }, '', toLocation.href);
|
||||
} else {
|
||||
history.replaceState({ ...history.state, intraPage }, '');
|
||||
history.pushState(
|
||||
{ index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
|
||||
'',
|
||||
toLocation.href
|
||||
);
|
||||
history.pushState({ index: ++currentHistoryIndex, scrollX, scrollY }, '', toLocation.href);
|
||||
}
|
||||
// now we are on the new page for non-history navigations!
|
||||
// (with history navigation page change happens before popstate is fired)
|
||||
// freshly loaded pages start from the top
|
||||
if (fresh) {
|
||||
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
||||
scrolledToTop = true;
|
||||
}
|
||||
}
|
||||
if (toLocation.hash) {
|
||||
|
@ -183,9 +171,7 @@ const updateHistoryAndScrollPosition = (toLocation: URL, replace: boolean, intra
|
|||
// that won't reload the page but instead scroll to the fragment
|
||||
location.href = toLocation.href;
|
||||
} else {
|
||||
if (!scrolledToTop) {
|
||||
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
||||
}
|
||||
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -212,6 +198,22 @@ async function updateDOM(
|
|||
const href = el.getAttribute('href');
|
||||
return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
|
||||
}
|
||||
// What follows is a fix for an issue (#8472) with missing client:only styles after transition.
|
||||
// That problem exists only in dev mode where styles are injected into the page by Vite.
|
||||
// Returning a noop element ensures that the styles are not removed from the old document.
|
||||
// Guarding the code below with the dev mode check
|
||||
// allows tree shaking to remove this code in production.
|
||||
if (import.meta.env.DEV) {
|
||||
if (el.tagName === 'STYLE' && el.dataset.viteDevId) {
|
||||
const devId = el.dataset.viteDevId;
|
||||
// If this same style tag exists, remove it from the new page
|
||||
return (
|
||||
newDocument.querySelector(`style[data-vite-dev-id="${devId}"]`) ||
|
||||
// Otherwise, keep it anyways. This is client:only styles.
|
||||
noopEl
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
@ -347,8 +349,6 @@ async function transition(
|
|||
toLocation = new URL(response.redirected);
|
||||
}
|
||||
|
||||
parser ??= new DOMParser();
|
||||
|
||||
const newDocument = parser.parseFromString(response.html, response.mediaType);
|
||||
// The next line might look like a hack,
|
||||
// but it is actually necessary as noscript elements
|
||||
|
@ -385,22 +385,7 @@ async function transition(
|
|||
}
|
||||
}
|
||||
|
||||
let navigateOnServerWarned = false;
|
||||
|
||||
export function navigate(href: string, options?: Options) {
|
||||
if (inBrowser === false) {
|
||||
if (!navigateOnServerWarned) {
|
||||
// instantiate an error for the stacktrace to show to user.
|
||||
const warning = new Error(
|
||||
'The view transtions client API was called during a server side render. This may be unintentional as the navigate() function is expected to be called in response to user interactions. Please make sure that your usage is correct.'
|
||||
);
|
||||
warning.name = 'Warning';
|
||||
console.warn(warning);
|
||||
navigateOnServerWarned = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// not ours
|
||||
if (!transitionEnabledOnThisPage()) {
|
||||
location.href = href;
|
||||
|
@ -418,61 +403,58 @@ export function navigate(href: string, options?: Options) {
|
|||
}
|
||||
}
|
||||
|
||||
function onPopState(ev: PopStateEvent) {
|
||||
if (!transitionEnabledOnThisPage() && ev.state) {
|
||||
// The current page doesn't have View Transitions enabled
|
||||
// but the page we navigate to does (because it set the state).
|
||||
// Do a full page refresh to reload the client-side router from the new page.
|
||||
// Scroll restauration will then happen during the reload when the router's code is re-executed
|
||||
if (supportsViewTransitions || getFallback() !== 'none') {
|
||||
addEventListener('popstate', (ev) => {
|
||||
if (!transitionEnabledOnThisPage() && ev.state) {
|
||||
// The current page doesn't have View Transitions enabled
|
||||
// but the page we navigate to does (because it set the state).
|
||||
// Do a full page refresh to reload the client-side router from the new page.
|
||||
// Scroll restauration will then happen during the reload when the router's code is re-executed
|
||||
if (history.scrollRestoration) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
// History entries without state are created by the browser (e.g. for hash links)
|
||||
// Our view transition entries always have state.
|
||||
// Just ignore stateless entries.
|
||||
// The browser will handle navigation fine without our help
|
||||
if (ev.state === null) {
|
||||
if (history.scrollRestoration) {
|
||||
history.scrollRestoration = 'auto';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// With the default "auto", the browser will jump to the old scroll position
|
||||
// before the ViewTransition is complete.
|
||||
if (history.scrollRestoration) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
// History entries without state are created by the browser (e.g. for hash links)
|
||||
// Our view transition entries always have state.
|
||||
// Just ignore stateless entries.
|
||||
// The browser will handle navigation fine without our help
|
||||
if (ev.state === null) {
|
||||
if (history.scrollRestoration) {
|
||||
history.scrollRestoration = 'auto';
|
||||
const state: State = history.state;
|
||||
if (state.intraPage) {
|
||||
// this is non transition intra-page scrolling
|
||||
scrollTo(state.scrollX, state.scrollY);
|
||||
} else {
|
||||
const nextIndex = state.index;
|
||||
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
|
||||
currentHistoryIndex = nextIndex;
|
||||
transition(direction, new URL(location.href), {}, state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// With the default "auto", the browser will jump to the old scroll position
|
||||
// before the ViewTransition is complete.
|
||||
if (history.scrollRestoration) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
addEventListener('load', onPageLoad);
|
||||
// There's not a good way to record scroll position before a back button.
|
||||
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
|
||||
const updateState = () => {
|
||||
persistState({ ...history.state, scrollX, scrollY });
|
||||
};
|
||||
|
||||
const state: State = history.state;
|
||||
if (state.intraPage) {
|
||||
// this is non transition intra-page scrolling
|
||||
scrollTo(state.scrollX, state.scrollY);
|
||||
} else {
|
||||
const nextIndex = state.index;
|
||||
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
|
||||
currentHistoryIndex = nextIndex;
|
||||
transition(direction, new URL(location.href), {}, state);
|
||||
}
|
||||
}
|
||||
|
||||
if (inBrowser) {
|
||||
if (supportsViewTransitions || getFallback() !== 'none') {
|
||||
addEventListener('popstate', onPopState);
|
||||
addEventListener('load', onPageLoad);
|
||||
// There's not a good way to record scroll position before a back button.
|
||||
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
|
||||
const updateState = () => {
|
||||
persistState({ ...history.state, scrollX, scrollY });
|
||||
};
|
||||
|
||||
if ('onscrollend' in window) addEventListener('scrollend', updateState);
|
||||
else addEventListener('scroll', throttle(updateState, 300));
|
||||
|
||||
markScriptsExec();
|
||||
}
|
||||
if ('onscrollend' in window) addEventListener('scrollend', updateState);
|
||||
else addEventListener('scroll', throttle(updateState, 300));
|
||||
|
||||
markScriptsExec();
|
||||
}
|
||||
|
|
|
@ -91,4 +91,6 @@ export default class DevPipeline extends Pipeline {
|
|||
async #handleEndpointResult(_: Request, response: Response): Promise<Response> {
|
||||
return response;
|
||||
}
|
||||
|
||||
async handleFallback() {}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,12 @@ import type {
|
|||
} from '../@types/astro.js';
|
||||
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
|
||||
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
|
||||
import { createRenderContext, getParamsAndProps, type SSROptions } from '../core/render/index.js';
|
||||
import {
|
||||
createRenderContext,
|
||||
getParamsAndProps,
|
||||
type RenderContext,
|
||||
type SSROptions,
|
||||
} from '../core/render/index.js';
|
||||
import { createRequest } from '../core/request.js';
|
||||
import { matchAllRoutes } from '../core/routing/index.js';
|
||||
import { isPage } from '../core/util.js';
|
||||
|
@ -23,6 +28,8 @@ import { preload } from './index.js';
|
|||
import { getComponentMetadata } from './metadata.js';
|
||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||
import { getScriptsForURL } from './scripts.js';
|
||||
import { createI18nMiddleware } from '../i18n/middleware.js';
|
||||
import { sequence } from '../core/middleware/index.js';
|
||||
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
|
@ -52,7 +59,8 @@ export async function matchRoute(
|
|||
): Promise<MatchedRoute | undefined> {
|
||||
const env = pipeline.getEnvironment();
|
||||
const { routeCache, logger } = env;
|
||||
const matches = matchAllRoutes(pathname, manifestData);
|
||||
let matches = matchAllRoutes(pathname, manifestData);
|
||||
|
||||
const preloadedMatches = await getSortedPreloadedMatches({
|
||||
pipeline,
|
||||
matches,
|
||||
|
@ -157,70 +165,133 @@ export async function handleRoute({
|
|||
const config = pipeline.getConfig();
|
||||
const moduleLoader = pipeline.getModuleLoader();
|
||||
const { logger } = env;
|
||||
if (!matchedRoute) {
|
||||
if (!matchedRoute && !config.experimental.i18n) {
|
||||
return handle404Response(origin, incomingRequest, incomingResponse);
|
||||
}
|
||||
|
||||
const filePath: URL | undefined = matchedRoute.filePath;
|
||||
const { route, preloadedComponent } = matchedRoute;
|
||||
const buildingToSSR = isServerLikeOutput(config);
|
||||
|
||||
// Headers are only available when using SSR.
|
||||
const request = createRequest({
|
||||
url,
|
||||
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
|
||||
method: incomingRequest.method,
|
||||
body,
|
||||
logger,
|
||||
ssr: buildingToSSR,
|
||||
clientAddress: buildingToSSR ? incomingRequest.socket.remoteAddress : undefined,
|
||||
locals: Reflect.get(incomingRequest, clientLocalsSymbol), // Allows adapters to pass in locals in dev mode.
|
||||
});
|
||||
|
||||
// Set user specified headers to response object.
|
||||
for (const [name, value] of Object.entries(config.server.headers ?? {})) {
|
||||
if (value) incomingResponse.setHeader(name, value);
|
||||
}
|
||||
|
||||
const options: SSROptions = {
|
||||
env,
|
||||
filePath,
|
||||
preload: preloadedComponent,
|
||||
pathname,
|
||||
request,
|
||||
route,
|
||||
};
|
||||
let request: Request;
|
||||
let renderContext: RenderContext;
|
||||
let mod: ComponentInstance | undefined = undefined;
|
||||
let options: SSROptions | undefined = undefined;
|
||||
let route: RouteData;
|
||||
const middleware = await loadMiddleware(moduleLoader, settings.config.srcDir);
|
||||
if (middleware) {
|
||||
options.middleware = middleware;
|
||||
|
||||
if (!matchedRoute) {
|
||||
if (config.experimental.i18n) {
|
||||
const locales = config.experimental.i18n.locales;
|
||||
const pathNameHasLocale = pathname
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.some((segment) => {
|
||||
return locales.includes(segment);
|
||||
});
|
||||
if (!pathNameHasLocale) {
|
||||
return handle404Response(origin, incomingRequest, incomingResponse);
|
||||
}
|
||||
request = createRequest({
|
||||
url,
|
||||
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
|
||||
logger,
|
||||
ssr: buildingToSSR,
|
||||
});
|
||||
route = {
|
||||
component: '',
|
||||
generate(_data: any): string {
|
||||
return '';
|
||||
},
|
||||
params: [],
|
||||
pattern: new RegExp(''),
|
||||
prerender: false,
|
||||
segments: [],
|
||||
type: 'fallback',
|
||||
route: '',
|
||||
};
|
||||
renderContext = await createRenderContext({
|
||||
request,
|
||||
pathname,
|
||||
env,
|
||||
mod,
|
||||
route,
|
||||
});
|
||||
} else {
|
||||
return handle404Response(origin, incomingRequest, incomingResponse);
|
||||
}
|
||||
} else {
|
||||
const filePath: URL | undefined = matchedRoute.filePath;
|
||||
const { preloadedComponent } = matchedRoute;
|
||||
route = matchedRoute.route;
|
||||
// Headers are only available when using SSR.
|
||||
request = createRequest({
|
||||
url,
|
||||
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
|
||||
method: incomingRequest.method,
|
||||
body,
|
||||
logger,
|
||||
ssr: buildingToSSR,
|
||||
clientAddress: buildingToSSR ? incomingRequest.socket.remoteAddress : undefined,
|
||||
locals: Reflect.get(incomingRequest, clientLocalsSymbol), // Allows adapters to pass in locals in dev mode.
|
||||
});
|
||||
|
||||
// Set user specified headers to response object.
|
||||
for (const [name, value] of Object.entries(config.server.headers ?? {})) {
|
||||
if (value) incomingResponse.setHeader(name, value);
|
||||
}
|
||||
|
||||
options = {
|
||||
env,
|
||||
filePath,
|
||||
preload: preloadedComponent,
|
||||
pathname,
|
||||
request,
|
||||
route,
|
||||
};
|
||||
if (middleware) {
|
||||
options.middleware = middleware;
|
||||
}
|
||||
|
||||
mod = options.preload;
|
||||
|
||||
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
||||
pipeline,
|
||||
filePath: options.filePath,
|
||||
});
|
||||
|
||||
renderContext = await createRenderContext({
|
||||
request: options.request,
|
||||
pathname: options.pathname,
|
||||
scripts,
|
||||
links,
|
||||
styles,
|
||||
componentMetadata: metadata,
|
||||
route: options.route,
|
||||
mod,
|
||||
env,
|
||||
});
|
||||
}
|
||||
const mod = options.preload;
|
||||
|
||||
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
||||
pipeline,
|
||||
filePath: options.filePath,
|
||||
});
|
||||
const onRequest = middleware?.onRequest as MiddlewareEndpointHandler | undefined;
|
||||
if (config.experimental.i18n) {
|
||||
const i18Middleware = createI18nMiddleware(config, logger);
|
||||
|
||||
const renderContext = await createRenderContext({
|
||||
request: options.request,
|
||||
pathname: options.pathname,
|
||||
scripts,
|
||||
links,
|
||||
styles,
|
||||
componentMetadata: metadata,
|
||||
route: options.route,
|
||||
mod,
|
||||
env,
|
||||
});
|
||||
const onRequest = options.middleware?.onRequest as MiddlewareEndpointHandler | undefined;
|
||||
if (onRequest) {
|
||||
if (i18Middleware) {
|
||||
if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(sequence(i18Middleware, onRequest));
|
||||
} else {
|
||||
pipeline.setMiddlewareFunction(i18Middleware);
|
||||
}
|
||||
} else if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(onRequest);
|
||||
}
|
||||
} else if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(onRequest);
|
||||
}
|
||||
|
||||
let response = await pipeline.renderRoute(renderContext, mod);
|
||||
if (response.status === 404 && has404Route(manifestData)) {
|
||||
const fourOhFourRoute = await matchRoute('/404', manifestData, pipeline);
|
||||
if (fourOhFourRoute?.route !== options.route)
|
||||
if (options && fourOhFourRoute?.route !== options.route)
|
||||
return handleRoute({
|
||||
...options,
|
||||
matchedRoute: fourOhFourRoute,
|
||||
|
|
|
@ -339,4 +339,159 @@ describe('Development Routing', () => {
|
|||
expect(await response.text()).includes('html: 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing/',
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
const response = await fixture.fetch('/en/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hello');
|
||||
|
||||
const response2 = await fixture.fetch('/en/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
const response = await fixture.fetch('/pt/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hola');
|
||||
|
||||
const response2 = await fixture.fetch('/pt/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
const response = await fixture.fetch('/it/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
const response = await fixture.fetch('/fr/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing, with base', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-base/',
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
const response = await fixture.fetch('/new-site/en/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hello');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/en/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
const response = await fixture.fetch('/new-site/pt/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hola');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/pt/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
const response = await fixture.fetch('/new-site/it/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
const response = await fixture.fetch('/new-site/fr/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing with fallback [redirect]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-fallback/',
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'pt', 'it'],
|
||||
fallback: {
|
||||
it: ['en'],
|
||||
},
|
||||
fallbackControl: 'redirect',
|
||||
},
|
||||
},
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
const response = await fixture.fetch('/new-site/en/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hello');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/en/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
const response = await fixture.fetch('/new-site/pt/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hola');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/pt/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it('should render the english locale, which is the first fallback', async () => {
|
||||
const response = await fixture.fetch('/new-site/it/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hello');
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
const response = await fixture.fetch('/new-site/fr/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
13
packages/astro/test/fixtures/i18n-routing-base/astro.config.mjs
vendored
Normal file
13
packages/astro/test/fixtures/i18n-routing-base/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
base: "new-site",
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'en', 'pt', 'it'
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
8
packages/astro/test/fixtures/i18n-routing-base/package.json
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-base/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/i18n-routing-base",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
18
packages/astro/test/fixtures/i18n-routing-base/src/pages/en/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-base/src/pages/en/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hello world" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/en/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/en/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
18
packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hola mundo" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hola
|
||||
</body>
|
||||
</html>
|
16
packages/astro/test/fixtures/i18n-routing-fallback/astro.config.mjs
vendored
Normal file
16
packages/astro/test/fixtures/i18n-routing-fallback/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
base: "new-site",
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'en', 'pt', 'it'
|
||||
],
|
||||
fallback: {
|
||||
"it": ["en"]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
8
packages/astro/test/fixtures/i18n-routing-fallback/package.json
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-fallback/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/i18n-routing-fallabck",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue