Skip to content

Commit

Permalink
tarsync watch mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh-Walker-GM committed Aug 22, 2024
1 parent a9c39c8 commit 01a4eec
Show file tree
Hide file tree
Showing 5 changed files with 408 additions and 112 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"project:copy": "node ./tasks/framework-tools/frameworkFilesToProject.mjs",
"project:deps": "node ./tasks/framework-tools/frameworkDepsToProject.mjs",
"project:sync": "node ./tasks/framework-tools/frameworkSyncToProject.mjs",
"project:tarsync": "node ./tasks/framework-tools/tarsync.mjs",
"project:tarsync": "tsx ./tasks/framework-tools/tarsync/bin.mts",
"rebuild-fragments-test-project-fixture": "tsx ./tasks/test-project/rebuild-fragments-test-project-fixture.ts",
"rebuild-test-project-fixture": "tsx ./tasks/test-project/rebuild-test-project-fixture.ts",
"smoke-tests": "node ./tasks/smoke-tests/smoke-tests.mjs",
Expand Down
84 changes: 84 additions & 0 deletions tasks/framework-tools/tarsync/bin.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import path from 'node:path'

import chokidar from 'chokidar'

import { FRAMEWORK_PATH, getOptions, IGNORED } from './lib.mjs'
import { tarsync } from './tarsync.mjs'

async function main() {
const { projectPath, watch, verbose } = await getOptions()

await tarsync(
{
projectPath,
verbose,
},
'CLI invocation',
)

if (!watch) {
return
}

let triggered = ''
let running = false

const watcher = chokidar.watch(path.join(FRAMEWORK_PATH, 'packages'), {
ignored: IGNORED,
// We don't want chokidar to emit events as it discovers paths, only as they change.
ignoreInitial: true,
// Debounce the events.
awaitWriteFinish: true,
})
watcher.on('all', async (_event, filePath) => {
if (!running) {
triggered = filePath
return
}

// If we're already running we don't trigger on certain files which are likely to be
// touched by the build process itself.

// Package.json files are touched when we switch to between esm and cjs builds.
if (filePath.endsWith('package.json')) {
return
}

triggered = filePath
})

const monitor = setInterval(async () => {
if (triggered && !running) {
const thisTrigger = triggered
triggered = ''
running = true
try {
await tarsync(
{
projectPath,
verbose,
},
`File change: ${path.relative(FRAMEWORK_PATH, thisTrigger)}`,
)
} finally {
running = false
}
}
}, 100)

let cleanedUp = false
async function cleanUp() {
if (cleanedUp) {
return
}

await watcher.close()
clearInterval(monitor)
cleanedUp = true
}

process.on('SIGINT', cleanUp)
process.on('exit', cleanUp)
}

main()
Original file line number Diff line number Diff line change
@@ -1,73 +1,79 @@
#!/usr/bin/env node

import { performance } from 'node:perf_hooks'
import { fileURLToPath } from 'node:url'
import { parseArgs as nodeUtilParseArgs } from 'node:util'

import ora from 'ora'
import { cd, chalk, fs, glob, path, within, $ } from 'zx'
import { $, cd, chalk, fs, glob, path, within } from 'zx'

const FRAMEWORK_PATH = fileURLToPath(new URL('../../', import.meta.url))
const TARBALL_DEST_DIRNAME = 'tarballs'
export const TARBALL_DEST_DIRNAME = 'tarballs'

async function main() {
const { projectPath, verbose } = await getOptions()
$.verbose = verbose
export const FRAMEWORK_PATH = fileURLToPath(
new URL('../../../', import.meta.url),
)

cd(FRAMEWORK_PATH)
performance.mark('startFramework')
export const IGNORE_EXTENSIONS = ['.DS_Store']

const spinner = getFrameworkSpinner({ text: 'building and packing packages' })
await buildTarballs()
/**
* Add to this array of strings, RegExps, or functions (whichever makes the most sense)
* to ignore files that we don't want triggering package rebuilds.
*/
export const IGNORED = [
/node_modules/,

spinner.text = 'moving tarballs'
await moveTarballs(projectPath)
/packages\/codemods/,
/packages\/create-redwood-app/,

spinner.text = 'updating resolutions'
await updateResolutions(projectPath)
/dist/,

performance.mark('endFramework')
performance.measure('framework', 'startFramework', 'endFramework')
const [entry] = performance.getEntriesByName('framework')
spinner.succeed(`finished in ${(entry.duration / 1000).toFixed(2)} seconds`)
/__fixtures__/,
/__mocks__/,
/__tests__/,
/\.test\./,
/jest.config.{js,ts}/,

await yarnInstall(projectPath)
/README.md/,

const entries = performance.getEntriesByType('measure').map((entry) => {
return `• ${entry.name} => ${(entry.duration / 1000).toFixed(2)} seconds`
})
// esbuild emits meta.json files that we sometimes suffix.
/meta.(\w*\.?)json/,

for (const entry of entries) {
verbose && console.log(entry)
}
}
/tsconfig.tsbuildinfo/,
/tsconfig.build.tsbuildinfo/,
/tsconfig.cjs.tsbuildinfo/,

main()
// The tarballs generated by `yarn build:pack`
/redwoodjs-.*\.tgz$/,

// Helpers
// -------
(filePath: string) => IGNORE_EXTENSIONS.some((ext) => filePath.endsWith(ext)),
]

async function parseArgs() {
export interface Options {
projectPath: string
watch: boolean
verbose: boolean
}

export async function getOptions(): Promise<Options> {
const { positionals, values } = nodeUtilParseArgs({
allowPositionals: true,

options: {
verbose: {
type: 'boolean',
default: false,
short: 'v',
},
watch: {
type: 'boolean',
default: false,
short: 'w',
},
},
})

const [projectPath] = positionals

const options = {
verbose: values.verbose,
const options: Options = {
projectPath: projectPath ?? process.env.RWJS_CWD ?? '',
watch: values.watch ?? false,
verbose: values.verbose ?? false,
}

options.projectPath = projectPath ? projectPath : process.env.RWJS_CWD

if (!options.projectPath) {
throw new Error(
[
Expand All @@ -90,47 +96,11 @@ async function parseArgs() {
return options
}

async function getOptions() {
let options

try {
options = await parseArgs()
} catch (e) {
console.error(e.message)
process.exitCode = 1
return
}

const { projectPath, verbose } = options

return {
projectPath,
verbose,
}
}

const mockSpinner = {
text: '',
succeed: () => {},
}

function getProjectSpinner({ text }) {
return $.verbose
? mockSpinner
: ora({ prefixText: `${chalk.green('[ project ]')}`, text }).start()
}

function getFrameworkSpinner({ text }) {
return $.verbose
? mockSpinner
: ora({ prefixText: `${chalk.cyan('[framework]')}`, text }).start()
}

async function buildTarballs() {
export async function buildTarballs() {
await $`yarn nx run-many -t build:pack --exclude create-redwood-app`
}

async function moveTarballs(projectPath) {
export async function moveTarballs(projectPath: string) {
const tarballDest = path.join(projectPath, TARBALL_DEST_DIRNAME)
await fs.ensureDir(tarballDest)

Expand All @@ -145,31 +115,11 @@ async function moveTarballs(projectPath) {
)
}

async function getReactResolutions() {
const packageConfig = await fs.readJson(
path.join(FRAMEWORK_PATH, 'packages/web/package.json'),
)

const react = packageConfig.peerDependencies.react
const reactDom = packageConfig.peerDependencies['react-dom']

if (!react || !reactDom) {
throw new Error(
"Couldn't find react or react-dom in @redwoodjs/web's peerDependencies",
)
}

return {
react,
'react-dom': reactDom,
}
}

async function updateResolutions(projectPath) {
export async function updateResolutions(projectPath: string) {
const resolutions = (await $`yarn workspaces list --json`).stdout
.trim()
.split('\n')
.map(JSON.parse)
.map((line) => JSON.parse(line))
// Filter out the root workspace.
.filter(({ name }) => name)
.reduce((resolutions, { name }) => {
Expand Down Expand Up @@ -201,20 +151,29 @@ async function updateResolutions(projectPath) {
)
}

async function yarnInstall(projectPath) {
await within(async () => {
cd(projectPath)
performance.mark('startProject')

const spinner = getProjectSpinner({ text: 'yarn install' })
export async function getReactResolutions() {
const packageConfig = await fs.readJson(
path.join(FRAMEWORK_PATH, 'packages/web/package.json'),
)

await $`yarn install`
const react = packageConfig.peerDependencies.react
const reactDom = packageConfig.peerDependencies['react-dom']

performance.mark('endProject')
performance.measure('project', 'startProject', 'endProject')
if (!react || !reactDom) {
throw new Error(
"Couldn't find react or react-dom in @redwoodjs/web's peerDependencies",
)
}

const [entry] = performance.getEntriesByName('project')
return {
react,
'react-dom': reactDom,
}
}

spinner.succeed(`finished in ${(entry.duration / 1000).toFixed(2)} seconds`)
export async function yarnInstall(projectPath: string) {
await within(async () => {
cd(projectPath)
await $`yarn install`
})
}
Loading

0 comments on commit 01a4eec

Please sign in to comment.