latest | cache bust 1/2
This commit is contained in:
parent
ac520a6e65
commit
ecde90f89d
@ -1,8 +0,0 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"@domain-expansion-test/*",
|
||||
"docs"
|
||||
]
|
||||
}
|
||||
19
packages/domain-expansion/.github/renovate.json
vendored
19
packages/domain-expansion/.github/renovate.json
vendored
@ -1,19 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"dependencyDashboard": true,
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
},
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "all dependencies",
|
||||
"groupSlug": "all",
|
||||
"matchPackagePatterns": ["*"],
|
||||
"schedule": ["before 4am on Monday"],
|
||||
"rangeStrategy": "bump"
|
||||
}
|
||||
],
|
||||
"ignoreDeps": ["node"]
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
name: Surface PR Changesets
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
checks: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files in the .changeset folder
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: |
|
||||
.changeset/**/*.md
|
||||
|
||||
- name: Check if any changesets contain minor or major changes
|
||||
id: check
|
||||
run: |
|
||||
echo "Checking for changesets marked as minor or major"
|
||||
echo "found=false" >> $GITHUB_OUTPUT
|
||||
|
||||
regex="[\"']astro[\"']: (minor|major)"
|
||||
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
|
||||
if [[ $(cat $file) =~ $regex ]]; then
|
||||
version="${BASH_REMATCH[1]}"
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
echo "found=true" >> $GITHUB_OUTPUT
|
||||
echo "$file has a $version release tag"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Add label
|
||||
uses: actions/github-script@v6
|
||||
if: steps.check.outputs.found == 'true'
|
||||
env:
|
||||
issue_number: ${{ github.event.number }}
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: process.env.issue_number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['semver: ${{ steps.check.outputs.version }}']
|
||||
});
|
||||
147
packages/domain-expansion/.github/workflows/ci.yml
vendored
147
packages/domain-expansion/.github/workflows/ci.yml
vendored
@ -1,147 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
||||
# Automatically cancel older in-progress jobs on the same branch
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
FORCE_COLOR: true
|
||||
ASTRO_TELEMETRY_DISABLED: true
|
||||
# 7 GiB by default on GitHub, setting to 6 GiB
|
||||
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
|
||||
jobs:
|
||||
# Build primes out Turbo build cache and pnpm cache
|
||||
build:
|
||||
name: "Build - Node ${{ matrix.NODE_VERSION }}"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: [20, 22]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup node@${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@main
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Packages
|
||||
run: pnpm run package:build
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
needs: build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Format Check
|
||||
run: pnpm run lint
|
||||
|
||||
test:
|
||||
name: "Test: Node ${{ matrix.NODE_VERSION }}"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: build
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: [20, 22]
|
||||
fail-fast: false
|
||||
env:
|
||||
NODE_VERSION: ${{ matrix.NODE_VERSION }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-${{ matrix.NODE_VERSION }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.NODE_VERSION }}-turbo-
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup node@${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Packages
|
||||
run: pnpm run package:build
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
working-directory: package
|
||||
|
||||
duplicated-packages:
|
||||
name: Check for duplicated dependencies
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_VERSION: 22
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup node@${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Check duplicated dependencies
|
||||
run: pnpm dedupe --prefer-offline --check
|
||||
@ -1,50 +0,0 @@
|
||||
name: Preview mode
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- synchronize
|
||||
- opened
|
||||
- reopened
|
||||
|
||||
env:
|
||||
FORCE_COLOR: true
|
||||
|
||||
jobs:
|
||||
no-preview:
|
||||
name: Block Preview mode
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Check for preview mode
|
||||
# Fails if in preview mode
|
||||
run: pnpm changeset pre enter foo
|
||||
|
||||
- name: Remove Preview Label
|
||||
uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: preview
|
||||
|
||||
- name: Add Label
|
||||
if: ${{ failure() }}
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: preview
|
||||
@ -1,68 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- labeled
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
FORCE_COLOR: true
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
name: Changelog PR or Release
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'preview') }}
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Packages
|
||||
run: pnpm run package:build
|
||||
|
||||
- name: Publish preview
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'preview') }}
|
||||
run: pnpm exec changeset publish
|
||||
env:
|
||||
# Use Node auth from above
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create Release Pull Request or Publish
|
||||
id: changesets
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
# Note: pnpm install after versioning is necessary to refresh lockfile
|
||||
version: pnpm run version
|
||||
publish: pnpm exec changeset publish
|
||||
commit: '[ci] release'
|
||||
title: '[ci] release'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.COMMIT_TOKEN }}
|
||||
# Needs access to publish to npm
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@ -1,25 +0,0 @@
|
||||
name: TODO Tracking
|
||||
|
||||
on:
|
||||
push:
|
||||
# branches: [main]
|
||||
|
||||
permissions:
|
||||
issues: read
|
||||
repository-projects: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
track-todos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run tdg-github-action
|
||||
uses: ribtoks/tdg-github-action@master
|
||||
with:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
SHA: ${{ github.sha }}
|
||||
REF: ${{ github.ref }}
|
||||
DRY_RUN: false
|
||||
COMMENT_ON_ISSUES: true
|
||||
@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
node_modules/.bin/lint-staged
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"editor.defaultFormatter": "prettier",
|
||||
"editor.gotoLocation.multipleDefinitions": "goto"
|
||||
}
|
||||
29
packages/domain-expansion/package-lock.json
generated
29
packages/domain-expansion/package-lock.json
generated
@ -343,6 +343,13 @@
|
||||
"fs-extra": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/find-root/node_modules/@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@manypkg/find-root/node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
@ -434,11 +441,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
|
||||
"version": "24.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz",
|
||||
"integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
@ -1920,6 +1932,15 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
|
||||
@ -20,26 +20,18 @@ export default async function load(id) {
|
||||
|
||||
const { search, searchParams } = fileURL;
|
||||
|
||||
id = id.replace(search, "");
|
||||
|
||||
id = id.replace(search, "");
|
||||
const ext = path.extname(id).slice(1);
|
||||
|
||||
if (!supportedImageTypes.includes(ext)) return null;
|
||||
debugger
|
||||
|
||||
const { default: astroViteConfigs } = await import(
|
||||
// @ts-ignore
|
||||
"../../astroViteConfigs.js"
|
||||
);
|
||||
|
||||
const { environment, projectBase, assetFileNames } = astroViteConfigs;
|
||||
|
||||
const src = await getSrcPath(id);
|
||||
|
||||
const rootRelativePosixSrc = path.posix.normalize(
|
||||
path.relative("", src).split(path.sep).join(path.posix.sep)
|
||||
);
|
||||
|
||||
const getHash = (width) =>
|
||||
objectHash(
|
||||
{ width, options, rootRelativePosixSrc },
|
||||
|
||||
@ -50,7 +50,7 @@ export default async function ({
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
imageFormat
|
||||
} = await getProcessedImage(src, transformConfigs, { skipCache: true });
|
||||
} = await getProcessedImage(src, transformConfigs, { skipCache: false });
|
||||
|
||||
src = path;
|
||||
|
||||
@ -114,9 +114,7 @@ export default async function ({
|
||||
|
||||
return returnObject;
|
||||
} catch (error) {
|
||||
console.error(`Error processing images:: ${src}`, error, error.stack);
|
||||
debugger
|
||||
|
||||
console.error(`Error processing images:: ${src}`, error, error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,12 +55,18 @@ export default async function getProcessedImage(
|
||||
|
||||
let path = src.replace(/\\/g, `/`);
|
||||
|
||||
// @todo : remove this
|
||||
let imagePath = isRemote ? join(cwd, path) : await getSrcPath(src);
|
||||
|
||||
if(!existsSync(imagePath)) {
|
||||
console.log("getProcessedImage::imagePath does not exist", imagePath);
|
||||
return {
|
||||
path,
|
||||
base,
|
||||
rest,
|
||||
};
|
||||
}
|
||||
const imageBuffer = await fs.readFile(imagePath);
|
||||
|
||||
const { image, imageWidth, imageHeight, imageFormat } =
|
||||
await getImageDetails(imageBuffer, width, height, aspect, skipCache);
|
||||
const { image, imageWidth, imageHeight, imageFormat } = await getImageDetails(imageBuffer, width, height, aspect);
|
||||
|
||||
return {
|
||||
path,
|
||||
|
||||
@ -27,9 +27,9 @@ export default async function getSrcset(
|
||||
: "";
|
||||
|
||||
const [cleanSrc] = src.split("?");
|
||||
|
||||
const id = `${cleanSrc}?${params.slice(1)}`;
|
||||
const fullPath = await getSrcPath(cleanSrc);
|
||||
// @todo : remove this
|
||||
const fullPath = await getSrcPath(id);
|
||||
const { default: load } = await import("../../plugin/hooks/load.js");
|
||||
// @ts-ignore
|
||||
let srcset = null
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
export default {
|
||||
"environment": "build",
|
||||
"environment": "dev",
|
||||
"isSsrBuild": false,
|
||||
"projectBase": "",
|
||||
"publicDir": "C:\\Users\\zx\\Desktop\\polymech\\site-min\\public\\",
|
||||
"rootDir": "C:\\Users\\zx\\Desktop\\polymech\\site-min\\",
|
||||
"mode": "production",
|
||||
"outDir": "C:\\Users\\zx\\Desktop\\polymech\\site-min\\dist\\",
|
||||
"assetsDir": "_astro",
|
||||
"publicDir": "C:\\Users\\zx\\Desktop\\polymech\\site2\\public\\",
|
||||
"rootDir": "C:\\Users\\zx\\Desktop\\polymech\\site2\\",
|
||||
"mode": "dev",
|
||||
"outDir": "dist",
|
||||
"assetsDir": "/_astro",
|
||||
"sourcemap": false,
|
||||
"assetFileNames": "/_astro/[name]@[width].[hash][extname]"
|
||||
}
|
||||
@ -2,9 +2,18 @@
|
||||
import renderImg from "../api/renderImg.js";
|
||||
import type { ImgConfigOptions } from "../types.d";
|
||||
|
||||
declare interface Props extends ImgConfigOptions {}
|
||||
declare interface Props extends ImgConfigOptions {
|
||||
s?: string;
|
||||
}
|
||||
|
||||
const { link, style, img } = await renderImg(Astro.props as Props);
|
||||
const { s, ...rest } = Astro.props as Props;
|
||||
|
||||
if (s) {
|
||||
const separator = rest.src.includes("?") ? "&" : "?";
|
||||
rest.src = `${rest.src}${separator}s=${s}`;
|
||||
}
|
||||
|
||||
const { link, style, img } = await renderImg(rest);
|
||||
---
|
||||
|
||||
<Fragment set:html={link + style + img} />
|
||||
|
||||
@ -55,11 +55,11 @@ export default {
|
||||
);
|
||||
|
||||
const { mode, outDir, assetsDir, isSsrBuild } = astroViteConfigs;
|
||||
|
||||
|
||||
if (mode === "production") {
|
||||
// 1. Define the manifest path in the current working directory.
|
||||
const manifestPath = path.join(process.cwd(), "./imagetools-manifest.json")
|
||||
|
||||
|
||||
|
||||
// 2. Read the manifest from the previous build, if it exists.
|
||||
let previousAssets = new Map();
|
||||
@ -78,7 +78,7 @@ export default {
|
||||
// 4. Merge previous and current assets. Current assets overwrite previous ones.
|
||||
const allAssets = new Map([...previousAssets, ...currentAssets]);
|
||||
logger.info(`[imagetools] Replaying ${allAssets.size} image(s) from the persistent manifest.`);
|
||||
if(allAssets.size === 0) {
|
||||
if (allAssets.size === 0) {
|
||||
logger.info('[imagetools] No images to process.');
|
||||
console.timeEnd('[imagetools] build:done');
|
||||
return;
|
||||
@ -88,6 +88,7 @@ export default {
|
||||
await pMap(
|
||||
[...allAssets.entries()],
|
||||
async ([assetPath, { hash, image, buffer }]) => {
|
||||
logger.info(`[imagetools] Processing image ${assetPath}...`);
|
||||
try {
|
||||
await saveAndCopyAsset(
|
||||
hash,
|
||||
@ -103,7 +104,7 @@ export default {
|
||||
}
|
||||
},
|
||||
// higher concurrency causes sharp/vips errors as well
|
||||
{ concurrency: 1 }
|
||||
{ concurrency: 10 }
|
||||
);
|
||||
|
||||
// 6. Write the updated asset list back to the manifest for the next build.
|
||||
@ -116,11 +117,11 @@ export default {
|
||||
return [key, { hash, buffer }];
|
||||
}
|
||||
);
|
||||
|
||||
const manifestContent = JSON.stringify(serializableAssets,null,2);
|
||||
|
||||
const manifestContent = JSON.stringify(serializableAssets, null, 2);
|
||||
logger.info(`[imagetools] Writing manifest (${(manifestContent.length / 1024).toFixed(2)} KB)...`);
|
||||
console.time('[imagetools] Manifest write took');
|
||||
await writeFile(manifestPath, manifestContent);
|
||||
await writeFile(manifestPath, manifestContent);
|
||||
console.timeEnd('[imagetools] Manifest write took');
|
||||
} catch (error) {
|
||||
logger.error("Failed to write the asset manifest.");
|
||||
|
||||
@ -14,8 +14,7 @@
|
||||
"scripts": {
|
||||
"test:watch": "vitest",
|
||||
"test": "vitest run",
|
||||
"test:src": "vitest run tests/src.js",
|
||||
"test:image": "vitest run tests/image.js"
|
||||
"test:src": "vitest run tests/src.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -106,6 +106,7 @@ export default async function load(id) {
|
||||
config,
|
||||
type,
|
||||
})
|
||||
|
||||
const dataUri = `data:${type};base64,${(
|
||||
buffer || (await getCachedBuffer(hash, image))
|
||||
).toString("base64")}`
|
||||
|
||||
@ -8,7 +8,6 @@ export async function getCachedBuffer(hash, image) {
|
||||
return fs.promises.readFile(cacheFilePath);
|
||||
}
|
||||
const buffer = await image.clone().toBuffer();
|
||||
console.log(`write ${cacheFilePath}`);
|
||||
await fs.promises.writeFile(cacheFilePath, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@ -85,12 +85,17 @@ fs.existsSync(fsCachePath) || fs.mkdirSync(fsCachePath, { recursive: true });
|
||||
|
||||
const cache_dir = () => {
|
||||
if (!GlobalConfigOptions.cacheRoot) return false;
|
||||
const dir = path.resolve(resolve(GlobalConfigOptions.cacheRoot, false, {
|
||||
"POLYMECH-CACHE": get_var("POLYMECH-CACHE")
|
||||
}))
|
||||
if (!exists(dir)) {
|
||||
mkdir(dir);
|
||||
}
|
||||
let dir;
|
||||
if (!exists(GlobalConfigOptions.cacheRoot)) {
|
||||
dir = path.resolve(resolve(GlobalConfigOptions.cacheRoot, false, {
|
||||
"POLYMECH-CACHE": get_var("POLYMECH-CACHE")
|
||||
}))
|
||||
if (!exists(dir)) {
|
||||
mkdir(dir);
|
||||
}
|
||||
}else{
|
||||
dir = GlobalConfigOptions.cacheRoot
|
||||
}
|
||||
return dir + "/";
|
||||
}
|
||||
// @todo : cache dir
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"./base/*": "./dist/base/*",
|
||||
"./model/*": "./dist/model/*",
|
||||
"./config/*": "./dist/config/*",
|
||||
"./utils/*": "./dist/utils/*",
|
||||
"./pages/*": "./src/pages/*",
|
||||
"./routes/*": "./src/routes/*",
|
||||
"./components/*": "./src/components/*",
|
||||
|
||||
@ -491,7 +491,7 @@ export async function generateBreadcrumbs(
|
||||
const segmentPath = arr.slice(0, index + 1).join('/');
|
||||
const label = segment
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
|
||||
breadcrumbs.push({
|
||||
|
||||
@ -34,7 +34,7 @@ function generateBreadcrumbs(path: string, collection: string, pageTitle?: strin
|
||||
// Format segment label
|
||||
let label = segment
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
|
||||
// Use page title for the last segment if provided
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
import { Img, Picture } from "imagetools/components";
|
||||
import { Img } from "imagetools/components";
|
||||
import Translate from "./i18n.astro"
|
||||
import { translate } from "@/base/i18n";
|
||||
|
||||
@ -46,7 +46,7 @@ const mergedLightboxSettings = {
|
||||
SHOW_DESCRIPTION: lightboxSettings.SHOW_DESCRIPTION ?? IMAGE_SETTINGS.LIGHTBOX.SHOW_DESCRIPTION,
|
||||
};
|
||||
const locale = Astro.currentLocale || "en";
|
||||
|
||||
console.log(`LGallery Images`, images)
|
||||
---
|
||||
|
||||
<div
|
||||
@ -61,6 +61,8 @@ const locale = Astro.currentLocale || "en";
|
||||
minSwipeDistance: 50,
|
||||
isSwiping: false,
|
||||
images: ${JSON.stringify(images)},
|
||||
lastTapTime: 0,
|
||||
doubleTapDelay: 300,
|
||||
handleSwipe() {
|
||||
if (!this.isSwiping) return;
|
||||
const swipeDistance = this.touchEndX - this.touchStartX;
|
||||
@ -84,6 +86,27 @@ const locale = Astro.currentLocale || "en";
|
||||
this.lightboxLoaded = true;
|
||||
this.open = true;
|
||||
};
|
||||
},
|
||||
preloadImage(index) {
|
||||
// Preload without affecting lightboxLoaded state (for navigation within lightbox)
|
||||
let img = new Image();
|
||||
img.src = this.images[index].src;
|
||||
// No need to wait for load when navigating within lightbox
|
||||
},
|
||||
handleThumbnailClick(index) {
|
||||
const currentTime = Date.now();
|
||||
const timeDiff = currentTime - this.lastTapTime;
|
||||
|
||||
if (timeDiff < this.doubleTapDelay) {
|
||||
// Double tap/click detected - open lightbox
|
||||
this.currentIndex = index;
|
||||
this.preloadAndOpen();
|
||||
} else {
|
||||
// Single tap/click - just change current image
|
||||
this.currentIndex = index;
|
||||
}
|
||||
|
||||
this.lastTapTime = currentTime;
|
||||
}
|
||||
}
|
||||
`}
|
||||
@ -91,15 +114,13 @@ const locale = Astro.currentLocale || "en";
|
||||
@keydown.window="if(open){
|
||||
if($event.key === 'ArrowRight' && currentIndex < total - 1){
|
||||
currentIndex++;
|
||||
lightboxLoaded = false;
|
||||
preloadAndOpen();
|
||||
preloadImage(currentIndex);
|
||||
} else if($event.key === 'ArrowLeft' && currentIndex > 0){
|
||||
currentIndex--;
|
||||
lightboxLoaded = false;
|
||||
preloadAndOpen();
|
||||
preloadImage(currentIndex);
|
||||
}
|
||||
}"
|
||||
class="product-gallery"><div class="flex flex-col h-full">
|
||||
class="product-gallery bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"><div class="flex flex-col h-full p-4">
|
||||
<!-- Main Image (with swipe functionality) -->
|
||||
<div
|
||||
class="flex-1 flex items-center justify-center cursor-pointer rounded-lg"
|
||||
@ -113,7 +134,7 @@ const locale = Astro.currentLocale || "en";
|
||||
<Img
|
||||
src={image.src}
|
||||
alt={image.alt,I18N_SOURCE_LANGUAGE,locale}
|
||||
objectFit="cover"
|
||||
objectFit="contain"
|
||||
format="avif"
|
||||
placeholder="blurred"
|
||||
sizes={mergedGallerySettings.SIZES_REGULAR}
|
||||
@ -125,29 +146,29 @@ const locale = Astro.currentLocale || "en";
|
||||
))}
|
||||
</div>
|
||||
<!-- Image Info -->
|
||||
{ (mergedGallerySettings.SHOW_TITLE || mergedGallerySettings.SHOW_DESCRIPTION) && (
|
||||
<div class="text-center py-4">
|
||||
{ (images.some(img => (mergedGallerySettings.SHOW_TITLE && img.title && img.title.trim().length > 0) || (mergedGallerySettings.SHOW_DESCRIPTION && img.description && img.description.trim().length > 0))) && (
|
||||
<div class="text-center py-4 border-t border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50 -mx-4 px-4">
|
||||
{images.map((image, index) => (
|
||||
<div x-show={`currentIndex === ${index}`} key={index}>
|
||||
{ mergedGallerySettings.SHOW_TITLE && ( <h2 id="imageTitle" class="text-xl font-bold">{image.title}</h2>)}
|
||||
{ mergedGallerySettings.SHOW_DESCRIPTION && (<p id="imageDescription" class="text-gray-600"><Translate>{ image.description}</Translate></p>)}
|
||||
{ mergedGallerySettings.SHOW_TITLE && image.title && image.title.trim().length > 0 && ( <h2 id="imageTitle" class="text-xl font-bold text-gray-900 dark:text-white">{image.title}</h2>)}
|
||||
{ mergedGallerySettings.SHOW_DESCRIPTION && image.description && image.description.trim().length > 0 && (<p id="imageDescription" class="text-gray-600 dark:text-gray-300"><Translate>{ image.description}</Translate></p>)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<!-- Thumbnails -->
|
||||
<div class="overflow-x-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-200">
|
||||
<div class="grid grid-cols-3 md:grid-cols-4 gap-2 p-2 mt-2 ml-2 mr-2 items-center justify-center">
|
||||
<div class="overflow-x-auto scrollbar-thin scrollbar-thumb-gray-400 dark:scrollbar-thumb-gray-500 scrollbar-track-gray-200 dark:scrollbar-track-gray-700 border-t border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/30 -mx-4 px-4 pt-3">
|
||||
<div class="grid grid-cols-3 md:grid-cols-4 gap-2 pb-2 items-center justify-center">
|
||||
{images.map((image, index) => (
|
||||
<button
|
||||
key={index}
|
||||
x-on:click={`currentIndex = ${index};`}
|
||||
x-on:click={`handleThumbnailClick(${index})`}
|
||||
:class={`currentIndex === ${index} ? 'ring-2 ring-orange-500' : ''`}
|
||||
class="thumbnail thumbnail-btn rounded-lg"
|
||||
>
|
||||
<Img
|
||||
src={image.src}
|
||||
objectFit="cover"
|
||||
objectFit="contain"
|
||||
format="avif"
|
||||
placeholder="blurred"
|
||||
sizes={mergedGallerySettings.SIZES_THUMB}
|
||||
@ -191,10 +212,10 @@ const locale = Astro.currentLocale || "en";
|
||||
img: { class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg lightbox-main" }
|
||||
}}
|
||||
/>
|
||||
{ (mergedLightboxSettings.SHOW_TITLE || mergedLightboxSettings.SHOW_DESCRIPTION) && (
|
||||
{ ((mergedLightboxSettings.SHOW_TITLE && image.title && image.title.trim().length > 0) || (mergedLightboxSettings.SHOW_DESCRIPTION && image.description && image.description.trim().length > 0)) && (
|
||||
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2 m-[8px] max-h-[32vh] p-2 text-white bg-black/50 rounded-lg" style="width: 90%;">
|
||||
{ mergedLightboxSettings.SHOW_TITLE && ( <h3 class="text-xl"><Translate>{image.title}</Translate></h3>)}
|
||||
{ mergedLightboxSettings.SHOW_DESCRIPTION && (<p><Translate>{image.description}</Translate></p>)} </div>
|
||||
{ mergedLightboxSettings.SHOW_TITLE && image.title && image.title.trim().length > 0 && ( <h3 class="text-xl"><Translate>{image.title}</Translate></h3>)}
|
||||
{ mergedLightboxSettings.SHOW_DESCRIPTION && image.description && image.description.trim().length > 0 && (<p><Translate>{image.description}</Translate></p>)} </div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@ -209,7 +230,7 @@ const locale = Astro.currentLocale || "en";
|
||||
<!-- Navigation Buttons -->
|
||||
<button
|
||||
x-show="currentIndex > 0"
|
||||
x-on:click="currentIndex--;"
|
||||
x-on:click="currentIndex--; preloadImage(currentIndex);"
|
||||
class="absolute left-0 top-1/2 transform -translate-y-1/2 p-4 m-[8px] text-white text-3xl bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
|
||||
aria-label="Previous"
|
||||
>
|
||||
@ -217,7 +238,7 @@ const locale = Astro.currentLocale || "en";
|
||||
</button>
|
||||
<button
|
||||
x-show="currentIndex < total - 1"
|
||||
x-on:click="currentIndex++; "
|
||||
x-on:click="currentIndex++; preloadImage(currentIndex);"
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2 p-4 m-[8px] text-white text-3xl bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
|
||||
aria-label="Next"
|
||||
>
|
||||
|
||||
@ -1,96 +1,134 @@
|
||||
---
|
||||
import { Img } from "imagetools/components";
|
||||
import { resolveImagePath } from '@/utils/path-resolution';
|
||||
|
||||
export interface Props {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
class?: string;
|
||||
entryPath?: string;
|
||||
lightbox?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const {
|
||||
src,
|
||||
alt,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
placeholder,
|
||||
objectFit,
|
||||
loading,
|
||||
sizes,
|
||||
class: className,
|
||||
entryPath,
|
||||
lightbox = true,
|
||||
...rest
|
||||
} = Astro.props;
|
||||
|
||||
const resolvedSrc = resolveImagePath(src, entryPath, Astro.url);
|
||||
const imgProps = { alt, width, height, format, placeholder, objectFit, loading, sizes };
|
||||
Object.keys(imgProps).forEach(key => imgProps[key] === undefined && delete imgProps[key]);
|
||||
---
|
||||
<button
|
||||
class:list={["w-full h-full", { 'lightbox-enabled': lightbox }]}
|
||||
>
|
||||
<Img src={resolvedSrc} {...imgProps} attributes={{
|
||||
img: {
|
||||
...(className && { class: className })
|
||||
}
|
||||
}} />
|
||||
</button>
|
||||
|
||||
<script define:vars={{ lightbox }}>
|
||||
const button = document.currentScript.previousElementSibling;
|
||||
|
||||
function getHighestResSrc(img) {
|
||||
let highestResSrc = img.src;
|
||||
const srcset = img.getAttribute('srcset');
|
||||
|
||||
if (srcset) {
|
||||
let maxWidth = 0;
|
||||
srcset.split(',').forEach(s => {
|
||||
const parts = s.trim().split(/\s+/);
|
||||
const url = parts[0];
|
||||
const widthStr = parts[1];
|
||||
if (widthStr && widthStr.endsWith('w')) {
|
||||
const width = parseInt(widthStr.slice(0, -1), 10);
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
highestResSrc = url;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return highestResSrc;
|
||||
}
|
||||
|
||||
button.addEventListener('click', (event) => {
|
||||
if (!lightbox) return;
|
||||
|
||||
const triggerImage = event.currentTarget.querySelector('img');
|
||||
if (!triggerImage) return;
|
||||
|
||||
const allImages = Array.from(document.querySelectorAll('.lightbox-enabled img')).map(img => ({
|
||||
src: getHighestResSrc(img),
|
||||
alt: img.getAttribute('alt')
|
||||
}));
|
||||
|
||||
const triggerSrc = getHighestResSrc(triggerImage);
|
||||
const currentIndex = allImages.findIndex(img => img.src === triggerSrc);
|
||||
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const lightboxEvent = new CustomEvent('open-lightbox', {
|
||||
detail: {
|
||||
images: allImages,
|
||||
currentIndex: currentIndex
|
||||
}
|
||||
});
|
||||
|
||||
window.dispatchEvent(lightboxEvent);
|
||||
});
|
||||
</script>
|
||||
---
|
||||
import { Img } from "imagetools/components";
|
||||
import { resolveImagePath } from '@/utils/path-resolution';
|
||||
|
||||
export interface Props {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
class?: string;
|
||||
entryPath?: string;
|
||||
lightbox?: boolean;
|
||||
preset?: 'small' | 'medium' | 'large';
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const {
|
||||
src,
|
||||
alt,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
placeholder,
|
||||
objectFit,
|
||||
loading,
|
||||
sizes,
|
||||
class: className,
|
||||
entryPath,
|
||||
lightbox = true,
|
||||
preset,
|
||||
...rest
|
||||
} = Astro.props;
|
||||
|
||||
const resolvedSrc = resolveImagePath(src, entryPath, Astro.url);
|
||||
const imgProps = { alt, width, height, format, placeholder, objectFit, loading, sizes };
|
||||
Object.keys(imgProps).forEach(key => imgProps[key] === undefined && delete imgProps[key]);
|
||||
|
||||
// Define preset styles
|
||||
const presetStyles = {
|
||||
small: "flex justify-center my-4 mx-auto w-[200px]",
|
||||
medium: "flex justify-center my-4 mx-auto w-[600px]",
|
||||
large: "flex justify-center my-4 mx-auto w-[800px]"
|
||||
};
|
||||
|
||||
const containerClass = preset ? presetStyles[preset] : "w-full h-full";
|
||||
const buttonClass = preset ? "w-full h-full" : "w-full h-full";
|
||||
const imgClass = preset ? "w-full h-auto object-contain" : "";
|
||||
---
|
||||
|
||||
{preset ? (
|
||||
<div class={containerClass}>
|
||||
<button
|
||||
class:list={[buttonClass, { 'lightbox-enabled': lightbox }]}
|
||||
>
|
||||
<Img
|
||||
src={resolvedSrc}
|
||||
{...imgProps}
|
||||
attributes={{
|
||||
img: {
|
||||
class: [imgClass, className].filter(Boolean).join(' ') || undefined
|
||||
}
|
||||
}
|
||||
} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
class:list={["w-full h-full", { 'lightbox-enabled': lightbox }]}
|
||||
>
|
||||
<Img
|
||||
src={resolvedSrc}
|
||||
{...imgProps}
|
||||
attributes={{
|
||||
img: {
|
||||
...(className && { class: className })
|
||||
}
|
||||
}
|
||||
} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<script define:vars={{ lightbox, preset }}>
|
||||
const button = preset
|
||||
? document.currentScript.previousElementSibling.querySelector('button')
|
||||
: document.currentScript.previousElementSibling;
|
||||
|
||||
function getHighestResSrc(img) {
|
||||
let highestResSrc = img.src;
|
||||
const srcset = img.getAttribute('srcset');
|
||||
|
||||
if (srcset) {
|
||||
let maxWidth = 0;
|
||||
srcset.split(',').forEach(s => {
|
||||
const parts = s.trim().split(/\s+/);
|
||||
const url = parts[0];
|
||||
const widthStr = parts[1];
|
||||
if (widthStr && widthStr.endsWith('w')) {
|
||||
const width = parseInt(widthStr.slice(0, -1), 10);
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
highestResSrc = url;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return highestResSrc;
|
||||
}
|
||||
|
||||
button.addEventListener('click', (event) => {
|
||||
if (!lightbox) return;
|
||||
|
||||
const triggerImage = event.currentTarget.querySelector('img');
|
||||
if (!triggerImage) return;
|
||||
|
||||
const allImages = Array.from(document.querySelectorAll('.lightbox-enabled img')).map(img => ({
|
||||
src: getHighestResSrc(img),
|
||||
alt: img.getAttribute('alt')
|
||||
}));
|
||||
|
||||
const triggerSrc = getHighestResSrc(triggerImage);
|
||||
const currentIndex = allImages.findIndex(img => img.src === triggerSrc);
|
||||
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const lightboxEvent = new CustomEvent('open-lightbox', {
|
||||
detail: {
|
||||
images: allImages,
|
||||
currentIndex: currentIndex
|
||||
}
|
||||
});
|
||||
|
||||
window.dispatchEvent(lightboxEvent);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -226,14 +226,7 @@ if (isPrevNext && entryPath) {
|
||||
)}
|
||||
</header>
|
||||
|
||||
{frontmatter.image?.url && (
|
||||
<RelativeImage
|
||||
src={frontmatter.image.url}
|
||||
alt={frontmatter.image.alt || frontmatter.title}
|
||||
class="w-full h-auto rounded-lg mb-8 object-cover"
|
||||
entryPath={entryPath}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
<div class="markdown-content">
|
||||
<slot />
|
||||
|
||||
@ -18,12 +18,9 @@ const paths = await getStaticPaths_fs(getCollection, collectionName, config.LANG
|
||||
export async function getStaticPaths() {
|
||||
const collectionName = 'resources';
|
||||
const config = PolymechInstance.getConfig();
|
||||
const paths = await getStaticPaths_fs(getCollection, collectionName, config.LANGUAGES_PROD, config.COLLECTION_FILTERS);
|
||||
// console.log(`getStaticPaths ${PolymechInstance.languages} ${PolymechInstance.products}`, paths);
|
||||
// Get configuration from registry (now has proper defaults)
|
||||
//const paths = await getStaticPaths_fs(getCollection, collectionName, config.LANGUAGES_PROD, config.COLLECTION_FILTERS);
|
||||
const languages = PolymechInstance.languages;
|
||||
const products = PolymechInstance.products;
|
||||
|
||||
const products = PolymechInstance.products;
|
||||
return languages.flatMap((lang: string) =>
|
||||
products.map((product: string) => ({
|
||||
params: { lang, slug: product }
|
||||
|
||||
@ -67,8 +67,7 @@ export function resolveImagePath(src: string, entryPath?: string, astroUrl?: URL
|
||||
// --- Strategy 2.2: Fallback to URL parsing ---
|
||||
else if (astroUrl) {
|
||||
strategy = 'URL';
|
||||
// if (enableDebugErrors) console.warn(`[resolveImagePath] [INFO-URL] No entryPath provided. Falling back to URL-based resolution for "${src}". This is less reliable.`);
|
||||
|
||||
// if (enableDebugErrors) console.warn(`[resolveImagePath] [INFO-URL] No entryPath provided. Falling back to URL-based resolution for "${src}". This is less reliable.`);
|
||||
const isFolderUrl = astroUrl.pathname.endsWith('/');
|
||||
const pathSegments = astroUrl.pathname.split('/').filter(p => p);
|
||||
const hasLocale = pathSegments.length > 0 && /^[a-z]{2}$/.test(pathSegments[0]);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user