├── .npmrc ├── .husky └── pre-commit ├── .gitignore ├── .prettierrc ├── .editorconfig ├── tsconfig.json ├── .eslintrc.cjs ├── index.d.ts ├── .github └── workflows │ ├── stale.yml │ └── test.yml ├── LICENSE ├── platforms.js ├── package.json ├── index.js ├── README.md ├── adapter.js └── pnpm-lock.yaml /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | bower_components/ 3 | node_modules/ 4 | vendor/ 5 | 6 | # package manager files 7 | npm-debug.log* 8 | yarn-error.log 9 | yarn.lock 10 | shrinkwrap.yaml 11 | 12 | # development 13 | .eslintcache 14 | tsconfig.tsbuildinfo 15 | 16 | # project specific 17 | todo.md 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "printWidth": 100, 7 | "overrides": [ 8 | { 9 | "files": ["*.svelte"], 10 | "options": { 11 | "svelteBracketNewLine": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{yml,yaml}] 12 | indent_style = space 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "noEmit": true, 6 | "noImplicitAny": true, 7 | "module": "es2022", 8 | "target": "es2022", 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "baseUrl": "." 12 | }, 13 | "include": ["index.js"] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | root: true, 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['svelte3'], 6 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:json/recommended', 11 | ], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Adapter } from '@sveltejs/kit'; 2 | 3 | export interface AdapterInjectTargets { 4 | head?: AdapterInjectPositions; 5 | body?: AdapterInjectPositions; 6 | } 7 | 8 | export interface AdapterInjectPositions { 9 | beforebegin?: string | string[]; 10 | afterbegin?: string | string[]; 11 | beforeend?: string | string[]; 12 | afterend?: string | string[]; 13 | } 14 | 15 | export interface AdapterReplace { 16 | from: string; 17 | to: string; 18 | many?: boolean; 19 | } 20 | 21 | export interface AdapterOptions { 22 | assets?: string; 23 | fallback?: string; 24 | injectTo?: AdapterInjectTargets; 25 | minify?: boolean; 26 | pages?: string; 27 | precompress?: boolean; 28 | prettify?: boolean; 29 | replace?: AdapterReplace[]; 30 | strict?: boolean; 31 | targetExtension?: string; 32 | } 33 | 34 | export default function plugin(options?: AdapterOptions): Adapter; 35 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Stale issues and PR' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v4 11 | with: 12 | stale-issue-label: stale 13 | stale-pr-label: stale 14 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 14 days.' 15 | stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 14 days.' 16 | close-issue-message: 'This issue was closed because it has been stalled for 14 days with no activity.' 17 | close-pr-message: 'This PR was closed because it has been stalled for 14 days with no activity.' 18 | days-before-issue-stale: 90 19 | days-before-pr-stale: 90 20 | days-before-issue-close: 30 21 | days-before-pr-close: 30 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-present Jan T. Sott & [these people](https://github.com/sveltejs/kit/graphs/contributors) 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'adapter.js' 7 | - 'index.js' 8 | - 'index.d.js' 9 | - 'platform.js' 10 | - 'package.json' 11 | - 'pnpm-lock.yaml' 12 | pull_request: 13 | paths: 14 | - 'adapter.js' 15 | - 'index.js' 16 | - 'index.d.js' 17 | - 'platform.js' 18 | - 'package.json' 19 | - 'pnpm-lock.yaml' 20 | workflow_dispatch: 21 | 22 | jobs: 23 | default: 24 | runs-on: ubuntu-latest 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | node-version: ['lts/*', '*'] 29 | 30 | steps: 31 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 32 | with: 33 | fetch-depth: 10 34 | 35 | - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 36 | with: 37 | node-version: ${{ matrix.node-version }} 38 | 39 | - name: Enable Corepack 40 | run: corepack enable 41 | 42 | - name: Get pnpm store directory 43 | id: pnpm-cache 44 | run: | 45 | echo "::set-output name=pnpm_store_path::$(pnpm store path)" 46 | 47 | - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 48 | name: Setup pnpm cache 49 | with: 50 | path: ${{ steps.pnpm-cache.outputs.pnpm_store_path }} 51 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 52 | restore-keys: | 53 | ${{ runner.os }}-pnpm-store- 54 | 55 | - name: Install dependencies 56 | run: pnpm install --frozen-lockfile --strict-peer-dependencies 57 | 58 | - name: Lint Source 59 | run: pnpm run --if-present lint 60 | 61 | - name: Build Source 62 | run: pnpm run --if-present build 63 | -------------------------------------------------------------------------------- /platforms.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | /** 4 | * @typedef {{ 5 | * name: string; 6 | * test: () => boolean; 7 | * defaults: import('./index').AdapterOptions; 8 | * done: (builder: import('@sveltejs/kit').Builder) => void; 9 | * }} 10 | * Platform */ 11 | 12 | // This function is duplicated in adapter-vercel 13 | /** @param {import('@sveltejs/kit').Builder} builder */ 14 | function static_vercel_config(builder) { 15 | /** @type {any[]} */ 16 | const prerendered_redirects = []; 17 | 18 | /** @type {Record} */ 19 | const overrides = {}; 20 | 21 | for (const [src, redirect] of builder.prerendered.redirects) { 22 | prerendered_redirects.push({ 23 | src, 24 | headers: { 25 | Location: redirect.location 26 | }, 27 | status: redirect.status 28 | }); 29 | } 30 | 31 | for (const [path, page] of builder.prerendered.pages) { 32 | if (path.endsWith('/') && path !== '/') { 33 | prerendered_redirects.push( 34 | { src: path, dest: path.slice(0, -1) }, 35 | { src: path.slice(0, -1), status: 308, headers: { Location: path } } 36 | ); 37 | 38 | overrides[page.file] = { path: path.slice(1, -1) }; 39 | } else { 40 | overrides[page.file] = { path: path.slice(1) }; 41 | } 42 | } 43 | 44 | return { 45 | version: 3, 46 | routes: [ 47 | ...prerendered_redirects, 48 | { 49 | src: `/${builder.getAppPath()}/immutable/.+`, 50 | headers: { 51 | 'cache-control': 'public, immutable, max-age=31536000' 52 | } 53 | }, 54 | { 55 | handle: 'filesystem' 56 | } 57 | ], 58 | overrides 59 | }; 60 | } 61 | 62 | /** @type {Platform[]} */ 63 | export const platforms = [ 64 | { 65 | name: 'Vercel', 66 | test: () => !!process.env.VERCEL, 67 | defaults: { 68 | pages: '.vercel/output/static' 69 | }, 70 | done: (builder) => { 71 | const config = static_vercel_config(builder); 72 | fs.writeFileSync('.vercel/output/config.json', JSON.stringify(config, null, ' ')); 73 | } 74 | } 75 | ]; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-adapter-html-like", 3 | "description": "SvelteKit adapter for HTML-like template engines such as PHP, Blade, Handlebars, etc.", 4 | "version": "0.3.2", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/idleberg/sveltekit-adapter-html-like" 8 | }, 9 | "license": "MIT", 10 | "keywords": [ 11 | "sveltekit-adapter", 12 | "svelte", 13 | "sveltekit", 14 | "php", 15 | "blade", 16 | "embedded ruby", 17 | "erb", 18 | "moustache", 19 | "handlebars", 20 | "ejs" 21 | ], 22 | "type": "module", 23 | "files": [ 24 | "adapter.js", 25 | "index.js", 26 | "index.d.ts", 27 | "platforms.js", 28 | "LICENSE", 29 | "README.md" 30 | ], 31 | "main": "index.js", 32 | "exports": { 33 | ".": { 34 | "import": "./index.js" 35 | } 36 | }, 37 | "types": "index.d.ts", 38 | "scripts": { 39 | "prepare": "husky install", 40 | "check-format": "prettier --check . --ignore-path .gitignore", 41 | "lint:md": "remark . --quiet --frail --ignore-path .gitignore", 42 | "lint:js": "eslint --ignore-path .gitignore \"**/*.{ts,js,svelte}\"", 43 | "lint": "npm-run-all --parallel lint:*", 44 | "test": "npm run lint" 45 | }, 46 | "dependencies": { 47 | "@iarna/toml": "^2.2.5", 48 | "cosmiconfig": "^8.3.6", 49 | "html-minifier-terser": "^7.2.0", 50 | "jsdom": "^20.0.3", 51 | "json5": "^2.2.3", 52 | "prettier": "^2.8.8", 53 | "tiny-glob": "^0.2.9" 54 | }, 55 | "devDependencies": { 56 | "@sveltejs/kit": "^2.0.0", 57 | "@types/html-minifier-terser": "^7.0.2", 58 | "@types/jsdom": "^20.0.1", 59 | "@types/prettier": "^2.7.3", 60 | "@typescript-eslint/eslint-plugin": "^5.62.0", 61 | "@typescript-eslint/parser": "^5.62.0", 62 | "eslint": "^8.56.0", 63 | "eslint-plugin-import": "^2.29.1", 64 | "eslint-plugin-json": "^3.1.0", 65 | "eslint-plugin-svelte3": "^4.0.0", 66 | "husky": "^8.0.3", 67 | "lint-staged": "^13.3.0", 68 | "npm-run-all": "^4.1.5", 69 | "remark-cli": "^11.0.0", 70 | "remark-preset-lint-recommended": "^6.1.3", 71 | "remark-preset-prettier": "^2.0.1", 72 | "sirv": "^2.0.3", 73 | "svelte": "^3.59.2", 74 | "typescript": "^4.9.5" 75 | }, 76 | "peerDependencies": { 77 | "@sveltejs/kit": "^1.0.0" 78 | }, 79 | "lint-staged": { 80 | "*.(js|ts|json)": [ 81 | "eslint --cache --fix", 82 | "prettier --write" 83 | ], 84 | "*.md": "prettier --write" 85 | }, 86 | "packageManager": "pnpm@9.0.5+sha512.a722575c18fd791f9ef71e387c6e16cc03c90d859b9b0998d0428ca0e3220828b32a96e6de73cea34edbc0f50824771b1a69c7ea36b86daa3b89c7e6d9a3d912" 87 | } 88 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { platforms } from './platforms.js'; 2 | import { transformFiles } from './adapter.js'; 3 | import path from 'node:path'; 4 | 5 | /** @type {import('.').default} */ 6 | export default function (options) { 7 | return { 8 | name: '@sveltejs/adapter-static', 9 | 10 | async adapt(builder) { 11 | if (!options?.fallback) { 12 | if (builder.routes.some((route) => route.prerender !== true) && options?.strict !== false) { 13 | const prefix = path.relative('.', builder.config.kit.files.routes); 14 | const has_param_routes = builder.routes.some((route) => route.id.includes('[')); 15 | const config_option = 16 | has_param_routes || JSON.stringify(builder.config.kit.prerender.entries) !== '["*"]' 17 | ? ` - adjust the \`prerender.entries\` config option ${ 18 | has_param_routes 19 | ? '(routes with parameters are not part of entry points by default)' 20 | : '' 21 | } — see https://kit.svelte.dev/docs/configuration#prerender for more info.` 22 | : ''; 23 | 24 | builder.log.error( 25 | `@sveltejs/adapter-static: all routes must be fully prerenderable, but found the following routes that are dynamic: 26 | ${builder.routes.map((route) => ` - ${path.posix.join(prefix, route.id)}`).join('\n')} 27 | 28 | You have the following options: 29 | - set the \`fallback\` option — see https://github.com/sveltejs/kit/tree/master/packages/adapter-static#spa-mode for more info. 30 | - add \`export const prerender = true\` to your root \`+layout.js/.ts\` or \`+layout.server.js/.ts\` file. This will try to prerender all pages. 31 | - add \`export const prerender = true\` to any \`+server.js/ts\` files that are not fetched by page \`load\` functions. 32 | ${config_option} 33 | - pass \`strict: false\` to \`adapter-static\` to ignore this error. Only do this if you are sure you don't need the routes in question in your final app, as they will be unavailable. See https://github.com/sveltejs/kit/tree/master/packages/adapter-static#strict for more info. 34 | 35 | If this doesn't help, you may need to use a different adapter. @sveltejs/adapter-static can only be used for sites that don't need a server for dynamic rendering, and can run on just a static file server. 36 | See https://kit.svelte.dev/docs/page-options#prerender for more details` 37 | ); 38 | throw new Error('Encountered dynamic routes'); 39 | } 40 | } 41 | 42 | const platform = platforms.find((platform) => platform.test()); 43 | 44 | if (platform) { 45 | if (options) { 46 | builder.log.warn( 47 | `Detected ${platform.name}. Please remove adapter-static options to enable zero-config mode` 48 | ); 49 | } else { 50 | builder.log.info(`Detected ${platform.name}, using zero-config mode`); 51 | } 52 | } 53 | 54 | const { 55 | pages = 'build', 56 | assets = 'build', 57 | fallback, 58 | precompress 59 | } = options ?? platform?.defaults ?? /** @type {import('./index').AdapterOptions} */ ({}); 60 | 61 | builder.rimraf(assets); 62 | builder.rimraf(pages); 63 | 64 | builder.writeClient(assets); 65 | builder.writePrerendered(pages); 66 | 67 | if (fallback) { 68 | await builder.generateFallback(path.join(pages, fallback)); 69 | } 70 | 71 | if (precompress) { 72 | builder.log.minor('Compressing assets and pages'); 73 | if (pages === assets) { 74 | await builder.compress(assets); 75 | } else { 76 | await Promise.all([builder.compress(assets), builder.compress(pages)]); 77 | } 78 | } 79 | 80 | if (pages === assets) { 81 | builder.log(`Wrote site to "${pages}"`); 82 | } else { 83 | builder.log(`Wrote pages to "${pages}" and assets to "${assets}"`); 84 | } 85 | 86 | if (!options) platform?.done(builder); 87 | 88 | transformFiles(builder, options); 89 | } 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sveltekit-adapter-html-like 2 | 3 | > **Warning** 4 | > 5 | > SvelteKit is still under heavy development and introduces breaking changes every now and then. If this adapter doesn't work in your setup, make sure to use `@svelte/kit@1.0.0-next.428`. 6 | 7 | [![License](https://img.shields.io/github/license/idleberg/sveltekit-adapter-html-like?color=blue&style=for-the-badge)](https://github.com/idleberg/sveltekit-adapter-html-like/blob/main/LICENSE) 8 | [![Version](https://img.shields.io/npm/v/sveltekit-adapter-html-like?style=for-the-badge)](https://www.npmjs.org/package/sveltekit-adapter-html-like) 9 | [![Build](https://img.shields.io/github/actions/workflow/status/idleberg/sveltekit-adapter-html-like/test.yml?style=for-the-badge)](https://github.com/idleberg/sveltekit-adapter-html-like/actions) 10 | 11 | [Adapter](https://kit.svelte.dev/docs#adapters) for SvelteKit apps that prerenders your site as static files for template engines such as PHP, Blade, Embedded Ruby (ERB), Handlebars, EJS etc. 12 | 13 | This package is a fork of [@sveltejs/adapter-static](https://github.com/sveltejs/kit/tree/master/packages/adapter-static) that adds a couple of extra features: 14 | 15 | - tag injection 16 | - string replacement 17 | - minify/prettify output 18 | - custom file extensions 19 | 20 | ## Usage 21 | 22 | Install with `npm i -D sveltekit-adapter-html-like`, then add the adapter to your `svelte.config.js`: 23 | 24 | ```js 25 | // svelte.config.js 26 | import adapter from 'sveltekit-adapter-html-like'; 27 | 28 | export default { 29 | kit: { 30 | adapter: adapter() 31 | } 32 | }; 33 | ``` 34 | 35 | Unless you're in [SPA mode](#spa-mode), the adapter will attempt to prerender every page of your app, regardless of whether the [`prerender`](https://kit.svelte.dev/docs#ssr-and-javascript-prerender) option is set. 36 | 37 | ## Options 38 | 39 | ### pages 40 | 41 | Type: `string` 42 | Default: `build` 43 | 44 | The directory to write prerendered pages to. It defaults to `build`. 45 | 46 | ### assets 47 | 48 | Type: `string` 49 | Default: `build` 50 | 51 | The directory to write static assets (the contents of `static`, plus client-side JS and CSS generated by SvelteKit) to. Ordinarily this should be the same as `pages`, and it will default to whatever the value of `pages` is, but in rare circumstances you might need to output pages and assets to separate locations. 52 | 53 | ### fallback 54 | 55 | Type: `string` 56 | Default: `null` 57 | 58 | Specify a fallback page for SPA mode, e.g. `index.html` or `200.html` or `404.html`. 59 | 60 | ### precompress 61 | 62 | Type: `boolean` 63 | Default: `false` 64 | 65 | If `true`, precompresses files with brotli and gzip. This will generate `.br` and `.gz` files. 66 | 67 | ### injectTo 68 | 69 | Type: `object` 70 | Default: `{}` 71 | 72 | Allows the injection of markup, valid HTML or otherwise, into the `` or ``. You can use the same positions as [`insertAdjacentHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML): 73 | 74 | - `beforebegin` 75 | - `afterbegin` 76 | - `beforeend` 77 | - `afterend` 78 | 79 | **Example** 80 | 81 | Let's inject some WordPress tags into the page 82 | 83 | ```js 84 | adapter({ 85 | injectTo: { 86 | head: { 87 | beforeend: [''] 88 | }, 89 | body: { 90 | beforeend: [''] 91 | } 92 | }, 93 | targetExtension: '.php' 94 | }); 95 | ``` 96 | 97 | ### replace 98 | 99 | Type: `array` 100 | Default: `[]` 101 | 102 | String replacements run on every page 103 | 104 | **Example** 105 | 106 | Once again, a WordPress example 107 | 108 | ```js 109 | adapter({ 110 | replace: [ 111 | { 112 | from: '', 113 | to: '>' 114 | // many: true (optional) 115 | } 116 | ], 117 | targetExtension: '.php' 118 | }); 119 | ``` 120 | 121 | ### minify 122 | 123 | Type: `boolean` 124 | Default: `false` 125 | 126 | Enable minification of output files 127 | 128 | ### targetExtension 129 | 130 | Type: `string` 131 | Default: `.html` 132 | 133 | Modifies the extension of the target file, e.g. `.php` or `.hbs` 134 | 135 | ## SPA mode 136 | 137 | You can use `sveltekit-adapter-html-like` to create a single-page app or SPA by specifying a **fallback page**. 138 | 139 | > In most situations this is not recommended: it harms SEO, tends to slow down perceived performance, and makes your app inaccessible to users if JavaScript fails or is disabled (which happens [more often than you probably think](https://kryogenix.org/code/browser/everyonehasjs.html)). 140 | 141 | The fallback page is a blank HTML page that loads your SvelteKit app and navigates to the correct route. For example [Surge](https://surge.sh/help/adding-a-200-page-for-client-side-routing), a static web host, lets you add a `200.html` file that will handle any requests that don't otherwise match. We can create that file like so: 142 | 143 | ```js 144 | // svelte.config.js 145 | import adapter from 'sveltekit-adapter-html-like'; 146 | 147 | export default { 148 | kit: { 149 | adapter: adapter({ 150 | fallback: '200.html' 151 | }) 152 | } 153 | }; 154 | ``` 155 | 156 | When operating in SPA mode, only pages that have the [`prerender`](https://kit.svelte.dev/docs#ssr-and-javascript-prerender) option set will be prerendered. 157 | 158 | ## Related 159 | 160 | - [Vite Assets in WordPress](https://github.com/idleberg/php-wordpress-vite-assets) 161 | 162 | ## License 163 | 164 | This work is licensed under [The MIT License](LICENSE) 165 | -------------------------------------------------------------------------------- /adapter.js: -------------------------------------------------------------------------------- 1 | import { basename, dirname, join, relative } from 'node:path'; 2 | import { cosmiconfig, defaultLoaders } from 'cosmiconfig'; 3 | import { JSDOM } from 'jsdom'; 4 | import { minify as minifier } from 'html-minifier-terser'; 5 | import { promises as $fs } from 'node:fs'; 6 | import crypto from 'node:crypto'; 7 | import glob from 'tiny-glob'; 8 | import JSON5 from 'json5'; 9 | import prettier from 'prettier'; 10 | import TOML from '@iarna/toml'; 11 | 12 | const { readFile, writeFile } = $fs; 13 | 14 | /** @type {import('./index').AdapterOptions} */ 15 | const defaultOptions = { 16 | pages: 'build', 17 | assets: 'build', 18 | fallback: '', 19 | precompress: false, 20 | minify: false, 21 | injectTo: {}, 22 | prettify: true, 23 | strict: false, 24 | targetExtension: '.html', 25 | replace: [] 26 | }; 27 | 28 | export async function transformFiles(builder, userOptions) { 29 | const options = { 30 | ...defaultOptions, 31 | ...userOptions 32 | }; 33 | 34 | const sessionUUID = crypto.randomUUID(); 35 | const htmlFiles = await glob('**/*.html', { 36 | cwd: options.pages, 37 | dot: true, 38 | absolute: true, 39 | filesOnly: true 40 | }); 41 | 42 | await Promise.all( 43 | htmlFiles.map(async (htmlFile) => { 44 | const htmlContents = await readFile(htmlFile, 'utf8'); 45 | const dom = new JSDOM(htmlContents); 46 | 47 | const targetElements = options?.injectTo ? Object.keys(options.injectTo) : []; 48 | 49 | targetElements.map((targetElement) => { 50 | if (!['head', 'body'].includes(targetElement)) { 51 | builder.log.warn(`Skipping unsupported injection element: ${targetElement}`); 52 | return; 53 | } 54 | 55 | const injectToPositions = Object.keys(options.injectTo[targetElement]); 56 | 57 | injectToPositions.map((injectToPosition) => { 58 | if (!['beforebegin', 'afterbegin', 'beforeend', 'afterend'].includes(injectToPosition)) { 59 | builder.log.warn( 60 | `Skipping unsupported insertAdjacentHTML position: ${injectToPosition}` 61 | ); 62 | return; 63 | } 64 | 65 | const injectToPositions = Array.isArray(options.injectTo[targetElement][injectToPosition]) 66 | ? options.injectTo[targetElement][injectToPosition] 67 | : Array(options.injectTo[targetElement][injectToPosition]); 68 | 69 | injectToPositions.map((injectToText) => { 70 | const injectToHash = crypto.createHash('sha256').update(injectToText).digest('hex'); 71 | const injectToTag = ``; 72 | 73 | if (!options.replace.some((item) => item.from === injectToTag)) { 74 | options.replace.push({ 75 | from: injectToTag, 76 | to: injectToText 77 | }); 78 | } 79 | 80 | builder.log.minor(`Injecting to ${injectToPosition}: '${injectToText}'`); 81 | dom.window.document[targetElement].insertAdjacentHTML(injectToPosition, injectToTag); 82 | }); 83 | }); 84 | }); 85 | 86 | let outputHTML; 87 | 88 | try { 89 | outputHTML = options.minify 90 | ? await minifier(dom.serialize(), { 91 | collapseWhitespace: true, 92 | minifyCSS: true, 93 | minifyJS: true, 94 | removeComments: false, 95 | removeRedundantAttributes: true, 96 | useShortDoctype: true 97 | }) 98 | : options.prettify 99 | ? prettier.format(dom.serialize(), await getPrettierConfig()) 100 | : dom.serialize(); 101 | 102 | builder.log.minor('Formatting markup'); 103 | } catch (err) { 104 | builder.log.error('Formatting markup failed'); 105 | throw Error(err); 106 | } 107 | 108 | const outFile = `${basename(htmlFile, '.html')}${options.targetExtension}`; 109 | const outPath = join(dirname(htmlFile), outFile); 110 | 111 | const phpContents = 112 | options.replace && Object.values(options.replace)?.length 113 | ? options.replace.reduce((previousValue, currentValue) => { 114 | const replacer = currentValue.many ? 'replaceAll' : 'replace'; 115 | 116 | if (!currentValue.from.startsWith('