Skip to content

Commit 8d9f417

Browse files
committed
add v4 migration
1 parent bda9abe commit 8d9f417

5 files changed

Lines changed: 124 additions & 11 deletions

File tree

e2e/README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,15 @@ runs `serverless package` to produce a CloudFormation template, and
2121
asserts on the generated resources.
2222

2323
Critically, **these tests do not deploy anything to AWS** — they only
24-
exercise the synthesis path. That makes them fast (~3s per fixture),
25-
fork-safe (no AWS credentials required), and suitable to run on every
26-
PR.
24+
exercise the synthesis path. That makes them fast (~3s per fixture)
25+
and suitable to run on every PR. No AWS credentials are required.
26+
27+
> **Serverless Framework v4 requires authentication.** As of v4, every
28+
> CLI invocation (including the `serverless package` these tests run)
29+
> must be authenticated, even though nothing is deployed. You need a
30+
> Serverless **Access Key** or **License Key** — free for individual
31+
> developers and organizations under the revenue threshold. See
32+
> [Authentication](#authentication-serverless-v4) below.
2733
2834
## Running locally
2935

@@ -38,6 +44,24 @@ npm run test:all
3844
npx jest --config jest.e2e.config.ts basic-api-key
3945
```
4046

47+
### Authentication (Serverless v4)
48+
49+
Set a Serverless Access Key (or License Key) before running, otherwise
50+
the spawned `serverless package` will fail / prompt for login:
51+
52+
```bash
53+
# One-time interactive login (writes a key to your machine)
54+
npx serverless login
55+
56+
# …or set an explicit key for this shell / CI
57+
export SERVERLESS_ACCESS_KEY=... # from Dashboard > Settings > Access Keys
58+
# or
59+
export SERVERLESS_LICENSE_KEY=...
60+
```
61+
62+
The harness passes the current environment through to the CLI, so any
63+
of the above is sufficient.
64+
4165
## Adding a new test
4266

4367
1. Create a new example under `examples/<feature>/`:
@@ -90,3 +114,7 @@ A dedicated `e2e` job in `.github/workflows/ci.yml` runs the full
90114
synthesis test suite on every PR and push to `master` or `alpha`. It
91115
runs after the unit-test matrix completes successfully (no point
92116
running E2E if unit tests already failed).
117+
118+
The job reads `SERVERLESS_ACCESS_KEY` from repository secrets to
119+
authenticate the Serverless v4 CLI. Add it under
120+
**Settings > Secrets and variables > Actions** before this job can pass.

e2e/helpers/synthesize.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,33 @@ export function synthesize(exampleDir: string): SynthesizeResult {
9393
);
9494
}
9595

96-
const templatePath = path.join(
96+
// Serverless writes the synthesized template as
97+
// `cloudformation-template-update-stack.json`. We look it up defensively
98+
// (preferring the canonical name, then falling back to any
99+
// `cloudformation-template-*-stack.json`) so the harness is resilient to
100+
// any path/name nuance across Serverless Framework major versions.
101+
const canonical = path.join(
97102
packageDir,
98103
'cloudformation-template-update-stack.json',
99104
);
105+
let templatePath = canonical;
106+
if (!fs.existsSync(templatePath)) {
107+
const fallback = fs
108+
.readdirSync(packageDir)
109+
.find(
110+
(f) =>
111+
/^cloudformation-template-.*-stack\.json$/.test(f) &&
112+
f.endsWith('.json'),
113+
);
114+
if (fallback) {
115+
templatePath = path.join(packageDir, fallback);
116+
}
117+
}
100118
if (!fs.existsSync(templatePath)) {
101119
fs.rmSync(packageDir, { recursive: true, force: true });
102120
throw new Error(
103-
`CloudFormation template was not produced at ${templatePath}.`,
121+
`CloudFormation template was not produced in ${packageDir} ` +
122+
`(expected ${canonical} or a cloudformation-template-*-stack.json).`,
104123
);
105124
}
106125

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"test:watch": "jest src/__tests__/.*\\.test\\.ts --watch",
1515
"test:e2e": "npm run build && jest --config jest.e2e.config.ts",
1616
"test:all": "npm run test && npm run test:e2e",
17-
"build": "tsc",
17+
"build": "tsc && npm run verify:imports",
18+
"verify:imports": "node scripts/check-no-serverless-deep-imports.js",
1819
"lint": "eslint . && tsc --noEmit",
1920
"lint:fix": "eslint . --fix",
2021
"prepare": "husky && rm -rf lib & npm run build"
@@ -40,7 +41,7 @@
4041
"lint-staged": "^16.2.3",
4142
"prettier": "^2.8.0",
4243
"semantic-release": "^25.0.3",
43-
"serverless": "^3.25.1",
44+
"serverless": "^4.0.0",
4445
"ts-jest": "^29.4.11",
4546
"ts-node": "^10.9.1",
4647
"typescript": "~4.9.5"
@@ -71,7 +72,7 @@
7172
"terminal-link": "^2.1.1"
7273
},
7374
"peerDependencies": {
74-
"serverless": ">=3.0.0"
75+
"serverless": "^4.0.0"
7576
},
7677
"lint-staged": {
7778
"*.{ts,js}": [
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Guard against regressing issue #632.
4+
*
5+
* Serverless Framework v4's `serverless` npm package is a thin installer with
6+
* no `lib/` directory, so any runtime `require('serverless/lib/...')` in our
7+
* compiled output throws `Cannot find module 'serverless/lib/...'` on v4.
8+
*
9+
* We intentionally only reference Serverless internals via `import type`
10+
* (erased at compile time) or via the injected `serverless`/`utils` objects.
11+
* This script fails the build if a deep runtime require ever sneaks into the
12+
* emitted `lib/` output.
13+
*/
14+
'use strict';
15+
/* eslint-disable @typescript-eslint/no-var-requires -- Node CJS tooling script */
16+
17+
const fs = require('fs');
18+
const path = require('path');
19+
20+
const LIB_DIR = path.resolve(__dirname, '..', 'lib');
21+
// Matches `require('serverless/lib/...')` / `require("serverless/lib/...")`.
22+
const FORBIDDEN = /require\(\s*['"]serverless\/lib\//;
23+
24+
/** @param {string} dir @returns {string[]} */
25+
function collectJsFiles(dir) {
26+
/** @type {string[]} */
27+
const out = [];
28+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
29+
const full = path.join(dir, entry.name);
30+
if (entry.isDirectory()) out.push(...collectJsFiles(full));
31+
else if (entry.isFile() && full.endsWith('.js')) out.push(full);
32+
}
33+
return out;
34+
}
35+
36+
if (!fs.existsSync(LIB_DIR)) {
37+
console.error(
38+
`[check-no-serverless-deep-imports] lib/ not found at ${LIB_DIR}. Run \`npm run build\` first.`,
39+
);
40+
process.exit(1);
41+
}
42+
43+
const offenders = collectJsFiles(LIB_DIR).filter((file) =>
44+
FORBIDDEN.test(fs.readFileSync(file, 'utf8')),
45+
);
46+
47+
if (offenders.length > 0) {
48+
console.error(
49+
'[check-no-serverless-deep-imports] Forbidden runtime require of ' +
50+
"'serverless/lib/...' found in compiled output (this reintroduces #632 " +
51+
'on Serverless v4). Use `import type` instead:\n' +
52+
offenders.map((f) => ` - ${path.relative(LIB_DIR, f)}`).join('\n'),
53+
);
54+
process.exit(1);
55+
}
56+
57+
console.log(
58+
'[check-no-serverless-deep-imports] OK — no deep serverless/lib requires in lib/.',
59+
);

src/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import Serverless from 'serverless/lib/Serverless';
2-
import Provider from 'serverless/lib/plugins/aws/provider.js';
1+
// NOTE: these MUST stay `import type`. They reference internal Serverless
2+
// paths that exist in v3 but NOT in the Serverless v4 npm package (v4 ships a
3+
// thin installer with no `lib/` tree). `import type` is fully erased at
4+
// compile time, so it never becomes a runtime `require('serverless/lib/...')`.
5+
// A plain `import` here is what caused issue #632
6+
// (`Cannot find module 'serverless/lib/serverless-error'`) on v4.
7+
import type Serverless from 'serverless/lib/Serverless';
8+
import type Provider from 'serverless/lib/plugins/aws/provider.js';
39
import { forEach, last, merge } from 'lodash';
410
import { getAppSyncConfig } from './getAppSyncConfig';
511
import { GraphQLError } from 'graphql';
@@ -31,7 +37,7 @@ import {
3137
ListApiKeysRequest,
3238
ListApiKeysResponse,
3339
} from 'aws-sdk/clients/appsync';
34-
import {
40+
import type {
3541
CommandsDefinition,
3642
Hook,
3743
VariablesSourcesDefinition,

0 commit comments

Comments
 (0)