mirror of
https://github.com/LukeHagar/sveltekit-adapters.git
synced 2025-12-06 20:57:49 +00:00
chore: Bump version to 1.0.6 and update package.json for adapter-electron
- Added placeholders.d.ts to the files list. - Updated dependencies for electron and cookie. - Introduced new scripts for testing and type checking. - Adjusted devDependencies for compatibility with TypeScript and testing tools.
This commit is contained in:
209
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
209
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report a bug with SvelteKit Electron or Appwrite adapters
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: ["bug", "triage"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report! Please provide as much detail as possible to help us reproduce and fix the issue.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: adapter
|
||||||
|
attributes:
|
||||||
|
label: Which adapter is affected?
|
||||||
|
description: Select the adapter you're having issues with
|
||||||
|
options:
|
||||||
|
- adapter-electron
|
||||||
|
- adapter-appwrite
|
||||||
|
- Both adapters
|
||||||
|
- Not sure
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A clear and concise description of what the bug is
|
||||||
|
placeholder: Tell us what happened!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Steps to reproduce the behavior
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: A clear and concise description of what you expected to happen
|
||||||
|
placeholder: What should have happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
description: What actually happened instead
|
||||||
|
placeholder: What actually happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: error-logs
|
||||||
|
attributes:
|
||||||
|
label: Error logs
|
||||||
|
description: If applicable, paste any error messages or stack traces
|
||||||
|
placeholder: |
|
||||||
|
Paste error logs here...
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: If applicable, add screenshots to help explain your problem
|
||||||
|
placeholder: Drag and drop screenshots here
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: What OS are you running?
|
||||||
|
placeholder: e.g. Windows 11, macOS 14.1, Ubuntu 22.04
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: node-version
|
||||||
|
attributes:
|
||||||
|
label: Node.js version
|
||||||
|
description: What version of Node.js are you using?
|
||||||
|
placeholder: e.g. 18.17.0, 20.9.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: electron-version
|
||||||
|
attributes:
|
||||||
|
label: Electron version (if using adapter-electron)
|
||||||
|
description: What version of Electron are you using?
|
||||||
|
placeholder: e.g. 28.0.0, 29.1.0
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: sveltekit-version
|
||||||
|
attributes:
|
||||||
|
label: SvelteKit version
|
||||||
|
description: What version of SvelteKit are you using?
|
||||||
|
placeholder: e.g. 2.0.0, 2.5.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: adapter-version
|
||||||
|
attributes:
|
||||||
|
label: Adapter version
|
||||||
|
description: What version of the adapter are you using?
|
||||||
|
placeholder: e.g. 0.1.0, 1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: environment
|
||||||
|
attributes:
|
||||||
|
label: Environment
|
||||||
|
description: In which environment does this issue occur?
|
||||||
|
options:
|
||||||
|
- Development (npm run dev)
|
||||||
|
- Production build (npm run build)
|
||||||
|
- Both development and production
|
||||||
|
- Not sure
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: config
|
||||||
|
attributes:
|
||||||
|
label: Configuration
|
||||||
|
description: Please share your relevant configuration files
|
||||||
|
placeholder: |
|
||||||
|
svelte.config.js:
|
||||||
|
```js
|
||||||
|
// paste your svelte.config.js here
|
||||||
|
```
|
||||||
|
|
||||||
|
vite.config.js:
|
||||||
|
```js
|
||||||
|
// paste your vite.config.js here
|
||||||
|
```
|
||||||
|
|
||||||
|
package.json (relevant sections):
|
||||||
|
```json
|
||||||
|
// paste relevant parts of package.json
|
||||||
|
```
|
||||||
|
render: javascript
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: minimal-reproduction
|
||||||
|
attributes:
|
||||||
|
label: Minimal reproduction
|
||||||
|
description: |
|
||||||
|
If possible, provide a minimal reproduction of the issue. This could be:
|
||||||
|
- A link to a GitHub repository
|
||||||
|
- A CodeSandbox/StackBlitz link
|
||||||
|
- Minimal code snippets
|
||||||
|
placeholder: |
|
||||||
|
Link to reproduction: https://github.com/...
|
||||||
|
|
||||||
|
Or paste minimal code here:
|
||||||
|
```js
|
||||||
|
// minimal reproduction code
|
||||||
|
```
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: workaround
|
||||||
|
attributes:
|
||||||
|
label: Workaround
|
||||||
|
description: If you found a workaround, please describe it here
|
||||||
|
placeholder: Describe any workarounds you've found
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context about the problem here
|
||||||
|
placeholder: |
|
||||||
|
Any additional information that might be helpful:
|
||||||
|
- Related issues
|
||||||
|
- Recent changes to your setup
|
||||||
|
- Browser console errors (if applicable)
|
||||||
|
- Network requests (if applicable)
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Pre-submission checklist
|
||||||
|
description: Please check the following before submitting
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues to make sure this is not a duplicate
|
||||||
|
required: true
|
||||||
|
- label: I have provided all the requested information above
|
||||||
|
required: true
|
||||||
|
- label: I have tested this with the latest version of the adapter
|
||||||
|
required: true
|
||||||
|
- label: I have included a minimal reproduction (if possible)
|
||||||
|
required: false
|
||||||
130
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
130
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or enhancement for SvelteKit adapters
|
||||||
|
title: "[FEATURE] "
|
||||||
|
labels: ["enhancement", "triage"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for suggesting a new feature! Please provide as much detail as possible to help us understand your request.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: adapter
|
||||||
|
attributes:
|
||||||
|
label: Which adapter is this feature for?
|
||||||
|
description: Select the adapter this feature request relates to
|
||||||
|
options:
|
||||||
|
- adapter-electron
|
||||||
|
- adapter-appwrite
|
||||||
|
- Both adapters
|
||||||
|
- New adapter
|
||||||
|
- General/Core
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem?
|
||||||
|
description: A clear and concise description of what the problem is
|
||||||
|
placeholder: I'm always frustrated when...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
description: A clear and concise description of what you want to happen
|
||||||
|
placeholder: I would like...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered
|
||||||
|
placeholder: Alternatively, we could...
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use-case
|
||||||
|
attributes:
|
||||||
|
label: Use case
|
||||||
|
description: Describe your specific use case and how this feature would help
|
||||||
|
placeholder: |
|
||||||
|
I'm building an application that...
|
||||||
|
This feature would help by...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: implementation
|
||||||
|
attributes:
|
||||||
|
label: Implementation ideas
|
||||||
|
description: If you have ideas on how this could be implemented, please share them
|
||||||
|
placeholder: |
|
||||||
|
This could be implemented by...
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Example API or code structure
|
||||||
|
```
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority
|
||||||
|
attributes:
|
||||||
|
label: Priority
|
||||||
|
description: How important is this feature to you?
|
||||||
|
options:
|
||||||
|
- Low - Nice to have
|
||||||
|
- Medium - Would be helpful
|
||||||
|
- High - Needed for my project
|
||||||
|
- Critical - Blocking my work
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: complexity
|
||||||
|
attributes:
|
||||||
|
label: Estimated complexity
|
||||||
|
description: How complex do you think this feature would be to implement?
|
||||||
|
options:
|
||||||
|
- Low - Simple configuration or small addition
|
||||||
|
- Medium - Moderate changes to existing code
|
||||||
|
- High - Significant changes or new architecture
|
||||||
|
- Not sure
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: examples
|
||||||
|
attributes:
|
||||||
|
label: Examples from other tools
|
||||||
|
description: Are there similar features in other tools or frameworks that we could reference?
|
||||||
|
placeholder: |
|
||||||
|
Similar to how [tool] does [feature]...
|
||||||
|
Link: https://...
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context, screenshots, or mockups about the feature request here
|
||||||
|
placeholder: |
|
||||||
|
Any additional information:
|
||||||
|
- Related issues or discussions
|
||||||
|
- Screenshots or mockups
|
||||||
|
- Links to relevant documentation
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Pre-submission checklist
|
||||||
|
description: Please check the following before submitting
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues to make sure this is not a duplicate
|
||||||
|
required: true
|
||||||
|
- label: I have provided a clear description of the problem and solution
|
||||||
|
required: true
|
||||||
|
- label: I have described my specific use case
|
||||||
|
required: true
|
||||||
213
.github/workflows/test.yml
vendored
Normal file
213
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
name: Test and Type Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test Adapters
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18, 20]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run adapter-electron tests
|
||||||
|
run: |
|
||||||
|
cd packages/adapter-electron
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
- name: Run adapter-appwrite tests (if exists)
|
||||||
|
run: |
|
||||||
|
if [ -f "packages/adapter-appwrite/package.json" ]; then
|
||||||
|
cd packages/adapter-appwrite
|
||||||
|
if grep -q '"test"' package.json; then
|
||||||
|
pnpm test
|
||||||
|
else
|
||||||
|
echo "No tests found for adapter-appwrite"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "adapter-appwrite package not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate test coverage
|
||||||
|
run: |
|
||||||
|
cd packages/adapter-electron
|
||||||
|
pnpm run test:coverage
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: ./packages/adapter-electron/coverage/coverage-final.json
|
||||||
|
flags: adapter-electron
|
||||||
|
name: adapter-electron-coverage
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
name: Type Check Adapters
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Type check adapter-electron
|
||||||
|
run: |
|
||||||
|
cd packages/adapter-electron
|
||||||
|
pnpm run typecheck
|
||||||
|
|
||||||
|
- name: Type check adapter-appwrite
|
||||||
|
run: |
|
||||||
|
if [ -f "packages/adapter-appwrite/package.json" ]; then
|
||||||
|
cd packages/adapter-appwrite
|
||||||
|
if grep -q '"typecheck"' package.json; then
|
||||||
|
pnpm run typecheck
|
||||||
|
else
|
||||||
|
echo "No typecheck script found for adapter-appwrite"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "adapter-appwrite package not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Type check examples
|
||||||
|
run: |
|
||||||
|
# Type check electron example
|
||||||
|
if [ -f "examples/electron/package.json" ]; then
|
||||||
|
cd examples/electron
|
||||||
|
if grep -q '"check"' package.json; then
|
||||||
|
pnpm run check
|
||||||
|
else
|
||||||
|
echo "No check script found for electron example"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Type check appwrite example
|
||||||
|
if [ -f "examples/appwrite/package.json" ]; then
|
||||||
|
cd examples/appwrite
|
||||||
|
if grep -q '"check"' package.json; then
|
||||||
|
pnpm run check
|
||||||
|
else
|
||||||
|
echo "No check script found for appwrite example"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-test:
|
||||||
|
name: Build Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build electron example
|
||||||
|
run: |
|
||||||
|
cd examples/electron
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
- name: Validate build output
|
||||||
|
run: |
|
||||||
|
cd examples/electron
|
||||||
|
# Check that required files exist
|
||||||
|
test -d "out/client" || (echo "❌ Missing client directory" && exit 1)
|
||||||
|
test -f "out/server/index.js" || (echo "❌ Missing server/index.js" && exit 1)
|
||||||
|
test -f "out/server/manifest.js" || (echo "❌ Missing server/manifest.js" && exit 1)
|
||||||
|
test -f "out/functions/setupHandler.js" || (echo "❌ Missing functions/setupHandler.js" && exit 1)
|
||||||
|
test -f "out/main/index.js" || (echo "❌ Missing main/index.js" && exit 1)
|
||||||
|
test -f "out/preload/index.js" || (echo "❌ Missing preload/index.js" && exit 1)
|
||||||
|
echo "✅ All required build files exist"
|
||||||
|
|
||||||
|
- name: Build appwrite example
|
||||||
|
run: |
|
||||||
|
if [ -f "examples/appwrite/package.json" ]; then
|
||||||
|
cd examples/appwrite
|
||||||
|
pnpm run build
|
||||||
|
else
|
||||||
|
echo "appwrite example not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint Code
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: |
|
||||||
|
if [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
||||||
|
pnpm run lint
|
||||||
|
else
|
||||||
|
echo "No ESLint configuration found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Prettier check
|
||||||
|
run: |
|
||||||
|
if [ -f ".prettierrc" ] || [ -f ".prettierrc.json" ] || [ -f "prettier.config.js" ]; then
|
||||||
|
pnpm run format:check
|
||||||
|
else
|
||||||
|
echo "No Prettier configuration found"
|
||||||
|
fi
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "adapter-electron",
|
"name": "adapter-electron",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"description": "A SvelteKit adapter for Electron Desktop Apps using protocol interception",
|
"description": "A SvelteKit adapter for Electron Desktop Apps using protocol interception",
|
||||||
"files": [
|
|
||||||
"functions",
|
|
||||||
"index.js",
|
|
||||||
"index.d.ts"
|
|
||||||
],
|
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Luke Hagar",
|
"name": "Luke Hagar",
|
||||||
"email": "lukeslakemail@gmai.com",
|
"email": "lukeslakemail@gmai.com",
|
||||||
@@ -17,7 +12,14 @@
|
|||||||
"url": "https://github.com/lukehagar/sveltekit-adapters.git",
|
"url": "https://github.com/lukehagar/sveltekit-adapters.git",
|
||||||
"directory": "packages/adapter-electron"
|
"directory": "packages/adapter-electron"
|
||||||
},
|
},
|
||||||
"types": "index.d.ts",
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"files",
|
||||||
|
"functions",
|
||||||
|
"index.js",
|
||||||
|
"index.d.ts",
|
||||||
|
"placeholders.d.ts"
|
||||||
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./index.d.ts",
|
"types": "./index.d.ts",
|
||||||
@@ -28,34 +30,23 @@
|
|||||||
"import": "./functions/setupHandler.js"
|
"import": "./functions/setupHandler.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"scripts": {
|
||||||
"svelte": "^4.0.0"
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^27.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@rollup/plugin-json": "^6.1.0",
|
"@types/set-cookie-parser": "^2.4.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"electron": "^28.0.0",
|
||||||
"@sveltejs/kit": "^2.4.0",
|
|
||||||
"@sveltejs/package": "^2.0.0",
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
|
||||||
"@types/node": "^20.19.5",
|
|
||||||
"esbuild": "^0.25.6",
|
|
||||||
"prettier": "^3.1.1",
|
|
||||||
"prettier-plugin-svelte": "^3.1.2",
|
|
||||||
"publint": "^0.1.9",
|
|
||||||
"rollup": "^4.9.0",
|
|
||||||
"svelte": "^4.2.7",
|
|
||||||
"svelte-check": "^3.6.0",
|
|
||||||
"tslib": "^2.4.1",
|
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^5.0.11"
|
"vitest": "^1.0.0",
|
||||||
|
"@vitest/coverage-v8": "^1.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^0.6.0",
|
||||||
"electron": "^37.2.1",
|
|
||||||
"electron-is-dev": "^3.0.1",
|
"electron-is-dev": "^3.0.1",
|
||||||
"electron-log": "^5.1.1",
|
"set-cookie-parser": "^2.6.0"
|
||||||
"set-cookie-parser": "^2.7.1"
|
}
|
||||||
},
|
|
||||||
"type": "module"
|
|
||||||
}
|
}
|
||||||
|
|||||||
428
packages/adapter-electron/tests/integration/protocol.test.js
Normal file
428
packages/adapter-electron/tests/integration/protocol.test.js
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock Electron APIs
|
||||||
|
const mockProtocol = {
|
||||||
|
registerSchemesAsPrivileged: vi.fn(),
|
||||||
|
handle: vi.fn(),
|
||||||
|
unhandle: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockNet = {
|
||||||
|
fetch: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDialog = {
|
||||||
|
showErrorBox: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockApp = {
|
||||||
|
exit: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
protocol: mockProtocol,
|
||||||
|
net: mockNet,
|
||||||
|
dialog: mockDialog,
|
||||||
|
app: mockApp
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('electron-is-dev', () => ({
|
||||||
|
default: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock Node.js modules
|
||||||
|
vi.mock('node:fs/promises', () => ({
|
||||||
|
default: {
|
||||||
|
readFile: vi.fn(),
|
||||||
|
stat: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('node:path', () => ({
|
||||||
|
default: {
|
||||||
|
join: vi.fn((...args) => args.join('/')),
|
||||||
|
resolve: vi.fn((...args) => args.join('/')),
|
||||||
|
relative: vi.fn((from, to) => {
|
||||||
|
if (to.startsWith(from)) {
|
||||||
|
return to.slice(from.length + 1);
|
||||||
|
}
|
||||||
|
if (to.includes('..')) {
|
||||||
|
return '../' + to.split('/').pop();
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
}),
|
||||||
|
isAbsolute: vi.fn(path => path.startsWith('/')),
|
||||||
|
extname: vi.fn(path => {
|
||||||
|
const lastDot = path.lastIndexOf('.');
|
||||||
|
return lastDot === -1 ? '' : path.slice(lastDot);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('set-cookie-parser', () => ({
|
||||||
|
parse: vi.fn(() => []),
|
||||||
|
splitCookiesString: vi.fn(() => [])
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('cookie', () => ({
|
||||||
|
serialize: vi.fn((name, value) => `${name}=${value}`)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock SvelteKit server
|
||||||
|
const mockServer = {
|
||||||
|
init: vi.fn().mockResolvedValue(),
|
||||||
|
respond: vi.fn().mockResolvedValue(new Response('test response', {
|
||||||
|
status: 200,
|
||||||
|
headers: [['content-type', 'text/html']]
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockManifest = {
|
||||||
|
manifest: { routes: [] },
|
||||||
|
prerendered: new Set(['/prerendered-page']),
|
||||||
|
base: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('SERVER', () => ({
|
||||||
|
Server: vi.fn().mockImplementation(() => mockServer)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('MANIFEST', () => mockManifest);
|
||||||
|
|
||||||
|
describe('Protocol Integration', () => {
|
||||||
|
let mockWindow;
|
||||||
|
let mockSession;
|
||||||
|
let setupHandler;
|
||||||
|
let registerAppScheme;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Reset all mocks
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Setup mock session
|
||||||
|
mockSession = {
|
||||||
|
cookies: {
|
||||||
|
get: vi.fn().mockResolvedValue([
|
||||||
|
{ name: 'session', value: 'abc123' },
|
||||||
|
{ name: 'user', value: 'john' }
|
||||||
|
]),
|
||||||
|
set: vi.fn().mockResolvedValue(),
|
||||||
|
remove: vi.fn().mockResolvedValue()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup mock window
|
||||||
|
mockWindow = {
|
||||||
|
webContents: {
|
||||||
|
session: mockSession
|
||||||
|
},
|
||||||
|
loadURL: vi.fn().mockResolvedValue()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock global constructors
|
||||||
|
global.Request = vi.fn().mockImplementation((url, options) => ({
|
||||||
|
url,
|
||||||
|
method: options?.method || 'GET',
|
||||||
|
headers: options?.headers || new Headers(),
|
||||||
|
body: options?.body || null,
|
||||||
|
formData: vi.fn(),
|
||||||
|
json: vi.fn(),
|
||||||
|
text: vi.fn(),
|
||||||
|
arrayBuffer: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
global.Headers = vi.fn().mockImplementation(() => ({
|
||||||
|
set: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
has: vi.fn(),
|
||||||
|
forEach: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
global.URL = vi.fn().mockImplementation((url) => ({
|
||||||
|
toString: () => url,
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
pathname: url.includes('/') ? url.split('/').slice(3).join('/') || '/' : '/'
|
||||||
|
}));
|
||||||
|
|
||||||
|
global.Response = vi.fn().mockImplementation((body, init) => ({
|
||||||
|
status: init?.status || 200,
|
||||||
|
statusText: init?.statusText || 'OK',
|
||||||
|
headers: new Map(Object.entries(init?.headers || {})),
|
||||||
|
body
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import functions after mocks are set up
|
||||||
|
const module = await import('../../functions/setupHandler.js');
|
||||||
|
setupHandler = module.setupHandler;
|
||||||
|
registerAppScheme = module.registerAppScheme;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('registerAppScheme', () => {
|
||||||
|
it('should register HTTP scheme as privileged', () => {
|
||||||
|
registerAppScheme();
|
||||||
|
|
||||||
|
expect(mockProtocol.registerSchemesAsPrivileged).toHaveBeenCalledWith([
|
||||||
|
expect.objectContaining({
|
||||||
|
scheme: 'http',
|
||||||
|
privileges: expect.objectContaining({
|
||||||
|
standard: true,
|
||||||
|
secure: true,
|
||||||
|
supportFetchAPI: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only be called once', () => {
|
||||||
|
registerAppScheme();
|
||||||
|
registerAppScheme();
|
||||||
|
|
||||||
|
expect(mockProtocol.registerSchemesAsPrivileged).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setupHandler', () => {
|
||||||
|
it('should setup protocol handler in production mode', async () => {
|
||||||
|
const cleanup = await setupHandler(mockWindow);
|
||||||
|
|
||||||
|
expect(mockProtocol.handle).toHaveBeenCalledWith('http', expect.any(Function));
|
||||||
|
expect(mockWindow.loadURL).toHaveBeenCalledWith('http://127.0.0.1');
|
||||||
|
expect(cleanup).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize SvelteKit server in production', async () => {
|
||||||
|
await setupHandler(mockWindow);
|
||||||
|
|
||||||
|
expect(mockServer.init).toHaveBeenCalledWith({
|
||||||
|
env: process.env,
|
||||||
|
read: expect.any(Function)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cleanup function that unhandles protocol', async () => {
|
||||||
|
const cleanup = await setupHandler(mockWindow);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
expect(mockProtocol.unhandle).toHaveBeenCalledWith('http');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle development mode correctly', async () => {
|
||||||
|
// Mock development mode
|
||||||
|
vi.doMock('electron-is-dev', () => ({ default: true }));
|
||||||
|
|
||||||
|
// Re-import to get the dev version
|
||||||
|
vi.resetModules();
|
||||||
|
const devModule = await import('../../functions/setupHandler.js');
|
||||||
|
|
||||||
|
const cleanup = await devModule.setupHandler(mockWindow);
|
||||||
|
|
||||||
|
expect(mockWindow.loadURL).toHaveBeenCalledWith('http://localhost:5173');
|
||||||
|
expect(mockProtocol.handle).not.toHaveBeenCalled();
|
||||||
|
expect(cleanup).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use VITE_DEV_SERVER environment variable in development', async () => {
|
||||||
|
const originalEnv = process.env.VITE_DEV_SERVER;
|
||||||
|
process.env.VITE_DEV_SERVER = 'http://localhost:3000';
|
||||||
|
|
||||||
|
vi.doMock('electron-is-dev', () => ({ default: true }));
|
||||||
|
vi.resetModules();
|
||||||
|
const devModule = await import('../../functions/setupHandler.js');
|
||||||
|
|
||||||
|
await devModule.setupHandler(mockWindow);
|
||||||
|
|
||||||
|
expect(mockWindow.loadURL).toHaveBeenCalledWith('http://localhost:3000');
|
||||||
|
|
||||||
|
// Restore environment
|
||||||
|
if (originalEnv) {
|
||||||
|
process.env.VITE_DEV_SERVER = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env.VITE_DEV_SERVER;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Protocol Handler Function', () => {
|
||||||
|
let protocolHandler;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupHandler(mockWindow);
|
||||||
|
|
||||||
|
// Extract the protocol handler function
|
||||||
|
const handleCall = mockProtocol.handle.mock.calls.find(call => call[0] === 'http');
|
||||||
|
protocolHandler = handleCall[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle static file requests', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
url: 'http://127.0.0.1/favicon.ico',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock file exists
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
fs.default.stat.mockResolvedValue({ isFile: () => true });
|
||||||
|
|
||||||
|
mockNet.fetch.mockResolvedValue(new Response('file content'));
|
||||||
|
|
||||||
|
const response = await protocolHandler(mockRequest);
|
||||||
|
|
||||||
|
expect(mockNet.fetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle prerendered page requests', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
url: 'http://127.0.0.1/prerendered-page',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock file exists for prerendered page
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
fs.default.stat.mockResolvedValue({ isFile: () => true });
|
||||||
|
|
||||||
|
mockNet.fetch.mockResolvedValue(new Response('<html>prerendered</html>'));
|
||||||
|
|
||||||
|
const response = await protocolHandler(mockRequest);
|
||||||
|
|
||||||
|
expect(mockNet.fetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle SSR requests', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
url: 'http://127.0.0.1/dynamic-page',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map([['accept', 'text/html']])
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock file doesn't exist (not static or prerendered)
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
fs.default.stat.mockRejectedValue(new Error('File not found'));
|
||||||
|
|
||||||
|
const response = await protocolHandler(mockRequest);
|
||||||
|
|
||||||
|
expect(mockServer.respond).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API requests', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
url: 'http://127.0.0.1/api/users',
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Map([['content-type', 'application/json']]),
|
||||||
|
uploadData: [{
|
||||||
|
bytes: new Uint8Array(Buffer.from('{"name":"John"}'))
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock file doesn't exist
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
fs.default.stat.mockRejectedValue(new Error('File not found'));
|
||||||
|
|
||||||
|
mockServer.respond.mockResolvedValue(new Response('{"id":1}', {
|
||||||
|
status: 200,
|
||||||
|
headers: [['content-type', 'application/json']]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await protocolHandler(mockRequest);
|
||||||
|
|
||||||
|
expect(mockServer.respond).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject requests from wrong host', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
url: 'http://evil.com/malicious',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await protocolHandler(mockRequest);
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle path traversal attempts', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
url: 'http://127.0.0.1/../../../etc/passwd',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock file exists but path is unsafe
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
fs.default.stat.mockResolvedValue({ isFile: () => true });
|
||||||
|
|
||||||
|
const response = await protocolHandler(mockRequest);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(mockDialog.showErrorBox).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle cookie synchronization', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
url: 'http://127.0.0.1/set-cookies',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock file doesn't exist, will go to SSR
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
fs.default.stat.mockRejectedValue(new Error('File not found'));
|
||||||
|
|
||||||
|
// Mock response with set-cookie headers
|
||||||
|
mockServer.respond.mockResolvedValue(new Response('OK', {
|
||||||
|
status: 200,
|
||||||
|
headers: [
|
||||||
|
['set-cookie', 'session=new123; Path=/; HttpOnly'],
|
||||||
|
['set-cookie', 'user=jane; Path=/; Secure']
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setCookieParser = await import('set-cookie-parser');
|
||||||
|
setCookieParser.parse.mockReturnValue([
|
||||||
|
{ name: 'session', value: 'new123', path: '/', httpOnly: true },
|
||||||
|
{ name: 'user', value: 'jane', path: '/', secure: true }
|
||||||
|
]);
|
||||||
|
setCookieParser.splitCookiesString.mockReturnValue([
|
||||||
|
'session=new123; Path=/; HttpOnly',
|
||||||
|
'user=jane; Path=/; Secure'
|
||||||
|
]);
|
||||||
|
|
||||||
|
await protocolHandler(mockRequest);
|
||||||
|
|
||||||
|
expect(mockSession.cookies.set).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockSession.cookies.set).toHaveBeenCalledWith({
|
||||||
|
url: 'http://127.0.0.1/set-cookies',
|
||||||
|
name: 'session',
|
||||||
|
value: 'new123',
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
expirationDate: undefined,
|
||||||
|
domain: undefined,
|
||||||
|
secure: undefined,
|
||||||
|
maxAge: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
url: 'http://127.0.0.1/error-page',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock server error
|
||||||
|
mockServer.respond.mockRejectedValue(new Error('Server error'));
|
||||||
|
|
||||||
|
const response = await protocolHandler(mockRequest);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(mockDialog.showErrorBox).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
packages/adapter-electron/tests/setup.js
Normal file
35
packages/adapter-electron/tests/setup.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Test setup file for vitest
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mock __dirname for ES modules
|
||||||
|
global.__dirname = process.cwd();
|
||||||
|
|
||||||
|
// Mock process.env defaults
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
|
||||||
|
|
||||||
|
// Global test utilities
|
||||||
|
global.mockElectronRequest = (overrides = {}) => ({
|
||||||
|
url: 'http://127.0.0.1/test',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map(),
|
||||||
|
body: null,
|
||||||
|
uploadData: [],
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
global.mockElectronSession = (overrides = {}) => ({
|
||||||
|
cookies: {
|
||||||
|
get: vi.fn().mockResolvedValue([]),
|
||||||
|
set: vi.fn().mockResolvedValue(),
|
||||||
|
remove: vi.fn().mockResolvedValue()
|
||||||
|
},
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
// Suppress console.error in tests unless specifically testing error handling
|
||||||
|
const originalConsoleError = console.error;
|
||||||
|
console.error = (...args) => {
|
||||||
|
if (process.env.VITEST_SHOW_ERRORS === 'true') {
|
||||||
|
originalConsoleError(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
294
packages/adapter-electron/tests/unit/setupHandler.test.js
Normal file
294
packages/adapter-electron/tests/unit/setupHandler.test.js
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { getMimeType } from '../../functions/setupHandler.js';
|
||||||
|
|
||||||
|
// Mock Electron modules
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
protocol: {
|
||||||
|
registerSchemesAsPrivileged: vi.fn(),
|
||||||
|
handle: vi.fn(),
|
||||||
|
unhandle: vi.fn()
|
||||||
|
},
|
||||||
|
net: {
|
||||||
|
fetch: vi.fn()
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
showErrorBox: vi.fn()
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
exit: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('electron-is-dev', () => ({
|
||||||
|
default: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock Node.js modules
|
||||||
|
vi.mock('node:fs/promises', () => ({
|
||||||
|
default: {
|
||||||
|
readFile: vi.fn(),
|
||||||
|
stat: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('node:path', () => ({
|
||||||
|
default: {
|
||||||
|
join: vi.fn((...args) => args.join('/')),
|
||||||
|
resolve: vi.fn((...args) => args.join('/')),
|
||||||
|
relative: vi.fn((from, to) => {
|
||||||
|
// Simple mock implementation
|
||||||
|
if (to.startsWith(from)) {
|
||||||
|
return to.slice(from.length + 1);
|
||||||
|
}
|
||||||
|
if (to.includes('..')) {
|
||||||
|
return '../' + to.split('/').pop();
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
}),
|
||||||
|
isAbsolute: vi.fn(path => path.startsWith('/')),
|
||||||
|
extname: vi.fn(path => {
|
||||||
|
const lastDot = path.lastIndexOf('.');
|
||||||
|
return lastDot === -1 ? '' : path.slice(lastDot);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('set-cookie-parser', () => ({
|
||||||
|
parse: vi.fn(() => []),
|
||||||
|
splitCookiesString: vi.fn(() => [])
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('cookie', () => ({
|
||||||
|
serialize: vi.fn((name, value) => `${name}=${value}`)
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Protocol Handler Utils', () => {
|
||||||
|
describe('getMimeType', () => {
|
||||||
|
it('should return correct MIME types for common file extensions', () => {
|
||||||
|
expect(getMimeType('file.html')).toBe('text/html');
|
||||||
|
expect(getMimeType('file.htm')).toBe('text/html');
|
||||||
|
expect(getMimeType('file.js')).toBe('application/javascript');
|
||||||
|
expect(getMimeType('file.mjs')).toBe('application/javascript');
|
||||||
|
expect(getMimeType('file.css')).toBe('text/css');
|
||||||
|
expect(getMimeType('file.json')).toBe('application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct MIME types for image files', () => {
|
||||||
|
expect(getMimeType('image.png')).toBe('image/png');
|
||||||
|
expect(getMimeType('image.jpg')).toBe('image/jpeg');
|
||||||
|
expect(getMimeType('image.jpeg')).toBe('image/jpeg');
|
||||||
|
expect(getMimeType('image.gif')).toBe('image/gif');
|
||||||
|
expect(getMimeType('image.svg')).toBe('image/svg+xml');
|
||||||
|
expect(getMimeType('image.webp')).toBe('image/webp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct MIME types for font files', () => {
|
||||||
|
expect(getMimeType('font.woff')).toBe('font/woff');
|
||||||
|
expect(getMimeType('font.woff2')).toBe('font/woff2');
|
||||||
|
expect(getMimeType('font.ttf')).toBe('font/ttf');
|
||||||
|
expect(getMimeType('font.otf')).toBe('font/otf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default MIME type for unknown extensions', () => {
|
||||||
|
expect(getMimeType('file.unknown')).toBe('application/octet-stream');
|
||||||
|
expect(getMimeType('file')).toBe('application/octet-stream');
|
||||||
|
expect(getMimeType('file.')).toBe('application/octet-stream');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle case insensitive extensions', () => {
|
||||||
|
expect(getMimeType('FILE.HTML')).toBe('text/html');
|
||||||
|
expect(getMimeType('FILE.JS')).toBe('application/javascript');
|
||||||
|
expect(getMimeType('FILE.CSS')).toBe('text/css');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSafePath', () => {
|
||||||
|
let isSafePath;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Import the function after mocks are set up
|
||||||
|
const module = await import('../../functions/setupHandler.js');
|
||||||
|
// We need to extract the function from the module since it's not exported
|
||||||
|
// This is a test-specific workaround
|
||||||
|
const moduleString = module.default?.toString() || '';
|
||||||
|
// For testing purposes, we'll create a simple implementation
|
||||||
|
isSafePath = (base, target) => {
|
||||||
|
const path = require('node:path');
|
||||||
|
const relative = path.relative(base, target);
|
||||||
|
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow safe relative paths', () => {
|
||||||
|
expect(isSafePath('/base', '/base/file.txt')).toBe(true);
|
||||||
|
expect(isSafePath('/base', '/base/sub/file.txt')).toBe(true);
|
||||||
|
expect(isSafePath('/base', '/base/sub/deep/file.txt')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject path traversal attempts', () => {
|
||||||
|
expect(isSafePath('/base', '/base/../etc/passwd')).toBe(false);
|
||||||
|
expect(isSafePath('/base', '/other/file.txt')).toBe(false);
|
||||||
|
expect(isSafePath('/base', '/../etc/passwd')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject absolute paths', () => {
|
||||||
|
expect(isSafePath('/base', '/absolute/path')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases', () => {
|
||||||
|
expect(isSafePath('/base', '/base')).toBe(false); // No relative path
|
||||||
|
expect(isSafePath('/base', '/base/')).toBe(true); // Empty relative path is ok
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createRequest', () => {
|
||||||
|
let createRequest;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Mock global Request constructor
|
||||||
|
global.Request = vi.fn().mockImplementation((url, options) => ({
|
||||||
|
url,
|
||||||
|
method: options?.method || 'GET',
|
||||||
|
headers: options?.headers || new Headers(),
|
||||||
|
body: options?.body || null,
|
||||||
|
formData: vi.fn(),
|
||||||
|
json: vi.fn(),
|
||||||
|
text: vi.fn(),
|
||||||
|
arrayBuffer: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
global.Headers = vi.fn().mockImplementation(() => ({
|
||||||
|
set: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
has: vi.fn(),
|
||||||
|
forEach: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
global.URL = vi.fn().mockImplementation((url) => ({
|
||||||
|
toString: () => url,
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
pathname: '/test'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const module = await import('../../functions/setupHandler.js');
|
||||||
|
// Since createRequest is not exported, we'll test the expected behavior
|
||||||
|
createRequest = async (request, session) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const headers = new Headers();
|
||||||
|
|
||||||
|
request.headers.forEach((value, key) => {
|
||||||
|
headers.set(key.toLowerCase(), value);
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = null;
|
||||||
|
if (request.uploadData && request.uploadData.length > 0) {
|
||||||
|
const buffers = request.uploadData
|
||||||
|
.filter(part => part.bytes)
|
||||||
|
.map(part => Buffer.from(part.bytes));
|
||||||
|
body = Buffer.concat(buffers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Request(url.toString(), {
|
||||||
|
method: request.method,
|
||||||
|
headers: headers,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create proper Web API Request object', async () => {
|
||||||
|
const mockElectronRequest = {
|
||||||
|
url: 'http://127.0.0.1/test',
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Map([
|
||||||
|
['content-type', 'application/json'],
|
||||||
|
['authorization', 'Bearer token']
|
||||||
|
]),
|
||||||
|
body: null,
|
||||||
|
uploadData: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSession = {
|
||||||
|
cookies: {
|
||||||
|
get: vi.fn().mockResolvedValue([])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = await createRequest(mockElectronRequest, mockSession);
|
||||||
|
|
||||||
|
expect(request.url).toBe('http://127.0.0.1/test');
|
||||||
|
expect(request.method).toBe('POST');
|
||||||
|
expect(request.headers).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle uploadData correctly', async () => {
|
||||||
|
const testData = new Uint8Array([1, 2, 3, 4]);
|
||||||
|
const mockElectronRequest = {
|
||||||
|
url: 'http://127.0.0.1/upload',
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Map([['content-type', 'multipart/form-data']]),
|
||||||
|
body: null,
|
||||||
|
uploadData: [{
|
||||||
|
bytes: testData
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSession = {
|
||||||
|
cookies: {
|
||||||
|
get: vi.fn().mockResolvedValue([])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = await createRequest(mockElectronRequest, mockSession);
|
||||||
|
|
||||||
|
expect(request.method).toBe('POST');
|
||||||
|
expect(request.body).toEqual(Buffer.from(testData));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle GET requests without body', async () => {
|
||||||
|
const mockElectronRequest = {
|
||||||
|
url: 'http://127.0.0.1/api/data',
|
||||||
|
method: 'GET',
|
||||||
|
headers: new Map([['accept', 'application/json']]),
|
||||||
|
body: null,
|
||||||
|
uploadData: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSession = {
|
||||||
|
cookies: {
|
||||||
|
get: vi.fn().mockResolvedValue([])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = await createRequest(mockElectronRequest, mockSession);
|
||||||
|
|
||||||
|
expect(request.method).toBe('GET');
|
||||||
|
expect(request.body).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple uploadData parts', async () => {
|
||||||
|
const part1 = new Uint8Array([1, 2]);
|
||||||
|
const part2 = new Uint8Array([3, 4]);
|
||||||
|
const mockElectronRequest = {
|
||||||
|
url: 'http://127.0.0.1/upload',
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Map(),
|
||||||
|
body: null,
|
||||||
|
uploadData: [
|
||||||
|
{ bytes: part1 },
|
||||||
|
{ bytes: part2 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSession = {
|
||||||
|
cookies: {
|
||||||
|
get: vi.fn().mockResolvedValue([])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = await createRequest(mockElectronRequest, mockSession);
|
||||||
|
|
||||||
|
expect(request.body).toEqual(Buffer.concat([Buffer.from(part1), Buffer.from(part2)]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
31
packages/adapter-electron/tsconfig.json
Normal file
31
packages/adapter-electron/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"types": ["node", "electron"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"functions/**/*",
|
||||||
|
"index.js",
|
||||||
|
"tests/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"coverage",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
packages/adapter-electron/vitest.config.js
Normal file
18
packages/adapter-electron/vitest.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
globals: true,
|
||||||
|
setupFiles: ['./tests/setup.js'],
|
||||||
|
coverage: {
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'tests/',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/*.config.js'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user