├── .node-version ├── examples ├── nuxt │ ├── .node-version │ ├── .npmrc │ ├── public │ │ └── favicon.ico │ ├── tsconfig.json │ ├── pages │ │ ├── another.vue │ │ └── index.vue │ ├── .gitignore │ ├── README.ja.md │ ├── package.json │ ├── nuxt.config.ts │ ├── assets │ │ └── css │ │ │ └── globals.css │ ├── README.md │ └── app.vue ├── next-app-router │ ├── .node-version │ ├── .npmrc │ ├── app │ │ ├── favicon.ico │ │ ├── page.tsx │ │ ├── another │ │ │ └── page.tsx │ │ ├── globals.css │ │ └── layout.tsx │ ├── next.config.mjs │ ├── README.ja.md │ ├── .gitignore │ ├── package.json │ ├── README.md │ ├── tsconfig.json │ └── components │ │ └── ViewportExtra.tsx ├── next-pages-router │ ├── .node-version │ ├── .npmrc │ ├── public │ │ └── favicon.ico │ ├── next.config.mjs │ ├── pages │ │ ├── another.tsx │ │ ├── index.tsx │ │ ├── _app.tsx │ │ └── _document.tsx │ ├── README.ja.md │ ├── .gitignore │ ├── package.json │ ├── styles │ │ └── globals.css │ ├── tsconfig.json │ ├── README.md │ └── components │ │ └── ViewportExtra.tsx └── no-framework │ ├── README.ja.md │ ├── styles │ └── globals.css │ ├── README.md │ ├── another.html │ └── index.html ├── .release-please-manifest.json ├── .npmrc ├── vite-env.d.ts ├── globals.d.ts ├── docs ├── images │ ├── spacer-0x0.svg │ └── spacer-100x0.svg ├── ja │ └── migration-from-v1.md └── en │ └── migration-from-v1.md ├── src ├── entries │ ├── extended │ │ └── viewport-extra.ts │ ├── viewport-extra.ts │ └── immediate │ │ ├── extended │ │ └── viewport-extra.ts │ │ └── viewport-extra.ts ├── internal │ ├── DeepPartial.ts │ ├── ArrayLike.ts │ ├── MatchMedia.ts │ ├── ArrayLike.spec.ts │ ├── string.ts │ ├── Media.ts │ ├── ArrayLike.es5.spec.ts │ ├── number.ts │ ├── MediaAttribute.ts │ ├── DecimalPlaces.ts │ ├── DecimalPlacesAttribute.ts │ ├── string.spec.ts │ ├── Document.ts │ ├── number.spec.ts │ ├── number.es5.spec.ts │ ├── MatchMedia.spec.ts │ ├── ContentAttribute.ts │ ├── DecimalPlaces.spec.ts │ ├── GlobalParameters.ts │ ├── Media.spec.ts │ ├── MediaAttribute.spec.ts │ ├── DecimalPlacesAttribute.spec.ts │ ├── DeepPartial.spec.ts │ ├── Document.spec.ts │ ├── Content.ts │ ├── HTMLMetaElement.ts │ ├── GlobalParameters.spec.ts │ ├── MediaSpecificParameters.ts │ ├── ContentAttribute.spec.ts │ └── Content.spec.ts └── apis │ ├── activateAttributes.node.spec.ts │ ├── setParameters.node.spec.ts │ ├── activateMediaSpecificAttributes.node.spec.ts │ ├── activateMediaSpecificAttributes.ts │ ├── setMediaSpecificParametersList.node.spec.ts │ ├── activateAttributes.ts │ ├── setMediaSpecificParametersList.ts │ └── setParameters.ts ├── CHANGELOG.md ├── scripts ├── print-latest-safe-release-date.ts └── create-declaration-with-dts.ts ├── tests ├── __fixtures__ │ └── src │ │ ├── assets │ │ └── scripts │ │ │ ├── unit │ │ │ ├── call-activateAttributes.ts │ │ │ ├── call-activateMediaSpecificAttributes.ts │ │ │ ├── call-setMediaSpecificParametersList.ts │ │ │ └── call-setParameters.ts │ │ │ └── e2e │ │ │ ├── immediate │ │ │ ├── iife │ │ │ │ ├── trigger-side-effects.ts │ │ │ │ └── call-apply.ts │ │ │ ├── extended │ │ │ │ ├── iife │ │ │ │ │ ├── trigger-side-effects.ts │ │ │ │ │ └── call-apply.ts │ │ │ │ ├── cjs │ │ │ │ │ ├── trigger-side-effects.ts │ │ │ │ │ └── call-apply.ts │ │ │ │ └── es │ │ │ │ │ ├── trigger-side-effects.ts │ │ │ │ │ └── call-apply.ts │ │ │ ├── cjs │ │ │ │ ├── trigger-side-effects.ts │ │ │ │ └── call-apply.ts │ │ │ └── es │ │ │ │ ├── trigger-side-effects.ts │ │ │ │ └── call-apply.ts │ │ │ ├── cjs │ │ │ └── call-apply.ts │ │ │ ├── es │ │ │ └── call-apply.ts │ │ │ ├── iife │ │ │ └── call-apply.ts │ │ │ └── extended │ │ │ ├── cjs │ │ │ └── call-apply.ts │ │ │ ├── es │ │ │ └── call-apply.ts │ │ │ └── iife │ │ │ └── call-apply.ts │ │ └── dummy.html ├── modules │ ├── PlaywrightFullProjectList.ts │ └── PlaywrightPage.ts └── specs │ └── e2e │ └── side-effects.spec.ts ├── prettier.config.mjs ├── SECURITY.md ├── lefthook.yml ├── tsconfig.json ├── commitlint.config.ts ├── cspell.json ├── eslint.config.es5.js ├── LICENSE.txt ├── vitest.config.ts ├── .github └── workflows │ ├── stale.yml │ ├── check.yml │ ├── release.yml │ ├── create-release-pull-request.yml │ ├── codeql.yml │ ├── create-stabilize-pull-request.yml │ ├── create-example-lockfile-update-pull-request.yml │ └── publish.yml ├── release-please-config.default.json ├── release-please-config.prerelease.json ├── biome.jsonc ├── rollup.config.declare.js ├── playwright.config.ts ├── vite.config.e2e.ts ├── .gitignore ├── rollup.config.build.js ├── package.json └── README.ja.md /.node-version: -------------------------------------------------------------------------------- 1 | 24.11.1 2 | -------------------------------------------------------------------------------- /examples/nuxt/.node-version: -------------------------------------------------------------------------------- 1 | 24.11.1 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {".":"3.0.0"} 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | ignore-scripts=true 3 | -------------------------------------------------------------------------------- /examples/next-app-router/.node-version: -------------------------------------------------------------------------------- 1 | 24.11.1 2 | -------------------------------------------------------------------------------- /examples/next-pages-router/.node-version: -------------------------------------------------------------------------------- 1 | 24.11.1 2 | -------------------------------------------------------------------------------- /vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | declare const __TYPESCRIPT_TARGET__: string; 2 | -------------------------------------------------------------------------------- /examples/nuxt/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | ignore-scripts=true 3 | -------------------------------------------------------------------------------- /examples/next-app-router/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | ignore-scripts=true 3 | -------------------------------------------------------------------------------- /examples/next-pages-router/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | ignore-scripts=true 3 | -------------------------------------------------------------------------------- /docs/images/spacer-0x0.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/images/spacer-100x0.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/entries/extended/viewport-extra.ts: -------------------------------------------------------------------------------- 1 | export { setParameters as apply } from "../../apis/setParameters.js"; 2 | -------------------------------------------------------------------------------- /examples/nuxt/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsktschy/viewport-extra/HEAD/examples/nuxt/public/favicon.ico -------------------------------------------------------------------------------- /examples/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /src/entries/viewport-extra.ts: -------------------------------------------------------------------------------- 1 | export { setMediaSpecificParametersList as apply } from "../apis/setMediaSpecificParametersList.js"; 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Please see the [GitHub Releases](https://github.com/dsktschy/viewport-extra/releases) for the changelog. 4 | -------------------------------------------------------------------------------- /examples/next-app-router/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsktschy/viewport-extra/HEAD/examples/next-app-router/app/favicon.ico -------------------------------------------------------------------------------- /scripts/print-latest-safe-release-date.ts: -------------------------------------------------------------------------------- 1 | process.stdout.write( 2 | `${new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString()}\n`, 3 | ); 4 | -------------------------------------------------------------------------------- /examples/next-pages-router/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsktschy/viewport-extra/HEAD/examples/next-pages-router/public/favicon.ico -------------------------------------------------------------------------------- /src/internal/DeepPartial.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = T extends object 2 | ? { 3 | [P in keyof T]?: DeepPartial; 4 | } 5 | : T; 6 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/unit/call-activateAttributes.ts: -------------------------------------------------------------------------------- 1 | import { activateAttributes } from "@@/src/apis/activateAttributes.js"; 2 | 3 | activateAttributes(); 4 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/iife/trigger-side-effects.ts: -------------------------------------------------------------------------------- 1 | document 2 | .querySelector("[data-asset-script]") 3 | ?.setAttribute("data-status", "complete"); 4 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/extended/iife/trigger-side-effects.ts: -------------------------------------------------------------------------------- 1 | document 2 | .querySelector("[data-asset-script]") 3 | ?.setAttribute("data-status", "complete"); 4 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/dummy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Must be .mjs for VS Code Prettier plugin 3 | */ 4 | 5 | /** @type {import("prettier").Config} */ 6 | export default { 7 | embeddedLanguageFormatting: "off", 8 | }; 9 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report a vulnerability, please use [GitHub Security Advisories](https://github.com/dsktschy/viewport-extra/security/advisories/new). 6 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/unit/call-activateMediaSpecificAttributes.ts: -------------------------------------------------------------------------------- 1 | import { activateMediaSpecificAttributes } from "@@/src/apis/activateMediaSpecificAttributes.js"; 2 | 3 | activateMediaSpecificAttributes(); 4 | -------------------------------------------------------------------------------- /src/entries/immediate/extended/viewport-extra.ts: -------------------------------------------------------------------------------- 1 | import { activateAttributes } from "../../../apis/activateAttributes.js"; 2 | 3 | activateAttributes(); 4 | 5 | export { setParameters as apply } from "../../../apis/setParameters.js"; 6 | -------------------------------------------------------------------------------- /examples/nuxt/pages/another.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /examples/nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/internal/ArrayLike.ts: -------------------------------------------------------------------------------- 1 | export const arrayFrom: typeof Array.from = 2 | __TYPESCRIPT_TARGET__ !== "es5" 3 | ? /* eslint-disable-next-line compat/compat */ 4 | Array.from 5 | : (arrayLike: ArrayLike) => 6 | (Array.prototype as T[]).slice.call(arrayLike); 7 | -------------------------------------------------------------------------------- /src/entries/immediate/viewport-extra.ts: -------------------------------------------------------------------------------- 1 | import { activateMediaSpecificAttributes } from "../../apis/activateMediaSpecificAttributes.js"; 2 | 3 | activateMediaSpecificAttributes(); 4 | 5 | export { setMediaSpecificParametersList as apply } from "../../apis/setMediaSpecificParametersList.js"; 6 | -------------------------------------------------------------------------------- /src/internal/MatchMedia.ts: -------------------------------------------------------------------------------- 1 | import type { Media } from "./Media.js"; 2 | 3 | export type MatchMedia = (query: string) => MediaQueryList; 4 | 5 | export const createMatchMediaPredicate = 6 | (mm: MatchMedia) => 7 | (media?: Media): boolean => 8 | typeof media !== "undefined" ? mm(media).matches : true; 9 | -------------------------------------------------------------------------------- /tests/modules/PlaywrightFullProjectList.ts: -------------------------------------------------------------------------------- 1 | import type { FullProject } from "@playwright/test"; 2 | 3 | type FullProjectList = FullProject[]; 4 | 5 | export const getViewportSize = ( 6 | fullProjectList: FullProjectList, 7 | projectName: string, 8 | ) => fullProjectList.find(({ name }) => name === projectName); 9 | -------------------------------------------------------------------------------- /examples/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /examples/next-app-router/next.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | output: "export", 7 | outputFileTracingRoot: path.dirname(fileURLToPath(import.meta.url)), 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/cjs/trigger-side-effects.ts: -------------------------------------------------------------------------------- 1 | __TYPESCRIPT_TARGET__ !== "es5" 2 | ? await import("@@/dist/immediate/viewport-extra.cjs") 3 | : await import("@@/dist/immediate/es5/viewport-extra.cjs"); 4 | document 5 | .querySelector("[data-asset-script]") 6 | ?.setAttribute("data-status", "complete"); 7 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/es/trigger-side-effects.ts: -------------------------------------------------------------------------------- 1 | __TYPESCRIPT_TARGET__ !== "es5" 2 | ? await import("@@/dist/immediate/viewport-extra.mjs") 3 | : await import("@@/dist/immediate/es5/viewport-extra.mjs"); 4 | document 5 | .querySelector("[data-asset-script]") 6 | ?.setAttribute("data-status", "complete"); 7 | -------------------------------------------------------------------------------- /src/internal/ArrayLike.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { arrayFrom } from "./ArrayLike.js"; 3 | 4 | describe("arrayFrom", () => { 5 | describe("case where target of TypeScript is not es5", () => { 6 | it("should return Array.from", async () => { 7 | expect(arrayFrom).toBe(Array.from); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/extended/cjs/trigger-side-effects.ts: -------------------------------------------------------------------------------- 1 | __TYPESCRIPT_TARGET__ !== "es5" 2 | ? await import("@@/dist/immediate/extended/viewport-extra.cjs") 3 | : await import("@@/dist/immediate/extended/es5/viewport-extra.cjs"); 4 | document 5 | .querySelector("[data-asset-script]") 6 | ?.setAttribute("data-status", "complete"); 7 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/extended/es/trigger-side-effects.ts: -------------------------------------------------------------------------------- 1 | __TYPESCRIPT_TARGET__ !== "es5" 2 | ? await import("@@/dist/immediate/extended/viewport-extra.mjs") 3 | : await import("@@/dist/immediate/extended/es5/viewport-extra.mjs"); 4 | document 5 | .querySelector("[data-asset-script]") 6 | ?.setAttribute("data-status", "complete"); 7 | -------------------------------------------------------------------------------- /examples/next-pages-router/next.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | output: "export", 8 | outputFileTracingRoot: path.dirname(fileURLToPath(import.meta.url)), 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /examples/next-app-router/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import type { FunctionComponent } from "react"; 3 | 4 | const Index: FunctionComponent = () => ( 5 | <> 6 |

Index Page

7 |

-------------- 412px wide text --------------

8 | Go to another page 9 | 10 | ); 11 | 12 | export default Index; 13 | -------------------------------------------------------------------------------- /examples/next-app-router/app/another/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import type { FunctionComponent } from "react"; 3 | 4 | const Another: FunctionComponent = () => ( 5 | <> 6 |

Another Page

7 |

-------------- 412px wide text --------------

8 | Go to index page 9 | 10 | ); 11 | 12 | export default Another; 13 | -------------------------------------------------------------------------------- /src/internal/string.ts: -------------------------------------------------------------------------------- 1 | export const camelizeKebabCaseString = (str: string): string => 2 | str 3 | .replace(/\s+/g, "") 4 | .toLowerCase() 5 | .replace(/-./g, (s) => s[1].toUpperCase()); 6 | 7 | export const kebabizeCamelCaseString = (str: string): string => 8 | str 9 | .replace(/\s+/g, "") 10 | .replace(/[A-Z]+/g, (s) => `-${s[0]}`) 11 | .toLowerCase(); 12 | -------------------------------------------------------------------------------- /examples/next-pages-router/pages/another.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import type { FunctionComponent } from "react"; 3 | 4 | const Another: FunctionComponent = () => ( 5 |
6 |

Another Page

7 |

-------------- 412px wide text --------------

8 | Go to index page 9 |
10 | ); 11 | 12 | export default Another; 13 | -------------------------------------------------------------------------------- /examples/next-pages-router/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import type { FunctionComponent } from "react"; 3 | 4 | const Index: FunctionComponent = () => ( 5 |
6 |

Index Page

7 |

-------------- 412px wide text --------------

8 | Go to another page 9 |
10 | ); 11 | 12 | export default Index; 13 | -------------------------------------------------------------------------------- /src/apis/activateAttributes.node.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { activateAttributes } from "./activateAttributes.js"; 3 | 4 | describe("activateAttributes", () => { 5 | describe("running in environments where no window object exists", () => { 6 | it("does not throw error", () => { 7 | expect(activateAttributes).not.toThrowError(); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/internal/Media.ts: -------------------------------------------------------------------------------- 1 | export type Media = string; 2 | 3 | export const defaultMedia = ""; 4 | 5 | export const createMedia = (optionalMedia: Media | undefined): Media => 6 | optionalMedia ?? defaultMedia; 7 | 8 | export const mergeOptionalMedia = ( 9 | precedingOptionalMedia: Media | undefined, 10 | followingOptionalMedia: Media | undefined, 11 | ): Media | undefined => followingOptionalMedia ?? precedingOptionalMedia; 12 | -------------------------------------------------------------------------------- /examples/nuxt/README.ja.md: -------------------------------------------------------------------------------- 1 | # Nuxt アプリケーションでの使用例 2 | 3 | [English](/examples/nuxt/README.md) | **日本語** 4 | 5 | この例では、[Nuxt](https://nuxt.com/) アプリケーションで Viewport Extra を使用する方法を示します。 6 | 7 | ## 補足 8 | 9 | - デスクトップ向けブラウザの開発者ツールで動作を確認する場合、Viewport Extra を使用するページへ移動するよりも先に、モバイル端末のシミュレーションを有効化し、ビューポートを目的のサイズに設定しておく必要があります。順番が逆である場合、ブラウザが `` 要素の `initial-scale` の設定を無視してしまう状態となります。これは、開発者ツールのシミュレーションに特有の現象であり、実際のモバイル向けブラウザでは発生しません。 10 | -------------------------------------------------------------------------------- /examples/no-framework/README.ja.md: -------------------------------------------------------------------------------- 1 | # フレームワークを使用しないアプリケーションでの使用例 2 | 3 | [English](/examples/no-framework/README.md) | **日本語** 4 | 5 | この例では、Viewport Extra をフレームワークを使用しないアプリケーションで使用する方法を示します。 6 | 7 | ## 補足 8 | 9 | - デスクトップ向けブラウザの開発者ツールで動作を確認する場合、Viewport Extra を使用するページへ移動するよりも先に、モバイル端末のシミュレーションを有効化し、ビューポートを目的のサイズに設定しておく必要があります。順番が逆である場合、ブラウザが `` 要素の `initial-scale` の設定を無視してしまう状態となります。これは、開発者ツールのシミュレーションに特有の現象であり、実際のモバイル向けブラウザでは発生しません。 10 | -------------------------------------------------------------------------------- /src/apis/setParameters.node.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { setParameters } from "./setParameters.js"; 3 | 4 | describe("setParameters", () => { 5 | describe("running in environments where no window object exists", () => { 6 | it("does not throw error", () => { 7 | expect(() => { 8 | setParameters([{ content: { minimumWidth: 412 } }]); 9 | }).not.toThrowError(); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewport-extra-nuxt-example", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "nuxt": "4.2.1", 14 | "viewport-extra": "3.0.0", 15 | "vue": "3.5.18", 16 | "vue-router": "4.5.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/apis/activateMediaSpecificAttributes.node.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { activateMediaSpecificAttributes } from "./activateMediaSpecificAttributes.js"; 3 | 4 | describe("activateMediaSpecificAttributes", () => { 5 | describe("running in environments where no window object exists", () => { 6 | it("does not throw error", () => { 7 | expect(activateMediaSpecificAttributes).not.toThrowError(); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | commit-msg: 2 | jobs: 3 | - run: npm run commitcheck -- {1} 4 | 5 | pre-commit: 6 | piped: true 7 | jobs: 8 | - run: npm run spellcheck -- {staged_files} 9 | - run: npm run stylecheck:code -- --write {staged_files} 10 | stage_fixed: true 11 | - run: npm run stylecheck:docs -- --write {staged_files} 12 | glob: "*.md" 13 | stage_fixed: true 14 | - run: npm run es5check 15 | - run: npm run typecheck 16 | - run: npm run test 17 | -------------------------------------------------------------------------------- /examples/next-app-router/README.ja.md: -------------------------------------------------------------------------------- 1 | # Next.js (App Router) アプリケーションでの使用例 2 | 3 | [English](/examples/next-app-router/README.md) | **日本語** 4 | 5 | この例では、[Next.js (App Router)](https://nextjs.org/docs/app) アプリケーションで Viewport Extra を使用する方法を示します。 6 | 7 | ## 補足 8 | 9 | - デスクトップ向けブラウザの開発者ツールで動作を確認する場合、Viewport Extra を使用するページへ移動するよりも先に、モバイル端末のシミュレーションを有効化し、ビューポートを目的のサイズに設定しておく必要があります。順番が逆である場合、ブラウザが `` 要素の `initial-scale` の設定を無視してしまう状態となります。これは、開発者ツールのシミュレーションに特有の現象であり、実際のモバイル向けブラウザでは発生しません。 10 | -------------------------------------------------------------------------------- /src/internal/ArrayLike.es5.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { arrayFrom } from "./ArrayLike.js"; 3 | 4 | describe("arrayFrom", () => { 5 | describe("case where target of TypeScript is es5", () => { 6 | it("should not return Array.from", async () => { 7 | expect(arrayFrom).not.toBe(Array.from); 8 | }); 9 | 10 | it("should behave same as Array.from", async () => { 11 | expect(arrayFrom("foo")).toStrictEqual(Array.from("foo")); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/next-pages-router/README.ja.md: -------------------------------------------------------------------------------- 1 | # Next.js (Pages Router) アプリケーションでの使用例 2 | 3 | [English](/examples/next-pages-router/README.md) | **日本語** 4 | 5 | この例では、[Next.js (Pages Router)](https://nextjs.org/docs/pages) アプリケーションで Viewport Extra を使用する方法を示します。 6 | 7 | ## 補足 8 | 9 | - デスクトップ向けブラウザの開発者ツールで動作を確認する場合、Viewport Extra を使用するページへ移動するよりも先に、モバイル端末のシミュレーションを有効化し、ビューポートを目的のサイズに設定しておく必要があります。順番が逆である場合、ブラウザが `` 要素の `initial-scale` の設定を無視してしまう状態となります。これは、開発者ツールのシミュレーションに特有の現象であり、実際のモバイル向けブラウザでは発生しません。 10 | -------------------------------------------------------------------------------- /src/apis/activateMediaSpecificAttributes.ts: -------------------------------------------------------------------------------- 1 | import { getMetaElementList } from "../internal/Document.js"; 2 | import { createPartialMediaSpecificParameters } from "../internal/HTMLMetaElement.js"; 3 | import { setMediaSpecificParametersList } from "./setMediaSpecificParametersList.js"; 4 | 5 | export const activateMediaSpecificAttributes = (): void => { 6 | if (typeof window === "undefined") return; 7 | setMediaSpecificParametersList( 8 | getMetaElementList(document).map(createPartialMediaSpecificParameters), 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/apis/setMediaSpecificParametersList.node.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { setMediaSpecificParametersList } from "./setMediaSpecificParametersList.js"; 3 | 4 | describe("setMediaSpecificParametersList", () => { 5 | describe("running in environments where no window object exists", () => { 6 | it("does not throw error", () => { 7 | expect(() => { 8 | setMediaSpecificParametersList([{ content: { minimumWidth: 412 } }]); 9 | }).not.toThrowError(); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/internal/number.ts: -------------------------------------------------------------------------------- 1 | export const mathTrunc: typeof Math.trunc = 2 | __TYPESCRIPT_TARGET__ !== "es5" 3 | ? /* eslint-disable-next-line compat/compat */ 4 | Math.trunc 5 | : (num) => (num < 0 ? Math.ceil : Math.floor)(num); 6 | 7 | export const truncateDecimalNumber = ( 8 | num: number, 9 | decimalPlaces: number, 10 | ): number => 11 | // biome-ignore lint/suspicious/noGlobalIsFinite: isFinite is safe to use here 12 | isFinite(decimalPlaces) 13 | ? mathTrunc(num * 10 ** decimalPlaces) / 10 ** decimalPlaces 14 | : num; 15 | -------------------------------------------------------------------------------- /src/internal/MediaAttribute.ts: -------------------------------------------------------------------------------- 1 | import type { Media } from "./Media.js"; 2 | 3 | export type MediaAttribute = string; 4 | 5 | export const mergeNullableMediaAttribute = ( 6 | precedingNullableMediaAttribute: MediaAttribute | null, 7 | followingNullableMediaAttribute: MediaAttribute | null, 8 | ): MediaAttribute | null => 9 | followingNullableMediaAttribute ?? precedingNullableMediaAttribute; 10 | 11 | export const createOptionalMedia = ( 12 | nullableMediaAttribute: MediaAttribute | null, 13 | ): Media | undefined => nullableMediaAttribute ?? undefined; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "lib": ["esnext", "dom"], 5 | "strict": true, 6 | "moduleResolution": "nodenext", 7 | "resolveJsonModule": true, 8 | "esModuleInterop": true, 9 | "exactOptionalPropertyTypes": true, 10 | "paths": { 11 | "@@/*": ["./*"] 12 | } 13 | }, 14 | "exclude": ["**/node_modules", "examples", "**/dist", ".types"], 15 | "include": [ 16 | "**/*.ts", 17 | "**/*.mts", 18 | "**/*.cts", 19 | "**/.*.ts", 20 | "**/.*.mts", 21 | "**/.*.cts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/internal/DecimalPlaces.ts: -------------------------------------------------------------------------------- 1 | export type DecimalPlaces = number; 2 | 3 | export const defaultDecimalPlaces = Infinity; 4 | 5 | export const createDecimalPlaces = ( 6 | optionalDecimalPlaces: DecimalPlaces | undefined, 7 | ): DecimalPlaces => optionalDecimalPlaces ?? defaultDecimalPlaces; 8 | 9 | export const mergeOptionalDecimalPlaces = ( 10 | precedingOptionalDecimalPlaces: DecimalPlaces | undefined, 11 | followingOptionalDecimalPlaces: DecimalPlaces | undefined, 12 | ): DecimalPlaces | undefined => 13 | followingOptionalDecimalPlaces ?? precedingOptionalDecimalPlaces; 14 | -------------------------------------------------------------------------------- /examples/next-app-router/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/next-pages-router/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/next-app-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewport-extra-next-app-router-example", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "next": "15.5.7", 12 | "react": "18.3.1", 13 | "react-dom": "18.3.1", 14 | "viewport-extra": "3.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "22.4.2", 18 | "@types/react": "18.3.4", 19 | "@types/react-dom": "18.3.0", 20 | "typescript": "5.5.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/next-pages-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewport-extra-next-pages-router-example", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "next": "15.5.7", 12 | "react": "18.3.1", 13 | "react-dom": "18.3.1", 14 | "viewport-extra": "3.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "22.4.2", 18 | "@types/react": "18.3.4", 19 | "@types/react-dom": "18.3.0", 20 | "typescript": "5.5.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/nuxt/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from "nuxt/config"; 2 | 3 | // https://nuxt.com/docs/api/configuration/nuxt-config 4 | export default defineNuxtConfig({ 5 | compatibilityDate: "2025-08-15", 6 | devtools: { enabled: true }, 7 | css: ["@/assets/css/globals.css"], 8 | app: { 9 | head: { 10 | title: "Usage Examples in Nuxt Application", 11 | meta: [ 12 | { 13 | name: "description", 14 | content: 15 | "This example shows how to use Viewport Extra in a Nuxt application.", 16 | }, 17 | ], 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /examples/nuxt/assets/css/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | font-size: inherit; 6 | } 7 | 8 | body { 9 | -webkit-text-size-adjust: 100%; 10 | text-size-adjust: 100%; 11 | font-family: "Open Sans", sans-serif; 12 | letter-spacing: 0; 13 | } 14 | 15 | a { 16 | color: inherit; 17 | } 18 | 19 | .page { 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-between; 23 | align-items: center; 24 | width: 412px; 25 | height: 100vh; 26 | padding: 40px 0; 27 | font-size: 24px; 28 | background-color: steelblue; 29 | color: white; 30 | } 31 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from "@commitlint/types"; 2 | 3 | export default { 4 | extends: ["@commitlint/config-conventional"], 5 | rules: { 6 | // Do not set max length because external systems create long message 7 | // https://github.com/conventional-changelog/commitlint/issues/2930 8 | "body-max-length": [0], 9 | "body-max-line-length": [0], 10 | "footer-max-length": [0], 11 | "footer-max-line-length": [0], 12 | "header-max-length": [0], 13 | "scope-max-length": [0], 14 | "subject-max-length": [0], 15 | "type-max-length": [0], 16 | }, 17 | } satisfies UserConfig; 18 | -------------------------------------------------------------------------------- /examples/next-app-router/app/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | font-size: inherit; 6 | } 7 | 8 | body { 9 | -webkit-text-size-adjust: 100%; 10 | text-size-adjust: 100%; 11 | font-family: "Open Sans", sans-serif; 12 | letter-spacing: 0; 13 | } 14 | 15 | a { 16 | color: inherit; 17 | } 18 | 19 | .page { 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-between; 23 | align-items: center; 24 | width: 412px; 25 | height: 100vh; 26 | padding: 40px 0; 27 | font-size: 24px; 28 | background-color: steelblue; 29 | color: white; 30 | } 31 | -------------------------------------------------------------------------------- /examples/no-framework/styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | font-size: inherit; 6 | } 7 | 8 | body { 9 | -webkit-text-size-adjust: 100%; 10 | text-size-adjust: 100%; 11 | font-family: "Open Sans", sans-serif; 12 | letter-spacing: 0; 13 | } 14 | 15 | a { 16 | color: inherit; 17 | } 18 | 19 | .page { 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-between; 23 | align-items: center; 24 | width: 412px; 25 | height: 100vh; 26 | padding: 40px 0; 27 | font-size: 24px; 28 | background-color: steelblue; 29 | color: white; 30 | } 31 | -------------------------------------------------------------------------------- /examples/next-pages-router/styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | font-size: inherit; 6 | } 7 | 8 | body { 9 | -webkit-text-size-adjust: 100%; 10 | text-size-adjust: 100%; 11 | font-family: "Open Sans", sans-serif; 12 | letter-spacing: 0; 13 | } 14 | 15 | a { 16 | color: inherit; 17 | } 18 | 19 | .page { 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-between; 23 | align-items: center; 24 | width: 412px; 25 | height: 100vh; 26 | padding: 40px 0; 27 | font-size: 24px; 28 | background-color: steelblue; 29 | color: white; 30 | } 31 | -------------------------------------------------------------------------------- /examples/next-pages-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "paths": { 16 | "@/*": ["./*"] 17 | }, 18 | "target": "ES2017" 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # Usage Examples in Nuxt Application 2 | 3 | **English** | [日本語](/examples/nuxt/README.ja.md) 4 | 5 | This example shows how to use Viewport Extra in a [Nuxt](https://nuxt.com/) application. 6 | 7 | ## Notes 8 | 9 | - When testing with developer tools of desktop browsers, mobile device simulation must be enabled and the viewport must be set to the desired size before navigating to a page that uses Viewport Extra. If the order is reversed, the browser may ignore the `initial-scale` setting of the `` element. This behavior is specific to simulation in developer tools and does not occur in actual mobile browsers. 10 | -------------------------------------------------------------------------------- /examples/no-framework/README.md: -------------------------------------------------------------------------------- 1 | # Usage Examples in No-Framework Application 2 | 3 | **English** | [日本語](/examples/no-framework/README.ja.md) 4 | 5 | This example shows how to use Viewport Extra in a no-framework application. 6 | 7 | ## Notes 8 | 9 | - When testing with developer tools of desktop browsers, mobile device simulation must be enabled and the viewport must be set to the desired size before navigating to a page that uses Viewport Extra. If the order is reversed, the browser may ignore the `initial-scale` setting of the `` element. This behavior is specific to simulation in developer tools and does not occur in actual mobile browsers. 10 | -------------------------------------------------------------------------------- /tests/modules/PlaywrightPage.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from "@playwright/test"; 2 | 3 | export const getViewportContentString = async (page: Page) => 4 | await page.evaluate( 5 | ({ document }) => 6 | document.querySelector('meta[name="viewport"]')?.getAttribute("content"), 7 | await page.evaluateHandle("window"), 8 | ); 9 | 10 | export const waitForAssetScriptComplete = async (page: Page) => 11 | await page.waitForFunction( 12 | ({ document }) => 13 | document 14 | .querySelector("[data-asset-script]") 15 | ?.getAttribute("data-status") === "complete", 16 | await page.evaluateHandle("window"), 17 | ); 18 | -------------------------------------------------------------------------------- /examples/nuxt/app.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/cjs/call-apply.ts: -------------------------------------------------------------------------------- 1 | type ApplyParameters = Parameters; 2 | 3 | const { apply } = 4 | __TYPESCRIPT_TARGET__ !== "es5" 5 | ? await import("@@/dist/viewport-extra.cjs") 6 | : await import("@@/dist/es5/viewport-extra.cjs"); 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") 11 | apply(JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0]); 12 | document 13 | .querySelector("[data-asset-script]") 14 | ?.setAttribute("data-status", "complete"); 15 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/es/call-apply.ts: -------------------------------------------------------------------------------- 1 | type ApplyParameters = Parameters; 2 | 3 | const { apply } = 4 | __TYPESCRIPT_TARGET__ !== "es5" 5 | ? await import("@@/dist/viewport-extra.mjs") 6 | : await import("@@/dist/es5/viewport-extra.mjs"); 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") 11 | apply(JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0]); 12 | document 13 | .querySelector("[data-asset-script]") 14 | ?.setAttribute("data-status", "complete"); 15 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/unit/call-setMediaSpecificParametersList.ts: -------------------------------------------------------------------------------- 1 | import { setMediaSpecificParametersList } from "@@/src/apis/setMediaSpecificParametersList.js"; 2 | 3 | type SetMediaSpecificParametersListParameters = Parameters< 4 | typeof setMediaSpecificParametersList 5 | >; 6 | 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") 11 | setMediaSpecificParametersList( 12 | JSON.parse( 13 | mediaSpecificParametersListAttribute, 14 | ) as SetMediaSpecificParametersListParameters[0], 15 | ); 16 | -------------------------------------------------------------------------------- /examples/next-app-router/README.md: -------------------------------------------------------------------------------- 1 | # Usage Examples in Next.js (App Router) Application 2 | 3 | **English** | [日本語](/examples/next-app-router/README.ja.md) 4 | 5 | This example shows how to use Viewport Extra in a [Next.js (App Router)](https://nextjs.org/docs/app) application. 6 | 7 | ## Notes 8 | 9 | - When testing with developer tools of desktop browsers, mobile device simulation must be enabled and the viewport must be set to the desired size before navigating to a page that uses Viewport Extra. If the order is reversed, the browser may ignore the `initial-scale` setting of the `` element. This behavior is specific to simulation in developer tools and does not occur in actual mobile browsers. 10 | -------------------------------------------------------------------------------- /examples/next-pages-router/README.md: -------------------------------------------------------------------------------- 1 | # Usage Examples in Next.js (Pages Router) Application 2 | 3 | **English** | [日本語](/examples/next-pages-router/README.ja.md) 4 | 5 | This example shows how to use Viewport Extra in a [Next.js (Pages Router)](https://nextjs.org/docs/pages) application. 6 | 7 | ## Notes 8 | 9 | - When testing with developer tools of desktop browsers, mobile device simulation must be enabled and the viewport must be set to the desired size before navigating to a page that uses Viewport Extra. If the order is reversed, the browser may ignore the `initial-scale` setting of the `` element. This behavior is specific to simulation in developer tools and does not occur in actual mobile browsers. 10 | -------------------------------------------------------------------------------- /examples/next-app-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/cjs/call-apply.ts: -------------------------------------------------------------------------------- 1 | type ApplyParameters = Parameters; 2 | 3 | const { apply } = 4 | __TYPESCRIPT_TARGET__ !== "es5" 5 | ? await import("@@/dist/immediate/viewport-extra.cjs") 6 | : await import("@@/dist/immediate/es5/viewport-extra.cjs"); 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") 11 | apply(JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0]); 12 | document 13 | .querySelector("[data-asset-script]") 14 | ?.setAttribute("data-status", "complete"); 15 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/es/call-apply.ts: -------------------------------------------------------------------------------- 1 | type ApplyParameters = Parameters; 2 | 3 | const { apply } = 4 | __TYPESCRIPT_TARGET__ !== "es5" 5 | ? await import("@@/dist/immediate/viewport-extra.mjs") 6 | : await import("@@/dist/immediate/es5/viewport-extra.mjs"); 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") 11 | apply(JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0]); 12 | document 13 | .querySelector("[data-asset-script]") 14 | ?.setAttribute("data-status", "complete"); 15 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "language": "en", 5 | "useGitignore": true, 6 | "enableGlobDot": true, 7 | "words": [ 8 | "nuxt", 9 | "dsktschy", 10 | "autorelease", 11 | "kebabize", 12 | "stabilizable", 13 | "pids", 14 | "jscoverage", 15 | "wscript", 16 | "jspm", 17 | "eslintcache", 18 | "stylelintcache", 19 | "Microbundle", 20 | "dotenv", 21 | "vuepress", 22 | "commitlint", 23 | "stylecheck", 24 | "npmjs", 25 | "vercel", 26 | "lockfiles", 27 | "unobserve", 28 | "lefthook", 29 | "srcset", 30 | "commitcheck", 31 | "nodenext" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/apis/activateAttributes.ts: -------------------------------------------------------------------------------- 1 | import { getMetaElementList } from "../internal/Document.js"; 2 | import { mergePartialGlobalParameters } from "../internal/GlobalParameters.js"; 3 | import { 4 | createPartialGlobalParameters, 5 | createPartialMediaSpecificParameters, 6 | } from "../internal/HTMLMetaElement.js"; 7 | import { setParameters } from "./setParameters.js"; 8 | 9 | export const activateAttributes = (): void => { 10 | if (typeof window === "undefined") return; 11 | const metaElementList = getMetaElementList(document); 12 | setParameters( 13 | metaElementList.map(createPartialMediaSpecificParameters), 14 | metaElementList 15 | .map(createPartialGlobalParameters) 16 | .reduce(mergePartialGlobalParameters), 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /examples/next-pages-router/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import Head from "next/head"; 4 | import type { FunctionComponent } from "react"; 5 | import ViewportExtra from "../components/ViewportExtra"; 6 | 7 | const App: FunctionComponent = ({ Component, pageProps }) => ( 8 | <> 9 | 10 | Usage Examples in Next.js (Pages Router) Application 11 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/internal/DecimalPlacesAttribute.ts: -------------------------------------------------------------------------------- 1 | import type { DecimalPlaces } from "./DecimalPlaces.js"; 2 | 3 | export type DecimalPlacesAttribute = string; 4 | 5 | export const mergeNullableDecimalPlacesAttribute = ( 6 | precedingNullableDecimalPlacesAttribute: DecimalPlacesAttribute | null, 7 | followingNullableDecimalPlacesAttribute: DecimalPlacesAttribute | null, 8 | ): DecimalPlacesAttribute | null => 9 | followingNullableDecimalPlacesAttribute ?? 10 | precedingNullableDecimalPlacesAttribute; 11 | 12 | export const createOptionalDecimalPlaces = ( 13 | nullableDecimalPlacesAttribute: DecimalPlacesAttribute | null, 14 | ): DecimalPlaces | undefined => 15 | nullableDecimalPlacesAttribute !== null 16 | ? +nullableDecimalPlacesAttribute 17 | : undefined; 18 | -------------------------------------------------------------------------------- /examples/next-pages-router/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from "next/document"; 2 | import type { FunctionComponent } from "react"; 3 | 4 | const Document: FunctionComponent = () => ( 5 | 6 | 7 | {/* Google Fonts */} 8 | 9 | 14 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | ); 25 | 26 | export default Document; 27 | -------------------------------------------------------------------------------- /src/internal/string.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { camelizeKebabCaseString, kebabizeCamelCaseString } from "./string.js"; 3 | 4 | describe("camelizeKebabCaseString", () => { 5 | it("should camelize kebab case", () => { 6 | expect(camelizeKebabCaseString("foo-bar-foo")).toBe("fooBarFoo"); 7 | }); 8 | 9 | it("should ignore whitespaces", () => { 10 | expect(camelizeKebabCaseString("foo bar - foo")).toBe("foobarFoo"); 11 | }); 12 | }); 13 | 14 | describe("kebabizeCamelCaseString", () => { 15 | it("should kebabize camel case", () => { 16 | expect(kebabizeCamelCaseString("fooBarFoo")).toBe("foo-bar-foo"); 17 | }); 18 | 19 | it("should ignore whitespaces", () => { 20 | expect(kebabizeCamelCaseString("foo bar Foo")).toBe("foobar-foo"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/internal/Document.ts: -------------------------------------------------------------------------------- 1 | import { arrayFrom } from "./ArrayLike.js"; 2 | 3 | export const ensureViewportMetaElement = (doc: Document): HTMLMetaElement => { 4 | const viewportMetaElement = doc.querySelector( 5 | 'meta[name="viewport"]', 6 | ); 7 | if (viewportMetaElement) return viewportMetaElement; 8 | const htmlMetaElement = doc.createElement("meta"); 9 | htmlMetaElement.setAttribute("name", "viewport"); 10 | doc.head.appendChild(htmlMetaElement); 11 | return htmlMetaElement; 12 | }; 13 | 14 | export const getMetaElementList = (doc: Document): HTMLMetaElement[] => 15 | arrayFrom( 16 | doc.querySelectorAll( 17 | 'meta[name="viewport"],meta[name="viewport-extra"]', 18 | ), 19 | ); 20 | 21 | export const getDocumentClientWidth = (doc: Document): number => 22 | doc.documentElement.clientWidth; 23 | -------------------------------------------------------------------------------- /src/internal/number.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { mathTrunc, truncateDecimalNumber } from "./number.js"; 3 | 4 | describe("mathTrunc", () => { 5 | describe("case where target of TypeScript is not es5", () => { 6 | it("should return Math.trunc", async () => { 7 | expect(mathTrunc).toBe(Math.trunc); 8 | }); 9 | }); 10 | }); 11 | 12 | describe("truncateDecimalNumber", () => { 13 | describe("case where second argument is finite", () => { 14 | it("should return truncated number", () => { 15 | expect(truncateDecimalNumber(0.123456789, 6)).toBe(0.123456); 16 | }); 17 | }); 18 | 19 | describe("case where second argument is infinite", () => { 20 | it("should return second argument", () => { 21 | expect(truncateDecimalNumber(0.123456789, Infinity)).toBe(0.123456789); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/iife/call-apply.ts: -------------------------------------------------------------------------------- 1 | import type * as TViewportExtra from "@@/dist/viewport-extra.d.mts"; 2 | 3 | type CustomWindow = Window & { 4 | ViewportExtra?: typeof TViewportExtra; 5 | }; 6 | type ApplyParameters = Parameters; 7 | 8 | const ViewportExtra = (window as CustomWindow).ViewportExtra; 9 | if (ViewportExtra) { 10 | const mediaSpecificParametersListAttribute = document 11 | .querySelector("[data-media-specific-parameters-list]") 12 | ?.getAttribute("data-media-specific-parameters-list"); 13 | if (typeof mediaSpecificParametersListAttribute === "string") 14 | ViewportExtra.apply( 15 | JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0], 16 | ); 17 | } 18 | document 19 | .querySelector("[data-asset-script]") 20 | ?.setAttribute("data-status", "complete"); 21 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/iife/call-apply.ts: -------------------------------------------------------------------------------- 1 | import type * as TViewportExtra from "@@/dist/immediate/viewport-extra.d.mts"; 2 | 3 | type CustomWindow = Window & { 4 | ViewportExtra?: typeof TViewportExtra; 5 | }; 6 | type ApplyParameters = Parameters; 7 | 8 | const ViewportExtra = (window as CustomWindow).ViewportExtra; 9 | if (ViewportExtra) { 10 | const mediaSpecificParametersListAttribute = document 11 | .querySelector("[data-media-specific-parameters-list]") 12 | ?.getAttribute("data-media-specific-parameters-list"); 13 | if (typeof mediaSpecificParametersListAttribute === "string") 14 | ViewportExtra.apply( 15 | JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0], 16 | ); 17 | } 18 | document 19 | .querySelector("[data-asset-script]") 20 | ?.setAttribute("data-status", "complete"); 21 | -------------------------------------------------------------------------------- /examples/next-app-router/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Open_Sans } from "next/font/google"; 3 | import type { FunctionComponent, ReactNode } from "react"; 4 | import ViewportExtra from "../components/ViewportExtra"; 5 | import "./globals.css"; 6 | 7 | const openSans = Open_Sans({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Usage Examples in Next.js (App Router) Application", 11 | description: 12 | "This example shows how to use Viewport Extra in a Next.js (App Router) application.", 13 | }; 14 | 15 | const RootLayout: FunctionComponent<{ children: ReactNode }> = ({ 16 | children, 17 | }) => ( 18 | 19 | 20 |
{children}
21 | 22 | 23 | 24 | ); 25 | 26 | export default RootLayout; 27 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/unit/call-setParameters.ts: -------------------------------------------------------------------------------- 1 | import { setParameters } from "@@/src/apis/setParameters.js"; 2 | 3 | type SetParametersParameters = Parameters; 4 | 5 | const mediaSpecificParametersListAttribute = document 6 | .querySelector("[data-media-specific-parameters-list]") 7 | ?.getAttribute("data-media-specific-parameters-list"); 8 | if (typeof mediaSpecificParametersListAttribute === "string") { 9 | const argumentList: SetParametersParameters = [ 10 | JSON.parse( 11 | mediaSpecificParametersListAttribute, 12 | ) as SetParametersParameters[0], 13 | ]; 14 | const globalParametersAttribute = document 15 | .querySelector("[data-global-parameters]") 16 | ?.getAttribute("data-global-parameters"); 17 | if (typeof globalParametersAttribute === "string") 18 | argumentList.push( 19 | JSON.parse(globalParametersAttribute) as SetParametersParameters[1], 20 | ); 21 | setParameters(...argumentList); 22 | } 23 | -------------------------------------------------------------------------------- /eslint.config.es5.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { includeIgnoreFile } from "@eslint/compat"; 3 | import typescriptEslintParser from "@typescript-eslint/parser"; 4 | import { defineConfig } from "eslint/config"; 5 | import eslintPluginCompat from "eslint-plugin-compat"; 6 | import tsconfigJson from "./tsconfig.json" with { type: "json" }; 7 | 8 | const { languageOptions, plugins, rules } = 9 | eslintPluginCompat.configs["flat/recommended"]; 10 | 11 | export default defineConfig([ 12 | { 13 | files: tsconfigJson.include, 14 | // Apply only root .gitignore as minimum necessary for now to improve performance 15 | ignores: includeIgnoreFile( 16 | fileURLToPath(new URL(".gitignore", import.meta.url)), 17 | ).ignores, 18 | languageOptions: { 19 | ...languageOptions, 20 | parser: typescriptEslintParser, 21 | }, 22 | plugins, 23 | rules, 24 | settings: { 25 | targets: ["fully supports es5", "not fully supports es6"], 26 | lintAllEsApis: true, 27 | }, 28 | }, 29 | ]); 30 | -------------------------------------------------------------------------------- /examples/next-pages-router/components/ViewportExtra.tsx: -------------------------------------------------------------------------------- 1 | import { usePathname } from "next/navigation"; 2 | import { Fragment, type FunctionComponent, useEffect, useRef } from "react"; 3 | 4 | const ViewportExtra: FunctionComponent<{ 5 | minimumWidth?: number; 6 | maximumWidth?: number; 7 | }> = ({ minimumWidth, maximumWidth }) => { 8 | const pathname = usePathname(); 9 | 10 | const previousPathname = useRef(""); 11 | 12 | useEffect(() => { 13 | if (pathname !== previousPathname.current) { 14 | previousPathname.current = pathname; 15 | import("viewport-extra").then(({ apply }) => { 16 | const content: Parameters[0][number]["content"] = {}; 17 | if (typeof minimumWidth === "number") 18 | content.minimumWidth = minimumWidth; 19 | if (typeof maximumWidth === "number") 20 | content.maximumWidth = maximumWidth; 21 | apply([{ content }]); 22 | }); 23 | } 24 | }, [pathname, minimumWidth, maximumWidth]); 25 | 26 | return ; 27 | }; 28 | 29 | export default ViewportExtra; 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018 dsktschy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /examples/next-app-router/components/ViewportExtra.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname } from "next/navigation"; 4 | import { Fragment, type FunctionComponent, useEffect, useRef } from "react"; 5 | 6 | const ViewportExtra: FunctionComponent<{ 7 | minimumWidth?: number; 8 | maximumWidth?: number; 9 | }> = ({ minimumWidth, maximumWidth }) => { 10 | const pathname = usePathname(); 11 | 12 | const previousPathname = useRef(""); 13 | 14 | useEffect(() => { 15 | if (pathname !== previousPathname.current) { 16 | previousPathname.current = pathname; 17 | import("viewport-extra").then(({ apply }) => { 18 | const content: Parameters[0][number]["content"] = {}; 19 | if (typeof minimumWidth === "number") 20 | content.minimumWidth = minimumWidth; 21 | if (typeof maximumWidth === "number") 22 | content.maximumWidth = maximumWidth; 23 | apply([{ content }]); 24 | }); 25 | } 26 | }, [pathname, minimumWidth, maximumWidth]); 27 | 28 | return ; 29 | }; 30 | 31 | export default ViewportExtra; 32 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | workspace: [ 6 | { 7 | test: { 8 | name: "node", 9 | environment: "node", 10 | include: ["src/**/*.node.{test,spec}.?(c|m)[jt]s"], 11 | }, 12 | define: { __TYPESCRIPT_TARGET__: '"es2021"' }, 13 | }, 14 | { 15 | test: { 16 | name: "jsdom.es5", 17 | environment: "jsdom", 18 | include: ["src/**/*.es5.{test,spec}.?(c|m)[jt]s"], 19 | }, 20 | define: { __TYPESCRIPT_TARGET__: '"es5"' }, 21 | }, 22 | { 23 | test: { 24 | name: "jsdom", 25 | environment: "jsdom", 26 | include: ["src/**/*.{test,spec}.?(c|m)[jt]s"], 27 | exclude: [ 28 | "src/**/*.node.{test,spec}.?(c|m)[jt]s", 29 | "src/**/*.es5.{test,spec}.?(c|m)[jt]s", 30 | ], 31 | }, 32 | define: { __TYPESCRIPT_TARGET__: '"es2021"' }, 33 | }, 34 | ], 35 | passWithNoTests: true, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /src/internal/number.es5.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { mathTrunc, truncateDecimalNumber } from "./number.js"; 3 | 4 | describe("mathTrunc", () => { 5 | describe("case where target of TypeScript is es5", () => { 6 | it("should not return Math.trunc", async () => { 7 | expect(mathTrunc).not.toBe(Math.trunc); 8 | }); 9 | 10 | it("should behave same as Math.trunc", async () => { 11 | expect(mathTrunc(0.123456789)).toBe(Math.trunc(0.123456789)); 12 | expect(mathTrunc(-0.123456789)).toBe(Math.trunc(-0.123456789)); 13 | }); 14 | }); 15 | }); 16 | 17 | describe("truncateDecimalNumber", () => { 18 | describe("case where second argument is finite", () => { 19 | it("should return truncated number", () => { 20 | expect(truncateDecimalNumber(0.123456789, 6)).toBe(0.123456); 21 | }); 22 | }); 23 | 24 | describe("case where second argument is infinite", () => { 25 | it("should return second argument", () => { 26 | expect(truncateDecimalNumber(0.123456789, Infinity)).toBe(0.123456789); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: "30 1 * * *" 11 | 12 | jobs: 13 | stale: 14 | timeout-minutes: 15 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@f7176fd3007623b69d27091f9b9d4ab7995f0a06 # v5.2.1 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity." 25 | stale-pr-message: "This pull request has been automatically marked as stale because it has not had recent activity." 26 | stale-issue-label: "no-issue-activity" 27 | stale-pr-label: "no-pr-activity" 28 | days-before-stale: 60 29 | days-before-close: 7 30 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/extended/cjs/call-apply.ts: -------------------------------------------------------------------------------- 1 | type ApplyParameters = Parameters; 2 | 3 | const { apply } = 4 | __TYPESCRIPT_TARGET__ !== "es5" 5 | ? await import("@@/dist/extended/viewport-extra.cjs") 6 | : await import("@@/dist/extended/es5/viewport-extra.cjs"); 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") { 11 | const argumentList: ApplyParameters = [ 12 | JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0], 13 | ]; 14 | const globalParametersAttribute = document 15 | .querySelector("[data-global-parameters]") 16 | ?.getAttribute("data-global-parameters"); 17 | if (typeof globalParametersAttribute === "string") 18 | argumentList.push( 19 | JSON.parse(globalParametersAttribute) as ApplyParameters[1], 20 | ); 21 | apply(...argumentList); 22 | } 23 | document 24 | .querySelector("[data-asset-script]") 25 | ?.setAttribute("data-status", "complete"); 26 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/extended/es/call-apply.ts: -------------------------------------------------------------------------------- 1 | type ApplyParameters = Parameters; 2 | 3 | const { apply } = 4 | __TYPESCRIPT_TARGET__ !== "es5" 5 | ? await import("@@/dist/extended/viewport-extra.mjs") 6 | : await import("@@/dist/extended/es5/viewport-extra.mjs"); 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") { 11 | const argumentList: ApplyParameters = [ 12 | JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0], 13 | ]; 14 | const globalParametersAttribute = document 15 | .querySelector("[data-global-parameters]") 16 | ?.getAttribute("data-global-parameters"); 17 | if (typeof globalParametersAttribute === "string") 18 | argumentList.push( 19 | JSON.parse(globalParametersAttribute) as ApplyParameters[1], 20 | ); 21 | apply(...argumentList); 22 | } 23 | document 24 | .querySelector("[data-asset-script]") 25 | ?.setAttribute("data-status", "complete"); 26 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/extended/cjs/call-apply.ts: -------------------------------------------------------------------------------- 1 | type ApplyParameters = Parameters; 2 | 3 | const { apply } = 4 | __TYPESCRIPT_TARGET__ !== "es5" 5 | ? await import("@@/dist/immediate/extended/viewport-extra.cjs") 6 | : await import("@@/dist/immediate/extended/es5/viewport-extra.cjs"); 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") { 11 | const argumentList: ApplyParameters = [ 12 | JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0], 13 | ]; 14 | const globalParametersAttribute = document 15 | .querySelector("[data-global-parameters]") 16 | ?.getAttribute("data-global-parameters"); 17 | if (typeof globalParametersAttribute === "string") 18 | argumentList.push( 19 | JSON.parse(globalParametersAttribute) as ApplyParameters[1], 20 | ); 21 | apply(...argumentList); 22 | } 23 | document 24 | .querySelector("[data-asset-script]") 25 | ?.setAttribute("data-status", "complete"); 26 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/extended/es/call-apply.ts: -------------------------------------------------------------------------------- 1 | type ApplyParameters = Parameters; 2 | 3 | const { apply } = 4 | __TYPESCRIPT_TARGET__ !== "es5" 5 | ? await import("@@/dist/immediate/extended/viewport-extra.mjs") 6 | : await import("@@/dist/immediate/extended/es5/viewport-extra.mjs"); 7 | const mediaSpecificParametersListAttribute = document 8 | .querySelector("[data-media-specific-parameters-list]") 9 | ?.getAttribute("data-media-specific-parameters-list"); 10 | if (typeof mediaSpecificParametersListAttribute === "string") { 11 | const argumentList: ApplyParameters = [ 12 | JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0], 13 | ]; 14 | const globalParametersAttribute = document 15 | .querySelector("[data-global-parameters]") 16 | ?.getAttribute("data-global-parameters"); 17 | if (typeof globalParametersAttribute === "string") 18 | argumentList.push( 19 | JSON.parse(globalParametersAttribute) as ApplyParameters[1], 20 | ); 21 | apply(...argumentList); 22 | } 23 | document 24 | .querySelector("[data-asset-script]") 25 | ?.setAttribute("data-status", "complete"); 26 | -------------------------------------------------------------------------------- /scripts/create-declaration-with-dts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Executes tsc --declaration with JavaScript API 3 | * because type checking during execution with CLI 4 | * ignores global declarations that are not imported 5 | */ 6 | import fsPromises from "node:fs/promises"; 7 | import { parseArgs } from "node:util"; 8 | import typescript from "typescript"; 9 | 10 | const { 11 | values: { "output-directory": outputDirectory }, 12 | positionals: targetPatternList, 13 | } = parseArgs({ 14 | options: { 15 | "output-directory": { type: "string", default: "" }, 16 | }, 17 | allowPositionals: true, 18 | }); 19 | if (!outputDirectory) 20 | throw new Error("Missing required option: --output-directory"); 21 | const targetPathSet = new Set(); 22 | for (const targetPattern of targetPatternList) { 23 | for await (const targetPath of fsPromises.glob(targetPattern)) { 24 | targetPathSet.add(targetPath); 25 | } 26 | } 27 | const emitResult = typescript 28 | .createProgram([...targetPathSet], { 29 | declaration: true, 30 | emitDeclarationOnly: true, 31 | outDir: outputDirectory, 32 | resolveJsonModule: true, 33 | }) 34 | .emit(); 35 | if (emitResult.diagnostics.length) 36 | throw new Error("typescript.createProgram failed"); 37 | -------------------------------------------------------------------------------- /release-please-config.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip-changelog": true, 3 | "include-component-in-tag": false, 4 | "pull-request-title-pattern": "chore: release${component} ${version}", 5 | "label": "autorelease: pending,versioning_strategy: default", 6 | "packages": { 7 | ".": { 8 | "release-type": "node", 9 | "extra-files": [ 10 | "README.ja.md", 11 | "README.md", 12 | "docs/en/migration-from-v1.md", 13 | "docs/en/migration-from-v2.md", 14 | "docs/ja/migration-from-v1.md", 15 | "docs/ja/migration-from-v2.md", 16 | "examples/no-framework/another.html", 17 | "examples/no-framework/index.html", 18 | { 19 | "type": "json", 20 | "path": "examples/next-app-router/package.json", 21 | "jsonpath": "$.dependencies['viewport-extra']" 22 | }, 23 | { 24 | "type": "json", 25 | "path": "examples/next-pages-router/package.json", 26 | "jsonpath": "$.dependencies['viewport-extra']" 27 | }, 28 | { 29 | "type": "json", 30 | "path": "examples/nuxt/package.json", 31 | "jsonpath": "$.dependencies['viewport-extra']" 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/extended/iife/call-apply.ts: -------------------------------------------------------------------------------- 1 | import type * as TViewportExtra from "@@/dist/extended/viewport-extra.d.mts"; 2 | 3 | type CustomWindow = Window & { 4 | ViewportExtra?: typeof TViewportExtra; 5 | }; 6 | type ApplyParameters = Parameters; 7 | 8 | const ViewportExtra = (window as CustomWindow).ViewportExtra; 9 | if (ViewportExtra) { 10 | const mediaSpecificParametersListAttribute = document 11 | .querySelector("[data-media-specific-parameters-list]") 12 | ?.getAttribute("data-media-specific-parameters-list"); 13 | if (typeof mediaSpecificParametersListAttribute === "string") { 14 | const argumentList: ApplyParameters = [ 15 | JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0], 16 | ]; 17 | const globalParametersAttribute = document 18 | .querySelector("[data-global-parameters]") 19 | ?.getAttribute("data-global-parameters"); 20 | if (typeof globalParametersAttribute === "string") 21 | argumentList.push( 22 | JSON.parse(globalParametersAttribute) as ApplyParameters[1], 23 | ); 24 | ViewportExtra.apply(...argumentList); 25 | } 26 | } 27 | document 28 | .querySelector("[data-asset-script]") 29 | ?.setAttribute("data-status", "complete"); 30 | -------------------------------------------------------------------------------- /src/apis/setMediaSpecificParametersList.ts: -------------------------------------------------------------------------------- 1 | import type { DeepPartial } from "../internal/DeepPartial.js"; 2 | import { 3 | ensureViewportMetaElement, 4 | getDocumentClientWidth, 5 | } from "../internal/Document.js"; 6 | import { applyMediaSpecificParameters } from "../internal/HTMLMetaElement.js"; 7 | import { createMatchMediaPredicate } from "../internal/MatchMedia.js"; 8 | import { 9 | createMediaSpecificParameters, 10 | createPartialMediaSpecificParametersMerger, 11 | type MediaSpecificParameters, 12 | } from "../internal/MediaSpecificParameters.js"; 13 | 14 | export const setMediaSpecificParametersList = ( 15 | partialMediaSpecificParametersList: DeepPartial[], 16 | ): void => { 17 | if (typeof window === "undefined") return; 18 | applyMediaSpecificParameters( 19 | ensureViewportMetaElement(document), 20 | () => getDocumentClientWidth(document), 21 | () => 22 | createMediaSpecificParameters( 23 | partialMediaSpecificParametersList.reduce( 24 | createPartialMediaSpecificParametersMerger( 25 | createMatchMediaPredicate(matchMedia), 26 | ), 27 | // Value that does not need to check matching current viewport 28 | createMediaSpecificParameters(), 29 | ), 30 | ), 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /tests/__fixtures__/src/assets/scripts/e2e/immediate/extended/iife/call-apply.ts: -------------------------------------------------------------------------------- 1 | import type * as TViewportExtra from "@@/dist/immediate/extended/viewport-extra.d.mts"; 2 | 3 | type CustomWindow = Window & { 4 | ViewportExtra?: typeof TViewportExtra; 5 | }; 6 | type ApplyParameters = Parameters; 7 | 8 | const ViewportExtra = (window as CustomWindow).ViewportExtra; 9 | if (ViewportExtra) { 10 | const mediaSpecificParametersListAttribute = document 11 | .querySelector("[data-media-specific-parameters-list]") 12 | ?.getAttribute("data-media-specific-parameters-list"); 13 | if (typeof mediaSpecificParametersListAttribute === "string") { 14 | const argumentList: ApplyParameters = [ 15 | JSON.parse(mediaSpecificParametersListAttribute) as ApplyParameters[0], 16 | ]; 17 | const globalParametersAttribute = document 18 | .querySelector("[data-global-parameters]") 19 | ?.getAttribute("data-global-parameters"); 20 | if (typeof globalParametersAttribute === "string") 21 | argumentList.push( 22 | JSON.parse(globalParametersAttribute) as ApplyParameters[1], 23 | ); 24 | ViewportExtra.apply(...argumentList); 25 | } 26 | } 27 | document 28 | .querySelector("[data-asset-script]") 29 | ?.setAttribute("data-status", "complete"); 30 | -------------------------------------------------------------------------------- /release-please-config.prerelease.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip-changelog": true, 3 | "include-component-in-tag": false, 4 | "pull-request-title-pattern": "chore: release${component} ${version}", 5 | "label": "autorelease: pending,versioning_strategy: prerelease", 6 | "versioning": "prerelease", 7 | "prerelease": true, 8 | "prerelease-type": "rc.0", 9 | "packages": { 10 | ".": { 11 | "release-type": "node", 12 | "extra-files": [ 13 | "README.ja.md", 14 | "README.md", 15 | "docs/en/migration-from-v1.md", 16 | "docs/en/migration-from-v2.md", 17 | "docs/ja/migration-from-v1.md", 18 | "docs/ja/migration-from-v2.md", 19 | "examples/no-framework/another.html", 20 | "examples/no-framework/index.html", 21 | { 22 | "type": "json", 23 | "path": "examples/next-app-router/package.json", 24 | "jsonpath": "$.dependencies['viewport-extra']" 25 | }, 26 | { 27 | "type": "json", 28 | "path": "examples/next-pages-router/package.json", 29 | "jsonpath": "$.dependencies['viewport-extra']" 30 | }, 31 | { 32 | "type": "json", 33 | "path": "examples/nuxt/package.json", 34 | "jsonpath": "$.dependencies['viewport-extra']" 35 | } 36 | ] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/no-framework/another.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 19 | 23 | 24 | 25 | Usage Examples in No-Framework Application 26 | 30 | 31 | 32 |
33 |

Another Page

34 |

-------------- 412px wide text --------------

35 | Go to index page 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/no-framework/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 19 | 23 | 24 | 25 | Usage Examples in No-Framework Application 26 | 30 | 31 | 32 |
33 |

Index Page

34 |

-------------- 412px wide text --------------

35 | Go to another page 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /src/internal/MatchMedia.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { createMatchMediaPredicate } from "./MatchMedia.js"; 3 | 4 | describe("createMatchMediaPredicate", () => { 5 | describe("case where argument of created function is media query that matches current viewport", () => { 6 | test("created function should return true", () => { 7 | expect( 8 | createMatchMediaPredicate( 9 | () => 10 | ({ 11 | matches: true, 12 | }) as MediaQueryList, 13 | )("(min-width: 768px)"), 14 | ).toBe(true); 15 | }); 16 | }); 17 | 18 | describe("case where argument of created function is media query that does not match current viewport", () => { 19 | test("created function should return false", () => { 20 | expect( 21 | createMatchMediaPredicate( 22 | () => 23 | ({ 24 | matches: false, 25 | }) as MediaQueryList, 26 | )("(min-width: 768px)"), 27 | ).toBe(false); 28 | }); 29 | }); 30 | 31 | describe("case where argument of created function is undefined", () => { 32 | test("created function should return true", () => { 33 | expect( 34 | createMatchMediaPredicate( 35 | () => 36 | ({ 37 | matches: false, 38 | }) as MediaQueryList, 39 | )(), 40 | ).toBe(true); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/apis/setParameters.ts: -------------------------------------------------------------------------------- 1 | import type { DeepPartial } from "../internal/DeepPartial.js"; 2 | import { 3 | ensureViewportMetaElement, 4 | getDocumentClientWidth, 5 | } from "../internal/Document.js"; 6 | import { 7 | createGlobalParameters, 8 | type GlobalParameters, 9 | } from "../internal/GlobalParameters.js"; 10 | import { applyMediaSpecificParametersTruncated } from "../internal/HTMLMetaElement.js"; 11 | import { createMatchMediaPredicate } from "../internal/MatchMedia.js"; 12 | import { 13 | createMediaSpecificParameters, 14 | createPartialMediaSpecificParametersMerger, 15 | type MediaSpecificParameters, 16 | } from "../internal/MediaSpecificParameters.js"; 17 | 18 | export const setParameters = ( 19 | partialMediaSpecificParametersList: DeepPartial[], 20 | partialGlobalParameters: Partial = {}, 21 | ): void => { 22 | if (typeof window === "undefined") return; 23 | applyMediaSpecificParametersTruncated( 24 | ensureViewportMetaElement(document), 25 | () => getDocumentClientWidth(document), 26 | () => 27 | createMediaSpecificParameters( 28 | partialMediaSpecificParametersList.reduce( 29 | createPartialMediaSpecificParametersMerger( 30 | createMatchMediaPredicate(matchMedia), 31 | ), 32 | // Value that does not need to check matching current viewport 33 | createMediaSpecificParameters(), 34 | ), 35 | ), 36 | createGlobalParameters(partialGlobalParameters), 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/internal/ContentAttribute.ts: -------------------------------------------------------------------------------- 1 | import type { Content } from "./Content.js"; 2 | import { camelizeKebabCaseString } from "./string.js"; 3 | 4 | export type ContentAttribute = string; 5 | 6 | export const mergeNullableContentAttributes = ( 7 | precedingNullableContentAttribute: ContentAttribute | null, 8 | followingNullableContentAttribute: ContentAttribute | null, 9 | ): ContentAttribute | null => 10 | precedingNullableContentAttribute 11 | ? followingNullableContentAttribute 12 | ? [ 13 | precedingNullableContentAttribute, 14 | followingNullableContentAttribute, 15 | ].join(",") 16 | : precedingNullableContentAttribute 17 | : followingNullableContentAttribute; 18 | 19 | export const createOptionalPartialContent = ( 20 | nullableContentAttribute: ContentAttribute | null, 21 | ): Partial | undefined => 22 | nullableContentAttribute 23 | ? nullableContentAttribute 24 | .split(",") 25 | .reduce>((partialContent, equalSeparatedContent) => { 26 | const [key, value] = equalSeparatedContent 27 | .split("=") 28 | .map((keyOrValue) => keyOrValue.trim()); 29 | if (key && value) { 30 | const numberValue = +value; 31 | // biome-ignore lint/suspicious/noGlobalIsNan: isNaN is safe to use here 32 | partialContent[camelizeKebabCaseString(key)] = isNaN(numberValue) 33 | ? value 34 | : numberValue; 35 | } 36 | return partialContent; 37 | }, {}) 38 | : undefined; 39 | -------------------------------------------------------------------------------- /src/internal/DecimalPlaces.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | createDecimalPlaces, 4 | mergeOptionalDecimalPlaces, 5 | } from "./DecimalPlaces.js"; 6 | 7 | describe("createDecimalPlaces", () => { 8 | describe("case where argument is not undefined", () => { 9 | it("should return argument", () => { 10 | expect(createDecimalPlaces(6)).toBe(6); 11 | }); 12 | }); 13 | 14 | describe("case where argument is undefined", () => { 15 | it("should return default value", () => { 16 | expect(createDecimalPlaces(undefined)).toBe(Infinity); 17 | }); 18 | }); 19 | }); 20 | 21 | describe("mergeOptionalDecimalPlaces", () => { 22 | describe("case where only first argument is not undefined", () => { 23 | it("should return first argument", () => { 24 | expect(mergeOptionalDecimalPlaces(6, undefined)).toBe(6); 25 | }); 26 | }); 27 | 28 | describe("case where only second argument is not undefined", () => { 29 | it("should return second argument", () => { 30 | expect(mergeOptionalDecimalPlaces(undefined, 6)).toBe(6); 31 | }); 32 | }); 33 | 34 | describe("case where first and second arguments are not undefined", () => { 35 | it("should return second argument", () => { 36 | expect(mergeOptionalDecimalPlaces(6, 0)).toBe(0); 37 | }); 38 | }); 39 | 40 | describe("case where first and second arguments are undefined", () => { 41 | it("should return undefined", () => { 42 | expect(mergeOptionalDecimalPlaces(undefined, undefined)).toBe(undefined); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | - reopened 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | check: 17 | timeout-minutes: 15 18 | runs-on: ubuntu-24.04 19 | permissions: 20 | contents: read 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 24 | 25 | - name: Setup node 26 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 27 | with: 28 | node-version-file: ".node-version" 29 | cache: "npm" 30 | 31 | - name: Install packages 32 | shell: bash 33 | run: npm ci --before $(node scripts/print-latest-safe-release-date.ts) 34 | 35 | - name: Run trusted postinstall scripts 36 | shell: bash 37 | run: npm run trusted-postinstall 38 | 39 | - name: Check spelling 40 | shell: bash 41 | run: npm run spellcheck -- . 42 | 43 | - name: Lint and format code 44 | shell: bash 45 | run: npm run stylecheck:code -- . 46 | 47 | - name: Format docs 48 | shell: bash 49 | run: npm run stylecheck:docs -- "**/*.md" 50 | 51 | - name: Check ES5 artifacts 52 | shell: bash 53 | run: npm run es5check 54 | 55 | - name: Check types 56 | shell: bash 57 | run: npm run typecheck 58 | 59 | - name: Test 60 | shell: bash 61 | run: npm run test 62 | -------------------------------------------------------------------------------- /src/internal/GlobalParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDecimalPlaces, 3 | type DecimalPlaces, 4 | mergeOptionalDecimalPlaces, 5 | } from "./DecimalPlaces.js"; 6 | 7 | export type GlobalParameters = { 8 | decimalPlaces: DecimalPlaces; 9 | }; 10 | 11 | export const createGlobalParameters = ( 12 | partialGlobalParameters: Partial = {}, 13 | ): GlobalParameters => ({ 14 | decimalPlaces: createDecimalPlaces(partialGlobalParameters.decimalPlaces), 15 | }); 16 | 17 | export const mergePartialGlobalParameters = ( 18 | precedingPartialGlobalParameters: Partial, 19 | followingPartialGlobalParameters: Partial, 20 | ): Partial => { 21 | const partialGlobalParameters: Partial = {}; 22 | const optionalDecimalPlaces = mergeOptionalDecimalPlaces( 23 | precedingPartialGlobalParameters.decimalPlaces, 24 | followingPartialGlobalParameters.decimalPlaces, 25 | ); 26 | if (typeof optionalDecimalPlaces !== "undefined") 27 | partialGlobalParameters.decimalPlaces = optionalDecimalPlaces; 28 | return partialGlobalParameters; 29 | }; 30 | 31 | export const assignOptionalDecimalPlaces = ( 32 | optionalPartialGlobalParameters: Partial | undefined, 33 | optionalDecimalPlaces: DecimalPlaces | undefined, 34 | ): Partial => 35 | typeof optionalDecimalPlaces !== "undefined" 36 | ? { 37 | ...(optionalPartialGlobalParameters ?? {}), 38 | decimalPlaces: optionalDecimalPlaces, 39 | } 40 | : (optionalPartialGlobalParameters ?? {}); 41 | 42 | export const getDecimalPlaces = (globalParameters: GlobalParameters) => 43 | globalParameters.decimalPlaces; 44 | -------------------------------------------------------------------------------- /src/internal/Media.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { createMedia, mergeOptionalMedia } from "./Media.js"; 3 | 4 | describe("createMedia", () => { 5 | describe("case where argument is not undefined", () => { 6 | it("should return argument", () => { 7 | expect(createMedia("(min-width: 768px)")).toBe("(min-width: 768px)"); 8 | }); 9 | }); 10 | 11 | describe("case where argument is undefined", () => { 12 | it("should return default value", () => { 13 | expect(createMedia(undefined)).toBe(""); 14 | }); 15 | }); 16 | }); 17 | 18 | describe("mergeOptionalMedia", () => { 19 | describe("case where only first argument is not undefined", () => { 20 | it("should return first argument", () => { 21 | expect(mergeOptionalMedia("(min-width: 768px)", undefined)).toBe( 22 | "(min-width: 768px)", 23 | ); 24 | }); 25 | }); 26 | 27 | describe("case where only second argument is not undefined", () => { 28 | it("should return second argument", () => { 29 | expect(mergeOptionalMedia(undefined, "(min-width: 768px)")).toBe( 30 | "(min-width: 768px)", 31 | ); 32 | }); 33 | }); 34 | 35 | describe("case where first and second arguments are not undefined", () => { 36 | it("should return second argument", () => { 37 | expect( 38 | mergeOptionalMedia("(min-width: 768px)", "(min-width: 1024px)"), 39 | ).toBe("(min-width: 1024px)"); 40 | }); 41 | }); 42 | 43 | describe("case where first and second arguments are undefined", () => { 44 | it("should return undefined", () => { 45 | expect(mergeOptionalMedia(undefined, undefined)).toBe(undefined); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /biome.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.1.1/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": true, 10 | "includes": ["**", "!**/.release-please-manifest.json"] 11 | }, 12 | "formatter": { 13 | "indentStyle": "space" 14 | }, 15 | "linter": { 16 | // Rules that are not included in recommended rules of Biome 17 | // but are included in eslint:recommended rules or plugin:@typescript-eslint/recommended rules 18 | "rules": { 19 | "correctness": { 20 | "noUndeclaredVariables": "error", 21 | "noUnusedImports": "error", 22 | "noUnusedPrivateClassMembers": "error", 23 | "noUnusedVariables": "error" 24 | }, 25 | "style": { 26 | "noNamespace": "error", 27 | "useArrayLiterals": "error", 28 | 29 | // To reduce artifact sizes 30 | "useNumberNamespace": "off", 31 | 32 | // Rules recommended in Biome v1 33 | "noParameterAssign": "error", 34 | "useAsConstAssertion": "error", 35 | "useDefaultParameterLast": "error", 36 | "useEnumInitializers": "error", 37 | "useSelfClosingElements": "error", 38 | "useSingleVarDeclarator": "error", 39 | "noUnusedTemplateLiteral": "error", 40 | "noInferrableTypes": "error", 41 | "noUselessElse": "error" 42 | }, 43 | "suspicious": { 44 | "noEmptyBlockStatements": "error" 45 | } 46 | } 47 | }, 48 | "javascript": { 49 | "globals": ["__TYPESCRIPT_TARGET__"] 50 | }, 51 | "html": { 52 | "formatter": { 53 | "enabled": true 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | release: 10 | if: >- 11 | github.event.pull_request.merged && 12 | contains(github.event.pull_request.labels.*.name, 'autorelease: pending') && 13 | contains(github.event.pull_request.labels.*.name, 'versioning_strategy: default') 14 | timeout-minutes: 15 15 | runs-on: ubuntu-24.04 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | steps: 20 | - name: Publish tag and release 21 | 22 | # Fork to replace only pre-execution version in extra-files 23 | uses: dsktschy/release-please-action@c135340c5ba36cf42122ea5508f80763d3b2eda2 24 | 25 | with: 26 | config-file: release-please-config.default.json 27 | skip-github-pull-request: true 28 | target-branch: ${{ github.event.pull_request.base.ref }} 29 | 30 | prerelease: 31 | if: >- 32 | github.event.pull_request.merged && 33 | contains(github.event.pull_request.labels.*.name, 'autorelease: pending') && 34 | contains(github.event.pull_request.labels.*.name, 'versioning_strategy: prerelease') 35 | timeout-minutes: 15 36 | runs-on: ubuntu-24.04 37 | permissions: 38 | contents: write 39 | pull-requests: write 40 | steps: 41 | - name: Publish tag and release 42 | 43 | # Fork to replace only pre-execution version in extra-files 44 | uses: dsktschy/release-please-action@c135340c5ba36cf42122ea5508f80763d3b2eda2 45 | 46 | with: 47 | config-file: release-please-config.prerelease.json 48 | skip-github-pull-request: true 49 | target-branch: ${{ github.event.pull_request.base.ref }} 50 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Create release pull request 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | versioning_strategy: 7 | description: Versioning Strategy 8 | required: true 9 | default: default 10 | type: choice 11 | options: 12 | - default 13 | - prerelease 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | create_release_pull_request: 21 | if: inputs.versioning_strategy == 'default' 22 | timeout-minutes: 15 23 | runs-on: ubuntu-24.04 24 | permissions: 25 | contents: write 26 | pull-requests: write 27 | steps: 28 | - name: Bump version and create pull request 29 | 30 | # Fork to replace only pre-execution version in extra-files 31 | uses: dsktschy/release-please-action@c135340c5ba36cf42122ea5508f80763d3b2eda2 32 | 33 | with: 34 | config-file: release-please-config.default.json 35 | skip-github-release: true 36 | target-branch: ${{ github.ref_name }} 37 | 38 | create_prerelease_pull_request: 39 | if: inputs.versioning_strategy == 'prerelease' 40 | timeout-minutes: 15 41 | runs-on: ubuntu-24.04 42 | permissions: 43 | contents: write 44 | pull-requests: write 45 | steps: 46 | - name: Bump version and create pull request 47 | 48 | # Fork to replace only pre-execution version in extra-files 49 | uses: dsktschy/release-please-action@c135340c5ba36cf42122ea5508f80763d3b2eda2 50 | 51 | with: 52 | config-file: release-please-config.prerelease.json 53 | skip-github-release: true 54 | target-branch: ${{ github.ref_name }} 55 | -------------------------------------------------------------------------------- /src/internal/MediaAttribute.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | createOptionalMedia, 4 | mergeNullableMediaAttribute, 5 | } from "./MediaAttribute.js"; 6 | 7 | describe("mergeNullableMediaAttribute", () => { 8 | describe("case where only first argument is not null", () => { 9 | it("should return first argument", () => { 10 | expect(mergeNullableMediaAttribute("(min-width: 768px)", null)).toBe( 11 | "(min-width: 768px)", 12 | ); 13 | }); 14 | }); 15 | 16 | describe("case where only second argument is not null", () => { 17 | it("should return second argument", () => { 18 | expect(mergeNullableMediaAttribute(null, "(min-width: 768px)")).toBe( 19 | "(min-width: 768px)", 20 | ); 21 | }); 22 | }); 23 | 24 | describe("case where first and second arguments are not null", () => { 25 | it("should return second argument", () => { 26 | expect( 27 | mergeNullableMediaAttribute( 28 | "(min-width: 768px)", 29 | "(min-width: 1024px)", 30 | ), 31 | ).toBe("(min-width: 1024px)"); 32 | }); 33 | }); 34 | 35 | describe("case where first and second arguments are null", () => { 36 | it("should return null", () => { 37 | expect(mergeNullableMediaAttribute(null, null)).toBe(null); 38 | }); 39 | }); 40 | }); 41 | 42 | describe("createOptionalMedia", () => { 43 | describe("case where argument is not null", () => { 44 | it("should return argument", () => { 45 | expect(createOptionalMedia("(min-width: 768px)")).toBe( 46 | "(min-width: 768px)", 47 | ); 48 | }); 49 | }); 50 | 51 | describe("case where argument is null", () => { 52 | it("should return undefined", () => { 53 | expect(createOptionalMedia(null)).toBe(undefined); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /rollup.config.declare.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "rollup"; 2 | import rollupPluginDelete from "rollup-plugin-delete"; 3 | import rollupPluginDts from "rollup-plugin-dts"; 4 | 5 | export default defineConfig( 6 | [ 7 | { 8 | input: ".types/entries/viewport-extra.d.ts", 9 | subDirectory: "", 10 | }, 11 | { 12 | input: ".types/entries/viewport-extra.d.ts", 13 | subDirectory: "es5/", 14 | }, 15 | { 16 | input: ".types/entries/extended/viewport-extra.d.ts", 17 | subDirectory: "extended/", 18 | }, 19 | { 20 | input: ".types/entries/extended/viewport-extra.d.ts", 21 | subDirectory: "extended/es5/", 22 | }, 23 | { 24 | input: ".types/entries/immediate/viewport-extra.d.ts", 25 | subDirectory: "immediate/", 26 | }, 27 | { 28 | input: ".types/entries/immediate/viewport-extra.d.ts", 29 | subDirectory: "immediate/es5/", 30 | }, 31 | { 32 | input: ".types/entries/immediate/extended/viewport-extra.d.ts", 33 | subDirectory: "immediate/extended/", 34 | }, 35 | { 36 | input: ".types/entries/immediate/extended/viewport-extra.d.ts", 37 | subDirectory: "immediate/extended/es5/", 38 | }, 39 | ].map(({ input, subDirectory }) => ({ 40 | input, 41 | output: [ 42 | { 43 | file: `dist/${subDirectory}viewport-extra.d.mts`, 44 | format: "es", 45 | exports: "named", 46 | }, 47 | { 48 | file: `dist/${subDirectory}viewport-extra.d.cts`, 49 | format: "cjs", 50 | exports: "named", 51 | }, 52 | ], 53 | plugins: [ 54 | rollupPluginDelete({ 55 | targets: [ 56 | `dist/${subDirectory}viewport-extra.d.mts`, 57 | `dist/${subDirectory}viewport-extra.d.cts`, 58 | ], 59 | }), 60 | rollupPluginDts(), 61 | ], 62 | })), 63 | ); 64 | -------------------------------------------------------------------------------- /src/internal/DecimalPlacesAttribute.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | createOptionalDecimalPlaces, 4 | mergeNullableDecimalPlacesAttribute, 5 | } from "./DecimalPlacesAttribute.js"; 6 | 7 | describe("mergeNullableDecimalPlacesAttribute", () => { 8 | describe("case where only first argument is not null", () => { 9 | it("should return first argument", () => { 10 | expect(mergeNullableDecimalPlacesAttribute("6", null)).toBe("6"); 11 | }); 12 | }); 13 | 14 | describe("case where only second argument is not null", () => { 15 | it("should return second argument", () => { 16 | expect(mergeNullableDecimalPlacesAttribute(null, "6")).toBe("6"); 17 | }); 18 | }); 19 | 20 | describe("case where first and second arguments are not null", () => { 21 | it("should return second argument", () => { 22 | expect(mergeNullableDecimalPlacesAttribute("6", "0")).toBe("0"); 23 | }); 24 | }); 25 | 26 | describe("case where first and second arguments are null", () => { 27 | it("should return null", () => { 28 | expect(mergeNullableDecimalPlacesAttribute(null, null)).toBe(null); 29 | }); 30 | }); 31 | }); 32 | 33 | describe("createOptionalDecimalPlaces", () => { 34 | describe("case where argument string is finite number", () => { 35 | it("should convert argument to number type and return it", () => { 36 | expect(createOptionalDecimalPlaces("6")).toBe(6); 37 | }); 38 | }); 39 | 40 | describe("case where argument string is Infinity", () => { 41 | it("should convert argument to number type and return it", () => { 42 | expect(createOptionalDecimalPlaces("Infinity")).toBe(Infinity); 43 | }); 44 | }); 45 | 46 | describe("case where argument is null", () => { 47 | it("should return undefined", () => { 48 | expect(createOptionalDecimalPlaces(null)).toBe(undefined); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/internal/DeepPartial.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, it } from "vitest"; 2 | import type { DeepPartial } from "./DeepPartial.js"; 3 | 4 | describe("DeepPartial", () => { 5 | describe("case where type argument is object with multiple levels", () => { 6 | it("should make all properties in all levels of type argument optional", () => { 7 | expectTypeOf< 8 | DeepPartial<{ 9 | firstA: { 10 | second: { 11 | third: string; 12 | }; 13 | }; 14 | firstB: number; 15 | firstC: boolean; 16 | }> 17 | >().toEqualTypeOf<{ 18 | firstA?: { 19 | second?: { 20 | third?: string; 21 | }; 22 | }; 23 | firstB?: number; 24 | firstC?: boolean; 25 | }>(); 26 | }); 27 | }); 28 | 29 | describe("case where type argument is object with single level", () => { 30 | it("should make all properties of type argument optional", () => { 31 | expectTypeOf< 32 | DeepPartial<{ 33 | firstA: string; 34 | firstB: number; 35 | firstC: boolean; 36 | }> 37 | >().toEqualTypeOf<{ 38 | firstA?: string; 39 | firstB?: number; 40 | firstC?: boolean; 41 | }>(); 42 | }); 43 | }); 44 | 45 | describe("case where type argument is not object", () => { 46 | it("should do nothing", () => { 47 | expectTypeOf>().toEqualTypeOf(); 48 | expectTypeOf>().toEqualTypeOf(); 49 | expectTypeOf>().toEqualTypeOf(); 50 | expectTypeOf>().toEqualTypeOf(); 51 | expectTypeOf>().toEqualTypeOf(); 52 | expectTypeOf>().toEqualTypeOf(); 53 | expectTypeOf>().toEqualTypeOf(); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * See https://playwright.dev/docs/test-configuration. 5 | */ 6 | export default defineConfig({ 7 | /* Run tests in files in parallel */ 8 | fullyParallel: true, 9 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 10 | forbidOnly: !!process.env.CI, 11 | /* Retry on CI only */ 12 | retries: 0, 13 | /* Opt out of parallel tests on CI. */ 14 | ...(process.env.CI ? { workers: 1 } : {}), 15 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 16 | reporter: process.env.CI ? "dot" : "list", 17 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 18 | use: { 19 | /* Base URL to use in actions like `await page.goto('/')`. */ 20 | baseURL: "http://localhost:3000", 21 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 22 | trace: "on-first-retry", 23 | }, 24 | 25 | /* Configure projects for devices of various viewport widths */ 26 | projects: [ 27 | /* Mobile device with viewport width of 320px */ 28 | { 29 | name: "xs", 30 | testDir: "tests/specs", 31 | use: { ...devices["iPhone SE"] }, 32 | }, 33 | /* Mobile device with viewport width of 412px */ 34 | { 35 | name: "sm", 36 | testDir: "tests/specs/e2e", 37 | use: { ...devices["Pixel 7"] }, 38 | }, 39 | /* Mobile device with viewport width of 768px */ 40 | { 41 | name: "lg", 42 | testDir: "tests/specs/e2e", 43 | use: { ...devices["iPad Mini"] }, 44 | }, 45 | /* Mobile device with viewport width of 1024px */ 46 | { 47 | name: "xl", 48 | testDir: "tests/specs/e2e", 49 | use: { ...devices["iPad Mini landscape"] }, 50 | }, 51 | ], 52 | 53 | /* Run local dev server before starting the tests */ 54 | webServer: { 55 | command: "npm run preview-e2e", 56 | url: "http://localhost:3000/tests/__fixtures__/src/dummy.html", 57 | reuseExistingServer: !process.env.CI, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /vite.config.e2e.ts: -------------------------------------------------------------------------------- 1 | import { globSync } from "node:fs"; 2 | import path from "node:path"; 3 | import { defineConfig } from "vite"; 4 | 5 | const sourceDirectory = "tests/__fixtures__/src/"; 6 | 7 | export default defineConfig({ 8 | build: { 9 | // npx playwright install --dry-run 10 | target: ["chrome128", "safari18"], 11 | rollupOptions: { 12 | input: { 13 | "dummy.html": `${sourceDirectory}dummy.html`, 14 | ...globSync(`${sourceDirectory}**/*.ts`).reduce>( 15 | (result, relativePath) => { 16 | result[ 17 | relativePath.replace(sourceDirectory, "").replace(/\.ts$/, ".js") 18 | ] = relativePath; 19 | return result; 20 | }, 21 | {}, 22 | ), 23 | }, 24 | // Set array in output because rollupOptions does not accept array 25 | output: [ 26 | { 27 | typescriptTarget: "es2021", 28 | subDirectory: "", 29 | }, 30 | { 31 | typescriptTarget: "es5", 32 | subDirectory: "es5/", 33 | }, 34 | ].map(({ typescriptTarget, subDirectory }) => ({ 35 | entryFileNames: ({ name }) => { 36 | const { dir, base } = path.parse(name); 37 | return dir ? `${dir}/${subDirectory}${base}` : "[name]"; 38 | }, 39 | plugins: [ 40 | // Use custom plugin to replace placeholders 41 | // because @rollup/plugin-replace cannot be used as plugin for output 42 | { 43 | name: "replace", 44 | renderChunk: (code) => 45 | Object.entries({ 46 | __TYPESCRIPT_TARGET__: `"${typescriptTarget}"`, 47 | }).reduce( 48 | (code, [placeholder, value]) => 49 | code.replaceAll(placeholder, value), 50 | code, 51 | ), 52 | }, 53 | ], 54 | })), 55 | }, 56 | outDir: "tests/__fixtures__/dist", 57 | }, 58 | publicDir: "dist", 59 | resolve: { 60 | alias: { 61 | "@@": import.meta.dirname, 62 | }, 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions/starter-workflows/blob/43f0e192265aa00b299d2f39ff83f1f6ba096193/code-scanning/codeql.yml 2 | name: Scan with CodeQL 3 | 4 | on: 5 | workflow_dispatch: 6 | # To suppress CodeQL warnings and check results without opening pull request 7 | push: 8 | pull_request: 9 | types: 10 | - opened 11 | - synchronize 12 | - reopened 13 | schedule: 14 | - cron: '0 7 * * 5' 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze (${{ matrix.language }}) 23 | timeout-minutes: 15 24 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 25 | permissions: 26 | security-events: write 27 | packages: read 28 | actions: read 29 | contents: read 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | include: 34 | - language: actions 35 | build-mode: none 36 | - language: javascript-typescript 37 | build-mode: none 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 41 | 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 44 | with: 45 | languages: ${{ matrix.language }} 46 | build-mode: ${{ matrix.build-mode }} 47 | 48 | - name: Run manual build steps 49 | if: matrix.build-mode == 'manual' 50 | shell: bash 51 | run: | 52 | echo 'If you are using a "manual" build mode for one or more of the' \ 53 | 'languages you are analyzing, replace this with the commands to build' \ 54 | 'your code, for example:' 55 | echo ' make bootstrap' 56 | echo ' make release' 57 | exit 1 58 | 59 | - name: Perform CodeQL Analysis 60 | uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 61 | with: 62 | category: "/language:${{matrix.language}}" 63 | -------------------------------------------------------------------------------- /.github/workflows/create-stabilize-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Create stabilize pull request 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | define_variables_to_stabilize: 12 | timeout-minutes: 15 13 | runs-on: ubuntu-24.04 14 | permissions: 15 | contents: read 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 19 | 20 | - name: Define variables 21 | id: define_variables 22 | shell: bash 23 | run: | 24 | version=$(jq -r .version package.json) 25 | echo "version: $version" 26 | stable_version=${version%%-*} 27 | stabilizable=no 28 | if [[ $version != $stable_version ]]; then 29 | stabilizable=yes 30 | fi 31 | echo "stabilizable: $stabilizable" 32 | echo "stable_version=$stable_version" >> $GITHUB_OUTPUT 33 | echo "stabilizable=$stabilizable" >> $GITHUB_OUTPUT 34 | outputs: 35 | stable_version: ${{ steps.define_variables.outputs.stable_version }} 36 | stabilizable: ${{ steps.define_variables.outputs.stabilizable }} 37 | 38 | create_stabilize_pull_request: 39 | needs: define_variables_to_stabilize 40 | if: needs.define_variables_to_stabilize.outputs.stabilizable == 'yes' 41 | timeout-minutes: 15 42 | runs-on: ubuntu-24.04 43 | permissions: 44 | contents: write 45 | pull-requests: write 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 49 | 50 | - name: Commit to stabilize 51 | shell: bash 52 | run: | 53 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 54 | git config --global user.name "github-actions[bot]" 55 | git commit --allow-empty -m "chore: stabilize ${{ needs.define_variables_to_stabilize.outputs.stable_version }}" -m "Release-As: ${{ needs.define_variables_to_stabilize.outputs.stable_version }}" 56 | 57 | - name: Create pull request 58 | uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5 59 | with: 60 | title: "chore: stabilize ${{ needs.define_variables_to_stabilize.outputs.stable_version }}" 61 | body: Created by the create-stabilize-pull-request workflow. 62 | labels: stabilize 63 | -------------------------------------------------------------------------------- /src/internal/Document.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | ensureViewportMetaElement, 4 | getDocumentClientWidth, 5 | getMetaElementList, 6 | } from "./Document.js"; 7 | 8 | describe("ensureViewportMetaElement", () => { 9 | describe("case where viewport meta element exists", () => { 10 | it("should return existing viewport meta element", () => { 11 | document.head.innerHTML = ` 12 | 13 | 14 | `; 15 | expect(ensureViewportMetaElement(document)).toBe( 16 | document.querySelector('meta[name="viewport"]'), 17 | ); 18 | }); 19 | }); 20 | 21 | describe("case where viewport meta element does not exist", () => { 22 | it("should append viewport meta element to HTML and returns appended viewport meta element", () => { 23 | document.head.innerHTML = ` 24 | 25 | `; 26 | const returnedViewportMetaElement = ensureViewportMetaElement(document); 27 | const selectedViewportMetaElement = document.querySelector( 28 | 'meta[name="viewport"]', 29 | ); 30 | expect(selectedViewportMetaElement).not.toBe(null); 31 | expect(returnedViewportMetaElement).toBe(selectedViewportMetaElement); 32 | }); 33 | }); 34 | }); 35 | 36 | describe("getMetaElementList", () => { 37 | describe("case where viewport and viewport-extra meta elements exist", () => { 38 | it("should return all existing viewport and viewport-extra meta elements as array", () => { 39 | document.head.innerHTML = ` 40 | 41 | 42 | 43 | 44 | 45 | `; 46 | expect( 47 | getMetaElementList(document).map( 48 | (returnedMetaElement) => returnedMetaElement.outerHTML, 49 | ), 50 | ).toStrictEqual([ 51 | '', 52 | '', 53 | '', 54 | '', 55 | ]); 56 | }); 57 | }); 58 | 59 | describe("case where viewport and viewport-extra meta elements do not exist", () => { 60 | it("should return empty array", () => { 61 | document.head.innerHTML = ` 62 | 63 | `; 64 | expect(getMetaElementList(document)).toStrictEqual([]); 65 | }); 66 | }); 67 | }); 68 | 69 | describe("getDocumentClientWidth", () => { 70 | it("should return document.documentElement.clientWidth", () => { 71 | Object.defineProperty(document.documentElement, "clientWidth", { 72 | value: 412, 73 | configurable: true, 74 | }); 75 | expect(getDocumentClientWidth(document)).toBe(412); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/github/gitignore/blob/main/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # vuepress v2.x temp and cache directory 106 | .temp 107 | .cache 108 | 109 | # Docusaurus cache and generated files 110 | .docusaurus 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # yarn v2 128 | .yarn/cache 129 | .yarn/unplugged 130 | .yarn/build-state.yml 131 | .yarn/install-state.gz 132 | .pnp.* 133 | 134 | # Playwright 135 | /test-results/ 136 | /playwright-report/ 137 | /blob-report/ 138 | /playwright/.cache/ 139 | 140 | # Mac OS 141 | .DS_Store 142 | 143 | # VS Code 144 | .vscode/ 145 | 146 | # Git 147 | /.git/ 148 | 149 | # viewport-extra 150 | /types/ 151 | /.types/ 152 | -------------------------------------------------------------------------------- /src/internal/Content.ts: -------------------------------------------------------------------------------- 1 | import type { ContentAttribute } from "./ContentAttribute.js"; 2 | import type { DecimalPlaces } from "./DecimalPlaces.js"; 3 | import { truncateDecimalNumber } from "./number.js"; 4 | import { kebabizeCamelCaseString } from "./string.js"; 5 | 6 | export type Content = { 7 | width: number | "device-width"; 8 | initialScale: number; 9 | minimumWidth: number; 10 | maximumWidth: number; 11 | /** Alternative to `minimumWidth` */ 12 | minWidth?: number; 13 | /** Alternative to `maximumWidth` */ 14 | maxWidth?: number; 15 | } & { 16 | [key: string]: string | number; 17 | }; 18 | 19 | export const defaultContent = { 20 | width: "device-width" as const, 21 | initialScale: 1, 22 | minimumWidth: 0, 23 | maximumWidth: Infinity, 24 | }; 25 | 26 | export const createContent = ( 27 | partialContent: Partial | undefined = {}, 28 | ): Content => ({ 29 | ...defaultContent, 30 | ...partialContent, 31 | }); 32 | 33 | export const mergeOptionalPartialContent = ( 34 | precedingOptionalPartialContent: Partial | undefined, 35 | followingOptionalPartialContent: Partial | undefined, 36 | ): Partial | undefined => 37 | precedingOptionalPartialContent 38 | ? { 39 | ...precedingOptionalPartialContent, 40 | ...(followingOptionalPartialContent ?? {}), 41 | } 42 | : followingOptionalPartialContent; 43 | 44 | export const createContentAttribute: { 45 | ( 46 | content: Content, 47 | documentClientWidth: number, 48 | decimalPlaces: DecimalPlaces, 49 | ): ContentAttribute; 50 | (): ContentAttribute; 51 | } = ( 52 | content: Content = { ...defaultContent }, 53 | // biome-ignore lint/style/noInferrableTypes: number type cannot be inferred from initialization 54 | documentClientWidth: number = 0, 55 | decimalPlaces: DecimalPlaces = 0, 56 | ) => { 57 | const { width, initialScale } = content; 58 | const { 59 | minimumWidth: _minimumWidth, 60 | maximumWidth: _maximumWidth, 61 | minWidth, 62 | maxWidth, 63 | ...contentWithoutExtraProperties 64 | } = content; 65 | const minimumWidth = minWidth ?? _minimumWidth; 66 | const maximumWidth = maxWidth ?? _maximumWidth; 67 | if (minimumWidth <= maximumWidth && width === "device-width") { 68 | if (documentClientWidth < minimumWidth) { 69 | contentWithoutExtraProperties.width = minimumWidth; 70 | contentWithoutExtraProperties.initialScale = 71 | (documentClientWidth / minimumWidth) * initialScale; 72 | } else if (documentClientWidth > maximumWidth) { 73 | contentWithoutExtraProperties.width = maximumWidth; 74 | contentWithoutExtraProperties.initialScale = 75 | (documentClientWidth / maximumWidth) * initialScale; 76 | } 77 | } 78 | return Object.keys(contentWithoutExtraProperties) 79 | .map( 80 | (key) => 81 | `${kebabizeCamelCaseString(key)}=${ 82 | typeof contentWithoutExtraProperties[key] === "number" 83 | ? truncateDecimalNumber( 84 | contentWithoutExtraProperties[key], 85 | decimalPlaces, 86 | ) 87 | : contentWithoutExtraProperties[key] 88 | }`, 89 | ) 90 | .sort() 91 | .join(","); 92 | }; 93 | -------------------------------------------------------------------------------- /.github/workflows/create-example-lockfile-update-pull-request.yml: -------------------------------------------------------------------------------- 1 | # Workflows triggered by workflow_run event always target default branch 2 | # Therefore, checkout and create-pull-request actions require branch to be specified 3 | name: Create example lockfile update pull request 4 | 5 | on: 6 | workflow_run: 7 | workflows: 8 | - 'Publish to npm' 9 | types: 10 | - completed 11 | 12 | jobs: 13 | define_variables_to_update_example_lockfile: 14 | if: github.event.workflow_run.conclusion == 'success' 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | permissions: 18 | contents: read 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 22 | with: 23 | ref: ${{ github.event.workflow_run.head_branch }} 24 | 25 | - name: Setup node 26 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 27 | with: 28 | node-version-file: '.node-version' 29 | cache: 'npm' 30 | 31 | - name: Define variables 32 | id: define_variables 33 | shell: bash 34 | run: | 35 | version=$(jq -r .version package.json) 36 | echo "version: $version" 37 | updatable=no 38 | if npm view viewport-extra@"$version" > /dev/null 2>&1; then 39 | updatable=yes 40 | fi 41 | echo "updatable: $updatable" 42 | echo "updatable=$updatable" >> $GITHUB_OUTPUT 43 | outputs: 44 | updatable: ${{ steps.define_variables.outputs.updatable }} 45 | 46 | create_example_lockfile_update_pull_request: 47 | needs: define_variables_to_update_example_lockfile 48 | # Ensure execution is not from fork 49 | # It cannot occur under current configuration, but must be checked to suppress CodeQL alert 50 | if: >- 51 | github.event.workflow_run.head_repository.full_name == github.repository && 52 | needs.define_variables_to_update_example_lockfile.outputs.updatable == 'yes' 53 | timeout-minutes: 15 54 | runs-on: ubuntu-24.04 55 | permissions: 56 | contents: write 57 | pull-requests: write 58 | steps: 59 | - name: Checkout repository 60 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 61 | with: 62 | ref: ${{ github.event.workflow_run.head_branch }} 63 | 64 | - name: Setup node 65 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 66 | with: 67 | node-version-file: '.node-version' 68 | cache: 'npm' 69 | 70 | - name: Update example lockfiles 71 | shell: bash 72 | run: | 73 | npm install viewport-extra --prefix examples/next-app-router 74 | npm install viewport-extra --prefix examples/next-pages-router 75 | npm install viewport-extra --prefix examples/nuxt 76 | 77 | - name: Commit 78 | shell: bash 79 | run: | 80 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 81 | git config --global user.name "github-actions[bot]" 82 | git commit -am "chore: update example lockfiles" 83 | 84 | - name: Create pull request 85 | uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5 86 | with: 87 | base: ${{ github.event.workflow_run.head_branch }} 88 | title: 'chore: update example lockfiles' 89 | body: Created by the create-example-lockfile-update-pull-request workflow. 90 | labels: example_lockfile_update 91 | -------------------------------------------------------------------------------- /src/internal/HTMLMetaElement.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ContentAttribute, 3 | createOptionalPartialContent, 4 | mergeNullableContentAttributes, 5 | } from "./ContentAttribute.js"; 6 | import { 7 | createOptionalDecimalPlaces, 8 | type DecimalPlacesAttribute, 9 | mergeNullableDecimalPlacesAttribute, 10 | } from "./DecimalPlacesAttribute.js"; 11 | import type { DeepPartial } from "./DeepPartial.js"; 12 | import { 13 | assignOptionalDecimalPlaces, 14 | type GlobalParameters, 15 | getDecimalPlaces, 16 | } from "./GlobalParameters.js"; 17 | import { 18 | createOptionalMedia, 19 | type MediaAttribute, 20 | mergeNullableMediaAttribute, 21 | } from "./MediaAttribute.js"; 22 | import { 23 | assignOptionalMedia, 24 | assignOptionalPartialContent, 25 | createContentAttribute, 26 | type MediaSpecificParameters, 27 | } from "./MediaSpecificParameters.js"; 28 | 29 | export const getNullableDecimalPlacesAttribute = ( 30 | htmlMetaElement: HTMLMetaElement, 31 | ): DecimalPlacesAttribute | null => 32 | mergeNullableDecimalPlacesAttribute( 33 | htmlMetaElement.getAttribute("data-decimal-places"), 34 | htmlMetaElement.getAttribute("data-extra-decimal-places"), 35 | ); 36 | 37 | export const createPartialGlobalParameters = ( 38 | htmlMetaElement: HTMLMetaElement, 39 | ): Partial => 40 | assignOptionalDecimalPlaces( 41 | undefined, 42 | createOptionalDecimalPlaces( 43 | getNullableDecimalPlacesAttribute(htmlMetaElement), 44 | ), 45 | ); 46 | 47 | export const getNullableContentAttribute = ( 48 | htmlMetaElement: HTMLMetaElement, 49 | ): ContentAttribute | null => 50 | mergeNullableContentAttributes( 51 | htmlMetaElement.getAttribute("content"), 52 | htmlMetaElement.getAttribute("data-extra-content"), 53 | ); 54 | 55 | export const getNullableMediaAttribute = ( 56 | htmlMetaElement: HTMLMetaElement, 57 | ): MediaAttribute | null => 58 | mergeNullableMediaAttribute( 59 | htmlMetaElement.getAttribute("data-media"), 60 | htmlMetaElement.getAttribute("data-extra-media"), 61 | ); 62 | 63 | export const createPartialMediaSpecificParameters = ( 64 | htmlMetaElement: HTMLMetaElement, 65 | ): DeepPartial => 66 | assignOptionalMedia( 67 | assignOptionalPartialContent( 68 | undefined, 69 | createOptionalPartialContent( 70 | getNullableContentAttribute(htmlMetaElement), 71 | ), 72 | ), 73 | createOptionalMedia(getNullableMediaAttribute(htmlMetaElement)), 74 | ); 75 | 76 | export const setContentAttribute = ( 77 | htmlMetaElement: HTMLMetaElement, 78 | contentAttribute: ContentAttribute, 79 | ): void => htmlMetaElement.setAttribute("content", contentAttribute); 80 | 81 | export const applyMediaSpecificParameters = ( 82 | htmlMetaElement: HTMLMetaElement, 83 | getDocumentClientWidth: () => number, 84 | getMediaSpecificParameters: () => MediaSpecificParameters, 85 | ): void => { 86 | setContentAttribute(htmlMetaElement, createContentAttribute()); 87 | setContentAttribute( 88 | htmlMetaElement, 89 | createContentAttribute( 90 | getMediaSpecificParameters(), 91 | getDocumentClientWidth(), 92 | Infinity, 93 | ), 94 | ); 95 | }; 96 | 97 | export const applyMediaSpecificParametersTruncated = ( 98 | htmlMetaElement: HTMLMetaElement, 99 | getDocumentClientWidth: () => number, 100 | getMediaSpecificParameters: () => MediaSpecificParameters, 101 | globalParameters: GlobalParameters, 102 | ): void => { 103 | setContentAttribute(htmlMetaElement, createContentAttribute()); 104 | setContentAttribute( 105 | htmlMetaElement, 106 | createContentAttribute( 107 | getMediaSpecificParameters(), 108 | getDocumentClientWidth(), 109 | getDecimalPlaces(globalParameters), 110 | ), 111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /src/internal/GlobalParameters.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | assignOptionalDecimalPlaces, 4 | createGlobalParameters, 5 | getDecimalPlaces, 6 | mergePartialGlobalParameters, 7 | } from "./GlobalParameters.js"; 8 | 9 | describe("createGlobalParameters", () => { 10 | describe("case where argument has properties", () => { 11 | it("should return object that deeply inherits properties of argument", () => { 12 | expect(createGlobalParameters({ decimalPlaces: 6 })).toStrictEqual({ 13 | decimalPlaces: 6, 14 | }); 15 | }); 16 | }); 17 | 18 | describe("case where argument has missing properties of GlobalParameters type", () => { 19 | it("should return object with missing properties set to default value deeply", () => { 20 | expect(createGlobalParameters({})).toStrictEqual({ 21 | decimalPlaces: Infinity, 22 | }); 23 | }); 24 | }); 25 | 26 | describe("case where argument is undefined", () => { 27 | it("should return object with all properties that have default value", () => { 28 | expect(createGlobalParameters()).toStrictEqual({ 29 | decimalPlaces: Infinity, 30 | }); 31 | }); 32 | }); 33 | }); 34 | 35 | describe("mergePartialGlobalParameters", () => { 36 | describe("case where properties exist in only first argument", () => { 37 | it("should return object that has properties in first argument", () => { 38 | expect( 39 | mergePartialGlobalParameters({ decimalPlaces: 6 }, {}), 40 | ).toStrictEqual({ decimalPlaces: 6 }); 41 | }); 42 | }); 43 | 44 | describe("case where properties exist in only second argument", () => { 45 | it("should return object that has properties in second argument", () => { 46 | expect( 47 | mergePartialGlobalParameters({}, { decimalPlaces: 6 }), 48 | ).toStrictEqual({ decimalPlaces: 6 }); 49 | }); 50 | }); 51 | 52 | describe("case where properties exist in both first and second arguments deeply", () => { 53 | describe("case where first and second arguments have same properties deeply", () => { 54 | it("should return object that values of second argument are used", () => { 55 | expect( 56 | mergePartialGlobalParameters( 57 | { decimalPlaces: 6 }, 58 | { decimalPlaces: 0 }, 59 | ), 60 | ).toStrictEqual({ decimalPlaces: 0 }); 61 | }); 62 | }); 63 | }); 64 | 65 | describe("case where properties do not exist in both first and second arguments", () => { 66 | it("should return empty object", () => { 67 | expect(mergePartialGlobalParameters({}, {})).toStrictEqual({}); 68 | }); 69 | }); 70 | }); 71 | 72 | describe("assignOptionalDecimalPlaces", () => { 73 | describe("case where first and second arguments are not undefined", () => { 74 | it("should return object that second argument is set to decimalPlaces property of first argument", () => { 75 | expect(assignOptionalDecimalPlaces({}, 6)).toStrictEqual({ 76 | decimalPlaces: 6, 77 | }); 78 | }); 79 | }); 80 | 81 | describe("case where first argument is undefined", () => { 82 | it("should return object that second argument is set to decimalPlaces property", () => { 83 | expect(assignOptionalDecimalPlaces(undefined, 6)).toStrictEqual({ 84 | decimalPlaces: 6, 85 | }); 86 | }); 87 | }); 88 | 89 | describe("case where second argument is undefined", () => { 90 | it("should do nothing", () => { 91 | expect(assignOptionalDecimalPlaces({}, undefined)).toStrictEqual({}); 92 | }); 93 | }); 94 | }); 95 | 96 | describe("getDecimalPlaces", () => { 97 | it("should return decimalPlaces property", () => { 98 | expect(getDecimalPlaces({ decimalPlaces: 6 })).toBe(6); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/internal/MediaSpecificParameters.ts: -------------------------------------------------------------------------------- 1 | import * as ContentModule from "./Content.js"; 2 | import { 3 | type Content, 4 | createContent, 5 | mergeOptionalPartialContent, 6 | } from "./Content.js"; 7 | import type { ContentAttribute } from "./ContentAttribute.js"; 8 | import type { DecimalPlaces } from "./DecimalPlaces.js"; 9 | import type { DeepPartial } from "./DeepPartial.js"; 10 | import { createMedia, type Media, mergeOptionalMedia } from "./Media.js"; 11 | 12 | export type MediaSpecificParameters = { 13 | content: Content; 14 | media: Media; 15 | }; 16 | 17 | export const createMediaSpecificParameters = ( 18 | partialMediaSpecificParameters: DeepPartial = {}, 19 | ): MediaSpecificParameters => ({ 20 | content: createContent(partialMediaSpecificParameters.content), 21 | media: createMedia(partialMediaSpecificParameters.media), 22 | }); 23 | 24 | export const mergePartialMediaSpecificParameters = ( 25 | precedingPartialMediaSpecificParameters: DeepPartial, 26 | followingPartialMediaSpecificParameters: DeepPartial, 27 | ): DeepPartial => { 28 | const partialMediaSpecificParameters: DeepPartial = 29 | {}; 30 | const optionalPartialContent = mergeOptionalPartialContent( 31 | precedingPartialMediaSpecificParameters.content, 32 | followingPartialMediaSpecificParameters.content, 33 | ); 34 | const optionalMedia = mergeOptionalMedia( 35 | precedingPartialMediaSpecificParameters.media, 36 | followingPartialMediaSpecificParameters.media, 37 | ); 38 | if (optionalPartialContent) 39 | partialMediaSpecificParameters.content = optionalPartialContent; 40 | if (typeof optionalMedia !== "undefined") 41 | partialMediaSpecificParameters.media = optionalMedia; 42 | return partialMediaSpecificParameters; 43 | }; 44 | 45 | export const createContentAttribute: { 46 | ( 47 | optionalMediaSpecificParameters: MediaSpecificParameters, 48 | optionalDocumentClientWidth: number, 49 | optionalDecimalPlaces: DecimalPlaces, 50 | ): ContentAttribute; 51 | (): ContentAttribute; 52 | } = ( 53 | optionalMediaSpecificParameters?: MediaSpecificParameters, 54 | optionalDocumentClientWidth?: number, 55 | optionalDecimalPlaces?: DecimalPlaces, 56 | ) => 57 | optionalMediaSpecificParameters && 58 | typeof optionalDocumentClientWidth !== "undefined" && 59 | typeof optionalDecimalPlaces !== "undefined" 60 | ? ContentModule.createContentAttribute( 61 | optionalMediaSpecificParameters.content, 62 | optionalDocumentClientWidth, 63 | optionalDecimalPlaces, 64 | ) 65 | : ContentModule.createContentAttribute(); 66 | 67 | export const assignOptionalPartialContent = ( 68 | optionalPartialMediaSpecificParameters: 69 | | DeepPartial 70 | | undefined, 71 | optionalPartialContent: Partial | undefined, 72 | ): DeepPartial => 73 | optionalPartialContent 74 | ? { 75 | ...(optionalPartialMediaSpecificParameters ?? {}), 76 | content: optionalPartialContent, 77 | } 78 | : (optionalPartialMediaSpecificParameters ?? {}); 79 | 80 | export const assignOptionalMedia = ( 81 | optionalPartialMediaSpecificParameters: 82 | | DeepPartial 83 | | undefined, 84 | optionalMedia: Media | undefined, 85 | ): DeepPartial => 86 | typeof optionalMedia !== "undefined" 87 | ? { 88 | ...(optionalPartialMediaSpecificParameters ?? {}), 89 | media: optionalMedia, 90 | } 91 | : (optionalPartialMediaSpecificParameters ?? {}); 92 | 93 | export const createPartialMediaSpecificParametersMerger = 94 | (isMatchingCurrentViewport: (media?: Media) => boolean) => 95 | ( 96 | precedingPartialMediaSpecificParameters: DeepPartial, 97 | followingPartialMediaSpecificParameters: DeepPartial, 98 | ): DeepPartial => 99 | isMatchingCurrentViewport(followingPartialMediaSpecificParameters.media) 100 | ? mergePartialMediaSpecificParameters( 101 | precedingPartialMediaSpecificParameters, 102 | followingPartialMediaSpecificParameters, 103 | ) 104 | : precedingPartialMediaSpecificParameters; 105 | -------------------------------------------------------------------------------- /rollup.config.build.js: -------------------------------------------------------------------------------- 1 | import rollupPluginReplace from "@rollup/plugin-replace"; 2 | import rollupPluginTerser from "@rollup/plugin-terser"; 3 | import rollupPluginTypescript from "@rollup/plugin-typescript"; 4 | import { defineConfig } from "rollup"; 5 | import rollupPluginDelete from "rollup-plugin-delete"; 6 | import packageJson from "./package.json" with { type: "json" }; 7 | import tsconfigJson from "./tsconfig.json" with { type: "json" }; 8 | 9 | // Copyright 10 | const banner = `/*! Viewport Extra v${packageJson.version} | (c) dsktschy | MIT License */`; 11 | 12 | // Global variable name for iife 13 | const name = "ViewportExtra"; 14 | 15 | export default defineConfig( 16 | [ 17 | { 18 | input: "src/entries/viewport-extra.ts", 19 | typescriptTarget: "es2021", 20 | subDirectory: "", 21 | }, 22 | { 23 | input: "src/entries/viewport-extra.ts", 24 | typescriptTarget: "es5", 25 | subDirectory: "es5/", 26 | }, 27 | { 28 | input: "src/entries/extended/viewport-extra.ts", 29 | typescriptTarget: "es2021", 30 | subDirectory: "extended/", 31 | }, 32 | { 33 | input: "src/entries/extended/viewport-extra.ts", 34 | typescriptTarget: "es5", 35 | subDirectory: "extended/es5/", 36 | }, 37 | { 38 | input: "src/entries/immediate/viewport-extra.ts", 39 | typescriptTarget: "es2021", 40 | subDirectory: "immediate/", 41 | }, 42 | { 43 | input: "src/entries/immediate/viewport-extra.ts", 44 | typescriptTarget: "es5", 45 | subDirectory: "immediate/es5/", 46 | }, 47 | { 48 | input: "src/entries/immediate/extended/viewport-extra.ts", 49 | typescriptTarget: "es2021", 50 | subDirectory: "immediate/extended/", 51 | }, 52 | { 53 | input: "src/entries/immediate/extended/viewport-extra.ts", 54 | typescriptTarget: "es5", 55 | subDirectory: "immediate/extended/es5/", 56 | }, 57 | ].map(({ input, typescriptTarget, subDirectory }) => ({ 58 | input, 59 | output: [ 60 | { 61 | file: `dist/${subDirectory}viewport-extra.mjs`, 62 | format: "es", 63 | exports: "named", 64 | sourcemap: true, 65 | banner, 66 | }, 67 | { 68 | file: `dist/${subDirectory}viewport-extra.cjs`, 69 | format: "cjs", 70 | exports: "named", 71 | sourcemap: true, 72 | banner, 73 | }, 74 | { 75 | file: `dist/${subDirectory}viewport-extra.js`, 76 | format: "iife", 77 | exports: "named", 78 | sourcemap: true, 79 | banner, 80 | name, 81 | }, 82 | { 83 | file: `dist/${subDirectory}viewport-extra.min.js`, 84 | format: "iife", 85 | exports: "named", 86 | sourcemap: false, 87 | name, 88 | plugins: [ 89 | rollupPluginTerser({ 90 | compress: { 91 | passes: 3, 92 | pure_getters: true, 93 | }, 94 | format: { 95 | // Copyright of tslib is not required 96 | // https://github.com/microsoft/tslib/pull/96 97 | comments: false, 98 | preamble: banner, 99 | wrap_func_args: false, 100 | }, 101 | }), 102 | ], 103 | }, 104 | ], 105 | plugins: [ 106 | rollupPluginDelete({ 107 | targets: [ 108 | `dist/${subDirectory}viewport-extra.mjs`, 109 | `dist/${subDirectory}viewport-extra.mjs.map`, 110 | `dist/${subDirectory}viewport-extra.cjs`, 111 | `dist/${subDirectory}viewport-extra.cjs.map`, 112 | `dist/${subDirectory}viewport-extra.js`, 113 | `dist/${subDirectory}viewport-extra.js.map`, 114 | `dist/${subDirectory}viewport-extra.min.js`, 115 | ], 116 | }), 117 | rollupPluginReplace({ 118 | preventAssignment: true, 119 | __TYPESCRIPT_TARGET__: `"${typescriptTarget}"`, 120 | }), 121 | rollupPluginTypescript({ 122 | target: typescriptTarget, 123 | include: [ 124 | ...tsconfigJson.include.map((pattern) => `src/${pattern}`), 125 | "globals.d.ts", 126 | ], 127 | // Exit on error if not watching 128 | // https://github.com/rollup/plugins/issues/258#issuecomment-848402026 129 | noEmitOnError: !process.env.ROLLUP_WATCH, 130 | }), 131 | ], 132 | })), 133 | ); 134 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Based on https://docs.github.com/ja/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages 2 | name: Publish to npm 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | dry_run: 8 | description: Dry Run 9 | required: true 10 | default: true 11 | type: boolean 12 | 13 | jobs: 14 | define_variables_to_publish: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | permissions: 18 | contents: read 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 22 | 23 | - name: Setup node 24 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 25 | with: 26 | node-version-file: '.node-version' 27 | cache: 'npm' 28 | 29 | - name: Install packages 30 | shell: bash 31 | run: npm ci --before $(node scripts/print-latest-safe-release-date.ts) 32 | 33 | - name: Run trusted postinstall scripts 34 | shell: bash 35 | run: npm run trusted-postinstall 36 | 37 | - name: Define variables 38 | id: define_variables 39 | shell: bash 40 | run: | 41 | version=$(jq -r .version package.json) 42 | echo "version: $version" 43 | dist_tag= 44 | if [[ $version == *-* ]]; then 45 | dist_tag="next-v${version%%.*}" 46 | next_tagged_version=$(npm view viewport-extra dist-tags.next) 47 | echo "next_tagged_version: $next_tagged_version" 48 | greater_version=$(npm run sort-versions -- "$version" "$next_tagged_version" | tail -n 1) 49 | if [[ $version == $greater_version ]]; then 50 | dist_tag=next 51 | fi 52 | else 53 | dist_tag="latest-v${version%%.*}" 54 | latest_tagged_version=$(npm view viewport-extra dist-tags.latest) 55 | echo "latest_tagged_version: $latest_tagged_version" 56 | greater_version=$(npm run sort-versions -- "$version" "$latest_tagged_version" | tail -n 1) 57 | if [[ $version == $greater_version ]]; then 58 | dist_tag=latest 59 | fi 60 | fi 61 | echo "dist_tag: $dist_tag" 62 | echo "dist_tag=$dist_tag" >> $GITHUB_OUTPUT 63 | outputs: 64 | dist_tag: ${{ steps.define_variables.outputs.dist_tag }} 65 | 66 | publish: 67 | needs: define_variables_to_publish 68 | if: inputs.dry_run == false 69 | timeout-minutes: 15 70 | runs-on: ubuntu-24.04 71 | permissions: 72 | contents: read 73 | id-token: write 74 | steps: 75 | - name: Checkout repository 76 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 77 | 78 | - name: Setup node 79 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 80 | with: 81 | node-version-file: ".node-version" 82 | cache: "npm" 83 | 84 | - name: Install packages 85 | shell: bash 86 | run: npm ci --before $(node scripts/print-latest-safe-release-date.ts) 87 | 88 | - name: Run trusted postinstall scripts 89 | shell: bash 90 | run: npm run trusted-postinstall 91 | 92 | - name: Build 93 | shell: bash 94 | run: npm run build 95 | 96 | - name: Generate type declaration files 97 | shell: bash 98 | run: npm run declare 99 | 100 | - name: Publish to npm 101 | shell: bash 102 | run: npm publish --tag ${{ needs.define_variables_to_publish.outputs.dist_tag }} 103 | 104 | dry_publish: 105 | needs: define_variables_to_publish 106 | if: inputs.dry_run == true 107 | timeout-minutes: 15 108 | runs-on: ubuntu-24.04 109 | permissions: 110 | contents: read 111 | steps: 112 | - name: Checkout repository 113 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 114 | 115 | - name: Setup node 116 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 117 | with: 118 | node-version-file: ".node-version" 119 | cache: "npm" 120 | 121 | - name: Install packages 122 | shell: bash 123 | run: npm ci --before $(node scripts/print-latest-safe-release-date.ts) 124 | 125 | - name: Run trusted postinstall scripts 126 | shell: bash 127 | run: npm run trusted-postinstall 128 | 129 | - name: Build 130 | shell: bash 131 | run: npm run build 132 | 133 | - name: Generate type declaration files 134 | shell: bash 135 | run: npm run declare 136 | 137 | - name: Publish to npm 138 | shell: bash 139 | run: npm publish --tag ${{ needs.define_variables_to_publish.outputs.dist_tag }} --dry-run 140 | -------------------------------------------------------------------------------- /src/internal/ContentAttribute.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | createOptionalPartialContent, 4 | mergeNullableContentAttributes, 5 | } from "./ContentAttribute.js"; 6 | 7 | describe("mergeNullableContentAttributes", () => { 8 | describe("case where only first argument is not null", () => { 9 | it("should return first argument", () => { 10 | expect( 11 | mergeNullableContentAttributes( 12 | "minimum-width=412,maximum-width=768", 13 | null, 14 | ), 15 | ).toBe("minimum-width=412,maximum-width=768"); 16 | }); 17 | }); 18 | 19 | describe("case where only second argument is not null", () => { 20 | it("should return second argument", () => { 21 | expect( 22 | mergeNullableContentAttributes( 23 | null, 24 | "minimum-width=412,maximum-width=768", 25 | ), 26 | ).toBe("minimum-width=412,maximum-width=768"); 27 | }); 28 | }); 29 | 30 | describe("case where first and second arguments are not null", () => { 31 | it("should return string that first and second arguments are joined with comma", () => { 32 | expect( 33 | mergeNullableContentAttributes( 34 | "width=device-width,initial-scale=1,interactive-widget=resizes-content", 35 | "minimum-width=412,maximum-width=768", 36 | ), 37 | ).toBe( 38 | "width=device-width,initial-scale=1,interactive-widget=resizes-content,minimum-width=412,maximum-width=768", 39 | ); 40 | }); 41 | }); 42 | 43 | describe("case where first and second arguments are null", () => { 44 | it("should return null", () => { 45 | expect(mergeNullableContentAttributes(null, null)).toBe(null); 46 | }); 47 | }); 48 | }); 49 | 50 | describe("createOptionalPartialContent", () => { 51 | describe("case where argument is string contains comma", () => { 52 | it("should create properties for each string split by commas, and return them as object", () => { 53 | expect( 54 | createOptionalPartialContent("width=device-width,foo=FOO,bar=BAR"), 55 | ).toStrictEqual({ 56 | width: "device-width", 57 | foo: "FOO", 58 | bar: "BAR", 59 | }); 60 | }); 61 | 62 | describe("case where properties that have same keys are created", () => { 63 | it("should return object that values of later properties are used", () => { 64 | expect( 65 | createOptionalPartialContent("width=device-width,foo=FOO,foo=BAZ"), 66 | ).toStrictEqual({ 67 | width: "device-width", 68 | foo: "BAZ", 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | describe("case where argument is string contains equal", () => { 75 | it("should create properties with part before equal as key and part after equal as value, and return them as object", () => { 76 | expect(createOptionalPartialContent("width=device-width")).toStrictEqual({ 77 | width: "device-width", 78 | }); 79 | }); 80 | 81 | describe("case where string before equal is kebab case", () => { 82 | it("should return object that key of created property is camel case", () => { 83 | expect( 84 | createOptionalPartialContent("interactive-widget=resizes-content"), 85 | ).toStrictEqual({ 86 | interactiveWidget: "resizes-content", 87 | }); 88 | }); 89 | }); 90 | 91 | describe("case where string after equal is number", () => { 92 | it("should return object that value of created property is number type", () => { 93 | expect(createOptionalPartialContent("width=412")).toStrictEqual({ 94 | width: 412, 95 | }); 96 | }); 97 | }); 98 | 99 | describe("case where string contains whitespace", () => { 100 | it("should return object that key and value of created property do not have leading or trailing whitespaces", () => { 101 | expect( 102 | createOptionalPartialContent(" width = device-width "), 103 | ).toStrictEqual({ width: "device-width" }); 104 | }); 105 | }); 106 | 107 | describe("case where string before equal is empty", () => { 108 | it("should return object that does not have property corresponding to string before equal is empty", () => { 109 | expect(createOptionalPartialContent("=device-width")).toStrictEqual({}); 110 | }); 111 | }); 112 | 113 | describe("case where string after equal is empty", () => { 114 | it("should return object that does not have property corresponding to string after equal is empty", () => { 115 | expect(createOptionalPartialContent("width=")).toStrictEqual({}); 116 | }); 117 | }); 118 | }); 119 | 120 | describe("case where argument is null", () => { 121 | it("should return undefined", () => { 122 | expect(createOptionalPartialContent(null)).toBe(undefined); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewport-extra", 3 | "version": "3.0.0", 4 | "description": "Enable setting minimum and maximum viewport width", 5 | "author": "dsktschy (https://github.com/dsktschy)", 6 | "license": "MIT", 7 | "homepage": "https://github.com/dsktschy/viewport-extra#readme", 8 | "bugs": "https://github.com/dsktschy/viewport-extra/issues", 9 | "keywords": [ 10 | "viewport", 11 | "minimum", 12 | "min", 13 | "maximum", 14 | "max", 15 | "width", 16 | "mobile", 17 | "device", 18 | "scale", 19 | "shrink", 20 | "expand", 21 | "media", 22 | "query" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/dsktschy/viewport-extra.git" 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "type": "module", 32 | "exports": { 33 | ".": { 34 | "import": { 35 | "types": "./dist/viewport-extra.d.mts", 36 | "default": "./dist/viewport-extra.mjs" 37 | }, 38 | "require": { 39 | "types": "./dist/viewport-extra.d.cts", 40 | "default": "./dist/viewport-extra.cjs" 41 | } 42 | }, 43 | "./es5": { 44 | "import": { 45 | "types": "./dist/es5/viewport-extra.d.mts", 46 | "default": "./dist/es5/viewport-extra.mjs" 47 | }, 48 | "require": { 49 | "types": "./dist/es5/viewport-extra.d.cts", 50 | "default": "./dist/es5/viewport-extra.cjs" 51 | } 52 | }, 53 | "./extended": { 54 | "import": { 55 | "types": "./dist/extended/viewport-extra.d.mts", 56 | "default": "./dist/extended/viewport-extra.mjs" 57 | }, 58 | "require": { 59 | "types": "./dist/extended/viewport-extra.d.cts", 60 | "default": "./dist/extended/viewport-extra.cjs" 61 | } 62 | }, 63 | "./extended/es5": { 64 | "import": { 65 | "types": "./dist/extended/es5/viewport-extra.d.mts", 66 | "default": "./dist/extended/es5/viewport-extra.mjs" 67 | }, 68 | "require": { 69 | "types": "./dist/extended/es5/viewport-extra.d.cts", 70 | "default": "./dist/extended/es5/viewport-extra.cjs" 71 | } 72 | }, 73 | "./immediate": { 74 | "import": { 75 | "types": "./dist/immediate/viewport-extra.d.mts", 76 | "default": "./dist/immediate/viewport-extra.mjs" 77 | }, 78 | "require": { 79 | "types": "./dist/immediate/viewport-extra.d.cts", 80 | "default": "./dist/immediate/viewport-extra.cjs" 81 | } 82 | }, 83 | "./immediate/es5": { 84 | "import": { 85 | "types": "./dist/immediate/es5/viewport-extra.d.mts", 86 | "default": "./dist/immediate/es5/viewport-extra.mjs" 87 | }, 88 | "require": { 89 | "types": "./dist/immediate/es5/viewport-extra.d.cts", 90 | "default": "./dist/immediate/es5/viewport-extra.cjs" 91 | } 92 | }, 93 | "./immediate/extended": { 94 | "import": { 95 | "types": "./dist/immediate/extended/viewport-extra.d.mts", 96 | "default": "./dist/immediate/extended/viewport-extra.mjs" 97 | }, 98 | "require": { 99 | "types": "./dist/immediate/extended/viewport-extra.d.cts", 100 | "default": "./dist/immediate/extended/viewport-extra.cjs" 101 | } 102 | }, 103 | "./immediate/extended/es5": { 104 | "import": { 105 | "types": "./dist/immediate/extended/es5/viewport-extra.d.mts", 106 | "default": "./dist/immediate/extended/es5/viewport-extra.mjs" 107 | }, 108 | "require": { 109 | "types": "./dist/immediate/extended/es5/viewport-extra.d.cts", 110 | "default": "./dist/immediate/extended/es5/viewport-extra.cjs" 111 | } 112 | } 113 | }, 114 | "main": "./dist/viewport-extra.cjs", 115 | "module": "./dist/viewport-extra.mjs", 116 | "types": "./dist/viewport-extra.d.mts", 117 | "jsdelivr": "./dist/immediate/viewport-extra.min.js", 118 | "unpkg": "./dist/immediate/viewport-extra.min.js", 119 | "sideEffects": [ 120 | "./dist/immediate/viewport-extra.mjs", 121 | "./dist/immediate/viewport-extra.cjs", 122 | "./dist/immediate/es5/viewport-extra.mjs", 123 | "./dist/immediate/es5/viewport-extra.cjs", 124 | "./dist/immediate/extended/viewport-extra.mjs", 125 | "./dist/immediate/extended/viewport-extra.cjs", 126 | "./dist/immediate/extended/es5/viewport-extra.mjs", 127 | "./dist/immediate/extended/es5/viewport-extra.cjs" 128 | ], 129 | "devDependencies": { 130 | "@biomejs/biome": "2.1.1", 131 | "@commitlint/cli": "19.4.0", 132 | "@commitlint/config-conventional": "19.2.2", 133 | "@eslint/compat": "1.2.8", 134 | "@playwright/test": "1.56.1", 135 | "@rollup/plugin-replace": "6.0.2", 136 | "@rollup/plugin-terser": "0.4.4", 137 | "@rollup/plugin-typescript": "12.1.0", 138 | "@types/node": "22.5.2", 139 | "@typescript-eslint/parser": "8.39.1", 140 | "cspell": "8.18.0", 141 | "eslint": "9.31.0", 142 | "eslint-plugin-compat": "6.0.2", 143 | "jsdom": "26.1.0", 144 | "lefthook": "1.11.0", 145 | "npm-run-all2": "7.0.2", 146 | "prettier": "3.6.2", 147 | "rimraf": "6.0.1", 148 | "rollup": "4.24.0", 149 | "rollup-plugin-delete": "3.0.1", 150 | "rollup-plugin-dts": "6.1.1", 151 | "semver": "7.7.2", 152 | "tslib": "2.6.3", 153 | "tsx": "4.19.3", 154 | "typescript": "5.5.4", 155 | "vite": "6.4.1", 156 | "vitest": "3.0.8" 157 | }, 158 | "scripts": { 159 | "build": "rollup --config rollup.config.build.js --environment NODE_ENV:production", 160 | "declare": "npm-run-all --sequential declare:clean declare:execute declare:bundle declare:clean", 161 | "declare:clean": "rimraf .types", 162 | "declare:execute": "tsx scripts/create-declaration-with-dts.ts \"src/entries/**/*.ts\" --output-directory .types", 163 | "declare:bundle": "rollup --config rollup.config.declare.js --environment NODE_ENV:production", 164 | "commitcheck": "commitlint --edit", 165 | "spellcheck": "cspell lint --no-must-find-files", 166 | "stylecheck:code": "biome check --no-errors-on-unmatched", 167 | "stylecheck:docs": "prettier --check --no-error-on-unmatched-pattern", 168 | "es5check": "eslint \"src/**/*\" --ignore-pattern \"src/**/*.spec.*\" --no-warn-ignored --config eslint.config.es5.js", 169 | "typecheck": "npm-run-all --sequential declare typecheck:execute", 170 | "typecheck:execute": "tsc --noEmit", 171 | "test": "npm-run-all --parallel test:*", 172 | "test:unit": "vitest run", 173 | "test:e2e": "npm-run-all --parallel test:e2e:install build --sequential test:e2e:build test:e2e:execute", 174 | "test:e2e:install": "playwright install --with-deps", 175 | "test:e2e:build": "vite build --config vite.config.e2e.ts", 176 | "test:e2e:execute": "playwright test --pass-with-no-tests", 177 | "preview-e2e": "vite preview --config vite.config.e2e.ts --strictPort --port 3000", 178 | "sort-versions": "semver", 179 | "trusted-postinstall": "npm-run-all --parallel trusted-postinstall:*", 180 | "trusted-postinstall:esbuild": "npm explore esbuild -- npm run postinstall" 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /docs/ja/migration-from-v1.md: -------------------------------------------------------------------------------- 1 | # v1 から v3 への移行ガイド 2 | 3 | [English](/docs/en/migration-from-v1.md) | **日本語** 4 | 5 | このガイドは、Viewport Extra v1 と v3 の違いを説明するものです。v1 も引き続き使用できますが、v3 へ移行することで、ページ表示遅延の回避、ブラウザの幅ごとに異なる最小幅・最大幅を適用する機能の追加といった改善が得られます。 6 | 7 | ## ハイライト 8 | 9 | ```diff 10 | - v1 の構文 11 | + v3 の構文 12 | ``` 13 | 14 | ### スクリプトを使用する場合 15 | 16 | 17 | 18 | ```diff 19 | 20 | + 21 | 26 | - 27 | 28 | 29 | + 30 | 35 | - 36 | 37 | 38 | 43 | ``` 44 | 45 | 46 | 47 | ### モジュールを使用する場合 48 | 49 | ```diff 50 | - import ViewportExtra from "viewport-extra" 51 | - new ViewportExtra(412) 52 | + import("viewport-extra").then(({ apply }) => { 53 | + apply([{ content: { minWidth: 412 } }]) 54 | + }) 55 | 56 | - import ViewportExtra from "viewport-extra" 57 | - new ViewportExtra({ minWidth: 412, maxWidth: 640 }) 58 | + import("viewport-extra").then(({ apply }) => { 59 | + apply([{ content: { minWidth: 412, maxWidth: 640 } }]) 60 | + }) 61 | 62 | /* ES2015+ をサポートしない環境で動作させる場合 */ 63 | - import "viewport-extra" 64 | + import("viewport-extra/es5") 65 | ``` 66 | 67 | ## 詳細 68 | 69 | - [ビルドの選択](#ビルドの選択) 70 | - [最小幅・最大幅適用 API](#最小幅最大幅適用-api) 71 | 72 | ### ビルドの選択 73 | 74 | v1 では、単一のビルドを提供しています。このビルドは、API の呼び出しが必要であり、` 92 | 93 | ``` 94 | 95 | ##### v3 の構文 96 | 97 | 複数のビルドを選択できます。 98 | 99 | | URL のファイルパス | `meta` 要素読み取り・即時適用 | 高度な機能 | レガシー環境での動作 | 100 | | ---------------------------------------------------: | :---------------------------: | :--------: | :------------------: | 101 | | `/dist/viewport-extra.min.js` | - | - | - | 102 | | `/dist/es5/viewport-extra.min.js` | - | - | ✔ | 103 | | `/dist/extended/viewport-extra.min.js` | - | ✔ | - | 104 | | `/dist/extended/es5/viewport-extra.min.js` | - | ✔ | ✔ | 105 | | `/dist/immediate/viewport-extra.min.js` | ✔ | - | - | 106 | | `/dist/immediate/es5/viewport-extra.min.js` | ✔ | - | ✔ | 107 | | `/dist/immediate/extended/viewport-extra.min.js` | ✔ | ✔ | - | 108 | | `/dist/immediate/extended/es5/viewport-extra.min.js` | ✔ | ✔ | ✔ | 109 | 110 | `meta` 要素読み取り・即時適用が可能なビルドを選択し、API の呼び出しを不要とすることで、` 119 | ``` 120 | 121 | 122 | 123 | ES2015+ をサポートしない環境で動作させる場合は、URL のファイルパスに `es5` を含むビルドが必要です。 124 | 125 | 126 | 127 | ```html 128 | 129 | 130 | 131 | 132 | ``` 133 | 134 | 135 | 136 | ビルドの選択が難しい場合は、URL のファイルパスが `/dist/immediate/extended/es5/viewport-extra.min.js` のビルドにすべての機能が含まれています。 137 | 138 | 139 | 140 | ```html 141 | 142 | 143 | 144 | 145 | ``` 146 | 147 | 148 | 149 | #### モジュールを使用する場合 150 | 151 | ##### v1 の構文 152 | 153 | 単一のビルドのみを選択できます。`import` 宣言を使用する必要があります。 154 | 155 | ```js 156 | import "viewport-extra" 157 | ``` 158 | 159 | ##### v3 の構文 160 | 161 | 複数のビルドを選択できます。 162 | 163 | | モジュール指定子 | `meta` 要素読み取り・即時適用 | 高度な機能 | レガシー環境での動作 | 164 | | --------------------------------------: | :---------------------------: | :--------: | :------------------: | 165 | | `viewport-extra` | - | - | - | 166 | | `viewport-extra/es5` | - | - | ✔ | 167 | | `viewport-extra/extended` | - | ✔ | - | 168 | | `viewport-extra/extended/es5` | - | ✔ | ✔ | 169 | | `viewport-extra/immediate` | ✔ | - | - | 170 | | `viewport-extra/immediate/es5` | ✔ | - | ✔ | 171 | | `viewport-extra/immediate/extended` | ✔ | ✔ | - | 172 | | `viewport-extra/immediate/extended/es5` | ✔ | ✔ | ✔ | 173 | 174 | `import()` 構文を使用することができます。モジュール指定子が `viewport-extra` のビルドが最小サイズであり、理想的です。 175 | 176 | ```js 177 | import("viewport-extra") 178 | ``` 179 | 180 | ES2015+ をサポートしない環境で動作させる場合は、モジュール指定子に `es5` を含むビルドが必要です。 181 | 182 | ```js 183 | import("viewport-extra/es5") 184 | ``` 185 | 186 | ビルドの選択が難しい場合は、モジュール指定子が `viewport-extra/immediate/extended/es5` のビルドにすべての機能が含まれています。 187 | 188 | ```js 189 | import("viewport-extra/immediate/extended/es5") 190 | ``` 191 | 192 | ### 最小幅・最大幅適用 API 193 | 194 | v1 の最小幅・最大幅適用 API である `ViewportExtra` コンストラクタは、引数として単一の最小幅・最大幅を受け取ります。ブラウザの幅ごとに異なる最小幅・最大幅を適用することはできません。 195 | 196 | v3 の最小幅・最大幅適用 API である `apply()` 関数は、引数として複数の最小幅・最大幅を、メディアクエリとともに受け取ります。 197 | 198 | #### スクリプトを使用する場合 199 | 200 | ##### v1 の構文 201 | 202 | 最小幅・最大幅の適用には、`ViewportExtra` コンストラクタを使用します。引数として単一の最小幅、もしくは単一の最小幅・最大幅を受け取ります。 203 | 204 | ```html 205 | 208 | ``` 209 | 210 | ```html 211 | 214 | ``` 215 | 216 | ##### v3 の構文 217 | 218 | 最小幅・最大幅の適用には、`apply()` 関数を使用します。 219 | 220 | ```html 221 | 224 | ``` 225 | 226 | 引数として複数の最小幅・最大幅を、メディアクエリとともに受け取ることができます。 227 | 228 | ```html 229 | 235 | ``` 236 | 237 | #### モジュールを使用する場合 238 | 239 | ##### v1 の構文 240 | 241 | 最小幅・最大幅の適用には、`ViewportExtra` コンストラクタを使用します。引数として単一の最小幅・最大幅を受け取ります。 242 | 243 | ```js 244 | import ViewportExtra from "viewport-extra" 245 | 246 | new ViewportExtra(412) 247 | ``` 248 | 249 | ```js 250 | import ViewportExtra from "viewport-extra" 251 | 252 | new ViewportExtra({ minWidth: 412 }) 253 | ``` 254 | 255 | ##### v3 の構文 256 | 257 | 最小幅・最大幅の適用には、`apply()` 関数を使用します。 258 | 259 | ```js 260 | import("viewport-extra").then(({ apply }) => { 261 | apply([{ content: { minWidth: 412 } }]) 262 | }) 263 | ``` 264 | 265 | 引数として複数の最小幅・最大幅を、メディアクエリとともに受け取ることができます。 266 | 267 | ```js 268 | import("viewport-extra").then(({ apply }) => { 269 | apply([ 270 | { content: { minWidth: 412 } }, // media を省略した場合はデフォルトの "" となる 271 | { content: { minWidth: 1024 }, media: "(min-width: 744px)" }, 272 | ]) 273 | }) 274 | ``` 275 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # Viewport Extra [![](https://data.jsdelivr.com/v1/package/npm/viewport-extra/badge)](https://www.jsdelivr.com/package/npm/viewport-extra) [![npm version](https://img.shields.io/npm/v/viewport-extra.svg?style=flat-square)](https://www.npmjs.com/package/viewport-extra) [![GitHub license](https://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](https://github.com/dsktschy/viewport-extra/blob/master/LICENSE.txt) 2 | 3 | [English](https://github.com/dsktschy/viewport-extra/blob/master/README.md) | **日本語** 4 | 5 | > [!IMPORTANT] 6 | > 7 | > **_v3 には破壊的変更が含まれます。_** 8 | > 9 | > - 参考: [v2 から v3 への移行ガイド](https://github.com/dsktschy/viewport-extra/blob/master/docs/ja/migration-from-v2.md) 10 | > - 参考: [v1 から v3 への移行ガイド](https://github.com/dsktschy/viewport-extra/blob/master/docs/ja/migration-from-v1.md) 11 | > 12 | > v2 および v1 もメンテナンスが継続されるため、引き続き使用可能です。 13 | 14 | Viewport Extra は、ビューポートの最小幅および最大幅の設定を可能にするライブラリです。これにより、スタイリング時に考慮すべきビューポートの範囲を狭めることができます。 15 | 16 | 21 |
22 | Viewport Extra 適用前 26 | 27 | 31 | 35 | 39 | 43 | 44 | Viewport Extra 適用後 48 |
49 | 50 | たとえば、幅 412px のページを、ビューポート幅 360px のモバイル向けブラウザ (例: 縦向きの Galaxy S24 上の Chrome) で表示すると、横方向のスクロールが発生してしまいます。これは、412px 未満のビューポート幅のためにスタイルを追加することで解決できますが、その作業は面倒です。しかし、Viewport Extra でビューポートの最小幅を 412px に設定すれば、そのページは 360px にぴったり収まるように縮小され、横方向のスクロールが発生しません。スタイルを追加することなく、簡単に解決できます。 51 | 52 | ページの拡大・縮小は、`` 要素の `content` 属性の書き換えにより行われます。 53 | 54 | Viewport Extra は、` 80 | ``` 81 | 82 | 83 | 84 | ##### モジュールを使用する場合 85 | 86 | ```js 87 | import("viewport-extra").then(({ apply }) => { 88 | apply([{ content: { minimumWidth: 412 } }]) 89 | }) 90 | ``` 91 | 92 | #### `` 要素の `content` 属性の書き換え結果 93 | 94 | ##### Galaxy S24 縦向き Chrome (360px) 95 | 96 | `initial-scale=0.8737864077669902,width=412` 97 | 98 | ##### iPhone 15 縦向き Safari (393px) 99 | 100 | `initial-scale=0.9538834951456311,width=412` 101 | 102 | ##### Google Pixel 8 縦向き Chrome (412px) 103 | 104 | `initial-scale=1,width=device-width` 105 | 106 | ##### iPhone 15 横向き Safari (734px) 107 | 108 | `initial-scale=1,width=device-width` 109 | 110 | ##### iPad Pro 12.9" 縦向き Safari (1024px) 111 | 112 | `initial-scale=1,width=device-width` 113 | 114 | ### 大きなビューポート幅でページを拡大する 115 | 116 | 次のコードを含むページは、ビューポート幅が 393px を超えるモバイル向けブラウザでは拡大され、それ以外のブラウザでは拡大されません。拡大すべきかどうかの判定は、ページが表示されるときに一度だけ行われます [(参考)](#ビューポート幅が変わるときにもページを拡大縮小する) 。 117 | 118 | #### 実装 119 | 120 | ##### スクリプトを使用する場合 121 | 122 | 123 | 124 | ```html 125 | 126 | 127 | 128 | 129 | ``` 130 | 131 | 132 | 133 | ##### モジュールを使用する場合 134 | 135 | ```js 136 | import("viewport-extra").then(({ apply }) => { 137 | apply([{ content: { maximumWidth: 393 } }]) 138 | }) 139 | ``` 140 | 141 | #### `` 要素の `content` 属性の書き換え結果 142 | 143 | ##### Galaxy S24 縦向き Chrome (360px) 144 | 145 | `initial-scale=1,width=device-width` 146 | 147 | ##### iPhone 15 縦向き Safari (393px) 148 | 149 | `initial-scale=1,width=device-width` 150 | 151 | ##### Google Pixel 8 縦向き Chrome (412px) 152 | 153 | `initial-scale=1.0483460559796438,width=393` 154 | 155 | ##### iPhone 15 横向き Safari (734px) 156 | 157 | `initial-scale=1.8676844783715012,width=393` 158 | 159 | ##### iPad Pro 12.9" 縦向き Safari (1024px) 160 | 161 | `initial-scale=2.6055979643765905,width=393` 162 | 163 | ### メディアクエリごとに異なる最小幅・最大幅を設定する 164 | 165 | 次のコードを含むページは、ビューポート幅が 412px 未満または 744px 以上 1024px 未満のモバイル向けブラウザでは縮小され、それ以外のブラウザでは縮小されません。縮小すべきかどうかの判定は、ページが表示されるときに一度だけ行われます [(参考)](#ビューポート幅が変わるときにもページを拡大縮小する) 。 166 | 167 | #### 実装 168 | 169 | ##### スクリプトを使用する場合 170 | 171 | 172 | 173 | ```html 174 | 175 | 176 | 177 | 178 | 179 | ``` 180 | 181 | 182 | 183 | ##### モジュールを使用する場合 184 | 185 | ```js 186 | import("viewport-extra").then(({ apply }) => { 187 | apply([ 188 | { content: { minimumWidth: 412 } }, 189 | { content: { minimumWidth: 1024 }, media: "(min-width: 744px)" }, 190 | ]) 191 | }) 192 | ``` 193 | 194 | #### `` 要素の `content` 属性の書き換え結果 195 | 196 | ##### Galaxy S24 縦向き Chrome (360px) 197 | 198 | `initial-scale=0.8737864077669902,width=412` 199 | 200 | ##### Google Pixel 8 縦向き Chrome (412px) 201 | 202 | `initial-scale=1,width=device-width` 203 | 204 | ##### iPad mini 第6世代 縦向き Safari (744px) 205 | 206 | `initial-scale=0.7265625,width=1024` 207 | 208 | ##### iPad Pro 12.9" 縦向き Safari (1024px) 209 | 210 | `initial-scale=1,width=device-width` 211 | 212 | ### ビューポート幅が変わるときにもページを拡大・縮小する 213 | 214 | 次のコードを含むページは、表示されるときだけでなく、ビューポート幅が変わるときにも拡大・縮小すべきかどうかの判定を行います。モバイル端末の縦向き・横向きの切り替えや、タブレットの画面分割が想定される場合に有用です。 215 | 216 | #### 実装 217 | 218 | ##### スクリプトを使用する場合 219 | 220 | 221 | 222 | ```html 223 | 224 | 225 | 230 | 231 | 252 | ``` 253 | 254 | 255 | 256 | ##### モジュールを使用する場合 257 | 258 | ```js 259 | import("viewport-extra").then(({ apply }) => { 260 | const updateViewportMetaEl = () => { 261 | // 無限リサイズを回避する 262 | new ResizeObserver((_, observer) => { 263 | observer.unobserve(document.documentElement) 264 | window.addEventListener("resize", updateViewportMetaEl, { once: true }) 265 | }).observe(document.documentElement) 266 | 267 | apply([ 268 | { content: { minimumWidth: 412 } }, 269 | { content: { minimumWidth: 744 }, media: "(min-width: 640px)" }, 270 | ]) 271 | } 272 | updateViewportMetaEl() 273 | }) 274 | ``` 275 | 276 | #### `` 要素の `content` 属性の書き換え結果 277 | 278 | ##### iPhone 15 縦向き Safari (393px) 279 | 280 | `initial-scale=0.9538834951456311,width=412` 281 | 282 | ##### iPhone 15 横向き Safari (734px) 283 | 284 | `initial-scale=0.9865591397849462,width=744` 285 | 286 | ### レガシーな環境でもページを拡大・縮小する 287 | 288 | ここまでに使用している標準的なビルドには、ES2021 の構文、および Viewport Extra v3.0.0 公開時点で [Web Platform Baseline](https://web.dev/baseline?hl=ja) の Widely Available ステージにある機能が含まれます。これらをサポートしない環境 (例: iOS Safari < 16, Android Chrome < 108) でも Viewport Extra を動作させるためには、es5 ビルドを使用します [(参考)](https://github.com/dsktschy/viewport-extra/blob/master/docs/ja/migration-from-v2.md#ビルドの選択) 。 289 | 290 | #### 実装 291 | 292 | ##### スクリプトを使用する場合 293 | 294 | 295 | 296 | ```html 297 | 298 | 299 | 300 | 301 | ``` 302 | 303 | 304 | 305 | ##### モジュールを使用する場合 306 | 307 | ```js 308 | import("viewport-extra/immediate/es5").then(({ apply }) => { 309 | apply([{ content: { minimumWidth: 412 } }]) 310 | }) 311 | ``` 312 | 313 | #### `` 要素の `content` 属性の書き換え結果 314 | 315 | ##### iPhone 7 縦向き Safari (375px) 316 | 317 | `initial-scale=0.9101941747572816,width=412` 318 | 319 | ##### iPhone 7 横向き Safari (667px) 320 | 321 | `initial-scale=1,width=device-width` 322 | 323 | ### `` 要素を使わずにページを拡大・縮小する 324 | 325 | 次のコードを含むページは、[`` 要素を使用した実装](#スクリプトを使用する場合-2)と同様に動作します。 326 | 327 | #### 実装 328 | 329 | 330 | 331 | ```html 332 | 333 | 334 | 335 | 336 | 337 | ``` 338 | 339 | 340 | 341 | #### `` 要素の `content` 属性の書き換え結果 342 | 343 | ##### Galaxy S24 縦向き Chrome (360px) 344 | 345 | `initial-scale=0.8737864077669902,width=412` 346 | 347 | ##### Google Pixel 8 縦向き Chrome (412px) 348 | 349 | `initial-scale=1,width=device-width` 350 | 351 | ##### iPad mini 第6世代 縦向き Safari (744px) 352 | 353 | `initial-scale=0.7265625,width=1024` 354 | 355 | ##### iPad Pro 12.9" 縦向き Safari (1024px) 356 | 357 | `initial-scale=1,width=device-width` 358 | 359 | ## 補足 360 | 361 | - `minimum-width` / `maximum-width` の代わりに、`min-width` / `max-width` を使用できます。ただし、両方が混在する場合の動作は保証されないため、どちらか一方に統一する必要があります。 362 | 363 | ```html 364 | 365 | ``` 366 | 367 | 同様に、`minimumWidth` / `maximumWidth` の代わりに、`minWidth` / `maxWidth` を使用できます。これらも、両方が混在する場合の動作は保証されないため、どちらか一方に統一する必要があります。 368 | 369 | ```js 370 | apply([{ content: { minWidth: 412, maxWidth: 640 } }]) 371 | ``` 372 | 373 | - 次のスタイルを併用することを推奨します。小さなモバイル端末における、ブラウザによる意図しないテキストサイズの調整を防ぎます [(参考)](https://stackoverflow.com/q/6210788) 。 374 | 375 | ```css 376 | body { 377 | -webkit-text-size-adjust: 100%; 378 | } 379 | ``` 380 | 381 | - デスクトップ向けブラウザの開発者ツールで動作を確認する場合、Viewport Extra を使用するページへ移動するよりも先に、モバイル端末のシミュレーションを有効化し、ビューポートを目的のサイズに設定しておく必要があります。順番が逆である場合、ブラウザが `` 要素の `initial-scale` の設定を無視してしまう状態となります。これは、開発者ツールのシミュレーションに特有の現象であり、実際のモバイル向けブラウザでは発生しません。 382 | -------------------------------------------------------------------------------- /docs/en/migration-from-v1.md: -------------------------------------------------------------------------------- 1 | # Migration Guide from v1 to v3 2 | 3 | **English** | [日本語](/docs/ja/migration-from-v1.md) 4 | 5 | This guide explains the differences between Viewport Extra v1 and v3. While v1 can still be used, migrating to v3 offers improvements such as avoiding page display delays and adding the feature to apply different minimum / maximum widths for each browser width. 6 | 7 | ## Highlights 8 | 9 | ```diff 10 | - v1 syntax 11 | + v3 syntax 12 | ``` 13 | 14 | ### Using Script 15 | 16 | 17 | 18 | ```diff 19 | 20 | + 21 | 26 | - 27 | 28 | 29 | + 30 | 35 | - 36 | 37 | 38 | 43 | ``` 44 | 45 | 46 | 47 | ### Using Module 48 | 49 | ```diff 50 | - import ViewportExtra from "viewport-extra" 51 | - new ViewportExtra(412) 52 | + import("viewport-extra").then(({ apply }) => { 53 | + apply([{ content: { minWidth: 412 } }]) 54 | + }) 55 | 56 | - import ViewportExtra from "viewport-extra" 57 | - new ViewportExtra({ minWidth: 412, maxWidth: 640 }) 58 | + import("viewport-extra").then(({ apply }) => { 59 | + apply([{ content: { minWidth: 412, maxWidth: 640 } }]) 60 | + }) 61 | 62 | /* For environments that do not support ES2015+ */ 63 | - import "viewport-extra" 64 | + import("viewport-extra/es5") 65 | ``` 66 | 67 | ## Details 68 | 69 | - [Build Selection](#build-selection) 70 | - [Minimum / Maximum Widths Application API](#minimum--maximum-widths-application-api) 71 | 72 | ### Build Selection 73 | 74 | In v1, a single build is provided. This build requires API calls, making it difficult to use with the `async` attribute of the ` 92 | 93 | ``` 94 | 95 | ##### v3 Syntax 96 | 97 | Multiple builds can be selected. 98 | 99 | | File Path in URL | `meta` Element Parsing and Immediate Application | Advanced Features | Support for Legacy Environments | 100 | | ---------------------------------------------------: | :----------------------------------------------: | :---------------: | :-----------------------------: | 101 | | `/dist/viewport-extra.min.js` | - | - | - | 102 | | `/dist/es5/viewport-extra.min.js` | - | - | ✔ | 103 | | `/dist/extended/viewport-extra.min.js` | - | ✔ | - | 104 | | `/dist/extended/es5/viewport-extra.min.js` | - | ✔ | ✔ | 105 | | `/dist/immediate/viewport-extra.min.js` | ✔ | - | - | 106 | | `/dist/immediate/es5/viewport-extra.min.js` | ✔ | - | ✔ | 107 | | `/dist/immediate/extended/viewport-extra.min.js` | ✔ | ✔ | - | 108 | | `/dist/immediate/extended/es5/viewport-extra.min.js` | ✔ | ✔ | ✔ | 109 | 110 | A build that supports `meta` element parsing and immediate application, eliminating the need to call the API, can be selected and used with the `async` attribute of the ` 119 | ``` 120 | 121 | 122 | 123 | To ensure compatibility with environments that do not support ES2015+, selecting a build that includes `es5` in the file path in the URL is required. 124 | 125 | 126 | 127 | ```html 128 | 129 | 130 | 131 | 132 | ``` 133 | 134 | 135 | 136 | If it's difficult to determine the appropriate build, the build with the file path `/dist/immediate/extended/es5/viewport-extra.min.js` in the URL includes all features. 137 | 138 | 139 | 140 | ```html 141 | 142 | 143 | 144 | 145 | ``` 146 | 147 | 148 | 149 | #### Using Module 150 | 151 | ##### v1 Syntax 152 | 153 | Only a single build can be selected. The `import` declaration must be used. 154 | 155 | ```js 156 | import "viewport-extra" 157 | ``` 158 | 159 | ##### v3 Syntax 160 | 161 | Multiple builds can be selected. 162 | 163 | | Module Specifier | `meta` Element Parsing and Immediate Application | Advanced Features | Support for Legacy Environments | 164 | | --------------------------------------: | :----------------------------------------------: | :---------------: | :-----------------------------: | 165 | | `viewport-extra` | - | - | - | 166 | | `viewport-extra/es5` | - | - | ✔ | 167 | | `viewport-extra/extended` | - | ✔ | - | 168 | | `viewport-extra/extended/es5` | - | ✔ | ✔ | 169 | | `viewport-extra/immediate` | ✔ | - | - | 170 | | `viewport-extra/immediate/es5` | ✔ | - | ✔ | 171 | | `viewport-extra/immediate/extended` | ✔ | ✔ | - | 172 | | `viewport-extra/immediate/extended/es5` | ✔ | ✔ | ✔ | 173 | 174 | The `import()` syntax can be used. The build with the module specifier `viewport-extra` is the lightest and ideal. 175 | 176 | ```js 177 | import("viewport-extra") 178 | ``` 179 | 180 | To ensure compatibility with environments that do not support ES2015+, selecting a build that includes `es5` in the module specifier is required. 181 | 182 | ```js 183 | import("viewport-extra/es5") 184 | ``` 185 | 186 | If it's difficult to determine the appropriate build, the build with the module specifier `viewport-extra/immediate/extended/es5` includes all features. 187 | 188 | ```js 189 | import("viewport-extra/immediate/extended/es5") 190 | ``` 191 | 192 | ### Minimum / Maximum Widths Application API 193 | 194 | The minimum / maximum widths application API in v1, the `ViewportExtra` constructor, accepts a single pair of minimum / maximum widths as arguments. It cannot apply different minimum / maximum widths for each browser width. 195 | 196 | The minimum / maximum widths application API in v3, `apply()` function, takes multiple minimum / maximum widths along with media queries as arguments. 197 | 198 | #### Using Script 199 | 200 | ##### v1 Syntax 201 | 202 | To apply minimum / maximum widths, use the `ViewportExtra` constructor. It accepts a single minimum width or a single pair of minimum / maximum widths as arguments. 203 | 204 | ```html 205 | 208 | ``` 209 | 210 | ```html 211 | 214 | ``` 215 | 216 | ##### v3 Syntax 217 | 218 | To apply minimum / maximum widths, use `apply()` function. 219 | 220 | ```html 221 | 224 | ``` 225 | 226 | It can accept multiple minimum / maximum widths along with media queries as arguments. 227 | 228 | ```html 229 | 235 | ``` 236 | 237 | #### Using Module 238 | 239 | ##### v1 Syntax 240 | 241 | To apply minimum / maximum widths, use the `ViewportExtra` constructor. It accepts a single minimum width or a single pair of minimum / maximum widths as arguments. 242 | 243 | ```js 244 | import ViewportExtra from "viewport-extra" 245 | 246 | new ViewportExtra(412) 247 | ``` 248 | 249 | ```js 250 | import ViewportExtra from "viewport-extra" 251 | 252 | new ViewportExtra({ minWidth: 412 }) 253 | ``` 254 | 255 | ##### v3 Syntax 256 | 257 | To apply minimum / maximum widths, use `apply()` function. 258 | 259 | ```js 260 | import("viewport-extra").then(({ apply }) => { 261 | apply([{ content: { minWidth: 412 } }]) 262 | }) 263 | ``` 264 | 265 | It can accept multiple minimum / maximum widths along with media queries as arguments. 266 | 267 | ```js 268 | import("viewport-extra").then(({ apply }) => { 269 | apply([ 270 | { content: { minWidth: 412 } }, // If media is omitted, the default is "" 271 | { content: { minWidth: 1024 }, media: "(min-width: 744px)" }, 272 | ]) 273 | }) 274 | ``` 275 | -------------------------------------------------------------------------------- /src/internal/Content.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | createContent, 4 | createContentAttribute, 5 | mergeOptionalPartialContent, 6 | } from "./Content.js"; 7 | 8 | describe("createContent", () => { 9 | describe("case where argument has properties", () => { 10 | it("should return object that inherits properties of argument", () => { 11 | expect( 12 | createContent({ 13 | width: "device-width", 14 | initialScale: 1, 15 | minimumWidth: 412, 16 | maximumWidth: 768, 17 | minWidth: 412, 18 | maxWidth: 768, 19 | interactiveWidget: "resizes-content", 20 | }), 21 | ).toStrictEqual({ 22 | width: "device-width", 23 | initialScale: 1, 24 | minimumWidth: 412, 25 | maximumWidth: 768, 26 | minWidth: 412, 27 | maxWidth: 768, 28 | interactiveWidget: "resizes-content", 29 | }); 30 | }); 31 | }); 32 | 33 | describe("case where argument is missing properties as Content type", () => { 34 | it("should return object with default values for missing properties as Content type", () => { 35 | expect(createContent({})).toStrictEqual({ 36 | width: "device-width", 37 | initialScale: 1, 38 | minimumWidth: 0, 39 | maximumWidth: Infinity, 40 | }); 41 | }); 42 | }); 43 | 44 | describe("case where argument is undefined", () => { 45 | it("should return object with default values of Content type", () => { 46 | expect(createContent()).toStrictEqual({ 47 | width: "device-width", 48 | initialScale: 1, 49 | minimumWidth: 0, 50 | maximumWidth: Infinity, 51 | }); 52 | }); 53 | }); 54 | }); 55 | 56 | describe("mergeOptionalPartialContent", () => { 57 | describe("case where only first argument is not undefined", () => { 58 | it("should return first argument", () => { 59 | expect( 60 | mergeOptionalPartialContent( 61 | { 62 | width: "device-width", 63 | minimumWidth: 412, 64 | maxWidth: 768, 65 | interactiveWidget: "resizes-content", 66 | }, 67 | undefined, 68 | ), 69 | ).toStrictEqual({ 70 | width: "device-width", 71 | minimumWidth: 412, 72 | maxWidth: 768, 73 | interactiveWidget: "resizes-content", 74 | }); 75 | }); 76 | }); 77 | 78 | describe("case where only second argument is not undefined", () => { 79 | it("should return second argument", () => { 80 | expect( 81 | mergeOptionalPartialContent(undefined, { 82 | width: "device-width", 83 | minimumWidth: 412, 84 | maxWidth: 768, 85 | interactiveWidget: "resizes-content", 86 | }), 87 | ).toStrictEqual({ 88 | width: "device-width", 89 | minimumWidth: 412, 90 | maxWidth: 768, 91 | interactiveWidget: "resizes-content", 92 | }); 93 | }); 94 | }); 95 | 96 | describe("case where first and second arguments are not undefined", () => { 97 | it("should return object that first and second arguments are merged", () => { 98 | expect( 99 | mergeOptionalPartialContent( 100 | { 101 | width: "device-width", 102 | initialScale: 1, 103 | minWidth: 412, 104 | maxWidth: 768, 105 | }, 106 | { 107 | minimumWidth: 412, 108 | maximumWidth: 768, 109 | interactiveWidget: "resizes-content", 110 | }, 111 | ), 112 | ).toStrictEqual({ 113 | width: "device-width", 114 | initialScale: 1, 115 | minimumWidth: 412, 116 | maximumWidth: 768, 117 | minWidth: 412, 118 | maxWidth: 768, 119 | interactiveWidget: "resizes-content", 120 | }); 121 | }); 122 | 123 | describe("case where first and second arguments have same properties", () => { 124 | it("should return object that values of second argument are used", () => { 125 | expect( 126 | mergeOptionalPartialContent( 127 | { 128 | width: "device-width", 129 | initialScale: 1, 130 | interactiveWidget: "resizes-content", 131 | }, 132 | { 133 | width: 412, 134 | initialScale: 2, 135 | interactiveWidget: "overlays-content", 136 | }, 137 | ), 138 | ).toStrictEqual({ 139 | width: 412, 140 | initialScale: 2, 141 | interactiveWidget: "overlays-content", 142 | }); 143 | }); 144 | }); 145 | }); 146 | 147 | describe("case where first and second arguments are undefined", () => { 148 | it("should return undefined", () => { 149 | expect(mergeOptionalPartialContent(undefined, undefined)).toBe(undefined); 150 | }); 151 | }); 152 | }); 153 | 154 | describe("createContentAttribute", () => { 155 | describe("case where all arguments are undefined", () => { 156 | it("should return string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth in default value of Content type", () => { 157 | expect(createContentAttribute()).toBe( 158 | "initial-scale=1,width=device-width", 159 | ); 160 | }); 161 | }); 162 | 163 | describe("case where second argument is greater than minimumWidth and less than maximumWidth in first argument", () => { 164 | it("should return string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth in first argument", () => { 165 | expect( 166 | createContentAttribute( 167 | { 168 | width: "device-width", 169 | initialScale: 2, 170 | minimumWidth: 412, 171 | maximumWidth: 768, 172 | interactiveWidget: "resizes-content", 173 | }, 174 | 640, 175 | Infinity, 176 | ), 177 | ).toBe( 178 | "initial-scale=2,interactive-widget=resizes-content,width=device-width", 179 | ); 180 | }); 181 | }); 182 | 183 | describe("case where second argument is less than minimumWidth in first argument", () => { 184 | it("should compute width and initialScale from first and second argument to fit minimum width into viewport, create string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth, and return it", () => { 185 | expect( 186 | createContentAttribute( 187 | { 188 | width: "device-width", 189 | initialScale: 2, 190 | minimumWidth: 412, 191 | maximumWidth: 768, 192 | interactiveWidget: "resizes-content", 193 | }, 194 | 375, 195 | Infinity, 196 | ), 197 | ).toBe( 198 | "initial-scale=1.8203883495145632,interactive-widget=resizes-content,width=412", 199 | ); 200 | }); 201 | }); 202 | 203 | describe("case where second argument is greater than maximumWidth in first argument", () => { 204 | it("should compute width and initialScale from first and second argument to fit maximum width into viewport, create string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth, and return it", () => { 205 | expect( 206 | createContentAttribute( 207 | { 208 | width: "device-width", 209 | initialScale: 2, 210 | minimumWidth: 412, 211 | maximumWidth: 768, 212 | interactiveWidget: "resizes-content", 213 | }, 214 | 1024, 215 | Infinity, 216 | ), 217 | ).toBe( 218 | "initial-scale=2.6666666666666665,interactive-widget=resizes-content,width=768", 219 | ); 220 | }); 221 | }); 222 | 223 | describe("case where minimumWidth is greater than maximumWidth in first argument", () => { 224 | it("should return string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth in first argument", () => { 225 | expect( 226 | createContentAttribute( 227 | { 228 | width: "device-width", 229 | initialScale: 2, 230 | minimumWidth: 768, 231 | maximumWidth: 412, 232 | interactiveWidget: "resizes-content", 233 | }, 234 | 375, 235 | Infinity, 236 | ), 237 | ).toBe( 238 | "initial-scale=2,interactive-widget=resizes-content,width=device-width", 239 | ); 240 | }); 241 | }); 242 | 243 | describe("case where second argument is greater than minWidth and less than maxWidth in first argument", () => { 244 | it("should return string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth in first argument", () => { 245 | expect( 246 | createContentAttribute( 247 | { 248 | width: "device-width", 249 | initialScale: 2, 250 | minimumWidth: 0, 251 | maximumWidth: Infinity, 252 | minWidth: 412, 253 | maxWidth: 768, 254 | interactiveWidget: "resizes-content", 255 | }, 256 | 640, 257 | Infinity, 258 | ), 259 | ).toBe( 260 | "initial-scale=2,interactive-widget=resizes-content,width=device-width", 261 | ); 262 | }); 263 | }); 264 | 265 | describe("case where second argument is less than minWidth in first argument", () => { 266 | it("should compute width and initialScale from first and second argument to fit minimum width into viewport, create string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth, and return it", () => { 267 | expect( 268 | createContentAttribute( 269 | { 270 | width: "device-width", 271 | initialScale: 2, 272 | minimumWidth: 0, 273 | maximumWidth: Infinity, 274 | minWidth: 412, 275 | maxWidth: 768, 276 | interactiveWidget: "resizes-content", 277 | }, 278 | 375, 279 | Infinity, 280 | ), 281 | ).toBe( 282 | "initial-scale=1.8203883495145632,interactive-widget=resizes-content,width=412", 283 | ); 284 | }); 285 | }); 286 | 287 | describe("case where second argument is greater than maxWidth in first argument", () => { 288 | it("should compute width and initialScale from first and second argument to fit maximum width into viewport, create string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth, and return it", () => { 289 | expect( 290 | createContentAttribute( 291 | { 292 | width: "device-width", 293 | initialScale: 2, 294 | minimumWidth: 0, 295 | maximumWidth: Infinity, 296 | minWidth: 412, 297 | maxWidth: 768, 298 | interactiveWidget: "resizes-content", 299 | }, 300 | 1024, 301 | Infinity, 302 | ), 303 | ).toBe( 304 | "initial-scale=2.6666666666666665,interactive-widget=resizes-content,width=768", 305 | ); 306 | }); 307 | }); 308 | 309 | describe("case where minWidth is greater than maxWidth in first argument", () => { 310 | it("should return string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth in first argument", () => { 311 | expect( 312 | createContentAttribute( 313 | { 314 | width: "device-width", 315 | initialScale: 2, 316 | minimumWidth: 0, 317 | maximumWidth: Infinity, 318 | minWidth: 768, 319 | maxWidth: 412, 320 | interactiveWidget: "resizes-content", 321 | }, 322 | 375, 323 | Infinity, 324 | ), 325 | ).toBe( 326 | "initial-scale=2,interactive-widget=resizes-content,width=device-width", 327 | ); 328 | }); 329 | }); 330 | 331 | describe("case where width is number in first argument", () => { 332 | it("should return string where keys and values are connected with equals and properties are connected with commas for properties other than minimumWidth, maximumWidth, minWidth, and maxWidth in first argument", () => { 333 | expect( 334 | createContentAttribute( 335 | { 336 | width: 1024, 337 | initialScale: 2, 338 | minimumWidth: 412, 339 | maximumWidth: 768, 340 | interactiveWidget: "resizes-content", 341 | }, 342 | 1024, 343 | Infinity, 344 | ), 345 | ).toBe("initial-scale=2,interactive-widget=resizes-content,width=1024"); 346 | }); 347 | }); 348 | 349 | describe("case where third argument is finite number", () => { 350 | it("should truncate numbers in returned value to decimal places specified as third argument when converting to string after computing", () => { 351 | expect( 352 | createContentAttribute( 353 | { 354 | width: "device-width", 355 | initialScale: 1.123456789, 356 | minimumWidth: 412, 357 | maximumWidth: Infinity, 358 | minimumScale: 0.123456789, 359 | }, 360 | 375, 361 | 6, 362 | ), 363 | ).toBe("initial-scale=1.022563,minimum-scale=0.123456,width=412"); 364 | }); 365 | }); 366 | 367 | describe("case where third argument is Infinity", () => { 368 | it("should not truncate numbers in return value", () => { 369 | expect( 370 | createContentAttribute( 371 | { 372 | width: "device-width", 373 | initialScale: 1.123456789, 374 | minimumWidth: 412, 375 | maximumWidth: Infinity, 376 | minimumScale: 0.123456789, 377 | }, 378 | 375, 379 | Infinity, 380 | ), 381 | ).toBe( 382 | "initial-scale=1.0225638249393205,minimum-scale=0.123456789,width=412", 383 | ); 384 | }); 385 | }); 386 | }); 387 | -------------------------------------------------------------------------------- /tests/specs/e2e/side-effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { getViewportSize } from "../../modules/PlaywrightFullProjectList.js"; 3 | import { 4 | getViewportContentString, 5 | waitForAssetScriptComplete, 6 | } from "../../modules/PlaywrightPage.js"; 7 | 8 | for (const { 9 | extended, 10 | typescriptTarget, 11 | format, 12 | minified, 13 | outputScriptSrc, 14 | assetScriptSrc, 15 | } of [ 16 | { 17 | extended: false, 18 | typescriptTarget: "es2021", 19 | format: "es", 20 | minified: false, 21 | outputScriptSrc: "/immediate/viewport-extra.js", 22 | assetScriptSrc: "/assets/scripts/e2e/immediate/es/trigger-side-effects.js", 23 | }, 24 | { 25 | extended: false, 26 | typescriptTarget: "es2021", 27 | format: "cjs", 28 | minified: false, 29 | outputScriptSrc: "/immediate/viewport-extra.js", 30 | assetScriptSrc: "/assets/scripts/e2e/immediate/cjs/trigger-side-effects.js", 31 | }, 32 | { 33 | extended: false, 34 | typescriptTarget: "es2021", 35 | format: "iife", 36 | minified: false, 37 | outputScriptSrc: "/immediate/viewport-extra.js", 38 | assetScriptSrc: 39 | "/assets/scripts/e2e/immediate/iife/trigger-side-effects.js", 40 | }, 41 | { 42 | extended: false, 43 | typescriptTarget: "es2021", 44 | format: "iife", 45 | minified: true, 46 | outputScriptSrc: "/immediate/viewport-extra.min.js", 47 | assetScriptSrc: 48 | "/assets/scripts/e2e/immediate/iife/trigger-side-effects.js", 49 | }, 50 | { 51 | extended: false, 52 | typescriptTarget: "es5", 53 | format: "es", 54 | minified: false, 55 | outputScriptSrc: "/immediate/es5/viewport-extra.js", 56 | assetScriptSrc: 57 | "/assets/scripts/e2e/immediate/es/es5/trigger-side-effects.js", 58 | }, 59 | { 60 | extended: false, 61 | typescriptTarget: "es5", 62 | format: "cjs", 63 | minified: false, 64 | outputScriptSrc: "/immediate/es5/viewport-extra.js", 65 | assetScriptSrc: 66 | "/assets/scripts/e2e/immediate/cjs/es5/trigger-side-effects.js", 67 | }, 68 | { 69 | extended: false, 70 | typescriptTarget: "es5", 71 | format: "iife", 72 | minified: false, 73 | outputScriptSrc: "/immediate/es5/viewport-extra.js", 74 | assetScriptSrc: 75 | "/assets/scripts/e2e/immediate/iife/es5/trigger-side-effects.js", 76 | }, 77 | { 78 | extended: false, 79 | typescriptTarget: "es5", 80 | format: "iife", 81 | minified: true, 82 | outputScriptSrc: "/immediate/es5/viewport-extra.min.js", 83 | assetScriptSrc: 84 | "/assets/scripts/e2e/immediate/iife/es5/trigger-side-effects.js", 85 | }, 86 | { 87 | extended: true, 88 | typescriptTarget: "es2021", 89 | format: "es", 90 | minified: false, 91 | outputScriptSrc: "/immediate/extended/viewport-extra.js", 92 | assetScriptSrc: 93 | "/assets/scripts/e2e/immediate/extended/es/trigger-side-effects.js", 94 | }, 95 | { 96 | extended: true, 97 | typescriptTarget: "es2021", 98 | format: "cjs", 99 | minified: false, 100 | outputScriptSrc: "/immediate/extended/viewport-extra.js", 101 | assetScriptSrc: 102 | "/assets/scripts/e2e/immediate/extended/cjs/trigger-side-effects.js", 103 | }, 104 | { 105 | extended: true, 106 | typescriptTarget: "es2021", 107 | format: "iife", 108 | minified: false, 109 | outputScriptSrc: "/immediate/extended/viewport-extra.js", 110 | assetScriptSrc: 111 | "/assets/scripts/e2e/immediate/extended/iife/trigger-side-effects.js", 112 | }, 113 | { 114 | extended: true, 115 | typescriptTarget: "es2021", 116 | format: "iife", 117 | minified: true, 118 | outputScriptSrc: "/immediate/extended/viewport-extra.min.js", 119 | assetScriptSrc: 120 | "/assets/scripts/e2e/immediate/extended/iife/trigger-side-effects.js", 121 | }, 122 | { 123 | extended: true, 124 | typescriptTarget: "es5", 125 | format: "es", 126 | minified: false, 127 | outputScriptSrc: "/immediate/extended/es5/viewport-extra.js", 128 | assetScriptSrc: 129 | "/assets/scripts/e2e/immediate/extended/es/es5/trigger-side-effects.js", 130 | }, 131 | { 132 | extended: true, 133 | typescriptTarget: "es5", 134 | format: "cjs", 135 | minified: false, 136 | outputScriptSrc: "/immediate/extended/es5/viewport-extra.js", 137 | assetScriptSrc: 138 | "/assets/scripts/e2e/immediate/extended/cjs/es5/trigger-side-effects.js", 139 | }, 140 | { 141 | extended: true, 142 | typescriptTarget: "es5", 143 | format: "iife", 144 | minified: false, 145 | outputScriptSrc: "/immediate/extended/es5/viewport-extra.js", 146 | assetScriptSrc: 147 | "/assets/scripts/e2e/immediate/extended/iife/es5/trigger-side-effects.js", 148 | }, 149 | { 150 | extended: true, 151 | typescriptTarget: "es5", 152 | format: "iife", 153 | minified: true, 154 | outputScriptSrc: "/immediate/extended/es5/viewport-extra.min.js", 155 | assetScriptSrc: 156 | "/assets/scripts/e2e/immediate/extended/iife/es5/trigger-side-effects.js", 157 | }, 158 | ]) { 159 | test.describe(`using ${minified ? "minified " : ""}${typescriptTarget} ${format} output for ${extended ? "extended " : ""}immediate entry`, () => { 160 | test.describe("updating content attribute of viewport meta element", () => { 161 | test.beforeEach(async ({ page }) => { 162 | await page.goto("/tests/__fixtures__/src/dummy.html"); 163 | }); 164 | 165 | test.describe("case where (data-extra-)content attribute with minimum-width, data-(extra-)media attribute are set in viewport(-extra) meta element", () => { 166 | test("width is updated to minimum width and initial-scale is updated to value that fits minimum width into viewport, on browser whose viewport width is less than minimum width. Last minimum-width in matching media queries is used", async ({ 167 | page, 168 | viewport, 169 | }, { config: { projects } }) => { 170 | const smViewportWidth = 171 | getViewportSize(projects, "sm")?.use.viewport?.width ?? 0; 172 | const xlViewportWidth = 173 | getViewportSize(projects, "xl")?.use.viewport?.width ?? 0; 174 | const documentClientWidth = viewport ? viewport.width : undefined; 175 | await page.setContent(` 176 | 177 | 178 | 179 | 180 | Document 181 | 182 | 183 | 184 | ${outputScriptSrc ? `` : ""} 185 | 186 | 187 | 188 | 189 | 190 | `); 191 | await waitForAssetScriptComplete(page); 192 | expect(await getViewportContentString(page)).toBe( 193 | documentClientWidth && smViewportWidth > 0 && xlViewportWidth > 0 194 | ? documentClientWidth < 640 195 | ? documentClientWidth < smViewportWidth 196 | ? `initial-scale=${(documentClientWidth / smViewportWidth) * 1},width=${smViewportWidth}` 197 | : "initial-scale=1,width=device-width" 198 | : documentClientWidth < xlViewportWidth 199 | ? `initial-scale=${(documentClientWidth / xlViewportWidth) * 1},width=${xlViewportWidth}` 200 | : "initial-scale=1,width=device-width" 201 | : "", 202 | ); 203 | }); 204 | }); 205 | 206 | test.describe("case where (data-extra-)content attribute with maximum-width, data-(extra-)media attribute are set in viewport(-extra) meta element", () => { 207 | test("width is updated to maximum width and initial-scale is updated to value that fits maximum width into viewport, on browser whose viewport width is greater than maximum width. Last maximum-width in matching media queries is used", async ({ 208 | page, 209 | viewport, 210 | }, { config: { projects } }) => { 211 | const xsViewportWidth = 212 | getViewportSize(projects, "xs")?.use.viewport?.width ?? 0; 213 | const lgViewportWidth = 214 | getViewportSize(projects, "lg")?.use.viewport?.width ?? 0; 215 | const documentClientWidth = viewport ? viewport.width : undefined; 216 | await page.setContent(` 217 | 218 | 219 | 220 | 221 | Document 222 | 223 | 224 | 225 | ${outputScriptSrc ? `` : ""} 226 | 227 | 228 | 229 | 230 | 231 | `); 232 | await waitForAssetScriptComplete(page); 233 | expect(await getViewportContentString(page)).toBe( 234 | documentClientWidth && 235 | xsViewportWidth < Infinity && 236 | lgViewportWidth < Infinity 237 | ? documentClientWidth < 640 238 | ? documentClientWidth > xsViewportWidth 239 | ? `initial-scale=${(documentClientWidth / xsViewportWidth) * 1},width=${xsViewportWidth}` 240 | : "initial-scale=1,width=device-width" 241 | : documentClientWidth > lgViewportWidth 242 | ? `initial-scale=${(documentClientWidth / lgViewportWidth) * 1},width=${lgViewportWidth}` 243 | : "initial-scale=1,width=device-width" 244 | : "", 245 | ); 246 | }); 247 | }); 248 | 249 | test.describe("case where (data-extra-)content attribute with min-width, data-(extra-)media attribute are set in viewport(-extra) meta element", () => { 250 | test("width is updated to minimum width and initial-scale is updated to value that fits minimum width into viewport, on browser whose viewport width is less than minimum width. Last min-width in matching media queries is used", async ({ 251 | page, 252 | viewport, 253 | }, { config: { projects } }) => { 254 | const smViewportWidth = 255 | getViewportSize(projects, "sm")?.use.viewport?.width ?? 0; 256 | const xlViewportWidth = 257 | getViewportSize(projects, "xl")?.use.viewport?.width ?? 0; 258 | const documentClientWidth = viewport ? viewport.width : undefined; 259 | await page.setContent(` 260 | 261 | 262 | 263 | 264 | Document 265 | 266 | 267 | 268 | ${outputScriptSrc ? `` : ""} 269 | 270 | 271 | 272 | 273 | 274 | `); 275 | await waitForAssetScriptComplete(page); 276 | expect(await getViewportContentString(page)).toBe( 277 | documentClientWidth && smViewportWidth > 0 && xlViewportWidth > 0 278 | ? documentClientWidth < 640 279 | ? documentClientWidth < smViewportWidth 280 | ? `initial-scale=${(documentClientWidth / smViewportWidth) * 1},width=${smViewportWidth}` 281 | : "initial-scale=1,width=device-width" 282 | : documentClientWidth < xlViewportWidth 283 | ? `initial-scale=${(documentClientWidth / xlViewportWidth) * 1},width=${xlViewportWidth}` 284 | : "initial-scale=1,width=device-width" 285 | : "", 286 | ); 287 | }); 288 | }); 289 | 290 | test.describe("case where (data-extra-)content attribute with max-width, data-(extra-)media attribute are set in viewport(-extra) meta element", () => { 291 | test("width is updated to maximum width and initial-scale is updated to value that fits maximum width into viewport, on browser whose viewport width is greater than maximum width. Last max-width in matching media queries is used", async ({ 292 | page, 293 | viewport, 294 | }, { config: { projects } }) => { 295 | const xsViewportWidth = 296 | getViewportSize(projects, "xs")?.use.viewport?.width ?? 0; 297 | const lgViewportWidth = 298 | getViewportSize(projects, "lg")?.use.viewport?.width ?? 0; 299 | const documentClientWidth = viewport ? viewport.width : undefined; 300 | await page.setContent(` 301 | 302 | 303 | 304 | 305 | Document 306 | 307 | 308 | 309 | ${outputScriptSrc ? `` : ""} 310 | 311 | 312 | 313 | 314 | 315 | `); 316 | await waitForAssetScriptComplete(page); 317 | expect(await getViewportContentString(page)).toBe( 318 | documentClientWidth && 319 | xsViewportWidth < Infinity && 320 | lgViewportWidth < Infinity 321 | ? documentClientWidth < 640 322 | ? documentClientWidth > xsViewportWidth 323 | ? `initial-scale=${(documentClientWidth / xsViewportWidth) * 1},width=${xsViewportWidth}` 324 | : "initial-scale=1,width=device-width" 325 | : documentClientWidth > lgViewportWidth 326 | ? `initial-scale=${(documentClientWidth / lgViewportWidth) * 1},width=${lgViewportWidth}` 327 | : "initial-scale=1,width=device-width" 328 | : "", 329 | ); 330 | }); 331 | }); 332 | }); 333 | }); 334 | } 335 | --------------------------------------------------------------------------------