gulpfile.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. const fs = require('fs')
  2. const path = require('path')
  3. const globby = require('globby')
  4. const handlebars = require('handlebars')
  5. const { src, dest, parallel, series } = require('gulp')
  6. const { readFile, writeFile, watch } = require('./scripts/lib/util')
  7. const replace = require('gulp-replace')
  8. const exec = require('./scripts/lib/shell').sync.withOptions({ // always SYNC!
  9. live: true,
  10. exitOnError: true
  11. // TODO: flag for echoing command?
  12. })
  13. const concurrently = require('concurrently')
  14. const { minifyBundleJs, minifyBundleCss } = require('./scripts/lib/minify')
  15. const modify = require('gulp-modify-file')
  16. const { allStructs, publicPackageStructs } = require('./scripts/lib/package-index')
  17. const semver = require('semver')
  18. const { eslintAll } = require('./scripts/eslint-dir')
  19. exports.archive = require('./scripts/lib/archive')
  20. /*
  21. copy over the vdom files that were externalized by rollup.
  22. we externalize these for two reasons:
  23. - when a consumer build system sees `import './vdom'` it's more likely to treat it with side effects.
  24. - rollup-plugin-dts was choking on the namespace declarations in the tsc-generated vdom.d.ts files.
  25. */
  26. const VDOM_FILE_MAP = {
  27. 'packages/core-preact/tsc/vdom.d.ts': 'packages/core',
  28. 'packages/common/tsc/vdom.d.ts': 'packages/common'
  29. }
  30. const copyVDomMisc = exports.copyVDomMisc = parallelMap(
  31. VDOM_FILE_MAP,
  32. (srcGlob, destDir) => src(srcGlob)
  33. .pipe(replace(/\/\/.*/g, '')) // remove sourcemap comments and ///<reference> don in rollup too
  34. .pipe(dest(destDir))
  35. )
  36. function parallelMap(map, execute) {
  37. return parallel.apply(null, Object.keys(map).map((key) => {
  38. let task = () => execute(key, map[key])
  39. task.displayName = key
  40. return task
  41. }))
  42. }
  43. const localesDts = exports.localesDts = parallel(localesAllDts, localesEachDts)
  44. function localesAllDts() { // needs tsc
  45. return src('packages/core/tsc/locales-all.d.ts')
  46. .pipe(removeSimpleComments())
  47. .pipe(dest('packages/core'))
  48. }
  49. function localesEachDts() { // needs tsc
  50. return src('packages/core/tsc/locales/*.d.ts')
  51. .pipe(removeSimpleComments())
  52. .pipe(dest('packages/core/locales')) // TODO: remove sourcemap comment
  53. }
  54. function removeSimpleComments() { // like a gulp plugin
  55. return modify(function(code) { // TODO: use gulp-replace instead
  56. return code.replace(/\/\/.*/g, '') // TODO: make a general util for this
  57. })
  58. }
  59. exports.build = series(
  60. series(removeTscDevLinks, writeTscDevLinks), // for tsc
  61. localesAllSrc, // before tsc
  62. execTask('tsc -b --verbose'),
  63. localesDts,
  64. removeTscDevLinks,
  65. execTask('webpack --config webpack.bundles.js --env NO_SOURCE_MAPS'), // always compile from SRC
  66. execTask('rollup -c rollup.locales.js'),
  67. execTask('rollup -c rollup.bundles.js'),
  68. execTask('rollup -c rollup.packages.js'),
  69. copyVDomMisc,
  70. minifyBundleJs,
  71. minifyBundleCss
  72. )
  73. exports.watch = series(
  74. series(removeTscDevLinks, writeTscDevLinks), // for tsc
  75. localesAllSrc, // before tsc
  76. execTask('tsc -b --verbose'), // initial run
  77. localesDts, // won't watch :(
  78. parallel(
  79. localesAllSrcWatch,
  80. execParallel({
  81. tsc: 'tsc -b --watch --preserveWatchOutput --pretty', // wont do pretty bc of piping
  82. bundles: 'webpack --config webpack.bundles.js --watch',
  83. locales: 'rollup -c rollup.locales.js --watch' // operates on src files. fyi: tests will need this instantly, if compiled together
  84. })
  85. )
  86. )
  87. exports.testsIndex = testsIndex
  88. exports.test = series(
  89. testsIndex,
  90. parallel(
  91. testsIndexWatch,
  92. execParallel({
  93. webpack: 'webpack --config webpack.tests.js --watch --env PACKAGES_FROM_SOURCE',
  94. karma: 'karma start karma.config.js'
  95. })
  96. )
  97. )
  98. exports.testCi = series(
  99. testsIndex,
  100. execTask('webpack --config webpack.tests.js'),
  101. execTask('karma start karma.config.js ci')
  102. )
  103. const LOCALES_SRC_DIR = 'packages/core/src/locales'
  104. const LOCALES_ALL_TPL = 'packages/core/src/locales-all.ts.tpl'
  105. const LOCALES_ALL_DEST = 'packages/core/src/locales-all.ts'
  106. exports.localesAllSrc = localesAllSrc
  107. exports.localesAllSrcWatch = localesAllSrcWatch
  108. async function localesAllSrc() {
  109. let localeFileNames = await globby('*.ts', { cwd: LOCALES_SRC_DIR })
  110. let localeCodes = localeFileNames.map((fileName) => path.basename(fileName, '.ts'))
  111. let localeImportPaths = localeCodes.map((localeCode) => `./locales/${localeCode}`)
  112. let templateText = await readFile(LOCALES_ALL_TPL)
  113. let template = handlebars.compile(templateText)
  114. let jsText = template({
  115. localeImportPaths
  116. })
  117. await writeFile(LOCALES_ALL_DEST, jsText)
  118. }
  119. function localesAllSrcWatch() {
  120. return watch([ LOCALES_SRC_DIR, LOCALES_ALL_TPL ], localesAllSrc)
  121. }
  122. exports.writeTscDevLinks = series(removeTscDevLinks, writeTscDevLinks)
  123. exports.removeTscDevLinks = removeTscDevLinks
  124. async function writeTscDevLinks() { // bad name. does js AND .d.ts. is it necessary to do the js?
  125. for (let struct of publicPackageStructs) {
  126. let jsOut = path.join(struct.dir, struct.mainDistJs)
  127. let dtsOut = path.join(struct.dir, struct.mainDistDts)
  128. exec([
  129. 'mkdir',
  130. '-p',
  131. path.dirname(jsOut),
  132. path.dirname(dtsOut),
  133. ])
  134. exec([ 'ln', '-s', struct.mainTscJs, jsOut ])
  135. exec([ 'ln', '-s', struct.mainTscDts, dtsOut ])
  136. }
  137. }
  138. async function removeTscDevLinks() {
  139. for (let struct of publicPackageStructs) {
  140. let jsLink = path.join(struct.dir, struct.mainDistJs)
  141. let dtsLink = path.join(struct.dir, struct.mainDistDts)
  142. exec([ 'rm', '-f', jsLink, dtsLink ])
  143. }
  144. }
  145. const exec2 = require('./scripts/lib/shell').sync
  146. exports.testsIndex = testsIndex
  147. exports.testsIndexWatch = testsIndexWatch
  148. async function testsIndex() {
  149. let res = exec2(
  150. "find packages*/__tests__/src -mindepth 2 -type f \\( -name '*.ts' -or -name '*.tsx' \\) -print0 | " +
  151. 'xargs -0 grep -E "(fdescribe|fit)\\("'
  152. )
  153. if (!res.success && res.stderr) { // means there was a real error
  154. throw new Error(res.stderr)
  155. }
  156. let files
  157. if (!res.success) { // means there were no files that matched
  158. let { stdout } = exec2("find packages*/__tests__/src -mindepth 2 -type f \\( -name '*.ts' -or -name '*.tsx' \\)")
  159. files = stdout.trim()
  160. files = !files ? [] : files.split('\n')
  161. files = uniqStrs(files)
  162. files.sort() // work around OS-dependent sorting ... TODO: better sorting that knows about filename slashes
  163. console.log(`[test-index] All ${files.length} test files.`) // TODO: use gulp log util?
  164. } else {
  165. files = res.stdout.trim()
  166. files = !files ? [] : files.split('\n')
  167. files = files.map((line) => line.trim().split(':')[0]) // TODO: do a max split of 1
  168. files = uniqStrs(files)
  169. files.sort() // work around OS-dependent sorting
  170. console.log(
  171. '[test-index] Only test files that have fdescribe/fit:\n' + // TODO: use gulp log util?
  172. files.map((file) => ` - ${file}`).join('\n')
  173. )
  174. }
  175. let mainFiles = globby.sync('packages*/__tests__/src/main.{js,ts}')
  176. files = mainFiles.concat(files)
  177. // need 'contrib:ci' to have already been run
  178. if (process.env.FULLCALENDAR_FORCE_REACT) {
  179. files = [ 'packages-contrib/react/dist/vdom-test-react18.js' ].concat(files)
  180. }
  181. let code =
  182. files.map(
  183. (file) => `import ${JSON.stringify('../../' + file)}`
  184. ).join('\n') +
  185. '\n'
  186. await writeFile('tmp/tests/index.js', code)
  187. }
  188. function testsIndexWatch() {
  189. return watch(
  190. [ 'packages/__tests__/src', 'packages-premium/__tests__/src' ], // wtf won't globs work for this?
  191. exports.testsIndex
  192. )
  193. }
  194. /*
  195. TODO: make unnecessary. have grep do this instead with the -l option:
  196. https://stackoverflow.com/questions/6637882/how-can-i-use-grep-to-show-just-filenames-on-linux
  197. */
  198. function uniqStrs(a) {
  199. let hash = {}
  200. for (let item of a) {
  201. hash[item] = true
  202. }
  203. return Object.keys(hash)
  204. }
  205. function execTask(args) {
  206. const exec = require('./scripts/lib/shell').promise.withOptions({ live: true })
  207. let name = Array.isArray(args) ? args[0] : args.match(/\w+/)[0]
  208. let taskFunc = () => exec(args)
  209. taskFunc.displayName = name
  210. return taskFunc
  211. }
  212. function execParallel(map) {
  213. let taskArray = []
  214. for (let taskName in map) {
  215. taskArray.push({ name: taskName, command: map[taskName] })
  216. }
  217. let func = () => concurrently(taskArray, { killOthers: ['failure'] })
  218. func.displayName = 'concurrently'
  219. return func
  220. }
  221. const exec3 = require('./scripts/lib/shell').sync.withOptions({
  222. live: true,
  223. exitOnError: false
  224. })
  225. exports.lintBuiltCss = function() {
  226. let anyFailures = false
  227. for (let struct of publicPackageStructs) {
  228. let builtCssFile = path.join(struct.dir, 'main.css')
  229. if (fs.existsSync(builtCssFile)) {
  230. let cmd = [
  231. 'stylelint', '--config', 'stylelint.config.js',
  232. builtCssFile
  233. ]
  234. console.log('Running stylelint on', struct.name, '...')
  235. console.log(cmd.join(' '))
  236. console.log()
  237. let { success } = exec3(cmd)
  238. if (!success) {
  239. anyFailures = true
  240. }
  241. }
  242. }
  243. if (anyFailures) {
  244. return Promise.reject(new Error('At least one linting job failed'))
  245. }
  246. return Promise.resolve()
  247. }
  248. exports.lintBuiltDts = function() {
  249. let anyFailures = false
  250. for (let struct of publicPackageStructs) {
  251. let dtsFile = path.join(struct.dir, 'main.d.ts')
  252. console.log(`Checking ${dtsFile}`)
  253. // look for bad module declarations (when relative, assumed NOT to be ambient, so BAD)
  254. // look for references to react/preact (should always use vdom instead)
  255. let { stdout } = require('./scripts/lib/shell').sync([
  256. 'grep', '-iEe', '(declare module [\'"]\\.|p?react)', dtsFile
  257. ])
  258. stdout = stdout.trim()
  259. if (stdout) { // don't worry about failure. grep gives failure if no results
  260. console.log(' BAD: ' + stdout)
  261. anyFailures = true
  262. }
  263. if (struct.isPremium && struct.name !== '@fullcalendar/premium-common') {
  264. let { stdout: stdout2 } = require('./scripts/lib/shell').sync([
  265. 'grep', '-e', '@fullcalendar/premium-common', dtsFile
  266. ])
  267. stdout2 = stdout2.trim()
  268. if (!stdout2) {
  269. console.warn(`The premium package ${struct.name} does not have @fullcalendar/premium-common reference in .d.ts`)
  270. anyFailures = true
  271. }
  272. }
  273. console.log()
  274. }
  275. if (anyFailures) {
  276. return Promise.reject(new Error('At least one dts linting job failed'))
  277. }
  278. return Promise.resolve()
  279. }
  280. const REQUIRED_TSLIB_SEMVER = '2'
  281. exports.lintPackageMeta = function() {
  282. let success = true
  283. for (let struct of publicPackageStructs) {
  284. let { meta } = struct
  285. if (!meta.main) {
  286. console.warn(`${struct.name} should have a 'main' entry`)
  287. success = false
  288. }
  289. if (!meta.module) {
  290. console.warn(`${struct.name} should have a 'module' entry`)
  291. success = false
  292. }
  293. if (meta.dependencies && meta.dependencies['@fullcalendar/core']) {
  294. console.warn(`${struct.name} should have @fullcalendar/common as a dep, NOT @fullcalendar/core`)
  295. success = false
  296. }
  297. let tslibSemver = (meta.dependencies || {}).tslib || ''
  298. if (!tslibSemver || !semver.intersects(tslibSemver, REQUIRED_TSLIB_SEMVER)) {
  299. console.warn(`${struct.name} has a tslib version ('${tslibSemver}') that does not satisfy '${REQUIRED_TSLIB_SEMVER}'`)
  300. success = false
  301. }
  302. if (!fs.existsSync(path.join(struct.dir, '.npmignore'))) {
  303. console.warn(`${struct.name} needs a .npmignore file`)
  304. success = false
  305. }
  306. }
  307. if (success) {
  308. return Promise.resolve()
  309. } else {
  310. return Promise.reject(new Error('At least one package.json has an error'))
  311. }
  312. }
  313. exports.lint = series(exports.lintPackageMeta, () => {
  314. return eslintAll() ? Promise.resolve() : Promise.reject(new Error('One or more lint tasks failed'))
  315. })
  316. exports.lintBuilt = series(exports.lintBuiltCss, exports.lintBuiltDts)