Skip to content

Commit

Permalink
Add liveDev feature
Browse files Browse the repository at this point in the history
Fix: #63
  • Loading branch information
isaacs committed Jun 6, 2024
1 parent 3ad79d6 commit 3e079d5
Show file tree
Hide file tree
Showing 15 changed files with 5,384 additions and 2,551 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,29 @@ This would export a file at `./src/foo.ts` as `./foo`, and a file
at `./src/utils/bar.ts` as `./utils/bar`, but would ignore a file
at `./internal/private.ts`.
### Live Dev
Set `"liveDev": true` in the tshy config in `package.json` to
build in link mode. In this mode, the files are hard-linked into
place in the `dist` folder, so that edits are immediately visible.
This is particularly beneficial in monorepo projects, where
workspaces may be edited in parallel, and so it's handy to have
changes reflected in real time without a rebuild.
Of course, tools that can't handle TypeScript will have a problem
with this, so any generic `node` program will not be able to run
your code. For this reason:
- `liveDev` is always disabled when the `npm_command` environment
variable is `'publish'` or `'pack'`. In these situations, your
code is being built for public consumption, and must be
compiled.
- Code in dist will not be able to be loaded in the node repl
unless you run it with a loader, such as `node --import=tsx`.
- Because it links files into place, a rebuild _is_ required when
a file is added or removed.
### Package `#imports`
You can use `"imports"` in your package.json, and it will be
Expand Down
43 changes: 43 additions & 0 deletions src/build-live-commonjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import chalk from 'chalk'
import { linkSync, mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
import { relative, resolve } from 'node:path/posix'
import config from './config.js'
import * as console from './console.js'
import ifExist from './if-exist.js'
import polyfills from './polyfills.js'
import setFolderDialect from './set-folder-dialect.js'
import sources from './sources.js'
import './tsconfig.js'

const { commonjsDialects = [] } = config

// don't actually do a build, just link files into places.
export const buildLiveCommonJS = () => {
for (const d of ['commonjs', ...commonjsDialects]) {
const pf = polyfills.get(d === 'commonjs' ? 'cjs' : d)
console.debug(chalk.cyan.dim('linking ' + d))
for (const s of sources) {
const source = s.substring('./src/'.length)
const target = resolve(`.tshy-build/${d}/${source}`)
mkdirSync(dirname(target), { recursive: true })
linkSync(s, target)
}
setFolderDialect('.tshy-build/' + d, 'commonjs')
for (const [override, orig] of pf?.map.entries() ?? []) {
const stemFrom = resolve(
`.tshy-build/${d}`,
relative(resolve('src'), resolve(override))
).replace(/\.cts$/, '')
const stemTo = resolve(
`.tshy-build/${d}`,
relative(resolve('src'), resolve(orig))
).replace(/\.tsx?$/, '')
ifExist.unlink(`${stemTo}.js.map`)
ifExist.unlink(`${stemTo}.d.ts.map`)
ifExist.rename(`${stemFrom}.cjs`, `${stemTo}.js`)
ifExist.rename(`${stemFrom}.d.cts`, `${stemTo}.d.ts`)
}
console.error(chalk.cyan.bold('linked commonjs'))
}
}
41 changes: 41 additions & 0 deletions src/build-live-esm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import chalk from 'chalk'
import { linkSync, mkdirSync } from 'node:fs'
import { dirname, relative, resolve } from 'node:path'
import config from './config.js'
import * as console from './console.js'
import ifExist from './if-exist.js'
import polyfills from './polyfills.js'
import setFolderDialect from './set-folder-dialect.js'
import sources from './sources.js'
import './tsconfig.js'

const { esmDialects = [] } = config

export const buildLiveESM = () => {
for (const d of ['esm', ...esmDialects]) {
const pf = polyfills.get(d)
console.debug(chalk.cyan.dim('linking ' + d))
for (const s of sources) {
const source = s.substring('./src/'.length)
const target = resolve(`.tshy-build/${d}/${source}`)
mkdirSync(dirname(target), { recursive: true })
linkSync(s, target)
}
setFolderDialect('.tshy-build/' + d, 'esm')
for (const [override, orig] of pf?.map.entries() ?? []) {
const stemFrom = resolve(
`.tshy-build/${d}`,
relative(resolve('src'), resolve(override))
).replace(/\.mts$/, '')
const stemTo = resolve(
`.tshy-build/${d}`,
relative(resolve('src'), resolve(orig))
).replace(/\.tsx?$/, '')
ifExist.unlink(`${stemTo}.js.map`)
ifExist.unlink(`${stemTo}.d.ts.map`)
ifExist.rename(`${stemFrom}.mjs`, `${stemTo}.js`)
ifExist.rename(`${stemFrom}.d.mts`, `${stemTo}.d.ts`)
}
console.error(chalk.cyan.bold('linked ' + d))
}
}
13 changes: 11 additions & 2 deletions src/build.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import chalk from 'chalk'
import config from './config.js'
import { syncContentSync } from 'sync-content'
import bins from './bins.js'
import { buildCommonJS } from './build-commonjs.js'
Expand All @@ -18,14 +19,22 @@ import {
unlink as unlinkImports,
} from './unbuilt-imports.js'
import writePackage from './write-package.js'
import { buildLiveESM } from './build-live-esm.js'
import { buildLiveCommonJS } from './build-live-commonjs.js'

export default async () => {
cleanBuildTmp()

linkSelfDep(pkg, 'src')
await linkImports(pkg, 'src')
if (dialects.includes('esm')) buildESM()
if (dialects.includes('commonjs')) buildCommonJS()
const liveDev =
config.liveDev &&
process.env.npm_command !== 'publish' &&
process.env.npm_command !== 'pack'
const esm = liveDev ? buildLiveESM : buildESM
const commonjs = liveDev ? buildLiveCommonJS : buildCommonJS
if (dialects.includes('esm')) esm()
if (dialects.includes('commonjs')) commonjs()
await unlinkImports(pkg, 'src')
unlinkSelfDep(pkg, 'src')

Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const validConfig = (e: any): e is TshyConfigMaybeGlobExports =>
(e.exclude === undefined || validExclude(e.exclude)) &&
validExtraDialects(e) &&
validBoolean(e, 'selfLink') &&
validBoolean(e, 'main')
validBoolean(e, 'main') &&
validBoolean(e, 'liveDev')

const match = (e: string, pattern: Minimatch[]): boolean =>
pattern.some(m => m.match(e))
Expand Down
57 changes: 37 additions & 20 deletions src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { resolveExport } from './resolve-export.js'
import { Package, TshyConfig, TshyExport } from './types.js'
const { esmDialects = [], commonjsDialects = [] } = config

const liveDev =
config.liveDev &&
process.env.npm_command !== 'publish' &&
process.env.npm_command !== 'pack'

const getTargetForDialectCondition = <T extends string>(
s: string | TshyExport | undefined | null,
dialect: T,
Expand All @@ -35,13 +40,17 @@ const getTargetForDialectCondition = <T extends string>(
const xts = type === 'commonjs' ? '.mts' : '.cts'
if (s.endsWith(xts)) return undefined
const pf = dialect === 'commonjs' ? 'cjs' : dialect
const rel = relative(
resolve('./src'),
resolve(polyfills.get(pf)?.map.get(s) ?? s)
)
const target = liveDev
? rel
: rel.replace(/\.([mc]?)tsx?$/, '.$1js')
return !s || !s.startsWith('./src/')
? s
: dialects.includes(type)
? `./dist/${dialect}/${relative(
resolve('./src'),
resolve(polyfills.get(pf)?.map.get(s) ?? s)
).replace(/\.([mc]?)tsx?$/, '.$1js')}`
? `./dist/${dialect}/${target}`
: undefined
}
return resolveExport(s, [condition])
Expand Down Expand Up @@ -106,10 +115,12 @@ const getExports = (
polyfills
)
if (target) {
exp[d] = {
types: target.replace(/\.js$/, '.d.ts'),
default: target,
}
exp[d] = liveDev
? target
: {
types: target.replace(/\.js$/, '.d.ts'),
default: target,
}
}
}
}
Expand All @@ -124,25 +135,31 @@ const getExports = (
polyfills
)
if (target) {
exp[d] = {
types: target.replace(/\.js$/, '.d.ts'),
default: target,
}
exp[d] = liveDev
? target
: {
types: target.replace(/\.js$/, '.d.ts'),
default: target,
}
}
}
}
// put the default import/require after all the other special ones.
if (impTarget) {
exp.import = {
types: impTarget.replace(/\.(m?)js$/, '.d.$1ts'),
default: impTarget,
}
exp.import = liveDev
? impTarget
: {
types: impTarget.replace(/\.(m?)js$/, '.d.$1ts'),
default: impTarget,
}
}
if (reqTarget) {
exp.require = {
types: reqTarget.replace(/\.(c?)js$/, '.d.$1ts'),
default: reqTarget,
}
exp.require = liveDev
? reqTarget
: {
types: reqTarget.replace(/\.(c?)js$/, '.d.$1ts'),
default: reqTarget,
}
}
}
return e
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type TshyConfigMaybeGlobExports = {
esmDialects?: string[]
project?: string
exclude?: string[]
liveDev?: boolean
}

export type TshyConfig = TshyConfigMaybeGlobExports & {
Expand Down
16 changes: 16 additions & 0 deletions tap-snapshots/test/build-live-commonjs.ts.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* IMPORTANT
* This snapshot file is auto-generated, but designed for humans.
* It should be checked into source control and tracked carefully.
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
* Make sure to inspect the output below. Do not ignore changes!
*/
'use strict'
exports[`test/build-live-commonjs.ts > TAP > commonjs live dev build > must match snapshot 1`] = `
Array [
"blah-blah.cts",
"blah-cjs.cts",
"blah.ts",
"index.ts",
"package.json",
]
`
16 changes: 16 additions & 0 deletions tap-snapshots/test/build-live-esm.ts.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* IMPORTANT
* This snapshot file is auto-generated, but designed for humans.
* It should be checked into source control and tracked carefully.
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
* Make sure to inspect the output below. Do not ignore changes!
*/
'use strict'
exports[`test/build-live-esm.ts > TAP > esm live dev build > must match snapshot 1`] = `
Array [
"blah-blah.mts",
"blah-cjs.cts",
"blah.ts",
"index.ts",
"package.json",
]
`

0 comments on commit 3e079d5

Please sign in to comment.