Skip to content

Commit

Permalink
feat: convert small ts files in node_dependencies to ts (#710)
Browse files Browse the repository at this point in the history
* chore: migrate a bunch of files to typescript

* chore: tighten typescript rules

* chore: package is a reserved keyword

* Update src/runtimes/node/bundlers/esbuild/plugin_native_modules.js

Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com>

* Update src/runtimes/node/bundlers/esbuild/plugin_native_modules.js

Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com>

* Update src/runtimes/node/bundlers/esbuild/plugin_native_modules.js

Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com>

* chore: add gypfile, files and binary fields to PackageJSON

* chore: replace module.exports

* chore: use export {}

Co-authored-by: Netlify Team Account 1 <netlify-team-account-1@users.noreply.github.com>
Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
4 people committed Oct 11, 2021
1 parent 2fbb4f7 commit 1d31869
Show file tree
Hide file tree
Showing 19 changed files with 198 additions and 96 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Expand Up @@ -4,6 +4,12 @@ module.exports = {
extends: '@netlify/eslint-config-node',
overrides: [
...overrides,
{
files: '*.ts',
rules: {
'import/no-namespace': 'off',
},
},
{
files: 'tests/*.js',
rules: {
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -93,6 +93,7 @@
},
"devDependencies": {
"@netlify/eslint-config-node": "^3.3.0",
"@types/semver": "^7.3.8",
"adm-zip": "0.5.5",
"ava": "^3.0.0",
"cpy": "^8.0.0",
Expand Down
7 changes: 7 additions & 0 deletions src/deps.d.ts
@@ -0,0 +1,7 @@
declare module 'require-package-name' {
export default function requirePackageName(requireStatement: string): string
}

declare module 'is-builtin-module' {
export default function isBuiltInModule(moduleName: string): boolean
}
6 changes: 3 additions & 3 deletions src/runtimes/node/bundlers/esbuild/plugin_native_modules.js
Expand Up @@ -29,15 +29,15 @@ const getNativeModulesPlugin = (externalizedModules) => ({

// eslint-disable-next-line complexity, max-statements
build.onResolve({ filter: packageFilter }, async (args) => {
const package = packageName.exec(args.path)
const pkg = packageName.exec(args.path)

if (!package) return
if (!pkg) return

let directory = args.resolveDir

while (true) {
if (path.basename(directory) !== 'node_modules') {
const modulePath = path.join(directory, 'node_modules', package[1])
const modulePath = path.join(directory, 'node_modules', pkg[1])
const packageJsonPath = path.join(modulePath, 'package.json')
// eslint-disable-next-line no-await-in-loop
const [isNative, packageJsonData] = await findNativeModule(packageJsonPath, cache)
Expand Down
@@ -1,16 +1,16 @@
const esbuild = require('@netlify/esbuild')
const isBuiltinModule = require('is-builtin-module')
const { tmpName } = require('tmp-promise')
import * as esbuild from '@netlify/esbuild'
import isBuiltinModule from 'is-builtin-module'
import { tmpName } from 'tmp-promise'

const { JS_BUNDLER_ZISI, RUNTIME_JS } = require('../../../../utils/consts')
const { safeUnlink } = require('../../../../utils/fs')
import { JS_BUNDLER_ZISI, RUNTIME_JS } from '../../../../utils/consts'
import { safeUnlink } from '../../../../utils/fs'

// Maximum number of log messages that an esbuild instance will produce. This
// limit is important to avoid out-of-memory errors due to too much data being
// sent in the Go<>Node IPC channel.
const ESBUILD_LOG_LIMIT = 10

const getListImportsPlugin = ({ imports, path }) => ({
const getListImportsPlugin = ({ imports, path }: { imports: Set<string>; path: string }): esbuild.Plugin => ({
name: 'list-imports',
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
Expand All @@ -29,15 +29,15 @@ const getListImportsPlugin = ({ imports, path }) => ({
},
})

const listImports = async ({ functionName, path }) => {
const listImports = async ({ functionName, path }: { functionName: string; path: string }): Promise<string[]> => {
// We're not interested in the output that esbuild generates, we're just
// using it for its parsing capabilities in order to find import/require
// statements. However, if we don't give esbuild a path in `outfile`, it
// will pipe the output to stdout, which we also don't want. So we create
// a temporary file to serve as the esbuild output and then get rid of it
// when we're done.
const targetPath = await tmpName()
const imports = new Set()
const imports = new Set<string>()

try {
await esbuild.build({
Expand All @@ -64,4 +64,4 @@ const listImports = async ({ functionName, path }) => {
return [...imports]
}

module.exports = { listImports }
export { listImports }
@@ -1,9 +1,15 @@
const { valid: validVersion, validRange, satisfies, gte: greaterThanEqual, ltr: lessThanRange } = require('semver')
import { valid as validVersion, validRange, satisfies, gte as greaterThanEqual, ltr as lessThanRange } from 'semver'

import { PackageJson } from '../../utils/package_json'

// Apply the Node.js module logic recursively on its own dependencies, using
// the `package.json` `dependencies`, `peerDependencies` and
// `optionalDependencies` keys
const getNestedDependencies = function ({ dependencies = {}, peerDependencies = {}, optionalDependencies = {} }) {
const getNestedDependencies = function ({
dependencies = {},
peerDependencies = {},
optionalDependencies = {},
}: PackageJson): string[] {
return [
...Object.keys(dependencies),
...Object.keys(peerDependencies).filter(shouldIncludePeerDependency),
Expand All @@ -14,7 +20,7 @@ const getNestedDependencies = function ({ dependencies = {}, peerDependencies =
// Workaround for https://github.com/netlify/zip-it-and-ship-it/issues/73
// TODO: remove this after adding proper modules exclusion as outlined in
// https://github.com/netlify/zip-it-and-ship-it/issues/68
const shouldIncludePeerDependency = function (name) {
const shouldIncludePeerDependency = function (name: string): boolean {
return !EXCLUDED_PEER_DEPENDENCIES.has(name)
}

Expand Down Expand Up @@ -42,7 +48,15 @@ const EXCLUDED_PEER_DEPENDENCIES = new Set(['@prisma/cli', 'prisma2', 'prisma'])
// `optionalDependencies`:
// - are not reported when missing
// - are included in module dependencies
const handleModuleNotFound = function ({ error, moduleName, packageJson }) {
const handleModuleNotFound = function ({
error,
moduleName,
packageJson,
}: {
error: Error & { code: string }
moduleName: string
packageJson: PackageJson
}): [] | never {
if (
error.code === 'MODULE_NOT_FOUND' &&
(isOptionalModule(moduleName, packageJson) || isExternalCrittersModule(moduleName, packageJson))
Expand All @@ -54,8 +68,8 @@ const handleModuleNotFound = function ({ error, moduleName, packageJson }) {
}

const isOptionalModule = function (
moduleName,
{ optionalDependencies = {}, peerDependenciesMeta = {}, peerDependencies = {} },
moduleName: string,
{ optionalDependencies = {}, peerDependenciesMeta = {}, peerDependencies = {} }: PackageJson,
) {
return (
optionalDependencies[moduleName] !== undefined ||
Expand All @@ -67,12 +81,15 @@ const isOptionalModule = function (

const MIN_NEXT_VERSION = '10.0.4'

const satisfiesRange = (range) =>
validRange(range) && (satisfies(MIN_NEXT_VERSION, range) || lessThanRange(MIN_NEXT_VERSION, range))
const satisfiesRange = (range: string): boolean =>
Boolean(validRange(range)) && (satisfies(MIN_NEXT_VERSION, range) || lessThanRange(MIN_NEXT_VERSION, range))

// 'critters' is used only in Next.js >= 10.0.4 when enabling an experimental option and has to be installed manually
// we ignore it if it's missing
const isExternalCrittersModule = function (moduleName, { dependencies = {}, devDependencies = {} }) {
const isExternalCrittersModule = function (
moduleName: string,
{ dependencies = {}, devDependencies = {} }: PackageJson,
) {
if (moduleName !== 'critters') {
return false
}
Expand All @@ -91,4 +108,4 @@ const isExternalCrittersModule = function (moduleName, { dependencies = {}, devD
return satisfiesRange(nextVersion)
}

module.exports = { getNestedDependencies, handleModuleNotFound }
export { getNestedDependencies, handleModuleNotFound }
@@ -1,11 +1,11 @@
const { promisify } = require('util')
import { promisify } from 'util'

const glob = require('glob')
import glob from 'glob'

const pGlob = promisify(glob)

// We use all the files published by the Node.js except some that are not needed
const getPublishedFiles = async function (modulePath) {
const getPublishedFiles = async function (modulePath: string): Promise<string[]> {
const ignore = getIgnoredFiles(modulePath)
const publishedFiles = await pGlob(`${modulePath}/**`, {
ignore,
Expand All @@ -16,7 +16,7 @@ const getPublishedFiles = async function (modulePath) {
return publishedFiles
}

const getIgnoredFiles = function (modulePath) {
const getIgnoredFiles = function (modulePath: string): string[] {
return IGNORED_FILES.map((ignoreFile) => `${modulePath}/${ignoreFile}`)
}

Expand All @@ -34,4 +34,4 @@ const IGNORED_FILES = [
'*.patch',
]

module.exports = { getPublishedFiles }
export { getPublishedFiles }
18 changes: 0 additions & 18 deletions src/runtimes/node/bundlers/zisi/side_files.js

This file was deleted.

18 changes: 18 additions & 0 deletions src/runtimes/node/bundlers/zisi/side_files.ts
@@ -0,0 +1,18 @@
import { getPublishedFiles } from './published'

// Some modules generate source files on `postinstall` that are not located
// inside the module's directory itself.
const getSideFiles = async function (modulePath: string, moduleName: string): Promise<string[]> {
const sideFiles = SIDE_FILES[moduleName]
if (sideFiles === undefined) {
return []
}

return await getPublishedFiles(`${modulePath}/${sideFiles}`)
}

const SIDE_FILES: Record<string, string> = {
'@prisma/client': '../../.prisma',
}

export { getSideFiles }
@@ -1,11 +1,13 @@
const { dirname } = require('path')
import { dirname } from 'path'

const { getModuleName } = require('../../utils/module')
import { getModuleName } from '../../utils/module'
import { PackageJson } from '../../utils/package_json'
import { TraversalCache } from '../../utils/traversal_cache'

const { getNestedDependencies, handleModuleNotFound } = require('./nested')
const { getPublishedFiles } = require('./published')
const { resolvePackage } = require('./resolve')
const { getSideFiles } = require('./side_files')
import { getNestedDependencies, handleModuleNotFound } from './nested'
import { getPublishedFiles } from './published'
import { resolvePackage } from './resolve'
import { getSideFiles } from './side_files'

const EXCLUDED_MODULES = new Set(['aws-sdk'])

Expand All @@ -17,7 +19,13 @@ const getDependencyPathsForDependency = async function ({
state,
packageJson,
pluginsModulesPath,
}) {
}: {
dependency: string
basedir: string
state: TraversalCache
packageJson: PackageJson
pluginsModulesPath: string
}): Promise<string[]> {
const moduleName = getModuleName(dependency)

// Happens when doing require("@scope") (not "@scope/name") or other oddities
Expand All @@ -33,7 +41,17 @@ const getDependencyPathsForDependency = async function ({
}
}

const getDependenciesForModuleName = async function ({ moduleName, basedir, state, pluginsModulesPath }) {
const getDependenciesForModuleName = async function ({
moduleName,
basedir,
state,
pluginsModulesPath,
}: {
moduleName: string
basedir: string
state: TraversalCache
pluginsModulesPath: string
}): Promise<string[]> {
if (isExcludedModule(moduleName)) {
return []
}
Expand All @@ -55,7 +73,7 @@ const getDependenciesForModuleName = async function ({ moduleName, basedir, stat
state.modulePaths.add(modulePath)

// The path depends on the user's build, i.e. must be dynamic
// eslint-disable-next-line import/no-dynamic-require, node/global-require
// eslint-disable-next-line import/no-dynamic-require, node/global-require, @typescript-eslint/no-var-requires
const packageJson = require(packagePath)

const [publishedFiles, sideFiles, depsPaths] = await Promise.all([
Expand All @@ -66,11 +84,21 @@ const getDependenciesForModuleName = async function ({ moduleName, basedir, stat
return [...publishedFiles, ...sideFiles, ...depsPaths]
}

const isExcludedModule = function (moduleName) {
const isExcludedModule = function (moduleName: string): boolean {
return EXCLUDED_MODULES.has(moduleName) || moduleName.startsWith('@types/')
}

const getNestedModules = async function ({ modulePath, state, packageJson, pluginsModulesPath }) {
const getNestedModules = async function ({
modulePath,
state,
packageJson,
pluginsModulesPath,
}: {
modulePath: string
state: TraversalCache
packageJson: PackageJson
pluginsModulesPath: string
}) {
const dependencies = getNestedDependencies(packageJson)

const depsPaths = await Promise.all(
Expand All @@ -80,9 +108,7 @@ const getNestedModules = async function ({ modulePath, state, packageJson, plugi
)
// TODO: switch to Array.flat() once we drop support for Node.js < 11.0.0
// eslint-disable-next-line unicorn/prefer-spread
return [].concat(...depsPaths)
return ([] as string[]).concat(...depsPaths)
}

module.exports = {
getDependencyPathsForDependency,
}
export { getDependencyPathsForDependency }
@@ -1,4 +1,4 @@
const { getModuleName } = require('../../utils/module')
import { getModuleName } from '../../utils/module'

const LOCAL_IMPORT_REGEXP = /^(\.|\/)/

Expand All @@ -7,12 +7,12 @@ const LOCAL_IMPORT_REGEXP = /^(\.|\/)/
// 1. doing dynamic requires (e.g. require(dynamicPath))
// 2. reading files (e.g. fs.readFileSync(someNonJsFile))
// an exception is when the function was generated by `next-on-netlify` to avoid bundling the entire `Next.js` framework
const shouldTreeShake = function (dependency, treeShakeNext) {
const shouldTreeShake = function (dependency: string, treeShakeNext: boolean): boolean {
if (LOCAL_IMPORT_REGEXP.test(dependency)) {
return true
}

return treeShakeNext && getModuleName(dependency) === 'next'
}

module.exports = { shouldTreeShake }
export { shouldTreeShake }

1 comment on commit 1d31869

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏱ Benchmark results

largeDepsEsbuild: 11.5s

largeDepsZisi: 1m 1.8s

Please sign in to comment.