Skip to content

Commit

Permalink
Replace polyfill with own path implementation
Browse files Browse the repository at this point in the history
Remove the need for browserify packages due to security alerts from
unused polyfills
  • Loading branch information
Trinovantes committed Aug 14, 2024
1 parent 2c09ab0 commit bbced59
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 763 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@
"typescript-eslint": "^8.0.0-alpha.24",
"vite": "^5.2.12",
"vite-plugin-dts": "^4.0.2",
"vite-plugin-node-polyfills": "^0.22.0",
"vitest": "^2.0.5",
"vue": "^3.4.30",
"vue-tsc": "^2.0.29"
Expand Down
30 changes: 0 additions & 30 deletions src/Generator/FilePath.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/Generator/FilePathWithoutRst.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Brand } from '@/@types/Brand.js'

export type FilePathWithoutRst = Brand<string, 'FilePathWithoutRst'>

export function getFilePathWithoutRst(filePath: string): FilePathWithoutRst {
const pathWithoutExt = /^(?<pathWithoutExt>.+?)(?:\.rst)?$/.exec(filePath)?.groups?.pathWithoutExt ?? filePath
return pathWithoutExt as FilePathWithoutRst
}
7 changes: 4 additions & 3 deletions src/Generator/RstGeneratorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SubstitutionResolver } from '@/Parser/Resolver/SubstitutionResolver.js'
import { HtmlAttrResolver } from '@/Parser/Resolver/HtmlAttrResolver.js'
import { RstDocument } from '@/RstNode/Block/Document.js'
import { RstNodeGenerator } from './RstGenerator.js'
import { FilePathWithoutRst, getFilePathWithoutRst, joinFilePath, normalizeFilePath, resolveFilePath } from './FilePath.js'
import { FilePathWithoutRst, getFilePathWithoutRst } from './FilePathWithoutRst.js'
import { RstSection } from '@/RstNode/Block/Section.js'
import { RstNodeType } from '@/RstNode/RstNodeType.js'
import { HtmlAttributeStore } from './HtmlAttributeStore.js'
Expand All @@ -22,6 +22,7 @@ import { RstParserOutput } from '@/Parser/RstParserState.js'
import { RstCitationRef } from '@/RstNode/Inline/CitationRef.js'
import { RstCitationDef } from '@/RstNode/ExplicitMarkup/CitationDef.js'
import { getAutoFootnoteSymbol } from '@/utils/getAutoFootnoteSymbol.js'
import { getPathDirname, joinFilePath, normalizeFilePath, resolveFilePath } from '@/utils/path.js'

export type RstGeneratorInput = {
basePath: string // Base url all pages will be deployed to (default /)
Expand Down Expand Up @@ -160,7 +161,7 @@ export class RstGeneratorState {
resolveExternalDoc(srcNode: RstNode, targetPath: string): { externalUrl: string; externalLabel?: string } {
const docFilePath = targetPath.startsWith('/')
? joinFilePath(this._basePath, targetPath) // If given abs path, assume we want to search from basePath
: resolveFilePath(this._currentDocPath, targetPath) // Otherwise, assume we want to search relative from current doc
: resolveFilePath(getPathDirname(this._currentDocPath), targetPath) // Otherwise, assume we want to search relative from current doc

const docPath = getFilePathWithoutRst(docFilePath)
const externalUrl = `${docPath}.html`
Expand Down Expand Up @@ -551,7 +552,7 @@ export class RstGeneratorState {
registerDownload(targetPath: string) {
const downloadSrc = targetPath.startsWith('/')
? joinFilePath(this._basePath, targetPath) // If given abs path, assume we want to search from basePath
: resolveFilePath(this._currentDocPath, targetPath) // Otherwise, assume we want to search relative from current doc
: resolveFilePath(getPathDirname(this._currentDocPath), targetPath) // Otherwise, assume we want to search relative from current doc

const fileHash = sha1(downloadSrc)
const fileName = downloadSrc.split('/').at(-1) ?? downloadSrc
Expand Down
Empty file added src/utils/normalizePath.ts
Empty file.
185 changes: 185 additions & 0 deletions src/utils/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// ----------------------------------------------------------------------------
// MARK: getPathDirname
// Replace path.dirname
// ----------------------------------------------------------------------------

export function getPathDirname(path: string): string {
if (path.length === 0) {
return '.'
}

const isAbsolute = path.at(0) === '/'

// Iterate BACKWARDS and stop when we find first slash preceding non-path-separator
// e.g. Stop on first slash after after seeing `b`
// `/a/b` -> `/a`
// `/a/b/` -> `/a`
let dirnameSlashIdx: number | null = null
let sawNonPathSeparator = false

for (let i = path.length - 1; i >= 1; i--) {
const currChar = path.charAt(i)
if (currChar !== '/') {
sawNonPathSeparator = true
continue
}

// currChar === '/'
// If we already see non-path-separator, then we can terminate the search
if (sawNonPathSeparator) {
dirnameSlashIdx = i
break
}
}

// Search failed, then path is empty
if (dirnameSlashIdx === null) {
return isAbsolute ? '/' : '.'
}

// Special case of UNC path (`//abc` should return `//`)
if (isAbsolute && dirnameSlashIdx === 1) {
return '//'
}

return path.slice(0, dirnameSlashIdx)
}

// ----------------------------------------------------------------------------
// MARK: joinFilePath
// Replace path.join
// ----------------------------------------------------------------------------

export function joinFilePath(...args: Array<string>): string {
let joined = ''

for (const arg of args) {
if (!arg) {
continue
}

if (joined.length === 0) {
joined = arg
} else {
joined += `/${arg}`
}
}

return normalizeFilePath(joined)
}

// ----------------------------------------------------------------------------
// MARK: resolveFilePath
// Replace path.resolve
// ----------------------------------------------------------------------------

export function resolveFilePath(...args: Array<string>): string {
let resolvedPath = ''
let lastFragmentWasAbsolute = false

// Iterate backwards and construct the resolved path
for (let i = args.length - 1; i >= 0 && !lastFragmentWasAbsolute; i--) {
const currPathFragment = args[i]
if (!currPathFragment) {
continue
}

if (resolvedPath.length === 0) {
resolvedPath = currPathFragment
} else {
resolvedPath = currPathFragment + '/' + resolvedPath
}

lastFragmentWasAbsolute = currPathFragment.charAt(0) === '/'
}

// Use normalizeString instead of normalizeFilePath because
// path.resolve is defined to trim trailing slash whereas normalizeFilePath preserves trailing slash
resolvedPath = normalizeString(resolvedPath, lastFragmentWasAbsolute)

if (lastFragmentWasAbsolute) {
return `/${resolvedPath}`
}
if (resolvedPath.length === 0) {
return '.'
}

return resolvedPath
}

// ----------------------------------------------------------------------------
// MARK: normalizeFilePath
// Replace path.normalize
// ----------------------------------------------------------------------------

export function normalizeFilePath(path: string): string {
if (path.length === 0) {
return '.'
}
if (path.length === 1) {
return path
}

const isAbsolute = path.charAt(0) === '/'
const hasTrailingSlash = path.charAt(path.length - 1) === '/'

let resultPath = normalizeString(path, isAbsolute)
if (resultPath.length === 0) {
if (isAbsolute) {
return '/'
} else {
if (hasTrailingSlash) {
return './'
} else {
return '.'
}
}
}

if (isAbsolute) {
resultPath = `/${resultPath}`
}
if (hasTrailingSlash) {
resultPath = `${resultPath}/`
}

return resultPath
}

function normalizeString(path: string, isAbsolute: boolean): string {
const parts = path.split('/').filter((part) => part.length > 0)
const outputStack = new Array<string>()

for (let i = 0; i < parts.length; i++) {
const currPart = parts.at(i)

// Skip over repeated slashes e.g. `////`
if (!currPart) {
continue
}

// Skip over single dots e.g. `/./`
if (currPart === '.') {
continue
}

// Skip over double dots and pop off previous part off stack
if (currPart === '..') {
switch (true) {
// Unless:
case !isAbsolute && outputStack.length === 0: // Original path is relative path and output is currently empty (at the start)
case outputStack.at(-1) === '..': // Already has '..' on the stack (i.e. continuing relative ../../../)
break

default: {
outputStack.pop()
continue
}
}
}

outputStack.push(currPart)
}

return outputStack.join('/')
}
13 changes: 13 additions & 0 deletions tests/unit/Generator/FilePathWithoutRst.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getFilePathWithoutRst } from '@/Generator/FilePathWithoutRst.js'
import { expect, test } from 'vitest'

test('when file path includes rst file extension, it removes the extension', () => {
const input = '/hello/world/mydoc.rst'
const output = '/hello/world/mydoc'
expect(getFilePathWithoutRst(input)).toBe(output)
})

test('when file path does not include rst file extension, it returns the original string', () => {
const input = '/hello/world/mydoc'
expect(getFilePathWithoutRst(input)).toBe(input)
})
96 changes: 96 additions & 0 deletions tests/unit/utils/path.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { expect, test, describe } from 'vitest'
import { getPathDirname, joinFilePath, resolveFilePath } from '@/utils/path.js'

// ----------------------------------------------------------------------------
// MARK: getPathDirname
// ----------------------------------------------------------------------------

describe('getPathDirname', () => {
test.each([
['/a/b/', '/a'],
['/a/b', '/a'],
['/a', '/'],
['', '.'],
['/', '/'],
['////', '/'],
['//a', '//'],
['a', '.'],
])('"%s" dirname = "%s"', (input, expectedOutput) => {
expect(getPathDirname(input)).toBe(expectedOutput)
})
})

// ----------------------------------------------------------------------------
// MARK: joinFilePath
// ----------------------------------------------------------------------------

describe('joinFilePath', () => {
test.each([
[['.', 'x/b', '..', '/b/c.js'], 'x/b/c.js'],
[[], '.'],
[['/.', 'x/b', '..', '/b/c.js'], '/x/b/c.js'],
[['/foo', '../../../bar'], '/bar'],
[['foo', '../../../bar'], '../../bar'],
[['foo/', '../../../bar'], '../../bar'],
[['foo/x', '../../../bar'], '../bar'],
[['foo/x', './bar'], 'foo/x/bar'],
[['foo/x/', './bar'], 'foo/x/bar'],
[['foo/x/', '.', 'bar'], 'foo/x/bar'],
[['./'], './'],
[['.', './'], './'],
[['.', '.', '.'], '.'],
[['.', './', '.'], '.'],
[['.', '/./', '.'], '.'],
[['.', '/////./', '.'], '.'],
[['.'], '.'],
[['', '.'], '.'],
[['', 'foo'], 'foo'],
[['foo', '/bar'], 'foo/bar'],
[['', '/foo'], '/foo'],
[['', '', '/foo'], '/foo'],
[['', '', 'foo'], 'foo'],
[['foo', ''], 'foo'],
[['foo/', ''], 'foo/'],
[['foo', '', '/bar'], 'foo/bar'],
[['./', '..', '/foo'], '../foo'],
[['./', '..', '..', '/foo'], '../../foo'],
[['.', '..', '..', '/foo'], '../../foo'],
[['', '..', '..', '/foo'], '../../foo'],
[['/'], '/'],
[['/', '.'], '/'],
[['/', '..'], '/'],
[['/', '..', '..'], '/'],
[[''], '.'],
[['', ''], '.'],
[[' /foo'], ' /foo'],
[[' ', 'foo'], ' /foo'],
[[' ', '.'], ' '],
[[' ', '/'], ' /'],
[[' ', ''], ' '],
[['/', 'foo'], '/foo'],
[['/', '/foo'], '/foo'],
[['/', '//foo'], '/foo'],
[['/', '', '/foo'], '/foo'],
[['', '/', 'foo'], '/foo'],
[['', '/', '/foo'], '/foo'],
])('%s joins to "%s"', (input, expectedOutput) => {
expect(joinFilePath(...input)).toBe(expectedOutput)
})
})

// ----------------------------------------------------------------------------
// MARK: resolveFilePath
// ----------------------------------------------------------------------------

describe('resolveFilePath', () => {
test.each([
[['/var/lib', '../', 'file/'], '/var/file'],
[['/var/lib', '/../', 'file/'], '/file'],
[['a/b/c/', '../../..'], '.'],
[['.'], '.'],
[['/some/dir', '.', '/absolute/'], '/absolute'],
[['/foo/tmp.3/', '../tmp.3/cycles/root.js'], '/foo/tmp.3/cycles/root.js'],
])('%s resolves to "%s"', (input, expectedOutput) => {
expect(resolveFilePath(...input)).toBe(expectedOutput)
})
})
5 changes: 0 additions & 5 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from 'node:path'
import { defineConfig, mergeConfig } from 'vitest/config'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import dts from 'vite-plugin-dts'

export const commonConfig = defineConfig({
Expand All @@ -10,10 +9,6 @@ export const commonConfig = defineConfig({
'tests': path.resolve(__dirname, 'tests'),
},
},

plugins: [
nodePolyfills(),
],
})

export default mergeConfig(commonConfig, defineConfig({
Expand Down
Loading

0 comments on commit bbced59

Please sign in to comment.