diff --git a/docs/guide/last-modified.md b/docs/guide/last-modified.md index b34d94e..ee3eeb0 100644 --- a/docs/guide/last-modified.md +++ b/docs/guide/last-modified.md @@ -57,7 +57,7 @@ const timestamp = defineSchema(() => addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' }) } const { stdout } = await execAsync(`git log -1 --format=%cd ${meta.path}`) - return new Date(stdout).toISOString() + return new Date(stdout || Date.now()).toISOString() }) ) ``` diff --git a/docs/other/snippets.md b/docs/other/snippets.md index 32c496b..f9f1902 100644 --- a/docs/other/snippets.md +++ b/docs/other/snippets.md @@ -48,7 +48,7 @@ const timestamp = defineSchema(() => addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' }) } const { stdout } = await execAsync(`git log -1 --format=%cd ${meta.path}`) - return new Date(stdout).toISOString() + return new Date(stdout || Date.now()).toISOString() }) ) diff --git a/examples/basic/velite.config.js b/examples/basic/velite.config.js index f915aee..8588da0 100644 --- a/examples/basic/velite.config.js +++ b/examples/basic/velite.config.js @@ -30,7 +30,7 @@ const timestamp = () => addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' }) } const { stdout } = await execAsync(`git log -1 --format=%cd ${meta.path}`) - return new Date(stdout).toISOString() + return new Date(stdout || Date.now()).toISOString() }) export default defineConfig({ diff --git a/examples/nextjs/velite.config.ts b/examples/nextjs/velite.config.ts index f331e9a..4dc4d96 100644 --- a/examples/nextjs/velite.config.ts +++ b/examples/nextjs/velite.config.ts @@ -30,7 +30,7 @@ const timestamp = () => addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' }) } const { stdout } = await execAsync(`git log -1 --format=%cd ${meta.path}`) - return new Date(stdout).toISOString() + return new Date(stdout || Date.now()).toISOString() }) const options = defineCollection({ diff --git a/package.json b/package.json index 3bb3ace..4ccf5e4 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,10 @@ "@rollup/plugin-node-resolve": "^15.3.0", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", - "@types/micromatch": "^4.0.9", "@types/node": "^20.16.11", + "@types/picomatch": "^3.0.1", "@zce/prettier-config": "^0.4.0", - "chokidar": "^3.6.0", + "chokidar": "^4.0.1", "fast-glob": "^3.3.2", "hast-util-raw": "^9.0.4", "hast-util-to-string": "^3.0.1", @@ -59,7 +59,7 @@ "mdast-util-from-markdown": "^2.0.1", "mdast-util-to-hast": "^13.2.0", "mdast-util-toc": "^7.1.0", - "micromatch": "^4.0.8", + "picomatch": "^4.0.2", "prettier": "^3.3.3", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbac7f5..4bb1429 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,18 +36,18 @@ importers: '@types/mdast': specifier: ^4.0.4 version: 4.0.4 - '@types/micromatch': - specifier: ^4.0.9 - version: 4.0.9 '@types/node': specifier: ^20.16.11 version: 20.16.11 + '@types/picomatch': + specifier: ^3.0.1 + version: 3.0.1 '@zce/prettier-config': specifier: ^0.4.0 version: 0.4.0(@vue/compiler-sfc@3.5.11)(prettier@3.3.3) chokidar: - specifier: ^3.6.0 - version: 3.6.0 + specifier: ^4.0.1 + version: 4.0.1 fast-glob: specifier: ^3.3.2 version: 3.3.2 @@ -69,9 +69,9 @@ importers: mdast-util-toc: specifier: ^7.1.0 version: 7.1.0 - micromatch: - specifier: ^4.0.8 - version: 4.0.8 + picomatch: + specifier: ^4.0.2 + version: 4.0.2 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -1051,9 +1051,6 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} - '@types/braces@3.0.4': - resolution: {integrity: sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1084,9 +1081,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/micromatch@4.0.9': - resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==} - '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} @@ -1096,6 +1090,9 @@ packages: '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/picomatch@3.0.1': + resolution: {integrity: sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==} + '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} @@ -1476,6 +1473,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -2754,6 +2755,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -2919,6 +2924,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -4254,8 +4263,6 @@ snapshots: dependencies: '@types/estree': 1.0.6 - '@types/braces@3.0.4': {} - '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -4287,10 +4294,6 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/micromatch@4.0.9': - dependencies: - '@types/braces': 3.0.4 - '@types/ms@0.7.34': {} '@types/node@20.16.11': @@ -4301,6 +4304,8 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/picomatch@3.0.1': {} + '@types/prop-types@15.7.13': {} '@types/react-dom@18.3.0': @@ -4772,6 +4777,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -6610,6 +6619,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.2: {} + pidtree@0.6.0: {} pify@2.3.0: {} @@ -6710,6 +6721,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.2: {} + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 diff --git a/src/build.ts b/src/build.ts index 0d8280e..df04717 100644 --- a/src/build.ts +++ b/src/build.ts @@ -1,7 +1,6 @@ import { mkdir, rm } from 'node:fs/promises' import { join, normalize } from 'node:path' import glob from 'fast-glob' -import micromatch from 'micromatch' import { reporter } from 'vfile-reporter' import { assets } from './assets' @@ -10,6 +9,7 @@ import { VeliteFile } from './file' import { logger } from './logger' import { outputAssets, outputData, outputEntry } from './output' import { getParsedType, ParseContext } from './schemas/zod' +import { matchPatterns } from './utils' import type { LogLevel } from './logger' import type { Schema, ZodMeta } from './schemas' @@ -98,7 +98,7 @@ const resolve = async (config: Config, changed?: string): Promise => { - if (changed != null && !micromatch.contains(changed, pattern) && resolved.has(name)) { + if (changed != null && !matchPatterns(changed, pattern, root) && resolved.has(name)) { // skip collection if changed file not match logger.log(`skipped resolve '${name}', using previous resolved`) return [name, resolved.get(name)!] @@ -176,35 +176,36 @@ const watch = async (config: Config) => { logger.info(`watching for changes in '${root}'`) - const files = Object.values(collections).flatMap(({ pattern }) => pattern) - files.push(...configImports) // watch config file and its dependencies + const patterns = Object.values(collections).flatMap(({ pattern }) => pattern) - const watcher = watch(files, { + const watcher = watch(['.', ...configImports], { cwd: root, - ignored: /(^|[\/\\])[\._]./, // ignore dot & underscore files - ignoreInitial: true, // ignore initial scan + ignoreInitial: true, awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 } }).on('all', async (event, filename) => { - if (event === 'addDir' || event === 'unlinkDir') return // ignore dir changes + if (event === 'addDir' || event === 'unlinkDir') return if (filename == null) return - filename = join(root, filename) - try { - // remove changed file cache - for (const [key, value] of config.cache.entries()) { - if (value === filename) config.cache.delete(key) - } + const fullpath = join(root, filename) - if (configImports.includes(filename)) { + if (configImports.includes(fullpath)) { logger.info('velite config changed, restarting...') watcher.close() return build({ config: config.configPath, clean: false, watch: true }) } + // skip if filename not match any collection pattern + if (!matchPatterns(filename, patterns)) return + + // remove changed file cache + for (const [key, value] of config.cache.entries()) { + if (value === fullpath) config.cache.delete(key) + } + const begin = performance.now() - logger.info(`changed: '${filename}', rebuilding...`) - await resolve(config, filename) + logger.info(`changed: '${fullpath}', rebuilding...`) + await resolve(config, fullpath) logger.info(`rebuild finished`, begin) } catch (err) { logger.warn(err) diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..e757338 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,23 @@ +import { relative } from 'node:path' +import pm from 'picomatch' + +export const matchPatterns = (input: string, patterns: string | string[], base?: string) => { + const list = Array.isArray(patterns) ? patterns : [patterns] + + // TODO: groupBy in feature + const { normal, negated } = list.reduce( + (acc, p) => { + acc[p.startsWith('!') ? 'negated' : 'normal'].push(p) + return acc + }, + { normal: [] as string[], negated: [] as string[] } + ) + + if (base != null) { + input = relative(base, input).replace(/^\.[\\/]/, '') + } + + input = input.replaceAll('\\', '/') + + return normal.some(i => pm(i)(input)) && negated.every(i => pm(i)(input)) +} diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..06c96f2 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,21 @@ +import { strictEqual } from 'node:assert' +import { describe, it } from 'node:test' + +import { matchPatterns } from '../src/utils' + +describe('matchPatterns function', async t => { + it('matches single pattern', () => { + strictEqual(matchPatterns('foo/bar.js', '**/*.js'), true) + strictEqual(matchPatterns('foo/bar.ts', '**/*.js'), false) + }) + + it('matches multiple patterns', () => { + strictEqual(matchPatterns('foo/bar.js', ['**/*.js', '**/*.ts']), true) + strictEqual(matchPatterns('foo/bar.css', ['**/*.js', '**/*.ts']), false) + }) + + it('handles negated patterns', () => { + strictEqual(matchPatterns('foo/bar.js', ['**/*.js', '!**/node_modules/**']), true) + strictEqual(matchPatterns('node_modules/foo/bar.js', ['**/*.js', '!**/node_modules/**']), false) + }) +})