mirror of https://github.com/alibaba/ice.git
test: use vitest instead of jest (#75)
* test: use vitest * chore: update test script in ci.yml * chore: remove coverage * chore: remove max-old-space-size * chore: not use threads * chore: add maxThreads * chore: set maxThreads to 2 * chore: set to node 16 * chore: set threads to false * chore: add coverage * chore: max-old-space-size
This commit is contained in:
parent
d11f461d6b
commit
ab9c50913b
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set branch name
|
- name: Set branch name
|
||||||
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
- run: npm run setup
|
- run: npm run setup
|
||||||
- run: npm run dependency:check
|
- run: npm run dependency:check
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm run test:ci
|
- run: npm run test
|
||||||
- run: npm run version:check
|
- run: npm run version:check
|
||||||
env:
|
env:
|
||||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||||
|
|
|
||||||
12
package.json
12
package.json
|
|
@ -16,9 +16,8 @@
|
||||||
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./",
|
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "npm run lint -- --fix",
|
||||||
"publish:alpha": "PUBLISH_TYPE=alpha esmo ./scripts/publishPackageWithDistTag.ts",
|
"publish:alpha": "PUBLISH_TYPE=alpha esmo ./scripts/publishPackageWithDistTag.ts",
|
||||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --config ./jest.config.mjs",
|
"test": "vitest run --coverage",
|
||||||
"test:ci": "npm run test -- --ci",
|
"test:watch": "vitest"
|
||||||
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch --config ./jest.config.mjs"
|
|
||||||
},
|
},
|
||||||
"author": "ice-admin@alibaba-inc.com",
|
"author": "ice-admin@alibaba-inc.com",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -31,10 +30,10 @@
|
||||||
"@types/eslint": "^8.4.1",
|
"@types/eslint": "^8.4.1",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/glob": "^7.2.0",
|
"@types/glob": "^7.2.0",
|
||||||
"@types/jest": "^27.4.0",
|
|
||||||
"@types/node": "^17.0.13",
|
"@types/node": "^17.0.13",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"build-scripts": "^2.0.0-15",
|
"build-scripts": "^2.0.0-15",
|
||||||
|
"c8": "^7.11.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"dependency-check": "^4.1.0",
|
"dependency-check": "^4.1.0",
|
||||||
|
|
@ -46,7 +45,6 @@
|
||||||
"glob": "^7.2.0",
|
"glob": "^7.2.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"ice-npm-utils": "^3.0.1",
|
"ice-npm-utils": "^3.0.1",
|
||||||
"jest": "^27.4.7",
|
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"prettier-plugin-organize-imports": "^2.3.4",
|
"prettier-plugin-organize-imports": "^2.3.4",
|
||||||
"prettier-plugin-packagejson": "^2.2.15",
|
"prettier-plugin-packagejson": "^2.2.15",
|
||||||
|
|
@ -55,8 +53,8 @@
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
"stylelint": "^14.3.0",
|
"stylelint": "^14.3.0",
|
||||||
"ts-jest": "^27.1.3",
|
"typescript": "^4.5.5",
|
||||||
"typescript": "^4.5.5"
|
"vitest": "^0.8.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm"
|
"packageManager": "pnpm"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const compilationPlugin = (options: Options): UnpluginOptions => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'compilation-plugin',
|
name: 'compilation-plugin',
|
||||||
|
// @ts-expect-error TODO: source map types
|
||||||
async transform(source: string, id: string) {
|
async transform(source: string, id: string) {
|
||||||
// TODO specific runtime plugin name
|
// TODO specific runtime plugin name
|
||||||
if ((/node_modules/.test(id) && !/[\\/]runtime[\\/]/.test(id))) {
|
if ((/node_modules/.test(id) && !/[\\/]runtime[\\/]/.test(id))) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { expect, describe, it } from 'vitest';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { expect, it, describe } from 'vitest';
|
||||||
import { generateExports, checkExportData, removeExportData } from '../src/service/runtimeGenerator';
|
import { generateExports, checkExportData, removeExportData } from '../src/service/runtimeGenerator';
|
||||||
|
|
||||||
describe('generateExports', () => {
|
describe('generateExports', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { expect, it, describe } from 'vitest';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { analyzeImports, getImportPath, resolveId, type Alias } from '../src/service/analyze';
|
import { analyzeImports, getImportPath, resolveId, type Alias } from '../src/service/analyze';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,41 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1
|
||||||
|
|
||||||
|
exports[`generateRouteManifest function > layout-routes 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"componentName": "PagesBlogIndex",
|
||||||
|
"file": "pages/blog/index.tsx",
|
||||||
|
"id": "pages/blog/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": "/blog",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"componentName": "PagesBlog$id",
|
||||||
|
"file": "pages/blog/$id.tsx",
|
||||||
|
"id": "pages/blog/$id",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": "/blog/:id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"componentName": "PagesAbout",
|
||||||
|
"file": "pages/about.tsx",
|
||||||
|
"id": "pages/about",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": "/about",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"componentName": "PagesIndex",
|
||||||
|
"file": "pages/index.tsx",
|
||||||
|
"id": "pages/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`generateRouteManifest function layout-routes 1`] = `
|
exports[`generateRouteManifest function layout-routes 1`] = `
|
||||||
Array [
|
Array [
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,213 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1
|
||||||
|
|
||||||
|
exports[`generateRouteManifest function > basic-routes 1`] = `
|
||||||
|
{
|
||||||
|
"pages/About/index": {
|
||||||
|
"componentName": "PagesAboutIndex",
|
||||||
|
"file": "pages/About/index.tsx",
|
||||||
|
"id": "pages/About/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/About",
|
||||||
|
},
|
||||||
|
"pages/About/me/index": {
|
||||||
|
"componentName": "PagesAboutMeIndex",
|
||||||
|
"file": "pages/About/me/index.tsx",
|
||||||
|
"id": "pages/About/me/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/About/me",
|
||||||
|
},
|
||||||
|
"pages/home": {
|
||||||
|
"componentName": "PagesHome",
|
||||||
|
"file": "pages/home.tsx",
|
||||||
|
"id": "pages/home",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/home",
|
||||||
|
},
|
||||||
|
"pages/index": {
|
||||||
|
"componentName": "PagesIndex",
|
||||||
|
"file": "pages/index.tsx",
|
||||||
|
"id": "pages/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"pages/layout": {
|
||||||
|
"componentName": "PagesLayout",
|
||||||
|
"file": "pages/layout.tsx",
|
||||||
|
"id": "pages/layout",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateRouteManifest function > doc-delimeters-routes 1`] = `
|
||||||
|
{
|
||||||
|
"pages/home.news": {
|
||||||
|
"componentName": "PagesHomeNews",
|
||||||
|
"file": "pages/home.news.tsx",
|
||||||
|
"id": "pages/home.news",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/home/news",
|
||||||
|
},
|
||||||
|
"pages/layout": {
|
||||||
|
"componentName": "PagesLayout",
|
||||||
|
"file": "pages/layout.tsx",
|
||||||
|
"id": "pages/layout",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateRouteManifest function > dynamic-routes 1`] = `
|
||||||
|
{
|
||||||
|
"pages/about": {
|
||||||
|
"componentName": "PagesAbout",
|
||||||
|
"file": "pages/about.tsx",
|
||||||
|
"id": "pages/about",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": "/about",
|
||||||
|
},
|
||||||
|
"pages/blog/$id": {
|
||||||
|
"componentName": "PagesBlog$id",
|
||||||
|
"file": "pages/blog/$id.tsx",
|
||||||
|
"id": "pages/blog/$id",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": "/blog/:id",
|
||||||
|
},
|
||||||
|
"pages/blog/index": {
|
||||||
|
"componentName": "PagesBlogIndex",
|
||||||
|
"file": "pages/blog/index.tsx",
|
||||||
|
"id": "pages/blog/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": "/blog",
|
||||||
|
},
|
||||||
|
"pages/index": {
|
||||||
|
"componentName": "PagesIndex",
|
||||||
|
"file": "pages/index.tsx",
|
||||||
|
"id": "pages/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateRouteManifest function > layout-routes 1`] = `
|
||||||
|
{
|
||||||
|
"pages/about": {
|
||||||
|
"componentName": "PagesAbout",
|
||||||
|
"file": "pages/about.tsx",
|
||||||
|
"id": "pages/about",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/about",
|
||||||
|
},
|
||||||
|
"pages/blog/$id": {
|
||||||
|
"componentName": "PagesBlog$id",
|
||||||
|
"file": "pages/blog/$id.tsx",
|
||||||
|
"id": "pages/blog/$id",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": "pages/blog/layout",
|
||||||
|
"path": "/:id",
|
||||||
|
},
|
||||||
|
"pages/blog/index": {
|
||||||
|
"componentName": "PagesBlogIndex",
|
||||||
|
"file": "pages/blog/index.tsx",
|
||||||
|
"id": "pages/blog/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": "pages/blog/layout",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"pages/blog/layout": {
|
||||||
|
"componentName": "PagesBlogLayout",
|
||||||
|
"file": "pages/blog/layout.tsx",
|
||||||
|
"id": "pages/blog/layout",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/blog",
|
||||||
|
},
|
||||||
|
"pages/home/index": {
|
||||||
|
"componentName": "PagesHomeIndex",
|
||||||
|
"file": "pages/home/index.tsx",
|
||||||
|
"id": "pages/home/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": "pages/home/layout",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"pages/home/layout": {
|
||||||
|
"componentName": "PagesHomeLayout",
|
||||||
|
"file": "pages/home/layout.tsx",
|
||||||
|
"id": "pages/home/layout",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/home",
|
||||||
|
},
|
||||||
|
"pages/home/layout/index": {
|
||||||
|
"componentName": "PagesHomeLayoutIndex",
|
||||||
|
"file": "pages/home/layout/index.tsx",
|
||||||
|
"id": "pages/home/layout/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": "pages/home/layout",
|
||||||
|
"path": "/layout",
|
||||||
|
},
|
||||||
|
"pages/index": {
|
||||||
|
"componentName": "PagesIndex",
|
||||||
|
"file": "pages/index.tsx",
|
||||||
|
"id": "pages/index",
|
||||||
|
"index": true,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
"pages/layout": {
|
||||||
|
"componentName": "PagesLayout",
|
||||||
|
"file": "pages/layout.tsx",
|
||||||
|
"id": "pages/layout",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`generateRouteManifest function > splat-routes 1`] = `
|
||||||
|
{
|
||||||
|
"pages/$": {
|
||||||
|
"componentName": "Pages$",
|
||||||
|
"file": "pages/$.tsx",
|
||||||
|
"id": "pages/$",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/*",
|
||||||
|
},
|
||||||
|
"pages/home": {
|
||||||
|
"componentName": "PagesHome",
|
||||||
|
"file": "pages/home.tsx",
|
||||||
|
"id": "pages/home",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": "pages/layout",
|
||||||
|
"path": "/home",
|
||||||
|
},
|
||||||
|
"pages/layout": {
|
||||||
|
"componentName": "PagesLayout",
|
||||||
|
"file": "pages/layout.tsx",
|
||||||
|
"id": "pages/layout",
|
||||||
|
"index": undefined,
|
||||||
|
"parentId": undefined,
|
||||||
|
"path": undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`generateRouteManifest function basic-routes 1`] = `
|
exports[`generateRouteManifest function basic-routes 1`] = `
|
||||||
Object {
|
Object {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { expect, test, describe } from 'vitest';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { generateRouteManifest, formatNestedRouteManifest } from '../src/index';
|
import { generateRouteManifest, formatNestedRouteManifest } from '../src/index';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { expect, test, describe } from 'vitest';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { generateRouteManifest } from '../src/index';
|
import { generateRouteManifest } from '../src/index';
|
||||||
|
|
|
||||||
1920
pnpm-lock.yaml
1920
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { expect, test, describe, afterAll } from 'vitest';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
@ -13,16 +14,16 @@ describe(`build ${example}`, () => {
|
||||||
let page: Page = null;
|
let page: Page = null;
|
||||||
let browser = null;
|
let browser = null;
|
||||||
|
|
||||||
buildFixture(example);
|
|
||||||
|
|
||||||
test('open /', async () => {
|
test('open /', async () => {
|
||||||
|
await buildFixture(example);
|
||||||
|
|
||||||
const res = await setupBrowser({ example });
|
const res = await setupBrowser({ example });
|
||||||
page = res.page;
|
page = res.page;
|
||||||
browser = res.browser;
|
browser = res.browser;
|
||||||
expect(await page.$$text('h2')).toStrictEqual(['Home Page']);
|
expect(await page.$$text('h2')).toStrictEqual(['Home Page']);
|
||||||
const bundleContent = fs.readFileSync(path.join(__dirname, `../../examples/${example}/build/index.js`), 'utf-8');
|
const bundleContent = fs.readFileSync(path.join(__dirname, `../../examples/${example}/build/index.js`), 'utf-8');
|
||||||
expect(bundleContent.includes('__REMOVED__')).toBe(false);
|
expect(bundleContent.includes('__REMOVED__')).toBe(false);
|
||||||
});
|
}, 120000);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,11 @@ interface IReturn {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get builtIn plugins
|
// get builtIn plugins
|
||||||
export const buildFixture = function(example: string) {
|
export const buildFixture = async function(example: string) {
|
||||||
test(`setup ${example}`, async () => {
|
|
||||||
const rootDir = path.join(__dirname, `../../examples/${example}`);
|
const rootDir = path.join(__dirname, `../../examples/${example}`);
|
||||||
const processCwdSpy = jest.spyOn(process, 'cwd');
|
|
||||||
processCwdSpy.mockReturnValue(rootDir);
|
|
||||||
process.env.DISABLE_FS_CACHE = 'true';
|
process.env.DISABLE_FS_CACHE = 'true';
|
||||||
process.env.JEST_TEST = 'true';
|
|
||||||
const service = await createService({ rootDir, command: 'build', commandArgs: {} });
|
const service = await createService({ rootDir, command: 'build', commandArgs: {} });
|
||||||
await service.run();
|
await service.run();
|
||||||
}, 120000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setupBrowser: SetupBrowser = async (options) => {
|
export const setupBrowser: SetupBrowser = async (options) => {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ export const startFixture = async function (example: string) {
|
||||||
const processCwdSpy = jest.spyOn(process, 'cwd');
|
const processCwdSpy = jest.spyOn(process, 'cwd');
|
||||||
processCwdSpy.mockReturnValue(rootDir);
|
processCwdSpy.mockReturnValue(rootDir);
|
||||||
process.env.DISABLE_FS_CACHE = 'true';
|
process.env.DISABLE_FS_CACHE = 'true';
|
||||||
process.env.JEST_TEST = 'true';
|
|
||||||
const service = await createService({ rootDir, command: 'start', commandArgs: {
|
const service = await createService({ rootDir, command: 'start', commandArgs: {
|
||||||
port,
|
port,
|
||||||
disableOpen: true,
|
disableOpen: true,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import { getHookFiles } from './packages/ice/esm/requireHook.js';
|
||||||
|
|
||||||
|
const moduleNameMapper = getHookFiles().reduce((mapper, [id, value]) => {
|
||||||
|
mapper[`^${id}$`] = value;
|
||||||
|
return mapper;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: { ...moduleNameMapper },
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
// disable threads to avoid `Segmentation fault (core dumped)` error: https://github.com/vitest-dev/vitest/issues/317
|
||||||
|
threads: false,
|
||||||
|
exclude: [
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/esm/**',
|
||||||
|
'**/tests/fixtures/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue