diff --git a/.gitignore b/.gitignore index 411ec4df..e0bd2dd9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ tap-testdir*/ !/SECURITY.md !/tap-snapshots/ !/test/ +!/tsconfig.json !/workspace/ /workspace/* !/workspace/test-workspace/ diff --git a/lib/config.js b/lib/config.js index e7e41c8b..4bcc36d1 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,18 +1,21 @@ -const { relative, dirname, join, extname, posix, win32 } = require('path') -const { defaults, pick, omit, uniq, isPlainObject } = require('lodash') +const { relative, dirname, join, extname } = require('path') +const { defaults, defaultsDeep, pick, omit, uniq, isPlainObject } = require('lodash') const ciVersions = require('./util/ci-versions.js') const parseDependabot = require('./util/dependabot.js') const git = require('./util/git.js') const gitignore = require('./util/gitignore.js') const { mergeWithCustomizers, customizers } = require('./util/merge.js') const { FILE_KEYS, parseConfig: parseFiles, getAddedFiles, mergeFiles } = require('./util/files.js') +const template = require('./util/template.js') +const getCmdPath = require('./util/get-cmd-path.js') +const importOrRequire = require('./util/import-or-require.js') +const { makePosix, deglob, posixDir, posixGlob } = require('./util/path.js') +const { name: NAME, version: LATEST_VERSION } = require('../package.json') const CONFIG_KEY = 'templateOSS' -const getPkgConfig = (pkg) => pkg[CONFIG_KEY] || {} - -const { name: NAME, version: LATEST_VERSION } = require('../package.json') const MERGE_KEYS = [...FILE_KEYS, 'defaultContent', 'content'] const DEFAULT_CONTENT = require.resolve(NAME) +const getPkgConfig = (pkg) => pkg[CONFIG_KEY] || {} const merge = mergeWithCustomizers( customizers.mergeArrays('branches', 'distPaths', 'allowPaths', 'ignorePaths'), @@ -23,43 +26,6 @@ const merge = mergeWithCustomizers( } ) -const makePosix = (v) => v.split(win32.sep).join(posix.sep) -const deglob = (v) => makePosix(v).replace(/[/*]+$/, '') -const posixDir = (v) => `${v === '.' ? '' : deglob(v).replace(/\/$/, '')}${posix.sep}` -const posixGlob = (str) => `${posixDir(str)}**` - -const getCmdPath = (key, { pkgConfig, rootConfig, isRoot, pkg, rootPkg }) => { - const result = (local, isRelative) => { - let root = local - const isLocal = local.startsWith('.') || local.startsWith('/') - - if (isLocal) { - if (isRelative) { - // Make a path relative from a workspace to the root if we are in a workspace - local = makePosix(join(relative(pkg.path, rootPkg.path), local)) - } - local = `node ${local}` - root = `node ${root}` - } - - return { - isLocal, - local, - root, - } - } - - if (pkgConfig[key]) { - return result(pkgConfig[key]) - } - - if (rootConfig[key]) { - return result(rootConfig[key], !isRoot) - } - - return result(key) -} - const mergeConfigs = (...configs) => { const mergedConfig = merge(...configs.map(c => pick(c, MERGE_KEYS))) return defaults(mergedConfig, { @@ -72,37 +38,33 @@ const mergeConfigs = (...configs) => { }) } -const readContentPath = (path) => { +const readContentPath = async (path) => { if (!path) { return {} } - let content = {} const index = extname(path) === '.js' ? path : join(path, 'index.js') const dir = dirname(index) - - try { - content = require(index) - } catch { - // its ok if this fails since the content dir - // might only be to provide other files. the - // index.js is optional - } + const content = await importOrRequire(index) return { content, dir } } -const getConfig = (path, rawConfig) => { - const config = omit(readContentPath(path).content, FILE_KEYS) +const getConfig = async (path, rawConfig) => { + const { content } = await readContentPath(path) + const config = omit(content, FILE_KEYS) return merge(config, rawConfig ? omit(rawConfig, FILE_KEYS) : {}) } -const getFiles = (path, rawConfig) => { - const { content, dir } = readContentPath(path) +const getFiles = async (path, rawConfig, templateSettings) => { + const { content, dir } = await readContentPath(path) if (!dir) { return [] } - return [parseFiles(pick(content, FILE_KEYS), dir, pick(rawConfig, FILE_KEYS)), dir] + return [ + parseFiles(pick(content, FILE_KEYS), dir, pick(rawConfig, FILE_KEYS), templateSettings), + dir, + ] } const getFullConfig = async ({ @@ -127,39 +89,15 @@ const getFullConfig = async ({ // These config items are merged betweent the root and child workspaces and only come from // the package.json because they can be used to read configs from other the content directories const mergedConfig = mergeConfigs(rootPkg.config, pkg.config) - - const defaultConfig = getConfig(DEFAULT_CONTENT) - const [defaultFiles, defaultDir] = getFiles(DEFAULT_CONTENT, mergedConfig) + const defaultConfig = await getConfig(DEFAULT_CONTENT) const useDefault = mergedConfig.defaultContent && defaultConfig - const rootConfig = getConfig(rootPkg.config.content, rootPkg.config) - const [rootFiles, rootDir] = getFiles(rootPkg.config.content, mergedConfig) + const rootConfig = await getConfig(rootPkg.config.content, rootPkg.config) // The content config only gets set from the package we are in, it doesn't inherit // anything from the root const rootPkgConfig = merge(useDefault, rootConfig) - const pkgConfig = merge(useDefault, getConfig(pkg.config.content, pkg.config)) - const [pkgFiles, pkgDir] = getFiles(mergedConfig.content, mergedConfig) - - // Files get merged in from the default content (that template-oss provides) as well - // as any content paths provided from the root or the workspace - const fileDirs = uniq([useDefault && defaultDir, rootDir, pkgDir].filter(Boolean)) - const files = mergeFiles(useDefault && defaultFiles, rootFiles, pkgFiles) - const repoFiles = isRoot ? files.rootRepo : files.workspaceRepo - const moduleFiles = isRoot ? files.rootModule : files.workspaceModule - - const allowRootDirs = [ - // Allways allow module files in root or workspaces - ...getAddedFiles(moduleFiles), - ...isRoot ? [ - // in the root allow all repo files - ...getAddedFiles(repoFiles), - // and allow all workspace repo level files in the root - ...pkgs - .filter(p => p.path !== rootPkg.path && p.config.workspaceRepo !== false) - .flatMap(() => getAddedFiles(files.workspaceRepo)), - ] : [], - ] + const pkgConfig = merge(useDefault, await getConfig(pkg.config.content, pkg.config)) const npmPath = getCmdPath('npm', { pkgConfig, rootConfig, isRoot, pkg, rootPkg }) const npxPath = getCmdPath('npx', { pkgConfig, rootConfig, isRoot, pkg, rootPkg }) @@ -185,6 +123,8 @@ const getFullConfig = async ({ ? pkgConfig.releaseBranch.replace(/\*/g, pkgConfig.backport) : defaultBranch + const esm = pkg.pkgJson?.type === 'module' || !!pkgConfig.typescript || !!pkgConfig.esm + // all derived keys const derived = { isRoot, @@ -209,18 +149,11 @@ const getFullConfig = async ({ releaseBranch, publishTag, dependabot: parseDependabot(pkgConfig, defaultConfig, gitBranches.branches), - // repo + // paths repoDir: rootPkg.path, - repoFiles, - applyRepo: !!repoFiles, - // module moduleDir: pkg.path, - moduleFiles, - applyModule: !!moduleFiles, - // package pkgName: pkg.pkgJson.name, pkgNameFs: pkg.pkgJson.name.replace(/\//g, '-').replace(/@/g, ''), - // paths pkgPath, pkgDir: posixDir(pkgPath), pkgGlob: posixGlob(pkgPath), @@ -228,6 +161,10 @@ const getFullConfig = async ({ allFlags: isMono ? '-ws -iwr --if-present' : '', workspacePaths, workspaceGlobs: workspacePaths.map(posixGlob), + // type + esm, + cjsExt: esm ? 'cjs' : 'js', + deleteJsExt: esm ? 'js' : 'cjs', // booleans to control application of updates isForce, isDogFood, @@ -243,20 +180,6 @@ const getFullConfig = async ({ lockfile: rootPkgConfig.lockfile, // ci versions / engines ciVersions: ciVersions.get(pkg.pkgJson.engines?.node, pkgConfig), - // gitignore - ignorePaths: [ - ...gitignore.sort([ - ...gitignore.allowRootDir(allowRootDirs), - ...isRoot && pkgConfig.lockfile ? ['!/package-lock.json'] : [], - ...(pkgConfig.allowPaths || []).map((p) => `!${p}`), - ...(pkgConfig.ignorePaths || []), - ]), - // these cant be sorted since they rely on order - // to allow a previously ignored directoy - ...isRoot - ? gitignore.allowDir(wsPkgs.map((p) => makePosix(relative(rootPkg.path, p.path)))) - : [], - ], // needs update if we are dogfooding this repo, with force argv, or its // behind the current version needsUpdate: isForce || isDogFood || !isLatest, @@ -264,15 +187,22 @@ const getFullConfig = async ({ __NAME__: NAME, __CONFIG_KEY__: CONFIG_KEY, __VERSION__: LATEST_VERSION, - __PARTIAL_DIRS__: fileDirs, } - if (!pkgConfig.eslint) { - derived.ignorePaths = derived.ignorePaths.filter(p => !p.includes('eslint')) - if (Array.isArray(pkgConfig.requiredPackages?.devDependencies)) { - pkgConfig.requiredPackages.devDependencies = - pkgConfig.requiredPackages.devDependencies.filter(p => !p.includes('eslint')) - } + if (!pkgConfig.eslint && Array.isArray(pkgConfig.requiredPackages?.devDependencies)) { + pkgConfig.requiredPackages.devDependencies = + pkgConfig.requiredPackages.devDependencies.filter(p => !p.includes('eslint')) + } + + if (pkgConfig.typescript) { + defaultsDeep(pkgConfig, { allowPaths: [], requiredPackages: { devDependencies: [] } }) + pkgConfig.distPaths = null + pkgConfig.allowPaths.push('/src/') + pkgConfig.requiredPackages.devDependencies.push( + 'typescript', + 'tshy', + '@typescript-eslint/parser' + ) } const gitUrl = await git.getUrl(rootPkg.path) @@ -284,10 +214,55 @@ const getFullConfig = async ({ } } - return { - ...pkgConfig, - ...derived, - } + const fullConfig = { ...pkgConfig, ...derived } + + // files, come at the end since file names can be based on config + const [defaultFiles, defaultDir] = await getFiles(DEFAULT_CONTENT, mergedConfig, fullConfig) + const [rootFiles, rootDir] = await getFiles(rootPkg.config.content, mergedConfig, fullConfig) + const [pkgFiles, pkgDir] = await getFiles(mergedConfig.content, mergedConfig, fullConfig) + + // Files get merged in from the default content (that template-oss provides) as well + // as any content paths provided from the root or the workspace + const fileDirs = uniq([useDefault && defaultDir, rootDir, pkgDir].filter(Boolean)) + const files = mergeFiles(useDefault && defaultFiles, rootFiles, pkgFiles) + const repoFiles = isRoot ? files.rootRepo : files.workspaceRepo + const moduleFiles = isRoot ? files.rootModule : files.workspaceModule + + Object.assign(fullConfig, { + repoFiles, + moduleFiles, + applyRepo: !!repoFiles, + applyModule: !!moduleFiles, + __PARTIAL_DIRS__: fileDirs, + // gitignore, these use the full config so need to come at the very end + ignorePaths: [ + ...gitignore.sort([ + ...gitignore.allowRootDir([ + // Allways allow module files in root or workspaces + ...getAddedFiles(moduleFiles).map(s => template(s, fullConfig)), + ...isRoot ? [ + // in the root allow all repo files + ...getAddedFiles(repoFiles).map(s => template(s, fullConfig)), + // and allow all workspace repo level files in the root + ...pkgs + .filter(p => p.path !== rootPkg.path && p.config.workspaceRepo !== false) + .flatMap(() => getAddedFiles(files.workspaceRepo)), + ] : [], + ]), + ...isRoot && pkgConfig.lockfile ? ['!/package-lock.json'] : [], + ...(pkgConfig.allowPaths || []).map((p) => `!${p}`), + ...(pkgConfig.distPaths || []).map((p) => `!/${p}`), + ...(pkgConfig.ignorePaths || []), + ]), + // these cant be sorted since they rely on order + // to allow a previously ignored directoy + ...isRoot + ? gitignore.allowDir(wsPkgs.map((p) => makePosix(relative(rootPkg.path, p.path)))) + : [], + ].filter(p => !pkgConfig.eslint ? !p.includes('eslint') : true), + }) + + return fullConfig } module.exports = getFullConfig diff --git a/lib/content/eslintrc-js.hbs b/lib/content/eslintrc-js.hbs index 96996354..bb38c366 100644 --- a/lib/content/eslintrc-js.hbs +++ b/lib/content/eslintrc-js.hbs @@ -13,7 +13,18 @@ module.exports = { {{#each workspaceGlobs}} '{{ . }}', {{/each}} + {{#if typescript}} + 'dist/', + {{/if}} ], + {{#if typescript}} + parser: '@typescript-eslint/parser', + settings: { + 'import/resolver': { + typescript: {}, + }, + }, + {{/if}} extends: [ '@npmcli', ...localConfigs, diff --git a/lib/content/index.js b/lib/content/index.js index bccb3711..5dda526f 100644 --- a/lib/content/index.js +++ b/lib/content/index.js @@ -15,16 +15,17 @@ const sharedRootAdd = (name) => ({ '.release-please-manifest.json': { file: 'release-please-manifest-json.hbs', filter: isPublic, - parser: (p) => class extends p.JsonMerge { - comment = null - }, + parser: (p) => p.JsonMergeNoComment, }, 'release-please-config.json': { file: 'release-please-config-json.hbs', filter: isPublic, - parser: (p) => class extends p.JsonMerge { - comment = null - }, + parser: (p) => p.JsonMergeNoComment, + }, + 'tsconfig.json': { + file: 'tsconfig-json.hbs', + filter: (p) => p.config.typescript, + parser: (p) => p.JsonMergeNoComment, }, // this lint commits which is only necessary for releases '.github/workflows/pull-request.yml': { @@ -61,7 +62,7 @@ const sharedRootRm = () => ({ // Changes applied to the root of the repo const rootRepo = { add: { - '.commitlintrc.js': 'commitlintrc-js.hbs', + '.commitlintrc.{{ cjsExt }}': 'commitlintrc-js.hbs', '.github/ISSUE_TEMPLATE/bug.yml': 'bug-yml.hbs', '.github/ISSUE_TEMPLATE/config.yml': 'config-yml.hbs', '.github/CODEOWNERS': 'CODEOWNERS.hbs', @@ -70,6 +71,7 @@ const rootRepo = { ...sharedRootAdd(), }, rm: { + '.commitlintrc.{{ deleteJsExt }}': true, '.github/workflows/release-test.yml': true, '.github/workflows/release-please.yml': true, ...sharedRootRm(), @@ -83,7 +85,7 @@ const rootRepo = { // dir. so we might want to combine these const rootModule = { add: { - '.eslintrc.js': { + '.eslintrc.{{ cjsExt }}': { file: 'eslintrc-js.hbs', filter: (p) => p.config.eslint, }, @@ -95,7 +97,7 @@ const rootModule = { 'package.json': 'package-json.hbs', }, rm: [ - '.eslintrc.!(js|local.*)', + '.eslintrc.!({{ cjsExt }}|local.*)', ], } @@ -114,7 +116,7 @@ const workspaceRepo = { // Changes for each workspace but applied to the relative workspace dir const workspaceModule = { add: { - '.eslintrc.js': { + '.eslintrc.{{ cjsExt }}': { file: 'eslintrc-js.hbs', filter: (p) => p.config.eslint, }, @@ -123,7 +125,7 @@ const workspaceModule = { }, rm: [ '.npmrc', - '.eslintrc.!(js|local.*)', + '.eslintrc.!({{ cjsExt }}|local.*)', 'SECURITY.md', ], } @@ -144,8 +146,6 @@ module.exports = { 'lib/', ], allowPaths: [ - '/bin/', - '/lib/', '/.eslintrc.local.*', '**/.gitignore', '/docs/', @@ -166,6 +166,8 @@ module.exports = { codeowner: '@npm/cli-team', eslint: true, publish: false, + typescript: false, + esm: false, updateNpm: true, dependabot: 'increase-if-necessary', unwantedPackages: [ diff --git a/lib/content/package-json.hbs b/lib/content/package-json.hbs index 28630c51..5282f19f 100644 --- a/lib/content/package-json.hbs +++ b/lib/content/package-json.hbs @@ -1,6 +1,7 @@ { "author": "GitHub Inc.", - "files": {{{ json distPaths }}}, + "files": {{#if typescript}}{{{ del }}}{{else}}{{{ json distPaths }}}{{/if}}, + "type": {{#if esm}}"module"{{else}}{{{ del }}}{{/if}}, "scripts": { "lint": "{{#if eslint}}eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"{{else}}echo linting disabled{{/if}}", "postlint": "template-oss-check", @@ -13,6 +14,9 @@ "test-all": "{{ localNpmPath }} run test {{ allFlags }}", "lint-all": "{{ localNpmPath }} run lint {{ allFlags }}", {{/if}} + {{#if typescript}} + "prepare": "tshy", + {{/if}} "template-copy": {{{ del }}}, "lint:fix": {{{ del }}}, "preversion": {{{ del }}}, diff --git a/lib/content/tsconfig-json.hbs b/lib/content/tsconfig-json.hbs new file mode 100644 index 00000000..42776f85 --- /dev/null +++ b/lib/content/tsconfig-json.hbs @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "nodenext", + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "es2022", + "module": "nodenext" + } +} diff --git a/lib/util/files.js b/lib/util/files.js index 3b2b5723..713c01e0 100644 --- a/lib/util/files.js +++ b/lib/util/files.js @@ -1,5 +1,5 @@ const { join } = require('path') -const { defaultsDeep, omit } = require('lodash') +const { defaultsDeep, omit, isPlainObject } = require('lodash') const deepMapValues = require('just-deep-map-values') const { glob } = require('glob') const { mergeWithCustomizers, customizers } = require('./merge.js') @@ -12,6 +12,11 @@ const FILE_KEYS = ['rootRepo', 'rootModule', 'workspaceRepo', 'workspaceModule'] const globify = pattern => pattern.split('\\').join('/') +const deepMapKeys = (obj, fn) => Object.entries(obj).reduce((acc, [key, value]) => { + acc[fn(key)] = isPlainObject(value) ? deepMapKeys(value, fn) : value + return acc +}, {}) + const mergeFiles = mergeWithCustomizers((value, srcValue, key, target, source, stack) => { // This will merge all files except if the src file has overwrite:false. Then // the files will be turned into an array so they can be applied on top of @@ -35,7 +40,7 @@ const fileEntries = (dir, files, options, { allowMultipleSources = true } = {}) continue } - // target paths need to be joinsed with dir and templated + // target paths need to be joined with dir and templated const target = join(dir, template(key, options)) if (Array.isArray(source)) { @@ -66,7 +71,7 @@ const getParsers = (dir, files, options, parseOptions) => { const clean = typeof shouldClean === 'function' ? shouldClean(options) : false if (parser) { - // allow files to extend base parsers or create new ones + // allow files to extend base parsers or create new ones return new (parser(Parser.Parsers))(target, file, options, { clean }) } @@ -105,23 +110,27 @@ const parseEach = async (dir, files, options, parseOptions, fn) => { return res.filter(Boolean) } -const parseConfig = (files, dir, overrides) => { - const normalizeFiles = (v) => deepMapValues(v, (value, key) => { - if (key === RM_KEY && Array.isArray(value)) { - return value.reduce((acc, k) => { - acc[k] = true - return acc - }, {}) - } - if (typeof value === 'string') { - const file = join(dir, value) - return key === 'file' ? file : { file } - } - if (value === true && FILE_KEYS.includes(key)) { - return {} - } - return value - }) +const parseConfig = (files, dir, overrides, templateSettings) => { + const normalizeFiles = (v) => { + v = deepMapKeys(v, (s) => template(s, templateSettings)) + return deepMapValues(v, (value, key) => { + if (key === RM_KEY && Array.isArray(value)) { + return value.reduce((acc, k) => { + // template files nows since they need to be normalized before merging + acc[template(k, templateSettings)] = true + return acc + }, {}) + } + if (typeof value === 'string') { + const file = join(dir, value) + return key === 'file' ? file : { file } + } + if (value === true && FILE_KEYS.includes(key)) { + return {} + } + return value + }) + } const merged = mergeFiles(normalizeFiles(files), normalizeFiles(overrides)) const withDefaults = defaultsDeep(merged, FILE_KEYS.reduce((acc, k) => { diff --git a/lib/util/get-cmd-path.js b/lib/util/get-cmd-path.js new file mode 100644 index 00000000..0c311044 --- /dev/null +++ b/lib/util/get-cmd-path.js @@ -0,0 +1,36 @@ +const { join, relative } = require('path') +const { makePosix } = require('./path.js') + +const getCmdPath = (key, { pkgConfig, rootConfig, isRoot, pkg, rootPkg }) => { + const result = (local, isRelative) => { + let root = local + const isLocal = local.startsWith('.') || local.startsWith('/') + + if (isLocal) { + if (isRelative) { + // Make a path relative from a workspace to the root if we are in a workspace + local = makePosix(join(relative(pkg.path, rootPkg.path), local)) + } + local = `node ${local}` + root = `node ${root}` + } + + return { + isLocal, + local, + root, + } + } + + if (pkgConfig[key]) { + return result(pkgConfig[key]) + } + + if (rootConfig[key]) { + return result(rootConfig[key], !isRoot) + } + + return result(key) +} + +module.exports = getCmdPath diff --git a/lib/util/import-or-require.js b/lib/util/import-or-require.js new file mode 100644 index 00000000..8fc7ef97 --- /dev/null +++ b/lib/util/import-or-require.js @@ -0,0 +1,29 @@ +// This fixes weird behavior I was seeing where calls to require(path) would +// fail the first time and then fetch via dynamic import which is correct, but +// then subsequent requires for the same path would return an empty object. Not +// sure if a bug or I'm doing something wrong but since the require/imports here +// are short lived, it is safe to create our own cache and use that. +const { pathToFileURL } = require('url') + +const importOrRequireCache = new Map() + +const importOrRequire = async (path) => { + if (importOrRequireCache.has(path)) { + return importOrRequireCache.get(path) + } + let content = {} + try { + content = require(path) + } catch { + try { + content = await import(pathToFileURL(path)).then(r => r.default) + } catch { + // its ok if this fails since the content dir might only be to provide + // other files. the index.js is optional + } + } + importOrRequireCache.set(path, content) + return content +} + +module.exports = importOrRequire diff --git a/lib/util/parser.js b/lib/util/parser.js index 3ca63e9e..27f498b7 100644 --- a/lib/util/parser.js +++ b/lib/util/parser.js @@ -173,7 +173,7 @@ class Gitignore extends Base { } class Js extends Base { - static types = ['*.js'] + static types = ['*.js', '*.cjs'] comment = (c) => `/* ${c} */` } @@ -306,6 +306,10 @@ class JsonMerge extends Json { merge = (t, s) => merge(t, s) } +class JsonMergeNoComment extends JsonMerge { + comment = null +} + class PackageJson extends JsonMerge { static types = ['package.json'] @@ -346,6 +350,7 @@ const Parsers = { YmlMerge, Json, JsonMerge, + JsonMergeNoComment, PackageJson, } diff --git a/lib/util/path.js b/lib/util/path.js new file mode 100644 index 00000000..e0582f59 --- /dev/null +++ b/lib/util/path.js @@ -0,0 +1,13 @@ +const { posix, win32 } = require('path') + +const makePosix = (v) => v.split(win32.sep).join(posix.sep) +const deglob = (v) => makePosix(v).replace(/[/*]+$/, '') +const posixDir = (v) => `${v === '.' ? '' : deglob(v).replace(/\/$/, '')}${posix.sep}` +const posixGlob = (str) => `${posixDir(str)}**` + +module.exports = { + makePosix, + deglob, + posixDir, + posixGlob, +} diff --git a/lib/util/template.js b/lib/util/template.js index 1eb67ed7..d035b3db 100644 --- a/lib/util/template.js +++ b/lib/util/template.js @@ -31,7 +31,7 @@ const makePartials = (dir, isBase) => { Handlebars.registerPartial(partials) } -const setupHandlebars = (baseDir, ...otherDirs) => { +const setupHandlebars = (dirs) => { Handlebars.registerHelper('obj', ({ hash }) => Object.fromEntries(safeValues(hash))) Handlebars.registerHelper('join', (arr, sep) => arr.join(typeof sep === 'string' ? sep : ', ')) Handlebars.registerHelper('pluck', (arr, key) => arr.map(a => a[key])) @@ -40,14 +40,17 @@ const setupHandlebars = (baseDir, ...otherDirs) => { Handlebars.registerHelper('json', (c) => JSON.stringify(c)) Handlebars.registerHelper('del', () => JSON.stringify(DELETE)) - makePartials(baseDir, true) - for (const dir of otherDirs) { - makePartials(dir) + if (Array.isArray(dirs)) { + const [baseDir, ...otherDirs] = dirs + makePartials(baseDir, true) + for (const dir of otherDirs) { + makePartials(dir) + } } } -const template = (str, { config, ...options }) => { - setupHandlebars(...config.__PARTIAL_DIRS__) +const template = (str, { config = {}, ...options }) => { + setupHandlebars(config.__PARTIAL_DIRS__) const t = Handlebars.compile(str, { strict: true }) diff --git a/tap-snapshots/test/apply/source-snapshots.js.test.cjs b/tap-snapshots/test/apply/source-snapshots.js.test.cjs index 49199804..901b2f94 100644 --- a/tap-snapshots/test/apply/source-snapshots.js.test.cjs +++ b/tap-snapshots/test/apply/source-snapshots.js.test.cjs @@ -1550,6 +1550,7 @@ tap-testdir*/ !/SECURITY.md !/tap-snapshots/ !/test/ +!/tsconfig.json .npmrc ======================================== @@ -3624,6 +3625,7 @@ tap-testdir*/ !/SECURITY.md !/tap-snapshots/ !/test/ +!/tsconfig.json !/workspaces/ /workspaces/* !/workspaces/a/ diff --git a/tap-snapshots/test/check/snapshots.js.test.cjs b/tap-snapshots/test/check/snapshots.js.test.cjs index d52c6cb2..245c3bcd 100644 --- a/tap-snapshots/test/check/snapshots.js.test.cjs +++ b/tap-snapshots/test/check/snapshots.js.test.cjs @@ -144,6 +144,7 @@ To correct it: move files to not match one of the following patterns: !/SECURITY.md !/tap-snapshots/ !/test/ + !/tsconfig.json ------------------------------------------------------------------- ` @@ -184,6 +185,7 @@ To correct it: move files to not match one of the following patterns: !/SECURITY.md !/tap-snapshots/ !/test/ + !/tsconfig.json !/workspaces/ /workspaces/* !/workspaces/a/ diff --git a/test/apply/esm.js b/test/apply/esm.js new file mode 100644 index 00000000..3afc80ca --- /dev/null +++ b/test/apply/esm.js @@ -0,0 +1,23 @@ +const t = require('tap') +const setup = require('../setup.js') + +t.test('basic', async (t) => { + const s = await setup(t, { + package: { + type: 'module', + templateOSS: { + content: 'content_dir', + }, + }, + testdir: { + content_dir: { + 'file.js': 'var x = 1;', + 'index.js': 'export default { rootRepo:{add:{"file.js":"file.js"}} }', + }, + }, + }) + await s.apply() + + const file = await s.readFile('file.js') + t.match(file, 'var x = 1;') +}) diff --git a/test/apply/index.js b/test/apply/index.js index f813b36a..3be5f2a5 100644 --- a/test/apply/index.js +++ b/test/apply/index.js @@ -1,5 +1,4 @@ const t = require('tap') -const fs = require('fs') const { join } = require('path') const setup = require('../setup.js') @@ -21,10 +20,10 @@ t.test('turn off root files', async (t) => { }, }) await s.apply() - t.notOk(fs.existsSync(s.join('.commitlintrc.js'))) - t.notOk(fs.existsSync(s.join('.eslintrc.js'))) - t.ok(fs.existsSync(s.join('.github', 'workflows', 'release-test.yml'))) - t.ok(fs.existsSync(s.join('.eslintrc.yml'))) + t.notOk(await s.exists('.commitlintrc.js')) + t.notOk(await s.exists('.eslintrc.js')) + t.ok(await s.exists('.github', 'workflows', 'release-test.yml')) + t.ok(await s.exists('.eslintrc.yml')) }) t.test('turn off root rm only', async (t) => { @@ -49,10 +48,10 @@ t.test('turn off root rm only', async (t) => { }, }) await s.apply() - t.ok(fs.existsSync(s.join('.commitlintrc.js'))) - t.ok(fs.existsSync(s.join('.eslintrc.js'))) - t.ok(fs.existsSync(s.join('.github', 'workflows', 'release-test.yml'))) - t.ok(fs.existsSync(s.join('.eslintrc.yml'))) + t.ok(await s.exists('.commitlintrc.js')) + t.ok(await s.exists('.eslintrc.js')) + t.ok(await s.exists('.github', 'workflows', 'release-test.yml')) + t.ok(await s.exists('.eslintrc.yml')) }) t.test('turn off root add only', async (t) => { @@ -77,10 +76,10 @@ t.test('turn off root add only', async (t) => { }, }) await s.apply() - t.notOk(fs.existsSync(s.join('.commitlintrc.js'))) - t.notOk(fs.existsSync(s.join('.eslintrc.js'))) - t.notOk(fs.existsSync(s.join('.github', 'workflows', 'release-test.yml'))) - t.notOk(fs.existsSync(s.join('.eslintrc.yml'))) + t.notOk(await s.exists('.commitlintrc.js')) + t.notOk(await s.exists('.eslintrc.js')) + t.notOk(await s.exists('.github', 'workflows', 'release-test.yml')) + t.notOk(await s.exists('.eslintrc.yml')) }) t.test('turn off specific files', async (t) => { @@ -115,10 +114,10 @@ t.test('turn off specific files', async (t) => { }, }) await s.apply() - t.notOk(fs.existsSync(s.join('.commitlintrc.js'))) - t.notOk(fs.existsSync(s.join('.eslintrc.js'))) - t.ok(fs.existsSync(s.join('.github', 'workflows', 'release-test.yml'))) - t.ok(fs.existsSync(s.join('.eslintrc.yml'))) + t.notOk(await s.exists('.commitlintrc.js')) + t.notOk(await s.exists('.eslintrc.js')) + t.ok(await s.exists('.github', 'workflows', 'release-test.yml')) + t.ok(await s.exists('.eslintrc.yml')) }) t.test('root can set workspace files', async (t) => { @@ -143,8 +142,8 @@ t.test('root can set workspace files', async (t) => { }, }) await s.apply() - t.notOk(fs.existsSync(s.join(s.workspaces.a, '.eslintrc.js'))) - t.ok(fs.existsSync(s.join(s.workspaces.a, '.npmrc'))) + t.notOk(await s.exists(s.workspaces.a, '.eslintrc.js')) + t.ok(await s.exists(s.workspaces.a, '.npmrc')) }) t.test('workspace config can override root', async (t) => { @@ -176,8 +175,8 @@ t.test('workspace config can override root', async (t) => { }, }) await s.apply() - t.ok(fs.existsSync(s.join(s.workspaces.a, '.eslintrc.js'))) - t.notOk(fs.existsSync(s.join(s.workspaces.a, '.npmrc'))) + t.ok(await s.exists(s.workspaces.a, '.eslintrc.js')) + t.notOk(await s.exists(s.workspaces.a, '.npmrc')) }) t.test('workspaces can override content', async (t) => { @@ -205,9 +204,9 @@ t.test('workspaces can override content', async (t) => { }, }) await s.apply() - t.notOk(fs.existsSync(s.join('.eslintrc.js'))) - t.ok(fs.existsSync(s.join(s.workspaces.a, '.eslintrc.js'))) - t.ok(fs.existsSync(s.join('x.js'))) + t.notOk(await s.exists('.eslintrc.js')) + t.ok(await s.exists(s.workspaces.a, '.eslintrc.js')) + t.ok(await s.exists('x.js')) }) t.test('content can override partials', async (t) => { @@ -339,8 +338,7 @@ t.test('private workspace', async (t) => { t.ok(rpConfig.packages['workspaces/b']) t.notOk(rpConfig.packages['workspaces/a']) - const rp = s.join('.github', 'workflows') - t.ok(fs.existsSync(join(rp, 'release.yml'))) - t.notOk(fs.existsSync(join(rp, 'release-please-b.yml'))) - t.notOk(fs.existsSync(join(rp, 'release-please-a.yml'))) + t.ok(await s.exists('.github', 'workflows', 'release.yml')) + t.notOk(await s.exists('.github', 'workflows', 'release-please-b.yml')) + t.notOk(await s.exists('.github', 'workflows', 'release-please-a.yml')) }) diff --git a/test/apply/release-config.js b/test/apply/release-config.js index 94e6b75b..270f6d32 100644 --- a/test/apply/release-config.js +++ b/test/apply/release-config.js @@ -1,5 +1,4 @@ const t = require('tap') -const { existsSync } = require('fs') const setup = require('../setup.js') t.test('root only', async (t) => { @@ -19,7 +18,7 @@ t.test('root only', async (t) => { await s.apply() const releaseConfig = await s.readJson('release-please-config.json').catch(() => ({})) - const pr = existsSync(s.join('.github', 'workflows', 'pull-request.yml')) + const pr = await s.exists('.github', 'workflows', 'pull-request.yml') t.strictSame(releaseConfig.plugins, expected.plugins ? ['node-workspace'] : undefined) t.equal(pr, expected.pr) diff --git a/test/apply/typescript.js b/test/apply/typescript.js new file mode 100644 index 00000000..63aae412 --- /dev/null +++ b/test/apply/typescript.js @@ -0,0 +1,57 @@ +const t = require('tap') +const setup = require('../setup.js') + +t.test('basic', async (t) => { + const s = await setup(t, { + ok: true, + package: { + templateOSS: { + typescript: true, + }, + }, + testdir: { + '.eslintrc.js': 'delete this', + '.commitlintrc.js': 'delete this', + }, + }) + + t.ok(await s.exists('.eslintrc.js')) + t.ok(await s.exists('.commitlintrc.js')) + + await s.apply() + const checks = await s.check() + const pkg = await s.readJson('package.json') + const eslint = await s.readFile('.eslintrc.cjs') + + t.strictSame(checks[0].body, ['typescript', 'tshy', '@typescript-eslint/parser']) + t.equal(pkg.scripts.prepare, 'tshy') + t.equal(pkg.type, 'module') + t.match(eslint, 'dist/') + t.match(eslint, '@typescript-eslint/parser') + t.notOk(await s.exists('.eslintrc.js')) + t.notOk(await s.exists('.commitlintrc.js')) + t.ok(await s.exists('.commitlintrc.cjs')) +}) + +t.test('no default content', async (t) => { + const s = await setup(t, { + ok: true, + package: { + templateOSS: { + typescript: true, + defaultContent: false, + content: 'content_dir', + }, + }, + testdir: { + content_dir: { + 'file.js': 'var x = 1;', + 'index.js': 'module.exports={rootModule:{add:{"file.js":"file.js"}}}', + }, + }, + }) + await s.apply() + const checks = await s.check() + + t.strictSame(checks[0].body, ['typescript', 'tshy', '@typescript-eslint/parser']) +}) diff --git a/test/setup.js b/test/setup.js index 2f9f8387..33305b6f 100644 --- a/test/setup.js +++ b/test/setup.js @@ -94,6 +94,7 @@ const setupRoot = async (t, root, mocks) => { readdir, readJson: async (f) => JSON.parse(await rootFs.readFile(f)), writeJson: (p, d) => rootFs.writeFile(p, JSON.stringify(d, null, 2)), + exists: (...p) => fs.access(rootPath(...p)).then(() => true).catch(() => false), join: rootPath, apply: () => apply(root), check: () => check(root),