Skip to content

Commit eff7291

Browse files
Weakkyjasonkuhrt
andauthored
feat: windows support (#1184)
Co-authored-by: Jason Kuhrt <[email protected]>
1 parent fecd3fb commit eff7291

File tree

15 files changed

+201
-131
lines changed

15 files changed

+201
-131
lines changed

.github/workflows/pr.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,36 @@ jobs:
2121
- run: yarn -s build
2222
- run: yarn -s test:unit
2323

24+
system-tests-windows:
25+
strategy:
26+
matrix:
27+
os: [windows-latest]
28+
node-version: [10.x, 12.x]
29+
test-case: [kitchen-sink]
30+
runs-on: ${{ matrix.os }}
31+
steps:
32+
- uses: actions/checkout@v2
33+
- name: Use Node.js ${{ matrix.node-version }}
34+
uses: actions/setup-node@v1
35+
with:
36+
node-version: ${{ matrix.node-version }}
37+
- name: Install deps
38+
run: yarn --frozen-lockfile
39+
- run: yarn -s build
40+
- name: Setup global git user
41+
run: |
42+
# For nexus create app flow which will make an init commit
43+
git config --global user.name prisma-labs
44+
git config --global user.email [email protected]
45+
- run: yarn -s test system/${{matrix.test-case}}
46+
2447
system-tests:
25-
runs-on: ubuntu-latest
2648
strategy:
2749
matrix:
50+
os: [ubuntu-latest]
2851
node-version: [10.x, 12.x]
2952
test-case: [kitchen-sink, create-prisma-app]
53+
runs-on: ${{ matrix.os }}
3054
steps:
3155
- uses: actions/checkout@v2
3256
- name: Use Node.js ${{ matrix.node-version }}

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"@types/lodash": "4.14.152",
7777
"@types/node": "13.13.9",
7878
"@types/parse-json": "^4.0.0",
79+
"@types/which": "^1.3.2",
7980
"cross-env": "^7.0.2",
8081
"docsify": "4.11.3",
8182
"docsify-cli": "4.4.0",
@@ -86,7 +87,8 @@
8687
"nock": "^12.0.3",
8788
"node-pty": "0.9.0",
8889
"prettier": "2.0.5",
89-
"ts-jest": "26.0.0"
90+
"ts-jest": "26.0.0",
91+
"which": "^2.0.2"
9092
},
9193
"engines": {
9294
"node": ">=10.0.0"
@@ -113,7 +115,7 @@
113115
"postpublish": "yarn clean",
114116
"postinstall": "node scripts/postinstall",
115117
"prepublishOnly": "yarn build",
116-
"test": "cross-env FORCE_COLOR=3 LOG_PRETTY=true DEBUG=true jest --verbose --testTimeout 360000 --forceExit",
118+
"test": "cross-env FORCE_COLOR=3 LOG_PRETTY=true DEBUG=true jest --verbose --testTimeout 1200000 --forceExit",
117119
"test:unit": "yarn test src",
118120
"dev:test": "yarn test --watch src",
119121
"dev": "yarn clean && node scripts/build-module-facades && tsc -b tsconfig.cjs.json -w"

src/cli/commands/create/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export async function runBootstrapper(configInput?: Partial<ConfigInput>): Promi
8383
sourceRoot: Path.join(process.cwd(), 'api'),
8484
...configInput,
8585
}
86-
const nexusVersion = await getNexusVersion()
86+
const nexusVersion = getNexusVersion()
8787
const packageManager = await getPackageManager(internalConfig.projectRoot)
8888

8989
if (packageManager === 'sigtermed') {
@@ -213,7 +213,7 @@ async function getPackageManager(projectRoot: string): Promise<PackageManager.Pa
213213
return packageManager
214214
}
215215

216-
async function getNexusVersion(): Promise<string> {
216+
function getNexusVersion(): string {
217217
const nexusVersionEnvVar = process.env.CREATE_APP_CHOICE_NEXUS_VERSION
218218
const nexusVersion = nexusVersionEnvVar ?? `${ownPackage.version}`
219219
return nexusVersion

src/lib/add-to-context-extractor/typegen.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { codeBlock } from 'common-tags'
22
import { Either, isLeft } from 'fp-ts/lib/Either'
33
import * as fs from 'fs-jetpack'
4+
import slash from 'slash'
45
import { hardWriteFile } from '../fs'
56
import * as Layout from '../layout'
67
import { rootLogger } from '../nexus-logger'
@@ -76,7 +77,7 @@ export async function writeContextTypeGenFile(contextTypes: ExtractedContextType
7677
}
7778

7879
function renderImport(input: { from: string; names: string[] }) {
79-
return `import { ${input.names.join(', ')} } from '${input.from}'`
80+
return `import { ${input.names.join(', ')} } from '${slash(input.from)}'`
8081
}
8182

8283
function renderContextInterfaceForExtractedReturnType(contribType: ContribType): string {

src/lib/e2e-testing.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import * as Logger from '@nexus/logger'
66
import * as FS from 'fs-jetpack'
77
import { IPty, IPtyForkOptions, IWindowsPtyForkOptions } from 'node-pty'
8+
import * as os from 'os'
89
import * as Path from 'path'
910
import { ConnectableObservable, Observable, Subject } from 'rxjs'
1011
import { multicast } from 'rxjs/operators'
1112
import stripAnsi from 'strip-ansi'
13+
import * as which from 'which'
1214
import { Database } from '../cli/commands/create/app'
1315
import { GraphQLClient } from '../lib/graphql-client'
1416
import { getTmpDir } from './fs'
@@ -59,6 +61,11 @@ interface Config {
5961
}
6062
}
6163

64+
/**
65+
* The path at which to spawn node processes
66+
*/
67+
const NODE_PATH = os.platform() === 'win32' ? 'node.exe' : 'node'
68+
6269
export function createE2EContext(config: Config) {
6370
Logger.log.settings({ filter: { level: 'trace' } })
6471
process.env.LOG_LEVEL = 'trace'
@@ -89,7 +96,7 @@ export function createE2EContext(config: Config) {
8996
fs: FS.cwd(projectDir),
9097
client: new GraphQLClient(`http://localhost:${config.serverPort}/graphql`),
9198
node(args: string[], opts: IPtyForkOptions = {}) {
92-
return spawn('node', args, { cwd: projectDir, ...opts })
99+
return spawn(NODE_PATH, args, { cwd: projectDir, ...opts })
93100
},
94101
spawn(binPathAndArgs: string[], opts: IPtyForkOptions = {}) {
95102
const [binPath, ...args] = binPathAndArgs
@@ -131,7 +138,7 @@ export function createE2EContext(config: Config) {
131138
},
132139
localNexus: config.localNexus
133140
? (args: string[]) => {
134-
return spawn('node', [localNexusBinPath!, ...args], {
141+
return spawn(NODE_PATH, [localNexusBinPath!, ...args], {
135142
cwd: projectDir,
136143
env: {
137144
...process.env,
@@ -142,7 +149,7 @@ export function createE2EContext(config: Config) {
142149
: null,
143150
localNexusCreateApp: config.localNexus
144151
? (options: CreateAppOptions) => {
145-
return spawn('node', [localNexusBinPath!], {
152+
return spawn(NODE_PATH, [localNexusBinPath!], {
146153
cwd: projectDir,
147154
env: {
148155
...process.env,
@@ -156,7 +163,7 @@ export function createE2EContext(config: Config) {
156163
: null,
157164
localNexusCreatePlugin: config.localNexus
158165
? (options: CreatePluginOptions) => {
159-
return spawn('node', [localNexusBinPath!, 'create', 'plugin'], {
166+
return spawn(NODE_PATH, [localNexusBinPath!, 'create', 'plugin'], {
160167
cwd: projectDir,
161168
env: {
162169
...process.env,
@@ -178,8 +185,10 @@ export function createE2EContext(config: Config) {
178185
export function spawn(command: string, args: string[], opts: IPtyForkOptions): ConnectableObservable<string> {
179186
const nodePty = requireNodePty()
180187
const subject = new Subject<string>()
188+
// On windows, node-pty needs an absolute path to the executable. `which` is used to find that path.
189+
const commandPath = which.sync(command)
181190
const ob = new Observable<string>((sub) => {
182-
const proc = nodePty.spawn(command, args, {
191+
const proc = nodePty.spawn(commandPath, args, {
183192
cols: process.stdout.columns ?? 80,
184193
rows: process.stdout.rows ?? 80,
185194
...opts,

src/lib/fs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ export async function hardWriteFile(filePath: string, data: string) {
102102
* Return the path to a temporary directory on the machine. This works around a
103103
* limitation in Node wherein a symlink is returned on macOS for `os.tmpdir`.
104104
*/
105-
export function getTmpDir(prefix: string = '') {
106-
const tmpDirPath = NodeFS.realpathSync(OS.tmpdir())
105+
export function getTmpDir(prefix: string = '', baseTmpDir?: string) {
106+
const tmpDirPath = NodeFS.realpathSync(baseTmpDir ?? OS.tmpdir())
107107
const id = Math.random().toString().slice(2)
108108
const dirName = [prefix, id].filter((x) => x).join('-')
109109

src/lib/glocal/detect-exec-layout.spec.ts

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import * as os from 'os'
12
import * as path from 'path'
23
import * as TestContext from '../test-context'
34
import { normalizePathsInData, Param1 } from '../utils'
45
import { detectExecLayout } from './detect-exec-layout'
56

7+
const isWindows = os.platform() === 'win32'
8+
69
const ctx = TestContext.compose(TestContext.tmpDir(), TestContext.fs(), (ctx) => {
710
return {
811
detectExecLayout: (input?: Partial<Param1<typeof detectExecLayout>>) => {
@@ -24,13 +27,25 @@ function nodeProject() {
2427
ctx.fs.write('package.json', '{}')
2528
}
2629
function installTool() {
27-
ctx.fs.write('package.json', '{ "dependencies": { "a": "foo" } }')
30+
ctx.fs.write('package.json', {
31+
dependencies: { a: 'foo' },
32+
})
2833
ctx.fs.write('node_modules/a/index.js', '')
29-
ctx.fs.symlink(ctx.fs.path('node_modules/a/index.js'), 'node_modules/.bin/a')
34+
ctx.fs.write('node_modules/a/package.json', {
35+
name: 'a',
36+
bin: {
37+
a: 'index.js',
38+
},
39+
})
40+
if (isWindows) {
41+
ctx.fs.write('node_modules/.bin/a', '')
42+
} else {
43+
ctx.fs.symlink(ctx.fs.path('node_modules/a/index.js'), 'node_modules/.bin/a')
44+
}
3045
}
3146

3247
beforeEach(() => {
33-
ctx.fs.dir('some/other/bin/a')
48+
ctx.fs.write('some/other/bin/a', '')
3449
ctx.fs.dir('node_modules/.bin')
3550
})
3651

@@ -105,28 +120,46 @@ describe('local available detection', () => {
105120
runningLocalTool: false,
106121
})
107122
})
108-
it('if just node_module/dir missing, discounts being available', () => {
109-
ctx.fs.remove('node_modules/a')
110-
expect(ctx.detectExecLayout()).toMatchObject({
111-
nodeProject: true,
112-
toolProject: true,
113-
toolCurrentlyPresentInNodeModules: false,
114-
runningLocalTool: false,
115-
})
116-
})
123+
// todo | Test disabled becuase on posix the symlink in node_modules/.bin
124+
// todo | dir is deleted when the node_modules package it links to is deleted
125+
// todo | meaning that the test is testing nothing.
126+
// todo | Bring back just for windows?
127+
// it('if just node_module/dir missing, discounts being available', () => {
128+
// ctx.fs.remove('node_modules/a')
129+
// expect(ctx.detectExecLayout()).toMatchObject({
130+
// nodeProject: true,
131+
// toolProject: true,
132+
// toolCurrentlyPresentInNodeModules: false,
133+
// runningLocalTool: false,
134+
// })
135+
// })
117136
})
118137

119138
describe('running locally detection', () => {
120139
beforeEach(installTool)
121-
it('if process script path matches path to tool in project bin then considered running locally', () => {
122-
expect(ctx.detectExecLayout({ scriptPath: ctx.fs.path('node_modules/.bin/a') })).toMatchObject({
140+
141+
describe('if process tool path matches project tool path in dot-bin then considered running locally', () => {
142+
const runningLocalToolResult = {
123143
nodeProject: true,
124144
toolProject: true,
125145
toolCurrentlyPresentInNodeModules: true,
126146
runningLocalTool: true,
147+
}
148+
it('process tool path as direct to script', () => {
149+
expect(ctx.detectExecLayout({ scriptPath: ctx.fs.path('node_modules/a/index.js') })).toMatchObject(
150+
runningLocalToolResult
151+
)
127152
})
153+
if (!isWindows) {
154+
it('on posix: process tool path as dot-bin path (b/c argv[0] symlink not followed in some cases)', () => {
155+
expect(ctx.detectExecLayout({ scriptPath: ctx.fs.path('node_modules/.bin/a') })).toMatchObject(
156+
runningLocalToolResult
157+
)
158+
})
159+
}
128160
})
129-
it('if process script path does not match path to tool in project bin then not considered running locally', () => {
161+
162+
it('if process tool path does not match project tool path in dot-bin then not considered running locally', () => {
130163
expect(ctx.detectExecLayout({ scriptPath: ctx.fs.path('some/other/bin/a') })).toMatchObject({
131164
nodeProject: true,
132165
toolProject: true,
@@ -136,34 +169,28 @@ describe('running locally detection', () => {
136169
})
137170
})
138171

139-
describe('this process analysis', () => {
172+
describe('analysis about "this process"', () => {
140173
beforeEach(() => {
141174
ctx.fs.write('a/b/c/real.js', '')
142-
ctx.fs.symlink(ctx.fs.path('a/b/c/real.js'), 'x/y/z/fake')
175+
if (isWindows) {
176+
ctx.fs.write('x/y/z/fake', '')
177+
} else {
178+
ctx.fs.symlink(ctx.fs.path('a/b/c/real.js'), 'x/y/z/fake')
179+
}
143180
})
144181
it('finds the real path of the script node executed', () => {
145182
const data = ctx.detectExecLayout({ scriptPath: ctx.fs.path('x/y/z/fake') })
146-
expect(data.thisProcessToolBin).toMatchInlineSnapshot(`
147-
Object {
148-
"dir": "/__dynamic__/x/y/z",
149-
"name": "fake",
150-
"path": "/__dynamic__/x/y/z/fake",
151-
"realDir": "/__dynamic__/a/b/c",
152-
"realPath": "/__dynamic__/a/b/c/real.js",
153-
}
154-
`)
183+
184+
if (isWindows) {
185+
expect(data.process.toolPath).toMatchInlineSnapshot(`"/__dynamic__/x/y/z/fake"`)
186+
} else {
187+
expect(data.process.toolPath).toMatchInlineSnapshot(`"/__dynamic__/a/b/c/real.js"`)
188+
}
155189
})
190+
156191
it('supports node running script without extension', () => {
157192
const data = ctx.detectExecLayout({ scriptPath: ctx.fs.path('a/b/c/real') })
158-
expect(data.thisProcessToolBin).toMatchInlineSnapshot(`
159-
Object {
160-
"dir": "/__dynamic__/a/b/c",
161-
"name": "real.js",
162-
"path": "/__dynamic__/a/b/c/real.js",
163-
"realDir": "/__dynamic__/a/b/c",
164-
"realPath": "/__dynamic__/a/b/c/real.js",
165-
}
166-
`)
193+
expect(data.process.toolPath).toMatchInlineSnapshot(`"/__dynamic__/a/b/c/real.js"`)
167194
})
168195
})
169196

@@ -177,13 +204,20 @@ describe('project analysis', () => {
177204
nodeProject()
178205
const data = ctx.detectExecLayout()
179206
expect(data.nodeProject).toEqual(true)
207+
// expect(data.project).toMatchInlineSnapshot(`
208+
// Object {
209+
// "binDir": "/__dynamic__/node_modules/.bin",
210+
// "dir": "/__dynamic__",
211+
// "nodeModulesDir": "/__dynamic__/node_modules",
212+
// "toolBinPath": "/__dynamic__/node_modules/.bin/a",
213+
// "toolBinRealPath": null,
214+
// }
215+
// `)
180216
expect(data.project).toMatchInlineSnapshot(`
181217
Object {
182-
"binDir": "/__dynamic__/node_modules/.bin",
183218
"dir": "/__dynamic__",
184219
"nodeModulesDir": "/__dynamic__/node_modules",
185-
"toolBinPath": "/__dynamic__/node_modules/.bin/a",
186-
"toolBinRealPath": null,
220+
"toolPath": null,
187221
}
188222
`)
189223
})
@@ -192,13 +226,11 @@ describe('project analysis', () => {
192226
const data = ctx.detectExecLayout()
193227
expect(data.toolProject).toEqual(true)
194228
expect(data.project).toMatchInlineSnapshot(`
195-
Object {
196-
"binDir": "/__dynamic__/node_modules/.bin",
197-
"dir": "/__dynamic__",
198-
"nodeModulesDir": "/__dynamic__/node_modules",
199-
"toolBinPath": "/__dynamic__/node_modules/.bin/a",
200-
"toolBinRealPath": "/__dynamic__/node_modules/a/index.js",
201-
}
202-
`)
229+
Object {
230+
"dir": "/__dynamic__",
231+
"nodeModulesDir": "/__dynamic__/node_modules",
232+
"toolPath": "/__dynamic__/node_modules/a/index.js",
233+
}
234+
`)
203235
})
204236
})

0 commit comments

Comments
 (0)