├── .npmignore ├── jest.config.cjs ├── .gitignore ├── tsconfig.json ├── src ├── test │ ├── src.css │ ├── expected │ │ ├── foo │ │ │ └── dist │ │ │ │ ├── _astro │ │ │ │ └── style.css │ │ │ │ ├── foo.html │ │ │ │ ├── index.html │ │ │ │ └── foo │ │ │ │ └── index.html │ │ └── dist │ │ │ ├── _astro │ │ │ └── style.css │ │ │ ├── foo.html │ │ │ ├── index.html │ │ │ └── foo │ │ │ └── index.html │ ├── index.test.ts │ └── src.html └── index.ts ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | !dist/ 2 | src/ 3 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | node_modules/ 3 | 4 | # Logfiles and tempfiles 5 | *.log 6 | /log/* 7 | !/log/.keep 8 | /tmp 9 | 10 | # Other unneeded files 11 | *.swp 12 | *~ 13 | .project 14 | .DS_Store 15 | .idea 16 | .secret 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "declaration": true, 6 | "moduleResolution": "Node16", 7 | "esModuleInterop": true, 8 | "allowJs": false, 9 | "module": "Node16", 10 | "target": "ES2022", 11 | "outDir": "./dist", 12 | "skipLibCheck": true, 13 | "verbatimModuleSyntax": false 14 | }, 15 | "include": ["src"], 16 | "exclude": ["src/**/*.test.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /src/test/src.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --foo: 3 | url(/assets/images/foo.jpg) 4 | url("/assets/images/foo.jpg") 5 | url('/assets/images/foo.jpg') 6 | url(/foo/assets/images/foo.jpg) 7 | url("/foo/assets/images/foo.jpg") 8 | url('/foo/assets/images/foo.jpg') 9 | url( 10 | /assets/images/foo.jpg 11 | ) 12 | url( 13 | "/assets/images/foo.jpg" 14 | ) 15 | url(" /assets/images/foo.jpg ") 16 | url( 17 | '/assets/images/foo.jpg' 18 | ) 19 | url(' /assets/images/foo.jpg ') 20 | url( 21 | /foo/assets/images/foo.jpg 22 | ) 23 | url( 24 | "/foo/assets/images/foo.jpg" 25 | ) 26 | url(" /foo/assets/images/foo.jpg ") 27 | url( 28 | '/foo/assets/images/foo.jpg' 29 | ) 30 | url(' /foo/assets/images/foo.jpg ') 31 | url(./assets/images/foo.jpg) 32 | url(https://example.com/foo.jpg) 33 | url(//example.com/foo.jpg) 34 | /assets/images/foo.jpg 35 | /foo/assets/images/foo.jpg; 36 | } -------------------------------------------------------------------------------- /src/test/expected/foo/dist/_astro/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --foo: 3 | url(/assets/images/foo.jpg) 4 | url("/assets/images/foo.jpg") 5 | url('/assets/images/foo.jpg') 6 | url(../assets/images/foo.jpg) 7 | url("../assets/images/foo.jpg") 8 | url('../assets/images/foo.jpg') 9 | url( 10 | /assets/images/foo.jpg 11 | ) 12 | url( 13 | "/assets/images/foo.jpg" 14 | ) 15 | url(" /assets/images/foo.jpg ") 16 | url( 17 | '/assets/images/foo.jpg' 18 | ) 19 | url(' /assets/images/foo.jpg ') 20 | url( 21 | ../assets/images/foo.jpg 22 | ) 23 | url( 24 | "../assets/images/foo.jpg" 25 | ) 26 | url(" ../assets/images/foo.jpg ") 27 | url( 28 | '../assets/images/foo.jpg' 29 | ) 30 | url(' ../assets/images/foo.jpg ') 31 | url(./assets/images/foo.jpg) 32 | url(https://example.com/foo.jpg) 33 | url(//example.com/foo.jpg) 34 | /assets/images/foo.jpg 35 | /foo/assets/images/foo.jpg; 36 | } -------------------------------------------------------------------------------- /src/test/expected/dist/_astro/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --foo: 3 | url(../assets/images/foo.jpg) 4 | url("../assets/images/foo.jpg") 5 | url('../assets/images/foo.jpg') 6 | url(../foo/assets/images/foo.jpg) 7 | url("../foo/assets/images/foo.jpg") 8 | url('../foo/assets/images/foo.jpg') 9 | url( 10 | ../assets/images/foo.jpg 11 | ) 12 | url( 13 | "../assets/images/foo.jpg" 14 | ) 15 | url(" ../assets/images/foo.jpg ") 16 | url( 17 | '../assets/images/foo.jpg' 18 | ) 19 | url(' ../assets/images/foo.jpg ') 20 | url( 21 | ../foo/assets/images/foo.jpg 22 | ) 23 | url( 24 | "../foo/assets/images/foo.jpg" 25 | ) 26 | url(" ../foo/assets/images/foo.jpg ") 27 | url( 28 | '../foo/assets/images/foo.jpg' 29 | ) 30 | url(' ../foo/assets/images/foo.jpg ') 31 | url(./assets/images/foo.jpg) 32 | url(https://example.com/foo.jpg) 33 | url(//example.com/foo.jpg) 34 | /assets/images/foo.jpg 35 | /foo/assets/images/foo.jpg; 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-relative-links", 3 | "type": "module", 4 | "version": "0.4.2", 5 | "description": "Build Astro with relative links.", 6 | "scripts": { 7 | "build": "tsc", 8 | "dev": "tsc --watch", 9 | "test": "jest", 10 | "prepublishOnly": "npm run build" 11 | }, 12 | "author": "Kite", 13 | "license": "MIT", 14 | "exports": { 15 | ".": "./dist/index.js" 16 | }, 17 | "types": "./dist/index.d.ts", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/ixkaito/astro-relative-links.git" 21 | }, 22 | "keywords": [ 23 | "astro", 24 | "withastro", 25 | "astro-component", 26 | "astro-integration", 27 | "renderer", 28 | "relative-link" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/ixkaito/astro-relative-links/issues" 32 | }, 33 | "homepage": "https://github.com/ixkaito/astro-relative-links#readme", 34 | "dependencies": { 35 | "glob": "^10.3.10" 36 | }, 37 | "devDependencies": { 38 | "astro": "^3.2.0", 39 | "@types/html-escaper": "^3.0.0", 40 | "@types/jest": "^29.5.5", 41 | "@types/node": "^20.8.0", 42 | "jest": "^29.7.0", 43 | "ts-jest": "^29.1.1", 44 | "typescript": "^5.2.2" 45 | }, 46 | "peerDependencies": { 47 | "astro": ">=3.0.0" 48 | }, 49 | "packageManager": "pnpm@9.1.0+sha512.67f5879916a9293e5cf059c23853d571beaf4f753c707f40cb22bed5fb1578c6aad3b6c4107ccb3ba0b35be003eb621a16471ac836c87beb53f9d54bb4612724" 50 | } 51 | -------------------------------------------------------------------------------- /src/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, readFileSync } from 'fs'; 2 | import path from 'path'; 3 | import { leadingTrailingSlash, replaceCSS, replaceHTML } from '..'; 4 | 5 | test.each([ 6 | [undefined, '/'], 7 | ['', '/'], 8 | ['/', '/'], 9 | ['foo', '/foo/'], 10 | ['/foo', '/foo/'], 11 | ['/foo/', '/foo/'], 12 | ['//foo//', '/foo/'], 13 | ['foo/bar', '/foo/bar/'], 14 | ['/foo/bar', '/foo/bar/'], 15 | ['foo/bar/', '/foo/bar/'], 16 | ['/foo/bar/', '/foo/bar/'], 17 | ['//foo/bar//', '/foo/bar/'], 18 | ])('Leading and trailing slashed base', (base, expected) => { 19 | expect(leadingTrailingSlash(base)).toBe(expected); 20 | }); 21 | 22 | test.each([ 23 | { 24 | base: '/', 25 | file: 'index.html', 26 | }, 27 | { 28 | base: '/', 29 | file: 'foo.html', 30 | }, 31 | { 32 | base: '/', 33 | file: 'foo/index.html', 34 | }, 35 | { 36 | base: '/foo/', 37 | file: 'index.html', 38 | }, 39 | { 40 | base: '/foo/', 41 | file: 'foo.html', 42 | }, 43 | { 44 | base: '/foo/', 45 | file: 'foo/index.html', 46 | }, 47 | ])('Replaced HTML', ({ file, base }) => { 48 | const outDirPath = '/dist/'; 49 | const filePath = outDirPath + file; 50 | 51 | expect( 52 | replaceHTML({ 53 | outDirPath, 54 | filePath, 55 | base, 56 | html: readFileSync(path.resolve(__dirname, 'src.html'), 'utf-8'), 57 | }) 58 | ).toBe( 59 | readFileSync(path.resolve(__dirname, 'expected' + base + filePath), 'utf-8') 60 | ); 61 | }); 62 | 63 | test.each([ 64 | { 65 | base: '/', 66 | file: '_astro/style.css', 67 | }, 68 | { 69 | base: '/foo/', 70 | file: '_astro/style.css', 71 | }, 72 | ])('Replaced CSS', ({ base, file }) => { 73 | const outDirPath = '/dist/'; 74 | const filePath = outDirPath + file; 75 | 76 | expect( 77 | replaceCSS({ 78 | outDirPath, 79 | filePath, 80 | base, 81 | css: readFileSync(path.resolve(__dirname, 'src.css'), 'utf-8'), 82 | }) 83 | ).toBe( 84 | readFileSync(path.resolve(__dirname, 'expected' + base + filePath), 'utf-8') 85 | ); 86 | }); 87 | -------------------------------------------------------------------------------- /src/test/expected/foo/dist/foo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 48 | / 49 | /foo/ 50 | /foobar/ 51 | /foo/bar/ 52 | /foo/baz/ 53 | /foo/baz/ 56 | /foo/baz/ 57 | /foo/baz/ 58 | 61 | /foo/baz/ 62 | 63 | /foo/baz/ 64 | example 65 | example 66 | parent 67 | foo 68 | 74 | 80 | 81 | 82 |

<a href="/foo/bar/">bar</a>

83 |
84 |
85 |
background-image: url(/assets/images/foo.jpg);
86 |
87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/test/expected/foo/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 48 | / 49 | /foo/ 50 | /foobar/ 51 | /foo/bar/ 52 | /foo/baz/ 53 | /foo/baz/ 56 | /foo/baz/ 57 | /foo/baz/ 58 | 61 | /foo/baz/ 62 | 63 | /foo/baz/ 64 | example 65 | example 66 | parent 67 | foo 68 | 74 | 80 | 81 | 82 |

<a href="/foo/bar/">bar</a>

83 |
84 |
85 |
background-image: url(/assets/images/foo.jpg);
86 |
87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/test/expected/foo/dist/foo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 48 | / 49 | /foo/ 50 | /foobar/ 51 | /foo/bar/ 52 | /foo/baz/ 53 | /foo/baz/ 56 | /foo/baz/ 57 | /foo/baz/ 58 | 61 | /foo/baz/ 62 | 63 | /foo/baz/ 64 | example 65 | example 66 | parent 67 | foo 68 | 74 | 80 | 81 | 82 |

<a href="/foo/bar/">bar</a>

83 |
84 |
85 |
background-image: url(/assets/images/foo.jpg);
86 |
87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/test/src.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 48 | / 49 | /foo/ 50 | /foobar/ 51 | /foo/bar/ 52 | /foo/baz/ 53 | /foo/baz/ 56 | /foo/baz/ 57 | /foo/baz/ 58 | 61 | /foo/baz/ 62 | 63 | /foo/baz/ 64 | example 65 | example 66 | parent 67 | foo 68 | 74 | 80 | 81 | 82 |

<a href="/foo/bar/">bar</a>

83 |
84 |
85 |
background-image: url(/assets/images/foo.jpg);
86 |
87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/test/expected/dist/foo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 48 | / 49 | /foo/ 50 | /foobar/ 51 | /foo/bar/ 52 | /foo/baz/ 53 | /foo/baz/ 56 | /foo/baz/ 57 | /foo/baz/ 58 | 61 | /foo/baz/ 62 | 63 | /foo/baz/ 64 | example 65 | example 66 | parent 67 | foo 68 | 74 | 80 | 81 | 82 |

<a href="/foo/bar/">bar</a>

83 |
84 |
85 |
background-image: url(/assets/images/foo.jpg);
86 |
87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/test/expected/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 48 | / 49 | /foo/ 50 | /foobar/ 51 | /foo/bar/ 52 | /foo/baz/ 53 | /foo/baz/ 56 | /foo/baz/ 57 | /foo/baz/ 58 | 61 | /foo/baz/ 62 | 63 | /foo/baz/ 64 | example 65 | example 66 | parent 67 | foo 68 | 74 | 80 | 81 | 82 |

<a href="/foo/bar/">bar</a>

83 |
84 |
85 |
background-image: url(/assets/images/foo.jpg);
86 |
87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/test/expected/dist/foo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 48 | / 49 | /foo/ 50 | /foobar/ 51 | /foo/bar/ 52 | /foo/baz/ 53 | /foo/baz/ 56 | /foo/baz/ 57 | /foo/baz/ 58 | 61 | /foo/baz/ 62 | 63 | /foo/baz/ 64 | example 65 | example 66 | parent 67 | foo 68 | 74 | 80 | 81 | 82 |

<a href="/foo/bar/">bar</a>

83 |
84 |
85 |
background-image: url(/assets/images/foo.jpg);
86 |
87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { AstroIntegration, AstroConfig } from 'astro'; 2 | import { writeFileSync, readFileSync } from 'fs'; 3 | import { globSync } from 'glob'; 4 | import path from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | /** 8 | * Add leading and trailing slashes to the `base`. 9 | * 10 | * @param {string} base 11 | * @returns {string} - Formatted base. 12 | */ 13 | export function leadingTrailingSlash(base?: string) { 14 | return base?.replace(/^\/*([^\/]+)(.*)([^\/]+)\/*$/, '/$1$2$3/') || '/'; 15 | } 16 | 17 | const pattern = { 18 | htmlAttr: `(href|src(set)?|poster|component-url|renderer-url)=["']?([^"']*,)?`, 19 | styleAttr: `style=("[^"]*|'[^']*|[^\\s]*)`, 20 | styleUrl: `url\\(\\s*?["']?`, 21 | }; 22 | 23 | /** 24 | * Replace absolute paths in HTML files with relative paths. 25 | * 26 | * @param {object} options 27 | * @param {string} options.outDirPath - The path of the directory that `astro build` writes final build to. 28 | * @param {string} options.filePath - The path of the target file. 29 | * @param {string} options.base - The base path to deploy to. 30 | * @param {string} options.html - The content of the HTML file. 31 | * @returns {string} - Replaced HTML 32 | */ 33 | export function replaceHTML({ 34 | outDirPath, 35 | filePath, 36 | base, 37 | html, 38 | }: { 39 | outDirPath: string; 40 | filePath: string; 41 | base: string; 42 | html: string; 43 | }) { 44 | const { htmlAttr, styleAttr, styleUrl } = pattern; 45 | const htmlPattern = 46 | `<[^>]+\\s(` + htmlAttr + `|` + styleAttr + styleUrl + `)`; 47 | const cssPattern = `