@verdaccio/e2e-ui 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -0
- package/build/cjs/commands/index.cjs +21 -5
- package/build/cjs/commands/index.cjs.map +1 -1
- package/build/cjs/index.cjs +65 -10
- package/build/cjs/index.cjs.map +1 -1
- package/build/cjs/tasks/publish.cjs +245 -0
- package/build/cjs/tasks/publish.cjs.map +1 -0
- package/build/cjs/testIds.cjs +104 -0
- package/build/cjs/testIds.cjs.map +1 -0
- package/build/cjs/tests/home.cjs +61 -7
- package/build/cjs/tests/home.cjs.map +1 -1
- package/build/cjs/tests/layout.cjs +71 -0
- package/build/cjs/tests/layout.cjs.map +1 -0
- package/build/cjs/tests/publish.cjs +83 -33
- package/build/cjs/tests/publish.cjs.map +1 -1
- package/build/cjs/tests/search.cjs +74 -0
- package/build/cjs/tests/search.cjs.map +1 -0
- package/build/cjs/tests/settings.cjs +66 -0
- package/build/cjs/tests/settings.cjs.map +1 -0
- package/build/cjs/tests/signin.cjs +15 -7
- package/build/cjs/tests/signin.cjs.map +1 -1
- package/build/commands/index.d.ts +15 -3
- package/build/esm/commands/index.js +21 -5
- package/build/esm/commands/index.js.map +1 -1
- package/build/esm/index.js +55 -8
- package/build/esm/index.js.map +1 -1
- package/build/esm/tasks/publish.js +243 -0
- package/build/esm/tasks/publish.js.map +1 -0
- package/build/esm/testIds.js +101 -0
- package/build/esm/testIds.js.map +1 -0
- package/build/esm/tests/home.js +61 -7
- package/build/esm/tests/home.js.map +1 -1
- package/build/esm/tests/layout.js +71 -0
- package/build/esm/tests/layout.js.map +1 -0
- package/build/esm/tests/publish.js +83 -33
- package/build/esm/tests/publish.js.map +1 -1
- package/build/esm/tests/search.js +74 -0
- package/build/esm/tests/search.js.map +1 -0
- package/build/esm/tests/settings.js +66 -0
- package/build/esm/tests/settings.js.map +1 -0
- package/build/esm/tests/signin.js +15 -7
- package/build/esm/tests/signin.js.map +1 -1
- package/build/index.d.ts +17 -2
- package/build/tasks/index.d.ts +2 -0
- package/build/tasks/publish.d.ts +94 -0
- package/build/testIds.d.ts +130 -0
- package/build/tests/index.d.ts +3 -0
- package/build/tests/layout.d.ts +12 -0
- package/build/tests/search.d.ts +10 -0
- package/build/tests/settings.d.ts +19 -0
- package/build/types.d.ts +84 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @verdaccio/e2e-ui
|
|
2
|
+
|
|
3
|
+
Reusable Cypress test suites for the Verdaccio web UI. Targets Verdaccio.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D @verdaccio/e2e-ui cypress
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Wire up
|
|
12
|
+
|
|
13
|
+
`cypress.config.ts`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineConfig } from 'cypress';
|
|
17
|
+
import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';
|
|
18
|
+
|
|
19
|
+
const registryUrl = process.env.VERDACCIO_URL || 'http://localhost:4873';
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
e2e: {
|
|
23
|
+
baseUrl: registryUrl,
|
|
24
|
+
supportFile: 'cypress/support/e2e.ts',
|
|
25
|
+
specPattern: 'cypress/e2e/**/*.cy.ts',
|
|
26
|
+
setupNodeEvents(on) {
|
|
27
|
+
setupVerdaccioTasks(on, { registryUrl });
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
env: { VERDACCIO_URL: registryUrl },
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`cypress/support/e2e.ts`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import '@verdaccio/e2e-ui/commands';
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Spec file (one per suite, or use `registerAllTests`):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createRegistryConfig, homeTests } from '@verdaccio/e2e-ui';
|
|
44
|
+
|
|
45
|
+
const config = createRegistryConfig({
|
|
46
|
+
registryUrl: Cypress.env('VERDACCIO_URL'),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
homeTests(config);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Available suites
|
|
53
|
+
|
|
54
|
+
| Export | Covers |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `homeTests` | Empty-registry landing page, help card, 404 |
|
|
57
|
+
| `signinTests` | Login dialog, greeting, logout |
|
|
58
|
+
| `layoutTests` | `/-/static/ui-options.js` health, header chrome, footer |
|
|
59
|
+
| `searchTests` | Search input, query fires, empty state, clear |
|
|
60
|
+
| `settingsTests` | Settings dialog, language picker, language switch |
|
|
61
|
+
| `publishTests` | Publishes via `cy.task`, asserts readme / sidebar / tabs |
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
createRegistryConfig({
|
|
67
|
+
registryUrl: 'http://localhost:4873',
|
|
68
|
+
credentials: { user: 'test', password: 'test' }, // optional
|
|
69
|
+
testIds: { header: { settingsTooltip: 'my-btn' } }, // optional per-field override
|
|
70
|
+
selectors: { loginDialog: { submitButton: '#go' } },// optional per-field override
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Every data-testid referenced by the suites is configurable. See
|
|
75
|
+
[`src/testIds.ts`](./src/testIds.ts) for the full `TestIds` + `Selectors`
|
|
76
|
+
shapes and defaults. Overrides are merged per section — unspecified fields
|
|
77
|
+
inherit from `DEFAULT_TEST_IDS` / `DEFAULT_SELECTORS`.
|
|
78
|
+
|
|
79
|
+
## `publishPackage` task
|
|
80
|
+
|
|
81
|
+
Publishes a throwaway package so downstream specs have something to assert on.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
cy.task('publishPackage', {
|
|
85
|
+
pkgName: '@verdaccio/pkg-scoped',
|
|
86
|
+
version: '1.0.0',
|
|
87
|
+
dependencies: { debug: '4.0.0' },
|
|
88
|
+
unique: true, // appends -t<timestamp> so reruns don't collide on 403
|
|
89
|
+
}).then((result) => {
|
|
90
|
+
// result: { pkgName, version, tempFolder, stdout, stderr, exitCode }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// in after():
|
|
94
|
+
cy.task('cleanupPublished', tempFolder);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Under the hood: creates a throwaway user via `PUT /-/user/...`, scaffolds a
|
|
98
|
+
temp project with an `.npmrc` carrying the legacy auth token, spawns
|
|
99
|
+
`npm publish --tag latest`.
|
|
100
|
+
|
|
101
|
+
## Verdaccio config requirements
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
web:
|
|
105
|
+
enable: true
|
|
106
|
+
login: true
|
|
107
|
+
showSettings: true # required by settingsTests
|
|
108
|
+
|
|
109
|
+
userRateLimit:
|
|
110
|
+
windowMs: 1000
|
|
111
|
+
max: 10000 # default (1000 / 15min) is too tight for a full suite run
|
|
112
|
+
|
|
113
|
+
packages:
|
|
114
|
+
'**':
|
|
115
|
+
access: $all
|
|
116
|
+
publish: $anonymous $authenticated
|
|
117
|
+
unpublish: $anonymous $authenticated
|
|
118
|
+
proxy: npmjs
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Restart Verdaccio after changing these — it reads them at startup.
|
|
122
|
+
|
|
123
|
+
## Custom commands
|
|
124
|
+
|
|
125
|
+
- `cy.getByTestId(selector)` — shortcut for `cy.get('[data-testid=<selector>]')`
|
|
126
|
+
- `cy.login(user, password, selectors?)` — fills + submits the login dialog;
|
|
127
|
+
pass `selectors` to override form field IDs for non-default builds
|
|
128
|
+
|
|
129
|
+
Task calls are strongly typed — `cy.task('publishPackage', …)` returns
|
|
130
|
+
`Chainable<PublishPackageResult>`, unknown task names fail at compile time.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
//#region src/commands/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default login form selectors. Kept in sync with `DEFAULT_SELECTORS`
|
|
4
|
+
* in ../testIds.ts — duplicating them here (as plain constants) lets
|
|
5
|
+
* `cy.login` call sites that don't pass an explicit selectors object
|
|
6
|
+
* still work without importing anything.
|
|
7
|
+
*/
|
|
8
|
+
var DEFAULT_LOGIN_SELECTORS = {
|
|
9
|
+
loginButton: "header--button-login",
|
|
10
|
+
usernameInput: "#login--dialog-username",
|
|
11
|
+
passwordInput: "#login--dialog-password",
|
|
12
|
+
submitButton: "#login--dialog-button-submit"
|
|
13
|
+
};
|
|
2
14
|
Cypress.Commands.add("getByTestId", (selector, ...args) => {
|
|
3
15
|
return cy.get(`[data-testid=${selector}]`, ...args);
|
|
4
16
|
});
|
|
5
|
-
Cypress.Commands.add("login", (user, password) => {
|
|
6
|
-
|
|
17
|
+
Cypress.Commands.add("login", (user, password, selectors = {}) => {
|
|
18
|
+
const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;
|
|
19
|
+
const usernameInput = selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;
|
|
20
|
+
const passwordInput = selectors.passwordInput ?? DEFAULT_LOGIN_SELECTORS.passwordInput;
|
|
21
|
+
const submitButton = selectors.submitButton ?? DEFAULT_LOGIN_SELECTORS.submitButton;
|
|
22
|
+
cy.getByTestId(loginButton).click();
|
|
7
23
|
cy.wait(300);
|
|
8
|
-
cy.get(
|
|
24
|
+
cy.get(usernameInput).type(user);
|
|
9
25
|
cy.wait(200);
|
|
10
|
-
cy.get(
|
|
26
|
+
cy.get(passwordInput).type(password);
|
|
11
27
|
cy.wait(500);
|
|
12
|
-
cy.get(
|
|
28
|
+
cy.get(submitButton).click();
|
|
13
29
|
});
|
|
14
30
|
//#endregion
|
|
15
31
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/commands/index.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\ndeclare global {\n namespace Cypress {\n interface Chainable {\n /**\n * Find element by data-testid attribute
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/commands/index.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\n/**\n * Default login form selectors. Kept in sync with `DEFAULT_SELECTORS`\n * in ../testIds.ts — duplicating them here (as plain constants) lets\n * `cy.login` call sites that don't pass an explicit selectors object\n * still work without importing anything.\n */\nconst DEFAULT_LOGIN_SELECTORS = {\n loginButton: 'header--button-login',\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n};\n\nexport interface LoginSelectors {\n /** data-testid of the header \"Login\" button that opens the dialog. */\n loginButton?: string;\n /** CSS selector for the username input. */\n usernameInput?: string;\n /** CSS selector for the password input. */\n passwordInput?: string;\n /** CSS selector for the submit button. */\n submitButton?: string;\n}\n\ndeclare global {\n namespace Cypress {\n interface Chainable {\n /**\n * Find element by data-testid attribute.\n */\n getByTestId(selector: string, ...args: any[]): Chainable<JQuery<HTMLElement>>;\n /**\n * Login to Verdaccio UI. Selectors default to Verdaccio 6.x\n * conventions; pass `selectors` to override any subset for\n * non-default builds.\n */\n login(\n user: string,\n password: string,\n selectors?: LoginSelectors\n ): Chainable<void>;\n }\n }\n}\n\nCypress.Commands.add('getByTestId', (selector: string, ...args: any[]) => {\n return cy.get(`[data-testid=${selector}]`, ...args);\n});\n\nCypress.Commands.add(\n 'login',\n (user: string, password: string, selectors: LoginSelectors = {}) => {\n const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;\n const usernameInput =\n selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;\n const passwordInput =\n selectors.passwordInput ?? DEFAULT_LOGIN_SELECTORS.passwordInput;\n const submitButton =\n selectors.submitButton ?? DEFAULT_LOGIN_SELECTORS.submitButton;\n\n cy.getByTestId(loginButton).click();\n cy.wait(300);\n cy.get(usernameInput).type(user);\n cy.wait(200);\n cy.get(passwordInput).type(password);\n cy.wait(500);\n cy.get(submitButton).click();\n }\n);\n\nexport {};\n"],"mappings":";;;;;;;AAQA,IAAM,0BAA0B;CAC9B,aAAa;CACb,eAAe;CACf,eAAe;CACf,cAAc;CACf;AAkCD,QAAQ,SAAS,IAAI,gBAAgB,UAAkB,GAAG,SAAgB;AACxE,QAAO,GAAG,IAAI,gBAAgB,SAAS,IAAI,GAAG,KAAK;EACnD;AAEF,QAAQ,SAAS,IACf,UACC,MAAc,UAAkB,YAA4B,EAAE,KAAK;CAClE,MAAM,cAAc,UAAU,eAAe,wBAAwB;CACrE,MAAM,gBACJ,UAAU,iBAAiB,wBAAwB;CACrD,MAAM,gBACJ,UAAU,iBAAiB,wBAAwB;CACrD,MAAM,eACJ,UAAU,gBAAgB,wBAAwB;AAEpD,IAAG,YAAY,YAAY,CAAC,OAAO;AACnC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,cAAc,CAAC,KAAK,KAAK;AAChC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,cAAc,CAAC,KAAK,SAAS;AACpC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,aAAa,CAAC,OAAO;EAE/B"}
|
package/build/cjs/index.cjs
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_publish = require("./tasks/publish.cjs");
|
|
3
|
+
const require_testIds = require("./testIds.cjs");
|
|
2
4
|
const require_home = require("./tests/home.cjs");
|
|
3
5
|
const require_signin = require("./tests/signin.cjs");
|
|
4
|
-
const require_publish = require("./tests/publish.cjs");
|
|
6
|
+
const require_publish$1 = require("./tests/publish.cjs");
|
|
7
|
+
const require_search = require("./tests/search.cjs");
|
|
8
|
+
const require_settings = require("./tests/settings.cjs");
|
|
9
|
+
const require_layout = require("./tests/layout.cjs");
|
|
5
10
|
//#region src/index.ts
|
|
6
11
|
/**
|
|
7
12
|
* Build a full RegistryConfig from user-provided options with defaults.
|
|
13
|
+
*
|
|
14
|
+
* `testIds` and `selectors` are deep-merged per section with the
|
|
15
|
+
* defaults in `./testIds`. Consumers targeting a non-default Verdaccio
|
|
16
|
+
* build can override just the fields that drifted:
|
|
17
|
+
*
|
|
18
|
+
* createRegistryConfig({
|
|
19
|
+
* registryUrl: 'http://localhost:4873',
|
|
20
|
+
* testIds: {
|
|
21
|
+
* header: { settingsTooltip: 'my-new-settings-btn' },
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
8
24
|
*/
|
|
9
25
|
function createRegistryConfig(options) {
|
|
10
26
|
const url = new URL(options.registryUrl);
|
|
@@ -15,7 +31,9 @@ function createRegistryConfig(options) {
|
|
|
15
31
|
user: "test",
|
|
16
32
|
password: "test"
|
|
17
33
|
},
|
|
18
|
-
title: options.title ?? "Verdaccio"
|
|
34
|
+
title: options.title ?? "Verdaccio",
|
|
35
|
+
testIds: require_testIds.mergeTestIds(require_testIds.DEFAULT_TEST_IDS, options.testIds),
|
|
36
|
+
selectors: require_testIds.mergeSelectors(require_testIds.DEFAULT_SELECTORS, options.selectors)
|
|
19
37
|
};
|
|
20
38
|
}
|
|
21
39
|
/**
|
|
@@ -34,12 +52,38 @@ function createRegistryConfig(options) {
|
|
|
34
52
|
*/
|
|
35
53
|
function setupVerdaccioTasks(on, options) {
|
|
36
54
|
const config = createRegistryConfig(options);
|
|
37
|
-
on("task", {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
on("task", {
|
|
56
|
+
registry() {
|
|
57
|
+
return {
|
|
58
|
+
registryUrl: config.registryUrl,
|
|
59
|
+
port: config.port
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
async publishPackage(input) {
|
|
63
|
+
return require_publish.publishPackage({
|
|
64
|
+
registryUrl: input.registryUrl ?? config.registryUrl,
|
|
65
|
+
credentials: input.credentials ?? config.credentials,
|
|
66
|
+
pkgName: input.pkgName,
|
|
67
|
+
version: input.version,
|
|
68
|
+
dependencies: input.dependencies,
|
|
69
|
+
devDependencies: input.devDependencies,
|
|
70
|
+
unique: input.unique
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
async cleanupPublished(tempFolder) {
|
|
74
|
+
await require_publish.cleanupPublished(tempFolder);
|
|
75
|
+
return null;
|
|
76
|
+
},
|
|
77
|
+
async unpublishPackage(input) {
|
|
78
|
+
return require_publish.unpublishPackage(typeof input === "string" ? {
|
|
79
|
+
pkgName: input,
|
|
80
|
+
registryUrl: config.registryUrl
|
|
81
|
+
} : {
|
|
82
|
+
...input,
|
|
83
|
+
registryUrl: input.registryUrl ?? config.registryUrl
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
43
87
|
}
|
|
44
88
|
/**
|
|
45
89
|
* Register all Verdaccio UI tests.
|
|
@@ -60,14 +104,25 @@ function setupVerdaccioTasks(on, options) {
|
|
|
60
104
|
function registerAllTests(config) {
|
|
61
105
|
require_home.homeTests(config);
|
|
62
106
|
require_signin.signinTests(config);
|
|
63
|
-
|
|
107
|
+
require_layout.layoutTests(config);
|
|
108
|
+
require_search.searchTests(config);
|
|
109
|
+
require_settings.settingsTests(config);
|
|
110
|
+
require_publish$1.publishTests(config);
|
|
64
111
|
}
|
|
65
112
|
//#endregion
|
|
113
|
+
exports.DEFAULT_SELECTORS = require_testIds.DEFAULT_SELECTORS;
|
|
114
|
+
exports.DEFAULT_TEST_IDS = require_testIds.DEFAULT_TEST_IDS;
|
|
115
|
+
exports.cleanupPublished = require_publish.cleanupPublished;
|
|
66
116
|
exports.createRegistryConfig = createRegistryConfig;
|
|
67
117
|
exports.homeTests = require_home.homeTests;
|
|
68
|
-
exports.
|
|
118
|
+
exports.layoutTests = require_layout.layoutTests;
|
|
119
|
+
exports.publishPackage = require_publish.publishPackage;
|
|
120
|
+
exports.publishTests = require_publish$1.publishTests;
|
|
69
121
|
exports.registerAllTests = registerAllTests;
|
|
122
|
+
exports.searchTests = require_search.searchTests;
|
|
123
|
+
exports.settingsTests = require_settings.settingsTests;
|
|
70
124
|
exports.setupVerdaccioTasks = setupVerdaccioTasks;
|
|
71
125
|
exports.signinTests = require_signin.signinTests;
|
|
126
|
+
exports.unpublishPackage = require_publish.unpublishPackage;
|
|
72
127
|
|
|
73
128
|
//# sourceMappingURL=index.cjs.map
|
package/build/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import { RegistryConfig, VerdaccioUiOptions } from './types';\n\nexport type {
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n cleanupPublished,\n publishPackage,\n PublishPackageResult,\n PublishPackageTaskInput,\n unpublishPackage,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nimport {\n DEFAULT_SELECTORS,\n DEFAULT_TEST_IDS,\n mergeSelectors,\n mergeTestIds,\n} from './testIds';\nimport { RegistryConfig, RegistryTaskResult, VerdaccioUiOptions } from './types';\n\nexport type {\n RegistryConfig,\n RegistryTaskResult,\n VerdaccioUiOptions,\n} from './types';\nexport type {\n PublishPackageInput,\n PublishPackageTaskInput,\n PublishPackageResult,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nexport type { DeepPartial, Selectors, TestIds } from './testIds';\nexport { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';\nexport { publishPackage, cleanupPublished, unpublishPackage } from './tasks';\nexport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n} from './tests';\n\n/**\n * Build a full RegistryConfig from user-provided options with defaults.\n *\n * `testIds` and `selectors` are deep-merged per section with the\n * defaults in `./testIds`. Consumers targeting a non-default Verdaccio\n * build can override just the fields that drifted:\n *\n * createRegistryConfig({\n * registryUrl: 'http://localhost:4873',\n * testIds: {\n * header: { settingsTooltip: 'my-new-settings-btn' },\n * },\n * });\n */\nexport function createRegistryConfig(options: VerdaccioUiOptions): RegistryConfig {\n const url = new URL(options.registryUrl);\n return {\n registryUrl: options.registryUrl,\n port: options.port ?? (parseInt(url.port, 10) || 4873),\n credentials: options.credentials ?? { user: 'test', password: 'test' },\n title: options.title ?? 'Verdaccio',\n testIds: mergeTestIds(DEFAULT_TEST_IDS, options.testIds),\n selectors: mergeSelectors(DEFAULT_SELECTORS, options.selectors),\n };\n}\n\n/**\n * Register Verdaccio Cypress tasks in setupNodeEvents.\n *\n * Usage in cypress.config.ts:\n * import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on) {\n * setupVerdaccioTasks(on, { registryUrl: 'http://localhost:4873' });\n * },\n * },\n * });\n */\nexport function setupVerdaccioTasks(\n on: Cypress.PluginEvents,\n options: VerdaccioUiOptions\n): void {\n const config = createRegistryConfig(options);\n\n on('task', {\n registry(): RegistryTaskResult {\n return {\n registryUrl: config.registryUrl,\n port: config.port,\n };\n },\n /**\n * Publish a throwaway package to the registry.\n *\n * Usage from a Cypress spec:\n *\n * cy.task('publishPackage', { pkgName: '@verdaccio/pkg-scoped' });\n *\n * Any field not provided falls back to the values configured here\n * (registryUrl, credentials). The task returns a PublishPackageResult.\n */\n async publishPackage(\n input: PublishPackageTaskInput\n ): Promise<PublishPackageResult> {\n return publishPackage({\n registryUrl: input.registryUrl ?? config.registryUrl,\n credentials: input.credentials ?? config.credentials,\n pkgName: input.pkgName,\n version: input.version,\n dependencies: input.dependencies,\n devDependencies: input.devDependencies,\n unique: input.unique,\n });\n },\n /**\n * Remove a temp project folder returned by a previous publishPackage\n * call. Returns null (Cypress tasks must return something serializable).\n *\n * cy.task('cleanupPublished', result.tempFolder);\n */\n async cleanupPublished(tempFolder: string): Promise<null> {\n await cleanupPublished(tempFolder);\n return null;\n },\n /**\n * Unpublish a package from the registry so the next test starts\n * from a clean slate. Accepts either a package name or a full input\n * object (to reuse a prior tempFolder's token).\n *\n * cy.task('unpublishPackage', '@verdaccio/pkg-scoped');\n * cy.task('unpublishPackage', { pkgName, tempFolder });\n */\n async unpublishPackage(\n input: string | (Omit<UnpublishPackageInput, 'registryUrl'> & {\n registryUrl?: string;\n })\n ): Promise<UnpublishPackageResult> {\n const normalized =\n typeof input === 'string'\n ? { pkgName: input, registryUrl: config.registryUrl }\n : { ...input, registryUrl: input.registryUrl ?? config.registryUrl };\n return unpublishPackage(normalized);\n },\n });\n}\n\n/**\n * Register all Verdaccio UI tests.\n *\n * Usage in a spec file:\n * import { registerAllTests, createRegistryConfig } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * registerAllTests(config);\n *\n * Or pick individual suites:\n * import { homeTests, signinTests } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * homeTests(config);\n * signinTests(config);\n */\nexport function registerAllTests(config: RegistryConfig): void {\n homeTests(config);\n signinTests(config);\n layoutTests(config);\n searchTests(config);\n settingsTests(config);\n publishTests(config);\n}\n\n// Re-export for convenience\nimport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n} from './tests';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,qBAAqB,SAA6C;CAChF,MAAM,MAAM,IAAI,IAAI,QAAQ,YAAY;AACxC,QAAO;EACL,aAAa,QAAQ;EACrB,MAAM,QAAQ,SAAS,SAAS,IAAI,MAAM,GAAG,IAAI;EACjD,aAAa,QAAQ,eAAe;GAAE,MAAM;GAAQ,UAAU;GAAQ;EACtE,OAAO,QAAQ,SAAS;EACxB,SAAS,gBAAA,aAAa,gBAAA,kBAAkB,QAAQ,QAAQ;EACxD,WAAW,gBAAA,eAAe,gBAAA,mBAAmB,QAAQ,UAAU;EAChE;;;;;;;;;;;;;;;;AAiBH,SAAgB,oBACd,IACA,SACM;CACN,MAAM,SAAS,qBAAqB,QAAQ;AAE5C,IAAG,QAAQ;EACT,WAA+B;AAC7B,UAAO;IACL,aAAa,OAAO;IACpB,MAAM,OAAO;IACd;;EAYH,MAAM,eACJ,OAC+B;AAC/B,UAAO,gBAAA,eAAe;IACpB,aAAa,MAAM,eAAe,OAAO;IACzC,aAAa,MAAM,eAAe,OAAO;IACzC,SAAS,MAAM;IACf,SAAS,MAAM;IACf,cAAc,MAAM;IACpB,iBAAiB,MAAM;IACvB,QAAQ,MAAM;IACf,CAAC;;EAQJ,MAAM,iBAAiB,YAAmC;AACxD,SAAM,gBAAA,iBAAiB,WAAW;AAClC,UAAO;;EAUT,MAAM,iBACJ,OAGiC;AAKjC,UAAO,gBAAA,iBAHL,OAAO,UAAU,WACb;IAAE,SAAS;IAAO,aAAa,OAAO;IAAa,GACnD;IAAE,GAAG;IAAO,aAAa,MAAM,eAAe,OAAO;IAAa,CACrC;;EAEtC,CAAC;;;;;;;;;;;;;;;;;;AAmBJ,SAAgB,iBAAiB,QAA8B;AAC7D,cAAA,UAAU,OAAO;AACjB,gBAAA,YAAY,OAAO;AACnB,gBAAA,YAAY,OAAO;AACnB,gBAAA,YAAY,OAAO;AACnB,kBAAA,cAAc,OAAO;AACrB,mBAAA,aAAa,OAAO"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
let child_process = require("child_process");
|
|
2
|
+
let fs_promises = require("fs/promises");
|
|
3
|
+
let os = require("os");
|
|
4
|
+
let path = require("path");
|
|
5
|
+
//#region src/tasks/publish.ts
|
|
6
|
+
function sanitizeFolderName(name) {
|
|
7
|
+
return name.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Obtain a registry-API-compatible (legacy) auth token for publish.
|
|
11
|
+
*
|
|
12
|
+
* Strategy: create a throwaway user per call. `PUT /-/user/org.couchdb.user:<name>`
|
|
13
|
+
* only returns a token on CREATE (and 409s on existing users), so we
|
|
14
|
+
* guarantee success by generating a unique username each time.
|
|
15
|
+
*
|
|
16
|
+
* We need a legacy token specifically because:
|
|
17
|
+
* - Verdaccio's default API middleware accepts legacy tokens but NOT
|
|
18
|
+
* JWTs from `/-/verdaccio/sec/login`
|
|
19
|
+
* - Modern npm (>= 10.x) refuses to run `npm publish` at all without
|
|
20
|
+
* an `_authToken` entry in `.npmrc`, even against a registry that
|
|
21
|
+
* allows `$anonymous` publish — it errors out client-side
|
|
22
|
+
*
|
|
23
|
+
* The throwaway user stays in the test registry's htpasswd store after
|
|
24
|
+
* the run, which is fine for ephemeral CI environments and local temp
|
|
25
|
+
* setups (both wipe storage between runs).
|
|
26
|
+
*/
|
|
27
|
+
async function obtainLegacyToken(registryUrl) {
|
|
28
|
+
const user = `e2e-bot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
29
|
+
const password = "e2e-bot-password";
|
|
30
|
+
const url = `${registryUrl.replace(/\/$/, "")}/-/user/org.couchdb.user:${encodeURIComponent(user)}`;
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
method: "PUT",
|
|
33
|
+
headers: { "content-type": "application/json" },
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
name: user,
|
|
36
|
+
password,
|
|
37
|
+
_id: `org.couchdb.user:${user}`,
|
|
38
|
+
type: "user",
|
|
39
|
+
roles: []
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const body = await res.text();
|
|
44
|
+
throw new Error(`[publishPackage] failed to create throwaway user "${user}" (HTTP ${res.status}): ${body}`);
|
|
45
|
+
}
|
|
46
|
+
const json = await res.json();
|
|
47
|
+
if (!json.token) throw new Error(`[publishPackage] user creation response did not contain a token: ${JSON.stringify(json)}`);
|
|
48
|
+
return {
|
|
49
|
+
user,
|
|
50
|
+
token: json.token
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function createTempProject(pkgName, version, registryUrl, token, dependencies, devDependencies) {
|
|
54
|
+
const tempFolder = await (0, fs_promises.mkdtemp)((0, path.join)((0, os.tmpdir)(), `verdaccio-e2e-ui-${sanitizeFolderName(pkgName)}-`));
|
|
55
|
+
const manifest = {
|
|
56
|
+
name: pkgName,
|
|
57
|
+
version,
|
|
58
|
+
description: `e2e test fixture ${pkgName}`,
|
|
59
|
+
main: "index.js",
|
|
60
|
+
dependencies,
|
|
61
|
+
devDependencies,
|
|
62
|
+
keywords: [
|
|
63
|
+
"verdaccio",
|
|
64
|
+
"e2e",
|
|
65
|
+
"test"
|
|
66
|
+
],
|
|
67
|
+
author: "Verdaccio E2E <verdaccio@example.org>",
|
|
68
|
+
license: "MIT",
|
|
69
|
+
publishConfig: {
|
|
70
|
+
access: "public",
|
|
71
|
+
registry: registryUrl
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "package.json"), JSON.stringify(manifest, null, 2));
|
|
75
|
+
await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "README.md"), `# ${pkgName}\n\nPublished by @verdaccio/e2e-ui for e2e testing.\n`);
|
|
76
|
+
await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "index.js"), `module.exports = ${JSON.stringify(pkgName)};\n`);
|
|
77
|
+
const registryHost = registryUrl.replace(/^https?:/, "");
|
|
78
|
+
const npmrc = [
|
|
79
|
+
`registry=${registryUrl}`,
|
|
80
|
+
`${registryHost}/:_authToken=${token}`,
|
|
81
|
+
"access=public",
|
|
82
|
+
""
|
|
83
|
+
].join("\n");
|
|
84
|
+
await (0, fs_promises.writeFile)((0, path.join)(tempFolder, ".npmrc"), npmrc);
|
|
85
|
+
return tempFolder;
|
|
86
|
+
}
|
|
87
|
+
function spawnNpmPublish(cwd, registryUrl) {
|
|
88
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
89
|
+
const proc = (0, child_process.spawn)("npm", [
|
|
90
|
+
"publish",
|
|
91
|
+
"--registry",
|
|
92
|
+
registryUrl,
|
|
93
|
+
"--tag",
|
|
94
|
+
"latest",
|
|
95
|
+
"--loglevel=error"
|
|
96
|
+
], {
|
|
97
|
+
cwd,
|
|
98
|
+
env: { ...process.env }
|
|
99
|
+
});
|
|
100
|
+
let stdout = "";
|
|
101
|
+
let stderr = "";
|
|
102
|
+
proc.stdout.on("data", (chunk) => {
|
|
103
|
+
stdout += chunk.toString();
|
|
104
|
+
});
|
|
105
|
+
proc.stderr.on("data", (chunk) => {
|
|
106
|
+
stderr += chunk.toString();
|
|
107
|
+
});
|
|
108
|
+
proc.on("error", rejectPromise);
|
|
109
|
+
proc.on("close", (code) => {
|
|
110
|
+
resolvePromise({
|
|
111
|
+
stdout,
|
|
112
|
+
stderr,
|
|
113
|
+
exitCode: code ?? -1
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Publish a throwaway npm package to the target Verdaccio registry.
|
|
120
|
+
*
|
|
121
|
+
* Flow:
|
|
122
|
+
* 1. Create a throwaway user via `PUT /-/user/...` and capture its
|
|
123
|
+
* legacy auth token. See `obtainLegacyToken` for why.
|
|
124
|
+
* 2. Scaffold a temp project with `package.json`, `README.md`,
|
|
125
|
+
* `index.js`, and an `.npmrc` that includes the token.
|
|
126
|
+
* 3. Spawn `npm publish` from that temp dir.
|
|
127
|
+
*
|
|
128
|
+
* `input.credentials` is kept on the signature for forward-compat but
|
|
129
|
+
* is currently unused — each call mints its own throwaway user.
|
|
130
|
+
*
|
|
131
|
+
* Throws on non-zero npm exit. Returns the temp folder path on success
|
|
132
|
+
* so callers can inspect or clean up.
|
|
133
|
+
*/
|
|
134
|
+
async function publishPackage(input) {
|
|
135
|
+
const baseVersion = input.version ?? "1.0.0";
|
|
136
|
+
const version = input.unique ? `${baseVersion}-t${Date.now()}` : baseVersion;
|
|
137
|
+
const { token } = await obtainLegacyToken(input.registryUrl);
|
|
138
|
+
const tempFolder = await createTempProject(input.pkgName, version, input.registryUrl, token, input.dependencies ?? {}, input.devDependencies ?? {});
|
|
139
|
+
const { stdout, stderr, exitCode } = await spawnNpmPublish(tempFolder, input.registryUrl);
|
|
140
|
+
if (exitCode !== 0) throw new Error(`[publishPackage] npm publish failed for ${input.pkgName}@${version} (exit ${exitCode}):\n${stderr || stdout}`);
|
|
141
|
+
return {
|
|
142
|
+
pkgName: input.pkgName,
|
|
143
|
+
version,
|
|
144
|
+
tempFolder,
|
|
145
|
+
stdout,
|
|
146
|
+
stderr,
|
|
147
|
+
exitCode
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Remove a temp project folder previously created by publishPackage.
|
|
152
|
+
* Safe to call with a missing path or a path outside the OS tmp dir —
|
|
153
|
+
* in the latter case it refuses rather than rm-rf'ing arbitrary paths.
|
|
154
|
+
*/
|
|
155
|
+
async function cleanupPublished(tempFolder) {
|
|
156
|
+
if (!tempFolder) return;
|
|
157
|
+
const tmpRoot = (0, os.tmpdir)();
|
|
158
|
+
if (!tempFolder.startsWith(tmpRoot)) throw new Error(`[cleanupPublished] refusing to remove "${tempFolder}" — not under ${tmpRoot}`);
|
|
159
|
+
await (0, fs_promises.rm)(tempFolder, {
|
|
160
|
+
recursive: true,
|
|
161
|
+
force: true
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function spawnNpmUnpublish(cwd, registryUrl, pkgSpec) {
|
|
165
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
166
|
+
const proc = (0, child_process.spawn)("npm", [
|
|
167
|
+
"unpublish",
|
|
168
|
+
pkgSpec,
|
|
169
|
+
"--force",
|
|
170
|
+
"--registry",
|
|
171
|
+
registryUrl,
|
|
172
|
+
"--loglevel=error"
|
|
173
|
+
], {
|
|
174
|
+
cwd,
|
|
175
|
+
env: { ...process.env }
|
|
176
|
+
});
|
|
177
|
+
let stdout = "";
|
|
178
|
+
let stderr = "";
|
|
179
|
+
proc.stdout.on("data", (chunk) => {
|
|
180
|
+
stdout += chunk.toString();
|
|
181
|
+
});
|
|
182
|
+
proc.stderr.on("data", (chunk) => {
|
|
183
|
+
stderr += chunk.toString();
|
|
184
|
+
});
|
|
185
|
+
proc.on("error", rejectPromise);
|
|
186
|
+
proc.on("close", (code) => {
|
|
187
|
+
resolvePromise({
|
|
188
|
+
stdout,
|
|
189
|
+
stderr,
|
|
190
|
+
exitCode: code ?? -1
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Unpublish a package from the target registry so subsequent tests
|
|
197
|
+
* start from a clean slate.
|
|
198
|
+
*
|
|
199
|
+
* If `tempFolder` is provided (typically from a prior `publishPackage`
|
|
200
|
+
* result), its `.npmrc` is reused so no extra throwaway user is
|
|
201
|
+
* minted. Otherwise this function creates its own temp folder + token.
|
|
202
|
+
* Either way the temp folder used by THIS call is removed on exit
|
|
203
|
+
* (but callers still own the lifecycle of a tempFolder they passed in).
|
|
204
|
+
*
|
|
205
|
+
* Treats HTTP 404 / "tarball does not exist" as success so reruns and
|
|
206
|
+
* parallel teardowns don't flap.
|
|
207
|
+
*/
|
|
208
|
+
async function unpublishPackage(input) {
|
|
209
|
+
let workingFolder = input.tempFolder;
|
|
210
|
+
let ownsWorkingFolder = false;
|
|
211
|
+
if (!workingFolder) {
|
|
212
|
+
ownsWorkingFolder = true;
|
|
213
|
+
const { token } = await obtainLegacyToken(input.registryUrl);
|
|
214
|
+
workingFolder = await (0, fs_promises.mkdtemp)((0, path.join)((0, os.tmpdir)(), `verdaccio-e2e-ui-unpublish-`));
|
|
215
|
+
const registryHost = input.registryUrl.replace(/^https?:/, "");
|
|
216
|
+
await (0, fs_promises.writeFile)((0, path.join)(workingFolder, ".npmrc"), [
|
|
217
|
+
`registry=${input.registryUrl}`,
|
|
218
|
+
`${registryHost}/:_authToken=${token}`,
|
|
219
|
+
""
|
|
220
|
+
].join("\n"));
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const { stdout, stderr, exitCode } = await spawnNpmUnpublish(workingFolder, input.registryUrl, input.pkgName);
|
|
224
|
+
const alreadyGone = /404|not found|no such package|does not (exist|match)/i.test(`${stderr}\n${stdout}`);
|
|
225
|
+
if (exitCode !== 0 && !alreadyGone) throw new Error(`[unpublishPackage] npm unpublish failed for ${input.pkgName} (exit ${exitCode}):\n${stderr || stdout}`);
|
|
226
|
+
return {
|
|
227
|
+
pkgName: input.pkgName,
|
|
228
|
+
stdout,
|
|
229
|
+
stderr,
|
|
230
|
+
exitCode,
|
|
231
|
+
alreadyGone
|
|
232
|
+
};
|
|
233
|
+
} finally {
|
|
234
|
+
if (ownsWorkingFolder && workingFolder) await (0, fs_promises.rm)(workingFolder, {
|
|
235
|
+
recursive: true,
|
|
236
|
+
force: true
|
|
237
|
+
}).catch(() => void 0);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
exports.cleanupPublished = cleanupPublished;
|
|
242
|
+
exports.publishPackage = publishPackage;
|
|
243
|
+
exports.unpublishPackage = unpublishPackage;
|
|
244
|
+
|
|
245
|
+
//# sourceMappingURL=publish.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.cjs","names":[],"sources":["../../../src/tasks/publish.ts"],"sourcesContent":["import { spawn } from 'child_process';\nimport { mkdtemp, rm, writeFile } from 'fs/promises';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport interface PublishPackageInput {\n pkgName: string;\n version?: string;\n registryUrl: string;\n credentials: { user: string; password: string };\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n /**\n * When true, append a timestamp suffix to the version so reruns against\n * a persistent registry don't collide on 403. The suffix is a valid\n * semver prerelease, e.g. `1.0.0-t1712345678`. Defaults to false.\n */\n unique?: boolean;\n}\n\n/**\n * Shape of the argument passed to `cy.task('publishPackage', ...)`.\n *\n * `registryUrl` and `credentials` are optional here (they fall back to\n * whatever was configured in `setupVerdaccioTasks`), but `pkgName` is\n * still required — every publish needs a name.\n */\nexport type PublishPackageTaskInput = Partial<\n Omit<PublishPackageInput, 'pkgName'>\n> & {\n pkgName: string;\n};\n\nexport interface PublishPackageResult {\n pkgName: string;\n version: string;\n tempFolder: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nfunction sanitizeFolderName(name: string): string {\n return name.replace(/[^a-zA-Z0-9-_]/g, '-');\n}\n\n/**\n * Obtain a registry-API-compatible (legacy) auth token for publish.\n *\n * Strategy: create a throwaway user per call. `PUT /-/user/org.couchdb.user:<name>`\n * only returns a token on CREATE (and 409s on existing users), so we\n * guarantee success by generating a unique username each time.\n *\n * We need a legacy token specifically because:\n * - Verdaccio's default API middleware accepts legacy tokens but NOT\n * JWTs from `/-/verdaccio/sec/login`\n * - Modern npm (>= 10.x) refuses to run `npm publish` at all without\n * an `_authToken` entry in `.npmrc`, even against a registry that\n * allows `$anonymous` publish — it errors out client-side\n *\n * The throwaway user stays in the test registry's htpasswd store after\n * the run, which is fine for ephemeral CI environments and local temp\n * setups (both wipe storage between runs).\n */\nasync function obtainLegacyToken(\n registryUrl: string\n): Promise<{ user: string; token: string }> {\n const user = `e2e-bot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n const password = 'e2e-bot-password';\n const base = registryUrl.replace(/\\/$/, '');\n const url = `${base}/-/user/org.couchdb.user:${encodeURIComponent(user)}`;\n const res = await fetch(url, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n name: user,\n password,\n _id: `org.couchdb.user:${user}`,\n type: 'user',\n roles: [],\n }),\n });\n if (!res.ok) {\n const body = await res.text();\n throw new Error(\n `[publishPackage] failed to create throwaway user \"${user}\" ` +\n `(HTTP ${res.status}): ${body}`\n );\n }\n const json = (await res.json()) as { token?: string };\n if (!json.token) {\n throw new Error(\n `[publishPackage] user creation response did not contain a token: ${JSON.stringify(\n json\n )}`\n );\n }\n return { user, token: json.token };\n}\n\nasync function createTempProject(\n pkgName: string,\n version: string,\n registryUrl: string,\n token: string,\n dependencies: Record<string, string>,\n devDependencies: Record<string, string>\n): Promise<string> {\n const tempFolder = await mkdtemp(\n join(tmpdir(), `verdaccio-e2e-ui-${sanitizeFolderName(pkgName)}-`)\n );\n const manifest = {\n name: pkgName,\n version,\n description: `e2e test fixture ${pkgName}`,\n main: 'index.js',\n dependencies,\n devDependencies,\n keywords: ['verdaccio', 'e2e', 'test'],\n author: 'Verdaccio E2E <verdaccio@example.org>',\n license: 'MIT',\n // Scoped packages default to `restricted` access which modern npm\n // refuses to publish anonymously. Pin it to `public` so the CLI\n // skips that check for fixtures like `@verdaccio/pkg-scoped`.\n publishConfig: {\n access: 'public',\n registry: registryUrl,\n },\n };\n await writeFile(join(tempFolder, 'package.json'), JSON.stringify(manifest, null, 2));\n await writeFile(\n join(tempFolder, 'README.md'),\n `# ${pkgName}\\n\\nPublished by @verdaccio/e2e-ui for e2e testing.\\n`\n );\n await writeFile(\n join(tempFolder, 'index.js'),\n `module.exports = ${JSON.stringify(pkgName)};\\n`\n );\n\n // `.npmrc` — include an `_authToken` scoped to the registry host so\n // modern npm (>= 10.x) is willing to run `npm publish`. Without this\n // the CLI errors client-side with \"This command requires you to be\n // logged in.\" even against a registry configured for `$anonymous`\n // publish. The token is a legacy Verdaccio auth token obtained by\n // creating a throwaway user via `PUT /-/user/...`.\n const registryHost = registryUrl.replace(/^https?:/, '');\n const npmrc = [\n `registry=${registryUrl}`,\n `${registryHost}/:_authToken=${token}`,\n // Force public access for scoped packages.\n 'access=public',\n '',\n ].join('\\n');\n await writeFile(join(tempFolder, '.npmrc'), npmrc);\n\n return tempFolder;\n}\n\nfunction spawnNpmPublish(\n cwd: string,\n registryUrl: string\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n return new Promise((resolvePromise, rejectPromise) => {\n // `--tag latest` is required so npm accepts prerelease versions\n // (e.g. `1.0.0-t<ts>` when `unique: true`). Without it npm bails\n // with \"You must specify a tag using --tag when publishing a\n // prerelease version.\" For non-prerelease versions it's a no-op.\n const proc = spawn(\n 'npm',\n [\n 'publish',\n '--registry',\n registryUrl,\n '--tag',\n 'latest',\n '--loglevel=error',\n ],\n {\n cwd,\n env: { ...process.env },\n }\n );\n let stdout = '';\n let stderr = '';\n proc.stdout.on('data', (chunk) => {\n stdout += chunk.toString();\n });\n proc.stderr.on('data', (chunk) => {\n stderr += chunk.toString();\n });\n proc.on('error', rejectPromise);\n proc.on('close', (code) => {\n resolvePromise({ stdout, stderr, exitCode: code ?? -1 });\n });\n });\n}\n\n/**\n * Publish a throwaway npm package to the target Verdaccio registry.\n *\n * Flow:\n * 1. Create a throwaway user via `PUT /-/user/...` and capture its\n * legacy auth token. See `obtainLegacyToken` for why.\n * 2. Scaffold a temp project with `package.json`, `README.md`,\n * `index.js`, and an `.npmrc` that includes the token.\n * 3. Spawn `npm publish` from that temp dir.\n *\n * `input.credentials` is kept on the signature for forward-compat but\n * is currently unused — each call mints its own throwaway user.\n *\n * Throws on non-zero npm exit. Returns the temp folder path on success\n * so callers can inspect or clean up.\n */\nexport async function publishPackage(\n input: PublishPackageInput\n): Promise<PublishPackageResult> {\n const baseVersion = input.version ?? '1.0.0';\n const version = input.unique ? `${baseVersion}-t${Date.now()}` : baseVersion;\n\n const { token } = await obtainLegacyToken(input.registryUrl);\n\n const tempFolder = await createTempProject(\n input.pkgName,\n version,\n input.registryUrl,\n token,\n input.dependencies ?? {},\n input.devDependencies ?? {}\n );\n const { stdout, stderr, exitCode } = await spawnNpmPublish(\n tempFolder,\n input.registryUrl\n );\n if (exitCode !== 0) {\n throw new Error(\n `[publishPackage] npm publish failed for ${input.pkgName}@${version} ` +\n `(exit ${exitCode}):\\n${stderr || stdout}`\n );\n }\n return { pkgName: input.pkgName, version, tempFolder, stdout, stderr, exitCode };\n}\n\n/**\n * Remove a temp project folder previously created by publishPackage.\n * Safe to call with a missing path or a path outside the OS tmp dir —\n * in the latter case it refuses rather than rm-rf'ing arbitrary paths.\n */\nexport async function cleanupPublished(tempFolder: string): Promise<void> {\n if (!tempFolder) return;\n const tmpRoot = tmpdir();\n if (!tempFolder.startsWith(tmpRoot)) {\n throw new Error(\n `[cleanupPublished] refusing to remove \"${tempFolder}\" — not under ${tmpRoot}`\n );\n }\n await rm(tempFolder, { recursive: true, force: true });\n}\n\nexport interface UnpublishPackageInput {\n registryUrl: string;\n pkgName: string;\n /**\n * Temp folder returned by a previous `publishPackage` call. If\n * provided, its existing `.npmrc` (which already carries a legacy\n * auth token) is reused for the unpublish call — no extra throwaway\n * user is minted. Otherwise this task creates its own.\n */\n tempFolder?: string;\n}\n\nexport interface UnpublishPackageResult {\n pkgName: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n /**\n * True if the package was already absent (HTTP 404 from the\n * registry). Tests typically want to treat this as success.\n */\n alreadyGone: boolean;\n}\n\nfunction spawnNpmUnpublish(\n cwd: string,\n registryUrl: string,\n pkgSpec: string\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n return new Promise((resolvePromise, rejectPromise) => {\n const proc = spawn(\n 'npm',\n [\n 'unpublish',\n pkgSpec,\n '--force',\n '--registry',\n registryUrl,\n '--loglevel=error',\n ],\n {\n cwd,\n env: { ...process.env },\n }\n );\n let stdout = '';\n let stderr = '';\n proc.stdout.on('data', (chunk) => {\n stdout += chunk.toString();\n });\n proc.stderr.on('data', (chunk) => {\n stderr += chunk.toString();\n });\n proc.on('error', rejectPromise);\n proc.on('close', (code) => {\n resolvePromise({ stdout, stderr, exitCode: code ?? -1 });\n });\n });\n}\n\n/**\n * Unpublish a package from the target registry so subsequent tests\n * start from a clean slate.\n *\n * If `tempFolder` is provided (typically from a prior `publishPackage`\n * result), its `.npmrc` is reused so no extra throwaway user is\n * minted. Otherwise this function creates its own temp folder + token.\n * Either way the temp folder used by THIS call is removed on exit\n * (but callers still own the lifecycle of a tempFolder they passed in).\n *\n * Treats HTTP 404 / \"tarball does not exist\" as success so reruns and\n * parallel teardowns don't flap.\n */\nexport async function unpublishPackage(\n input: UnpublishPackageInput\n): Promise<UnpublishPackageResult> {\n let workingFolder = input.tempFolder;\n let ownsWorkingFolder = false;\n\n if (!workingFolder) {\n ownsWorkingFolder = true;\n const { token } = await obtainLegacyToken(input.registryUrl);\n workingFolder = await mkdtemp(\n join(tmpdir(), `verdaccio-e2e-ui-unpublish-`)\n );\n const registryHost = input.registryUrl.replace(/^https?:/, '');\n await writeFile(\n join(workingFolder, '.npmrc'),\n [\n `registry=${input.registryUrl}`,\n `${registryHost}/:_authToken=${token}`,\n '',\n ].join('\\n')\n );\n }\n\n try {\n const { stdout, stderr, exitCode } = await spawnNpmUnpublish(\n workingFolder,\n input.registryUrl,\n input.pkgName\n );\n\n // Treat \"already absent\" as success. npm prints slightly different\n // messages depending on version — match loosely.\n const alreadyGone =\n /404|not found|no such package|does not (exist|match)/i.test(\n `${stderr}\\n${stdout}`\n );\n\n if (exitCode !== 0 && !alreadyGone) {\n throw new Error(\n `[unpublishPackage] npm unpublish failed for ${input.pkgName} ` +\n `(exit ${exitCode}):\\n${stderr || stdout}`\n );\n }\n\n return {\n pkgName: input.pkgName,\n stdout,\n stderr,\n exitCode,\n alreadyGone,\n };\n } finally {\n if (ownsWorkingFolder && workingFolder) {\n await rm(workingFolder, { recursive: true, force: true }).catch(\n () => undefined\n );\n }\n }\n}\n"],"mappings":"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"}
|