-
Notifications
You must be signed in to change notification settings - Fork 143
/
gulpfile.ts
298 lines (277 loc) · 12.4 KB
/
gulpfile.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
import { generateNamespace } from '@gql2ts/from-schema'
import { DEFAULT_OPTIONS, DEFAULT_TYPE_MAP } from '@gql2ts/language-typescript'
import { ChildProcess, spawn } from 'child_process'
import execa from 'execa'
import log from 'fancy-log'
import globby from 'globby'
import { buildSchema, graphql, introspectionQuery, IntrospectionQuery } from 'graphql'
import gulp from 'gulp'
import httpProxyMiddleware from 'http-proxy-middleware'
import { compile as compileJSONSchema } from 'json-schema-to-typescript'
// @ts-ignore
import convert from 'koa-connect'
import latestVersion from 'latest-version'
import mkdirp from 'mkdirp-promise'
import { readFile, stat, writeFile } from 'mz/fs'
import * as path from 'path'
import PluginError from 'plugin-error'
import { format, resolveConfig } from 'prettier'
import * as semver from 'semver'
// ironically, has no published typings (but will soon)
// @ts-ignore
import tsUnusedExports from 'ts-unused-exports'
import createWebpackCompiler, { Stats } from 'webpack'
import serve from 'webpack-serve'
import webpackConfig from './webpack.config'
const WEBPACK_STATS_OPTIONS = {
all: false,
timings: true,
errors: true,
warnings: true,
warningsFilter: warning =>
// This is intended, so ignore warning
/node_modules\/monaco-editor\/.*\/editorSimpleWorker.js.*\n.*dependency is an expression/.test(warning),
colors: true,
} as Stats.ToStringOptions
const logWebpackStats = (stats: Stats) => log(stats.toString(WEBPACK_STATS_OPTIONS))
export async function webpack(): Promise<void> {
const compiler = createWebpackCompiler(webpackConfig)
const stats = await new Promise<Stats>((resolve, reject) => {
compiler.run((err, stats) => (err ? reject(err) : resolve(stats)))
})
logWebpackStats(stats)
if (stats.hasErrors()) {
throw Object.assign(new Error('Failed to compile'), { showStack: false })
}
}
export async function webpackServe(): Promise<void> {
await serve(
{},
{
config: {
...webpackConfig,
serve: {
clipboard: false,
content: './ui/assets',
port: 3080,
hotClient: false,
devMiddleware: {
publicPath: '/.assets/',
stats: WEBPACK_STATS_OPTIONS,
},
add: (app, middleware) => {
// Since we're manipulating the order of middleware added, we need to handle adding these
// two internal middleware functions.
//
// The `as any` cast is necessary because the `middleware.webpack` typings are incorrect
// (the related issue https://github.com/webpack-contrib/webpack-serve/issues/238 perhaps
// explains why: the webpack-serve docs incorrectly state that resolving
// `middleware.webpack()` is not necessary).
;(middleware.webpack() as any).then(() => {
middleware.content()
// Proxy *must* be the last middleware added.
app.use(
convert(
// Proxy all requests (that are not for webpack-built assets) to the Sourcegraph
// frontend server, and we make the Sourcegraph appURL equal to the URL of
// webpack-serve. This is how webpack-serve needs to work (because it does a bit
// more magic in injecting scripts that use WebSockets into proxied requests).
httpProxyMiddleware({
target: 'http://localhost:3081',
ws: true,
// Avoid crashing on "read ECONNRESET".
onError: err => console.error(err),
onProxyReqWs: (_proxyReq, _req, socket) =>
socket.on('error', err => console.error('WebSocket proxy error:', err)),
})
)
)
})
},
compiler: createWebpackCompiler(webpackConfig),
},
},
}
)
}
const GRAPHQL_SCHEMA_PATH = __dirname + '/cmd/frontend/graphqlbackend/schema.graphql'
export async function watchGraphQLTypes(): Promise<void> {
await graphQLTypes()
await new Promise<never>((resolve, reject) => {
gulp.watch(GRAPHQL_SCHEMA_PATH, graphQLTypes).on('error', reject)
})
}
/** Generates the TypeScript types for the GraphQL schema */
export async function graphQLTypes(): Promise<void> {
const schemaStr = await readFile(GRAPHQL_SCHEMA_PATH, 'utf8')
const schema = buildSchema(schemaStr)
const result = (await graphql(schema, introspectionQuery)) as { data: IntrospectionQuery }
const formatOptions = (await resolveConfig(__dirname, { config: __dirname + '/prettier.config.js' }))!
const typings =
'export type ID = string\n\n' +
generateNamespace(
'',
result,
{
typeMap: {
...DEFAULT_TYPE_MAP,
ID: 'ID',
},
},
{
generateNamespace: (name: string, interfaces: string) => interfaces,
interfaceBuilder: (name: string, body: string) =>
'export ' + DEFAULT_OPTIONS.interfaceBuilder(name, body),
enumTypeBuilder: (name: string, values: string) =>
'export ' + DEFAULT_OPTIONS.enumTypeBuilder(name, values),
typeBuilder: (name: string, body: string) => 'export ' + DEFAULT_OPTIONS.typeBuilder(name, body),
wrapList: (type: string) => `${type}[]`,
postProcessor: (code: string) => format(code, { ...formatOptions, parser: 'typescript' }),
}
)
await writeFile(__dirname + '/src/backend/graphqlschema.ts', typings)
}
/**
* Generates the TypeScript types for the JSON schemas and copies the schemas to src/ so they can be imported
*/
export async function schema(): Promise<void> {
await Promise.all([mkdirp(__dirname + '/src/schema'), mkdirp(__dirname + '/dist/schema')])
await Promise.all(
['json-schema', 'settings', 'site', 'extension'].map(async file => {
let schema = await readFile(__dirname + `/schema/${file}.schema.json`, 'utf8')
// HACK: Rewrite absolute $refs to be relative. They need to be absolute for Monaco to resolve them
// when the schema is in a oneOf (to be merged with extension schemas).
schema = schema.replace(
/https:\/\/sourcegraph\.com\/v1\/settings\.schema\.json#\/definitions\//g,
'#/definitions/'
)
const types = await compileJSONSchema(JSON.parse(schema), 'settings.schema', {
cwd: __dirname + '/schema',
})
await Promise.all([
writeFile(__dirname + `/src/schema/${file}.schema.d.ts`, types),
// Copy schema to src/ so it can be imported in TypeScript
writeFile(__dirname + `/src/schema/${file}.schema.json`, schema),
// Copy schema to dist/ so it's part of the dist package
// This would not be needed with tsconfig `resolevJsonModule: true`,
// but we cannot enable that because of https://github.com/Microsoft/TypeScript/issues/25755
// and TS3.1 has blocking compiler bugs
writeFile(__dirname + `/dist/schema/${file}.schema.json`, schema),
])
})
)
}
export async function watchSchema(): Promise<void> {
await schema()
await new Promise<never>((resolve, reject) => {
gulp.watch(__dirname + '/schema/*.schema.json', schema).on('error', reject)
})
}
export async function unusedExports(): Promise<void> {
// TODO(sqs): Improve our usage of ts-unused-exports when its API improves (see
// https://github.com/pzavolinsky/ts-unused-exports/pull/17 for one possible improvement).
const analysis: { [file: string]: string[] } = tsUnusedExports(
path.join(__dirname, 'tsconfig.json'),
await globby('src/**/*.{ts?(x),js?(x),json}') // paths are relative to tsconfig.json
)
const filesWithUnusedExports = Object.keys(analysis).sort()
if (filesWithUnusedExports.length > 0) {
// Convert to absolute file paths with extensions to enable clickable file paths in VS Code console
const filesWithExtensions = await Promise.all(
filesWithUnusedExports.map(async file => {
for (const ext of ['ts', 'tsx']) {
try {
const fullPath = path.resolve(__dirname, `${file}.${ext}`)
await stat(fullPath)
return fullPath
} catch (err) {
continue
}
}
return file
})
)
throw new PluginError(
'ts-unused-exports',
[
'Unused exports found (must unexport or remove):',
...filesWithExtensions.map((f, i) => `${f}: ${analysis[filesWithUnusedExports[i]].join(' ')}`),
].join('\n\t')
)
}
}
/**
* Builds and typechecks the TypeScript code, outputting compiled JavaScript, declaration files and sourcemaps to dist/
*/
export function typescript(): ChildProcess {
return spawn(__dirname + '/node_modules/.bin/tsc', ['-p', __dirname + '/tsconfig.dist.json', '--pretty'], {
stdio: 'inherit',
})
}
export function watchTypescript(): ChildProcess {
return spawn(
__dirname + '/node_modules/.bin/tsc',
['-p', __dirname + '/tsconfig.dist.json', '--watch', '--preserveWatchOutput', '--pretty'],
{
stdio: 'inherit',
}
)
}
const SASS_FILES = './src/**/*.scss'
/**
* Copies the .scss files from src/ to dist/.
* These are not precompiled so that they can be imported individually and variables be set.
*/
export function sass(): NodeJS.ReadWriteStream {
return gulp.src(SASS_FILES).pipe(gulp.dest('./dist'))
}
export const watchSass = gulp.series(sass, async function watchSass(): Promise<void> {
await new Promise<never>((_, reject) => {
gulp.watch(SASS_FILES, sass).on('error', reject)
})
})
/**
* Builds only the dist/ folder.
*/
export const dist = gulp.parallel(sass, gulp.series(gulp.parallel(schema, graphQLTypes), typescript))
export const watchDist = gulp.parallel(watchSass, watchSchema, watchGraphQLTypes, watchTypescript)
/**
* Builds everything.
*/
export const build = gulp.parallel(
sass,
gulp.series(gulp.parallel(schema, graphQLTypes), gulp.parallel(webpack, typescript))
)
/**
* Watches everything and rebuilds on file changes.
*/
export const watch = gulp.parallel(watchSass, watchSchema, watchGraphQLTypes, watchTypescript, webpackServe)
/**
* Publishes a new version of @sourcegraph/webapp to npm.
* Gets the last release from the npm registry, increases the patch version, writes it to package.json and publishes the package.
* It is not a goal to parse commit messages or follow semantic versioning - every commit gets released as a 0.0.x release.
* No git tags or GitHub releases are created.
*/
export async function release(): Promise<void> {
const packageJson = require('./package.json')
try {
const currentVersion = await latestVersion(packageJson.name)
log(`Current version is ${currentVersion}`)
packageJson.version = semver.inc(currentVersion, 'patch')
} catch (err) {
if (/doesn't exist/.test(err.message)) {
log('Package is not released yet')
packageJson.version = '0.0.0'
} else {
throw err
}
}
log(`New version is ${packageJson.version}`)
if (!process.env.CI) {
log('Not running in CI, aborting')
return
}
await writeFile(__dirname + '/package.json', JSON.stringify(packageJson, null, 2))
await execa('npm', ['publish'], { stdio: 'inherit' })
await execa('buildkite-agent', ['meta-data', 'set', 'oss-webapp-version', packageJson.version])
}