refactor: move to Typescript #146

Merged
mcataford merged 8 commits from typescript into master 2020-12-13 00:18:26 +00:00
5 changed files with 393 additions and 312 deletions
Showing only changes of commit a6c7a01fef - Show all commits

7
src/cli.ts Normal file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env node
import runPackwatch from '.'
const isUpdatingManifest = process.argv.includes('--update-manifest')
const processExit = runPackwatch({ isUpdatingManifest })
process.exit(processExit)

View file

@ -1,89 +0,0 @@
import { spawnSync } from 'child_process'
import { readFileSync, writeFileSync } from 'fs'
const PACKAGE_SIZE_PATT = /package size:\s+([0-9]+\.?[0-9]*\s+[A-Za-z]+)/g
const UNPACKED_SIZE_PATT = /unpacked size:\s+([0-9]+\.?[0-9]*\s+[A-Za-z]+)/g
const SIZE_SUFFIX_PATT = /([A-Za-z]+)/
const SIZE_MAGNITUDE_PATT = /([0-9]+\.?[0-9]*)/
export const MANIFEST_FILENAME = '.packwatch.json'
type Report = {
packageSize: string
unpackedSize: string
packageSizeBytes?: number
unpackedSizeBytes?: number
}
type Digest = {
limit: string
packageSize: string
}
export function convertSizeToBytes(sizeString: string): number {
const sizeSuffix = SIZE_SUFFIX_PATT.exec(sizeString)[1]
const sizeMagnitude = SIZE_MAGNITUDE_PATT.exec(sizeString)[1]
let multiplier = 1
if (sizeSuffix === 'kB') multiplier = 1000
else if (sizeSuffix === 'mB') {
multiplier = 1000000
}
return multiplier * parseFloat(sizeMagnitude)
}
export function getCurrentPackageStats(): Report {
const { stderr } = spawnSync('npm', ['pack', '--dry-run'], {
encoding: 'utf-8',
})
const stderrString = String(stderr)
const packageSize = PACKAGE_SIZE_PATT.exec(stderrString)[1]
const unpackedSize = UNPACKED_SIZE_PATT.exec(stderrString)[1]
return {
packageSize,
unpackedSize,
packageSizeBytes: convertSizeToBytes(packageSize),
unpackedSizeBytes: convertSizeToBytes(unpackedSize),
}
}
export function getPreviousPackageStats(): Report | null {
try {
const currentManifest = readFileSync(MANIFEST_FILENAME, {
encoding: 'utf-8',
})
const parsedManifest = JSON.parse(currentManifest)
return {
...parsedManifest,
packageSizeBytes: convertSizeToBytes(parsedManifest.packageSize),
unpackedSizeBytes: convertSizeToBytes(parsedManifest.unpackedSize),
limitBytes: convertSizeToBytes(parsedManifest.limit),
}
} catch {
/* No manifest */
}
}
export function createOrUpdateManifest({
previous,
current,
updateLimit = false,
}: {
previous?: Digest
current: Report
updateLimit?: boolean
}) {
const { limit } = previous || {}
const { packageSize, unpackedSize } = current
const newManifest = {
limit: updateLimit ? packageSize : limit || packageSize,
packageSize: packageSize,
unpackedSize: unpackedSize,
}
writeFileSync(MANIFEST_FILENAME, JSON.stringify(newManifest))
}

8
src/index.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
export type Report = {
packageSize: string
unpackedSize?: string
packageSizeBytes?: number
unpackedSizeBytes?: number
limit?: string
limitBytes?: number
}

View file

@ -3,22 +3,10 @@ import { readFileSync } from 'fs'
import mockFS from 'mock-fs' import mockFS from 'mock-fs'
jest.mock('child_process', () => ({ jest.mock('child_process')
spawnSync: () => ({ stderr: mockPackOutput })
}))
import { function getPackOutput({ packageSize, unpackedSize }) {
MANIFEST_FILENAME, return `
convertSizeToBytes,
createOrUpdateManifest,
getCurrentPackageStats,
getPreviousPackageStats,
} from './helpers'
const mockPackageSize = '1.1 kB'
const mockUnpackedSize = '9000 kB'
const mockPackOutput = `
npm notice npm notice
npm notice 📦 footprint@0.0.0 npm notice 📦 footprint@0.0.0
npm notice === Tarball Contents === npm notice === Tarball Contents ===
@ -29,150 +17,246 @@ npm notice === Tarball Details ===
npm notice name: footprint npm notice name: footprint
npm notice version: 0.0.0 npm notice version: 0.0.0
npm notice filename: footprint-0.0.0.tgz npm notice filename: footprint-0.0.0.tgz
npm notice package size: ${mockPackageSize} npm notice package size: ${packageSize}
npm notice unpacked size: ${mockUnpackedSize} npm notice unpacked size: ${unpackedSize}
npm notice shasum: bdf33d471543cd8126338a82a27b16a9010b8dbd npm notice shasum: bdf33d471543cd8126338a82a27b16a9010b8dbd
npm notice integrity: sha512-ZZvTg9GVcJw8J[...]bkE0xlqQhlt4Q== npm notice integrity: sha512-ZZvTg9GVcJw8J[...]bkE0xlqQhlt4Q==
npm notice total files: 3 npm notice total files: 3
npm notice npm notice
` `
describe('Helpers', () => { }
beforeEach(() => {
mockFS.restore() import runPackwatch from '.'
jest.restoreAllMocks()
function getManifest() {
try {
return JSON.parse(readFileSync('.packwatch.json', { encoding: 'utf8' }))
} catch {
/* No manifest */
}
}
function setupMockFS({
hasPackageJSON,
hasManifest,
manifestLimit,
manifestSize,
}) {
const fs = {}
if (hasPackageJSON) fs['package.json'] = '{}'
if (hasManifest)
fs['.packwatch.json'] = JSON.stringify({
unpackedSize: '0.5 B',
limitBytes: manifestLimit,
limit: `${manifestLimit} B`,
packageSize: `${manifestSize} B`,
packageSizeBytes: manifestSize,
}) })
mockFS(fs)
}
describe('Packwatch', () => {
let mockLogger
beforeEach(() => {
mockFS({})
mockLogger = jest.spyOn(global.console, 'log').mockImplementation()
})
afterEach(jest.restoreAllMocks)
afterAll(mockFS.restore) afterAll(mockFS.restore)
describe('Size string conversion', () => { it('warns the user and errors if run away from package.json', () => {
it.each` mockFS({})
sizeString | expectedValue runPackwatch()
${'1 B'} | ${1}
${'1.1 B'} | ${1.1} expect(mockLogger.mock.calls).toHaveLength(1)
${'1 kB'} | ${1000} expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(
${'1.1kB'} | ${1100} '"🤔 There is no package.json file here. Are you in the root directory of your project?"',
${'1 mB'} | ${1000000}
${'1.1 mB'} | ${1100000}
`(
'converts $sizeString properly to $expectedValue bytes',
({ sizeString, expectedValue }) => {
expect(convertSizeToBytes(sizeString)).toEqual(expectedValue)
},
) )
}) })
describe('Current package statistics', () => { describe('without manifest', () => {
it('constructs the current package report properly', () => { beforeEach(() => {
const packageSizeBytes = 1100 setupMockFS({ hasPackageJSON: true })
const unpackedSizeBytes = 9000000
expect(getCurrentPackageStats()).toEqual({
packageSize: mockPackageSize,
packageSizeBytes,
unpackedSize: mockUnpackedSize,
unpackedSizeBytes,
}) })
it.each(['1 B', '1.1 B', '1 kB', '1.1 kB', '1 mB', '1.1 mB'])(
'generates the initial manifest properly (size = %s)',
mockSize => {
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: mockSize,
unpackedSize: mockSize,
}),
})
const returnCode = runPackwatch()
const manifest = getManifest()
expect(returnCode).toEqual(1)
expect(manifest).toEqual({
limit: mockSize,
packageSize: mockSize,
unpackedSize: mockSize,
})
},
)
it('outputs expected messaging', () => {
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: '1 B',
unpackedSize: '2 B',
}),
})
runPackwatch()
expect(mockLogger.mock.calls).toHaveLength(2)
expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(`
"📝 No Manifest to compare against! Current package stats written to .packwatch.json!
Package size (1 B) adopted as new limit."
`)
expect(mockLogger.mock.calls[1][0]).toMatchInlineSnapshot(
'"❗ It looks like you ran PackWatch without a manifest. To prevent accidental passes in CI or hooks, packwatch will terminate with an error. If you are running packwatch for the first time in your project, this is expected!"',
)
})
it('outputs expected messaging when not updating the manifest', () => {
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: '1 B',
unpackedSize: '2 B',
}),
})
runPackwatch({ isUpdatingManifest: true })
expect(mockLogger.mock.calls).toHaveLength(1)
expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(`
"📝 No Manifest to compare against! Current package stats written to .packwatch.json!
Package size (1 B) adopted as new limit."
`)
}) })
}) })
describe('Previous package statistics', () => { describe('with manifest', () => {
it('constructs the previous package report properly', () => { it('messages when the size is equal to the limit', () => {
const packageSize = '0.9 kB' setupMockFS({
const packageSizeBytes = 900 hasPackageJSON: true,
const unpackedSize = '90 kB' hasManifest: true,
const unpackedSizeBytes = 90000 manifestLimit: 1,
const limit = '1 kB' manifestSize: 1,
const limitBytes = 1000
const mockReport = { packageSize, unpackedSize, limit }
mockFS({ [MANIFEST_FILENAME]: JSON.stringify(mockReport) })
expect(getPreviousPackageStats()).toEqual({
packageSize,
packageSizeBytes,
unpackedSize,
unpackedSizeBytes,
limit,
limitBytes,
}) })
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: '1 B',
unpackedSize: '2 B',
}),
})
runPackwatch()
expect(mockLogger.mock.calls).toHaveLength(1)
expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(
'"📦 Nothing to report! Your package is the same size as the latest manifest reports! (Limit: 1 B)"',
)
}) })
it('returns an empty manifest if it fails to reads the manifest file', () => { it('messages when the size is lower than the limit (no growth)', () => {
mockFS({ setupMockFS({
[MANIFEST_FILENAME]: 'not valid JSON', hasPackageJSON: true,
hasManifest: true,
manifestLimit: 5,
manifestSize: 1,
})
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: '1 B',
unpackedSize: '2 B',
}),
})
runPackwatch()
expect(mockLogger.mock.calls).toHaveLength(1)
expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(
'"📦 Nothing to report! Your package is the same size as the latest manifest reports! (Limit: 5 B)"',
)
})
it('messages when the size is lower than the limit (growth)', () => {
setupMockFS({
hasPackageJSON: true,
hasManifest: true,
manifestLimit: 5,
manifestSize: 2,
})
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: '3 B',
unpackedSize: '2 B',
}),
})
runPackwatch()
expect(mockLogger.mock.calls).toHaveLength(1)
expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(
'"📦 👀 Your package grew! 3 B > 2 B (Limit: 5 B)"',
)
})
it('messages when the size is lower than the limit (shrinkage)', () => {
setupMockFS({
hasPackageJSON: true,
hasManifest: true,
manifestLimit: 5,
manifestSize: 2,
})
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: '1 B',
unpackedSize: '2 B',
}),
})
runPackwatch()
expect(mockLogger.mock.calls).toHaveLength(1)
expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(
'"📦 💯 Your package shrank! 1 B < 2 B (Limit: 5 B)"',
)
})
it('messages when the size exceeds the limit', () => {
setupMockFS({
hasPackageJSON: true,
hasManifest: true,
manifestLimit: 0.5,
manifestSize: 0.5,
})
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: '1 B',
unpackedSize: '2 B',
}),
})
runPackwatch()
expect(mockLogger.mock.calls).toHaveLength(1)
expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(`
"🔥🔥📦🔥🔥 Your package exceeds the limit set in .packwatch.json! 1 B > 0.5 B
Either update the limit by using the --update-manifest flag or trim down your packed files!"
`)
}) })
expect(getPreviousPackageStats()).toBeUndefined() it('messages when updating the manifest', () => {
setupMockFS({
hasPackageJSON: true,
hasManifest: true,
manifestLimit: 0.5,
manifestSize: 0.5,
}) })
jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
stderr: getPackOutput({
packageSize: '1 B',
unpackedSize: '2 B',
}),
}) })
runPackwatch({ isUpdatingManifest: true })
describe('Creating or updating the manifest', () => { expect(mockLogger.mock.calls).toHaveLength(1)
const currentStats = { expect(mockLogger.mock.calls[0][0]).toMatchInlineSnapshot(
packageSize: '1 kB', '"📝 Updated the manifest! Package size: 1 B, Limit: 1 B"',
unpackedSize: '10 kB', )
}
const previousManifest = {
limit: '2 kB',
packageSize: '1.5 kB',
}
it('creates a anifest from the current data if no previous data is provided', () => {
mockFS({})
createOrUpdateManifest({ current: currentStats })
const writtenManifest = readFileSync(MANIFEST_FILENAME, {
encoding: 'utf-8',
})
expect(JSON.parse(writtenManifest)).toEqual({
packageSize: currentStats.packageSize,
unpackedSize: currentStats.unpackedSize,
limit: currentStats.packageSize,
})
})
it('updates the previous manifest sizes if previous data exists', () => {
mockFS({
[MANIFEST_FILENAME]: JSON.stringify(previousManifest),
})
createOrUpdateManifest({
current: currentStats,
previous: previousManifest,
updateLimit: false,
})
const writtenManifest = readFileSync(MANIFEST_FILENAME, {
encoding: 'utf-8',
})
expect(JSON.parse(writtenManifest)).toEqual({
packageSize: currentStats.packageSize,
unpackedSize: currentStats.unpackedSize,
limit: previousManifest.limit,
})
})
it('updates the previous manifest sizes and limit if previous data exists and updateLimit is set', () => {
mockFS({
[MANIFEST_FILENAME]: JSON.stringify(previousManifest),
})
createOrUpdateManifest({
current: currentStats,
previous: previousManifest,
updateLimit: true,
})
const writtenManifest = readFileSync(MANIFEST_FILENAME, {
encoding: 'utf-8',
})
expect(JSON.parse(writtenManifest)).toEqual({
packageSize: currentStats.packageSize,
unpackedSize: currentStats.unpackedSize,
limit: currentStats.packageSize,
})
}) })
}) })
}) })

View file

@ -1,23 +1,98 @@
#!/usr/bin/env node import { spawnSync } from 'child_process'
import { existsSync, readFileSync, writeFileSync } from 'fs'
const { existsSync } = require('fs') import { Report } from './index.d'
const { const PACKAGE_SIZE_PATT = /package size:\s*([0-9]+\.?[0-9]*\s+[A-Za-z]{1,2})/
MANIFEST_FILENAME, const UNPACKED_SIZE_PATT = /unpacked size:\s*([0-9]+\.?[0-9]*\s+[A-Za-z]{1,2})/
getCurrentPackageStats, const SIZE_SUFFIX_PATT = /([A-Za-z]+)/
getPreviousPackageStats, const SIZE_MAGNITUDE_PATT = /([0-9]+\.?[0-9]*)/
createOrUpdateManifest,
} = require('./helpers')
const MANIFEST_FILENAME = '.packwatch.json'
function convertSizeToBytes(sizeString: string): number {
const sizeSuffix = SIZE_SUFFIX_PATT.exec(sizeString)[1]
const sizeMagnitude = SIZE_MAGNITUDE_PATT.exec(sizeString)[1]
let multiplier = 1
if (sizeSuffix === 'kB') multiplier = 1000
else if (sizeSuffix === 'mB') {
multiplier = 1000000
}
return multiplier * parseFloat(sizeMagnitude)
}
function getCurrentPackageStats(): Report {
const { stderr } = spawnSync('npm', ['pack', '--dry-run'], {
encoding: 'utf-8',
})
const stderrString = String(stderr)
const packageSize = PACKAGE_SIZE_PATT.exec(stderrString)[1]
const unpackedSize = UNPACKED_SIZE_PATT.exec(stderrString)[1]
return {
packageSize,
unpackedSize,
packageSizeBytes: convertSizeToBytes(packageSize),
unpackedSizeBytes: convertSizeToBytes(unpackedSize),
}
}
function getPreviousPackageStats(): Report | null {
try {
const currentManifest = readFileSync(MANIFEST_FILENAME, {
encoding: 'utf-8',
})
const parsedManifest = JSON.parse(currentManifest)
return {
...parsedManifest,
packageSizeBytes: convertSizeToBytes(parsedManifest.packageSize),
unpackedSizeBytes: convertSizeToBytes(parsedManifest.unpackedSize),
limitBytes: convertSizeToBytes(parsedManifest.limit),
}
} catch {
/* No manifest */
}
}
function createOrUpdateManifest({
previous,
current,
updateLimit = false,
}: {
previous?: Report
current: Report
updateLimit?: boolean
}): void {
const { limit } = previous || {}
const { packageSize, unpackedSize } = current
const newManifest = {
limit: updateLimit ? packageSize : limit || packageSize,
packageSize: packageSize,
unpackedSize: unpackedSize,
}
writeFileSync(MANIFEST_FILENAME, JSON.stringify(newManifest))
}
export default function run(
{
manifestFn = MANIFEST_FILENAME,
isUpdatingManifest,
}: { manifestFn?: string; isUpdatingManifest?: boolean } = {
manifestFn: MANIFEST_FILENAME,
isUpdatingManifest: false,
},
): number {
if (!existsSync('package.json')) { if (!existsSync('package.json')) {
console.log( console.log(
'🤔 There is no package.json file here. Are you in the root directory of your project?', '🤔 There is no package.json file here. Are you in the root directory of your project?',
) )
process.exit(1) return 1
} }
const isUpdatingManifest = process.argv.includes('--update-manifest')
const currentStats = getCurrentPackageStats() const currentStats = getCurrentPackageStats()
/* /*
@ -25,13 +100,10 @@ const currentStats = getCurrentPackageStats()
* a base to build one. The current package size becomes the limit. * a base to build one. The current package size becomes the limit.
*/ */
if (!existsSync(MANIFEST_FILENAME)) { if (!existsSync(manifestFn)) {
createOrUpdateManifest({ current: currentStats }) createOrUpdateManifest({ current: currentStats })
console.log( console.log(
`📝 No Manifest to compare against! Current package stats written to ${MANIFEST_FILENAME}!`, `📝 No Manifest to compare against! Current package stats written to ${MANIFEST_FILENAME}!\nPackage size (${currentStats.packageSize}) adopted as new limit.`,
)
console.log(
`Package size (${currentStats.packageSize}) adopted as new limit.`,
) )
if (!isUpdatingManifest) { if (!isUpdatingManifest) {
@ -41,7 +113,7 @@ if (!existsSync(MANIFEST_FILENAME)) {
} }
// If the update flag wasn't specified, exit with a non-zero code so we // If the update flag wasn't specified, exit with a non-zero code so we
// don't "accidentally" pass CI builds if the manifest didn't exist // don't "accidentally" pass CI builds if the manifest didn't exist
process.exit(isUpdatingManifest ? 0 : 1) return isUpdatingManifest ? 0 : 1
} }
const previousStats = getPreviousPackageStats() const previousStats = getPreviousPackageStats()
@ -67,7 +139,7 @@ if (isUpdatingManifest) {
console.log( console.log(
`📝 Updated the manifest! Package size: ${packageSize}, Limit: ${packageSize}`, `📝 Updated the manifest! Package size: ${packageSize}, Limit: ${packageSize}`,
) )
process.exit(0) return 0
} }
/* /*
@ -77,12 +149,9 @@ if (isUpdatingManifest) {
if (hasExceededLimit) { if (hasExceededLimit) {
console.log( console.log(
`🔥🔥📦🔥🔥 Your package exceeds the limit set in ${MANIFEST_FILENAME}! ${packageSize} > ${limit}`, `🔥🔥📦🔥🔥 Your package exceeds the limit set in ${MANIFEST_FILENAME}! ${packageSize} > ${limit}\nEither update the limit by using the --update-manifest flag or trim down your packed files!`,
) )
console.log( return 1
'Either update the limit by using the --update-manifest flag or trim down your packed files!',
)
process.exit(1)
} }
/* /*
@ -104,3 +173,5 @@ if (packageSizeBytes > previousSizeBytes) {
`📦 Nothing to report! Your package is the same size as the latest manifest reports! (Limit: ${limit})`, `📦 Nothing to report! Your package is the same size as the latest manifest reports! (Limit: ${limit})`,
) )
} }
return 0
}