Skip to content

Commit bda3784

Browse files
feat: publish netlify package (#7293)
* feat: publish `netlify` package * fix: await command * refactor: update publish flow * chore: add E2E test * chore: run script in cwd * chore: debug CI * chore: use `execa.node` * chore: update release flow * chore: enable debugging * chore: debug * chore: skip check on Windows * chore: remove debug * refactor: rename binary * chore: skip on Windows * chore: increase timeout * chore: skip on Windows * chore: skip script only * chore: skip on Windows
1 parent 298a379 commit bda3784

File tree

6 files changed

+168
-38
lines changed

6 files changed

+168
-38
lines changed

.github/workflows/release-please.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,15 @@ jobs:
5454
- name: Run E2E tests
5555
run: npm run test:e2e
5656

57-
- name: Publish package
57+
- name: Publish netlify-cli package
58+
run: npm publish --provenance
59+
env:
60+
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
61+
62+
- name: Prepare netlify package
63+
run: node scripts/netlifyPackage.js
64+
65+
- name: Publish netlify package
5866
run: npm publish --provenance
5967
env:
6068
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

e2e/install.e2e.ts

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ const itWithMockNpmRegistry = it.extend<{ registry: { address: string; cwd: stri
7070
access: '$all',
7171
publish: '$all',
7272
},
73+
netlify: {
74+
access: '$all',
75+
publish: '$all',
76+
},
7377
'**': {
7478
access: '$all',
7579
publish: 'noone',
@@ -116,6 +120,20 @@ const itWithMockNpmRegistry = it.extend<{ registry: { address: string; cwd: stri
116120
cwd: publishWorkspace,
117121
stdio: debug.enabled ? 'inherit' : 'ignore',
118122
})
123+
124+
// TODO: Figure out why calling this script is failing on Windows.
125+
if (platform() !== 'win32') {
126+
// Publishing `netlify` package
127+
await execa.node(path.resolve(projectRoot, 'scripts/netlifyPackage.js'), {
128+
cwd: publishWorkspace,
129+
stdio: debug.enabled ? 'inherit' : 'ignore',
130+
})
131+
await execa('npm', ['publish', `--registry=${registryURL.toString()}`, '--tag=testing'], {
132+
cwd: publishWorkspace,
133+
stdio: debug.enabled ? 'inherit' : 'ignore',
134+
})
135+
}
136+
119137
await fs.rm(publishWorkspace, { force: true, recursive: true })
120138

121139
const testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), tempdirPrefix))
@@ -135,53 +153,91 @@ const itWithMockNpmRegistry = it.extend<{ registry: { address: string; cwd: stri
135153
},
136154
})
137155

138-
const tests: [packageManager: string, config: { install: [cmd: string, args: string[]]; lockfile: string }][] = [
156+
type Test = { packageName: string }
157+
type InstallTest = Test & { install: [cmd: string, args: string[]]; lockfile: string }
158+
type RunTest = Test & { run: [cmd: string, args: string[]] }
159+
160+
const tests: [packageManager: string, config: InstallTest | RunTest][] = [
139161
[
140162
'npm',
141163
{
164+
packageName: 'netlify-cli',
142165
install: ['npm', ['install', 'netlify-cli@testing']],
143166
lockfile: 'package-lock.json',
144167
},
145168
],
146169
[
147170
'pnpm',
148171
{
172+
packageName: 'netlify-cli',
149173
install: ['pnpm', ['add', 'netlify-cli@testing']],
150174
lockfile: 'pnpm-lock.yaml',
151175
},
152176
],
153177
[
154178
'yarn',
155179
{
180+
packageName: 'netlify-cli',
156181
install: ['yarn', ['add', 'netlify-cli@testing']],
157182
lockfile: 'yarn.lock',
158183
},
159184
],
185+
[
186+
'npx',
187+
{
188+
packageName: 'netlify',
189+
run: ['npx', ['-y', 'netlify@testing']],
190+
},
191+
],
160192
]
161193

162194
describe.each(tests)('%s → installs the cli and runs the help command without error', (_, config) => {
163-
itWithMockNpmRegistry('installs the cli and runs the help command without error', async ({ registry }) => {
164-
const cwd = registry.cwd
165-
await execa(...config.install, {
166-
cwd,
167-
env: { npm_config_registry: registry.address },
168-
stdio: debug.enabled ? 'inherit' : 'ignore',
169-
})
195+
// TODO: Figure out why this flow is failing on Windows.
196+
const npxOnWindows = platform() === 'win32' && 'run' in config
170197

171-
expect(
172-
existsSync(path.join(cwd, config.lockfile)),
173-
`Generated lock file ${config.lockfile} does not exist in ${cwd}`,
174-
).toBe(true)
198+
itWithMockNpmRegistry.skipIf(npxOnWindows)(
199+
'installs the cli and runs the help command without error',
200+
async ({ registry }) => {
201+
const cwd = registry.cwd
175202

176-
const binary = path.resolve(path.join(cwd, `./node_modules/.bin/netlify${platform() === 'win32' ? '.cmd' : ''}`))
177-
const { stdout } = await execa(binary, ['help'], { cwd })
203+
let stdout: string
178204

179-
expect(stdout.trim(), `Help command does not start with '⬥ Netlify CLI'\\n\\nVERSION: ${stdout}`).toMatch(
180-
/^ Netlify CLI\n\nVERSION/,
181-
)
182-
expect(stdout, `Help command does not include 'netlify-cli/${pkg.version}':\n\n${stdout}`).toContain(
183-
`netlify-cli/${pkg.version}`,
184-
)
185-
expect(stdout, `Help command does not include '$ netlify [COMMAND]':\n\n${stdout}`).toMatch('$ netlify [COMMAND]')
186-
})
205+
if ('install' in config) {
206+
await execa(...config.install, {
207+
cwd,
208+
env: { npm_config_registry: registry.address },
209+
stdio: debug.enabled ? 'inherit' : 'ignore',
210+
})
211+
212+
expect(
213+
existsSync(path.join(cwd, config.lockfile)),
214+
`Generated lock file ${config.lockfile} does not exist in ${cwd}`,
215+
).toBe(true)
216+
217+
const binary = path.resolve(
218+
path.join(cwd, `./node_modules/.bin/netlify${platform() === 'win32' ? '.cmd' : ''}`),
219+
)
220+
const result = await execa(binary, ['help'], { cwd })
221+
222+
stdout = result.stdout
223+
} else {
224+
const [cmd, args] = config.run
225+
const result = await execa(cmd, args, {
226+
env: {
227+
npm_config_registry: registry.address,
228+
},
229+
})
230+
231+
stdout = result.stdout
232+
}
233+
234+
expect(stdout.trim(), `Help command does not start with '⬥ Netlify CLI'\\n\\nVERSION: ${stdout}`).toMatch(
235+
/^ Netlify CLI\n\nVERSION/,
236+
)
237+
expect(stdout, `Help command does not include '${config.packageName}/${pkg.version}':\n\n${stdout}`).toContain(
238+
`${config.packageName}/${pkg.version}`,
239+
)
240+
expect(stdout, `Help command does not include '$ netlify [COMMAND]':\n\n${stdout}`).toMatch('$ netlify [COMMAND]')
241+
},
242+
)
187243
})

scripts/netlifyPackage.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// @ts-check
2+
import assert from 'node:assert'
3+
import { dirname, resolve } from 'node:path'
4+
import { readFile, stat, writeFile } from 'node:fs/promises'
5+
6+
import execa from 'execa'
7+
8+
/**
9+
* @import {Package} from "normalize-package-data"
10+
*/
11+
12+
const packageJSON = await getPackageJSON()
13+
14+
async function getPackageJSON() {
15+
const packageJSONPath = resolve('package.json')
16+
17+
/**
18+
* @type {Package}
19+
*/
20+
const contents = JSON.parse(await readFile(packageJSONPath, 'utf8'))
21+
22+
return {
23+
contents,
24+
path: packageJSONPath,
25+
}
26+
}
27+
28+
async function preparePackageJSON() {
29+
const binPath = Object.values(packageJSON.contents.bin ?? {})[0]
30+
if (!binPath) {
31+
throw new Error('Did not find a non-empty binary entry in `package.json`, so the `npx` flow will not work.')
32+
}
33+
34+
const newPackageJSON = {
35+
...packageJSON.contents,
36+
main: './dist/index.js',
37+
name: 'netlify',
38+
scripts: {
39+
...packageJSON.contents.scripts,
40+
41+
// We don't need the pre-publish script because we expect the work in
42+
// there to be done when publishing the `netlify-cli` package. We'll
43+
// ensure this is the case by throwing if a shrinkwrap file isn't found.
44+
prepublishOnly: undefined,
45+
},
46+
bin: {
47+
npxnetlify: binPath,
48+
},
49+
}
50+
51+
try {
52+
const shrinkwrap = await stat(resolve(packageJSON.path, '../npm-shrinkwrap.json'))
53+
54+
assert.ok(shrinkwrap.isFile())
55+
} catch {
56+
throw new Error('Failed to find npm-shrinkwrap.json file. Did you run the pre-publish script?')
57+
}
58+
59+
console.log(`Writing updated package.json to ${packageJSON.path}...`)
60+
await writeFile(packageJSON.path, `${JSON.stringify(newPackageJSON, null, 2)}\n`)
61+
62+
console.log('Regenerating shrinkwrap file with updated package name...')
63+
await execa('npm', ['shrinkwrap'], {
64+
cwd: dirname(packageJSON.path),
65+
})
66+
}
67+
68+
await preparePackageJSON()

scripts/prepublishOnly.js

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,16 @@ const main = async () => {
1414
const packageJSONPath = path.join(process.cwd(), 'package.json')
1515
const rawPackageJSON = await fs.readFile(packageJSONPath, 'utf8')
1616

17-
try {
18-
// Remove dev dependencies from the package.json...
19-
const packageJSON = JSON.parse(rawPackageJSON)
20-
Reflect.deleteProperty(packageJSON, 'devDependencies')
21-
await fs.writeFile(packageJSONPath, JSON.stringify(packageJSON, null, 2))
17+
// Remove dev dependencies from the package.json...
18+
const packageJSON = JSON.parse(rawPackageJSON)
19+
Reflect.deleteProperty(packageJSON, 'devDependencies')
20+
await fs.writeFile(packageJSONPath, JSON.stringify(packageJSON, null, 2))
2221

23-
// Prune out dev dependencies (this updates the `package-lock.json` lockfile)
24-
cp.spawnSync('npm', ['prune'], { stdio: 'inherit' })
22+
// Prune out dev dependencies (this updates the `package-lock.json` lockfile)
23+
cp.spawnSync('npm', ['prune'], { stdio: 'inherit' })
2524

26-
// Convert `package-lock.json` lockfile to `npm-shrinkwrap.json`
27-
cp.spawnSync('npm', ['shrinkwrap'], { stdio: 'inherit' })
28-
} finally {
29-
// Restore the original `package.json`. (This makes no functional difference in a publishing
30-
// environment, it's purely to minimize how destructive this script is.)
31-
await fs.writeFile(packageJSONPath, rawPackageJSON)
32-
}
25+
// Convert `package-lock.json` lockfile to `npm-shrinkwrap.json`
26+
cp.spawnSync('npm', ['shrinkwrap'], { stdio: 'inherit' })
3327
}
3428

3529
await main()

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This is an entrypoint that mirrors the interface that the `netlify` package
2+
// used to have, before it was renamed to `@netlify/api`. We keep it for
3+
// backwards-compatibility.
4+
export { NetlifyAPI, methods } from '@netlify/api'

vitest.e2e.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'
33
export default defineConfig({
44
test: {
55
include: ['e2e/**/*.e2e.[jt]s'],
6-
testTimeout: 600_000,
6+
testTimeout: 1200_000,
77
// Pin to vitest@1 behavior: https://vitest.dev/guide/migration.html#default-pool-is-forks.
88
// TODO(serhalp) Remove this and fix flaky hanging e2e tests on Windows.
99
pool: 'threads',

0 commit comments

Comments
 (0)