Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dynamic import retry plugin [KM-865] #2

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/actions/setup-pnpm-with-dependencies/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,21 @@ runs:
with:
path: './node_modules'
key: ${{ steps.node-version.outputs.cache-key }}

- name: Set Playwright path
id: playwright-path
shell: bash
run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright-bin" >> $GITHUB_OUTPUT

- name: Cache Playwright's binary
id: playwright-cache
uses: actions/cache@v4
with:
key: playwright-bin-v1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we tie the cache key here to a dynamic string that includes the playwright dependency version from package.json?

path: ${{ steps.playwright-path.outputs.PLAYWRIGHT_BROWSERS_PATH }}

- name: Install Playwright
id: playwright-install
shell: bash
# does not need to explicitly set chromium after https://github.com/microsoft/playwright/issues/14862 is solved
run: pnpm playwright install chromium
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ dist

# Local History
.history

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe some additional playwright artifact paths will need to be added

playground-temp
temp
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,11 @@ pnpm lint:fix
Unit tests are run with [Vitest](https://vitest.dev/).

```shell
# Run tests
pnpm test
# Run tests in the Vitest UI
pnpm test:ui
```

See the [Steps to Test Your Plugin](./playground/README.md) for more information on how to write tests.

### Build

Build for production and inspect the files in the `/dist` directory.
Expand Down
1 change: 1 addition & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default defineBuildConfig({
// Each separate plugin's entry file should be listed here
entries: [
'./src/plugin-example-one/index.ts',
'./src/plugin-dynamic-import-retry/index.ts',
],
// Generates .d.ts declaration file(s)
declaration: true,
Expand Down
28 changes: 25 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "vitest run --passWithNoTests",
"test:ui": "vitest --ui --passWithNoTests",
"test": "vitest run -c vitest.config.e2e.ts",
"typecheck": "vue-tsc --noEmit",
"build": "unbuild",
"commit": "cz"
Expand All @@ -16,9 +15,15 @@
"./plugin-example-one": {
"import": "./dist/plugin-example-one/index.mjs",
"types": "./dist/plugin-example-one/index.d.ts"
},
"./plugin-dynamic-import-retry": {
"import": "./dist/plugin-dynamic-import-retry/index.mjs",
"types": "./dist/plugin-dynamic-import-retry/index.d.ts"
}
},
"files": ["dist"],
"files": [
"dist"
],
"author": "Kong, Inc.",
"license": "Apache-2.0",
"devDependencies": {
Expand All @@ -27,11 +32,20 @@
"@digitalroute/cz-conventional-changelog-for-jira": "^8.0.1",
"@evilmartians/lefthook": "^1.10.1",
"@kong/eslint-config-kong-ui": "^1.2.2",
"@types/fs-extra": "^11.0.4",
"@vitejs/plugin-vue": "^5.2.1",
"@vitest/ui": "^2.1.8",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.17.0",
"fs-extra": "^11.2.0",
"npm-run-all2": "^7.0.2",
"playwright-chromium": "^1.49.1",
"typescript": "^5.7.2",
"unbuild": "^3.2.0",
"vite": "^6.0.7",
"vitest": "^2.1.8",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tsc": "^2.2.0"
},
"engines": {
Expand All @@ -52,5 +66,13 @@
"jiraPrepend": "[",
"jiraAppend": "]"
}
},
"dependencies": {
"@rollup/pluginutils": "^5.1.4",
"acorn-walk": "^8.3.4",
"magic-string": "^0.30.17"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0"
}
}
12 changes: 12 additions & 0 deletions playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Directory Overview

This directory serves as a testing environment for plugins. Each subdirectory corresponds to a specific plugin, identified by its name.

* `vitestGlobalSetup.ts`: This is the global setup file for Vitest, executed once before all tests. It initializes a headless browser environment.
* `vitestSetup.ts`: This is the per-test setup file for Vitest, executed before each individual test. It provides references to the browser instance and the page context for use during testing.

## Steps to Test Your Plugin
1. Create a new subdirectory within the playground directory, using your plugin’s name as the folder name.
2. Within this folder, set up one or more Vite projects for testing.
3. Write test files with the `.spec.ts` extension to validate your plugin’s functionality.
4. If browser-based testing is required, you can import `page` or `browser` from the `vitestSetup.ts` file to interact with the headless browser environment.
78 changes: 78 additions & 0 deletions playground/dynamic-import-retry/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { beforeAll, expect, test } from 'vitest'
import { resolve } from 'node:path'
import type { InlineConfig } from 'vite'
import { build, preview } from 'vite'

import { page, browserErrors, browserLogs } from '../vitestSetup'

const root = resolve(__dirname, 'vue-router')

let viteTestUrl: string

beforeAll(async () => {
const testConfig: InlineConfig = {
configFile: resolve(root, 'vite.config.ts'),
}

await build(testConfig)
const previewServer = await preview(testConfig)

viteTestUrl = previewServer.resolvedUrls!.local[0]
await page.goto(viteTestUrl)

return async () => {
previewServer.close()
}
})

test('should dynamic import module successful', async () => {
await page.click('#nav-about')
await page.waitForTimeout(100)
expect(await page.textContent('h2')).toMatch('AboutView')
})

const resources = ['css', 'js']

for (const resourceType of resources) {
const filter = (url: URL) => url.pathname.includes('AboutView') && url.pathname.includes(`.${resourceType}`)

test('should recover by retrying import ' + resourceType, async () => {
await page.goto(viteTestUrl)

let retries = 0
await page.route(filter, route => {
retries++
if (retries < 3) {
return route.fulfill({ status: 404, body: 'Not Found' })
}
return route.continue()
})
await page.click('#nav-about')
await page.waitForTimeout(1000)
expect(await page.textContent('h2')).toMatch('AboutView')
expect(retries).toBe(3)
})

test('should has an error when reach max attempts on loading ' + resourceType, async () => {
await page.goto(viteTestUrl)

await page.route(filter, route => {
return route.fulfill({ status: 404, body: 'Not Found' })
})
await page.click('#nav-about')
await page.waitForTimeout(1500)

if (resourceType === 'js') {
expect(await page.textContent('h2')).toMatch('HomeView')
}

if (resourceType === 'css') {
const e = browserErrors.find(e => e.message.includes('[preload-css-retried]'))
expect(e).toBeDefined()
} else {
const e = browserLogs.find(l => l.includes('[dynamic-import-retry]'))
expect(e).toBeDefined()
}
})
}

12 changes: 12 additions & 0 deletions playground/dynamic-import-retry/vue-router/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions playground/dynamic-import-retry/vue-router/src/AboutView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const router = useRouter()
const route = useRoute()

const search = computed({
get() {
return route.query.search ?? ''
},
set(search) {
router.replace({ query: { search } })
},
})
</script>

<template>
<h2>AboutView</h2>
<label>
Search: <input
v-model.trim="search"
maxlength="20"
>
</label>
</template>

<style scoped>
h2 {
color: blue;
}
</style>
20 changes: 20 additions & 0 deletions playground/dynamic-import-retry/vue-router/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<h1>Hello App!</h1>
<nav>
<RouterLink
id="nav-home"
to="/"
>
Go to Home
</RouterLink>
<RouterLink
id="nav-about"
to="/about"
>
Go to About
</RouterLink>
</nav>
<main>
<RouterView />
</main>
</template>
18 changes: 18 additions & 0 deletions playground/dynamic-import-retry/vue-router/src/HomeView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goToAbout = () => router.push('/about')
</script>

<template>
<h2>HomeView</h2>
<button @click="goToAbout">
Go to About
</button>
</template>

<style scoped>
h2 {
color: red;
}
</style>
16 changes: 16 additions & 0 deletions playground/dynamic-import-retry/vue-router/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createApp } from 'vue'
import App from './App.vue'

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./HomeView.vue') },
{ path: '/about', component: () => import('./AboutView.vue') },
],
})

createApp(App)
.use(router)
.mount('#app')
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we can remove this file and reference the types in the tsConfig

10 changes: 10 additions & 0 deletions playground/dynamic-import-retry/vue-router/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist",
"declaration": false,
"declarationDir":null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"declarationDir":null
"declarationDir": null

},
"include": ["vite.config.ts"]
}
21 changes: 21 additions & 0 deletions playground/dynamic-import-retry/vue-router/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'node:path'

import { DynamicImportRetryPlugin } from '../../../src/plugin-dynamic-import-retry'

// https://vite.dev/config/
export default defineConfig({
logLevel: 'silent',
root: resolve(__dirname),
plugins: [
vue(),
DynamicImportRetryPlugin(),
],
build: {
minify: false,
outDir: 'dist',
target: 'esnext',
emptyOutDir: false,
},
})
25 changes: 25 additions & 0 deletions playground/vitestGlobalSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os from 'node:os'
import path from 'node:path'
import fs from 'fs-extra'
import type { BrowserServer } from 'playwright-chromium'
import { chromium } from 'playwright-chromium'

const DIR = path.join(os.tmpdir(), 'vitest_playwright_global_setup')

let browserServer: BrowserServer | undefined

export async function setup(): Promise<void> {
adamdehaven marked this conversation as resolved.
Show resolved Hide resolved
browserServer = await chromium.launchServer({
headless: !process.env.VITE_DEBUG_SERVE,
args: process.env.CI
? ['--no-sandbox', '--disable-setuid-sandbox']
: undefined,
})

await fs.mkdirp(DIR)
await fs.writeFile(path.join(DIR, 'wsEndpoint'), browserServer.wsEndpoint())
}

export async function teardown(): Promise<void> {
await browserServer?.close()
}
Loading
Loading