diff --git a/.git2gus/config.json b/.git2gus/config.json index cfd63f875f..72eb702823 100644 --- a/.git2gus/config.json +++ b/.git2gus/config.json @@ -1,5 +1,5 @@ { "productTag": "a1aB0000000g309IAA", - "defaultBuild": "256", + "defaultBuild": "258", "hideWorkItemUrl": true } diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index c8c67cfe7f..7ac321fb3f 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -51,7 +51,7 @@ jobs: - name: Check package.json integrity run: node ./scripts/tasks/check-and-rewrite-package-json.js --test - name: Check licenses are up to date - run: node ./scripts/tasks/generate-license-files.js --test + run: node ./scripts/tasks/generate-license-files.mjs --test - name: Verify @lwc/shared is tree-shakable run: node ./scripts/tasks/verify-treeshakable.mjs ./packages/@lwc/shared/dist/index.js - name: Verify that dependencies are declared diff --git a/.husky/pre-push b/.husky/pre-push index 943cce231d..8fe3bf053d 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,5 +1,5 @@ set -e node ./scripts/tasks/check-and-rewrite-package-json.js --test -node ./scripts/tasks/generate-license-files.js --test +node ./scripts/tasks/generate-license-files.mjs --test node ./scripts/tasks/verify-treeshakable.mjs ./packages/@lwc/shared/dist/index.js node ./scripts/tasks/check-imports-are-declared-dependencies.js diff --git a/package.json b/package.json index 1a0b95ee95..9c69e6f4a8 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "*.{js,mjs,ts,only,skip}": "eslint --cache", "*.{css,js,json,md,mjs,ts,yaml,yml}": "prettier --check", "{packages/**/package.json,scripts/tasks/check-and-rewrite-package-json.js}": "node ./scripts/tasks/check-and-rewrite-package-json.js --test", - "{LICENSE-CORE.md,**/LICENSE.md,yarn.lock,scripts/tasks/generate-license-files.js,scripts/shared/bundled-dependencies.js}": "node ./scripts/tasks/generate-license-files.js --test" + "{LICENSE-CORE.md,**/LICENSE.md,yarn.lock,scripts/tasks/generate-license-files.mjs,scripts/shared/bundled-dependencies.js}": "node ./scripts/tasks/generate-license-files.mjs --test" }, "workspaces": [ "packages/@lwc/*", diff --git a/scripts/tasks/generate-license-files.js b/scripts/tasks/generate-license-files.js deleted file mode 100644 index b8bb4c676b..0000000000 --- a/scripts/tasks/generate-license-files.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2024, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: MIT - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT - */ -const path = require('node:path'); -const { readFile, writeFile, stat, readdir } = require('node:fs/promises'); -const prettier = require('prettier'); -const { BUNDLED_DEPENDENCIES } = require('../shared/bundled-dependencies.js'); - -// Generate our LICENSE files for each package, including any bundled dependencies -// This is modeled after how Rollup does it: -// https://github.com/rollup/rollup/blob/0b665c3/build-plugins/generate-license-file.ts - -// Async fs.existsSync -async function exists(filename) { - try { - await stat(filename); - return true; - } catch (_err) { - return false; - } -} - -async function findLicenseText(depName) { - // Iterate through possible names for the license file - const names = ['LICENSE', 'LICENSE.md', 'LICENSE.txt']; - - // We would use require.resolve, but 1) that doesn't work if the module lacks a "main" in its `package.json`, - // and 2) it gives us a deep `./path/to/index.js` which makes it harder to find a top-level LICENSE file. So - // just assume that our deps are hoisted to the top-level `node_modules`. - const resolvedDepPath = path.join(process.cwd(), 'node_modules', depName); - - for (const name of names) { - const fullFilePath = path.join(resolvedDepPath, name); - if (await exists(fullFilePath)) { - return (await readFile(fullFilePath, 'utf-8')).trim(); - } - } - - // Get the license from the package.json if we can't find it elsewhere - const pkgJson = JSON.parse(await readFile(path.join(resolvedDepPath, 'package.json'), 'utf-8')); - - const { license, version } = pkgJson; - - return `${license} license defined in package.json in v${version}.`; -} - -async function main() { - const coreLicense = await readFile('LICENSE-CORE.md', 'utf-8'); - - const bundledLicenses = await Promise.all( - BUNDLED_DEPENDENCIES.map(async (depName) => { - return `## ${depName}\n\n` + (await findLicenseText(depName)); - }) - ); - const newLicense = - `# LWC core license\n\n${coreLicense}\n# Licenses of bundled dependencies\n\n${bundledLicenses.join( - '\n' - )}`.trim() + '\n'; - - const formattedLicense = await prettier.format(newLicense, { - parser: 'markdown', - }); - - // Check against current top-level license for changes - const shouldWarnChanges = - process.argv.includes('--test') && - formattedLicense !== (await readFile('LICENSE.md', 'utf-8')); - - // Top level license - await writeFile('LICENSE.md', formattedLicense, 'utf-8'); - - // License file for each package as well, so that we publish it to npm - const atLwcPackages = (await readdir('packages/@lwc')) - // skip dotfiles like .DS_Store - .filter((_) => !_.startsWith('.')) - .map((_) => `@lwc/${_}`); - const packages = ['lwc', ...atLwcPackages]; - - await Promise.all( - packages.map(async (pkg) => { - await writeFile(path.join('packages/', pkg, 'LICENSE.md'), formattedLicense, 'utf-8'); - }) - ); - - if (shouldWarnChanges) { - const relativeFilename = path.relative(process.cwd(), __filename); - throw new Error( - 'Either the LWC core license or the license of a bundled dependency has been updated.\n' + - `Please run \`node ${relativeFilename}\` and commit the result.` - ); - } -} - -main().catch((err) => { - console.error(err); - process.exitCode ||= 1; -}); diff --git a/scripts/tasks/generate-license-files.mjs b/scripts/tasks/generate-license-files.mjs new file mode 100644 index 0000000000..6354119247 --- /dev/null +++ b/scripts/tasks/generate-license-files.mjs @@ -0,0 +1,122 @@ +import path from 'node:path'; +import { readFile, writeFile, stat, readdir } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import prettier from 'prettier'; +import { BUNDLED_DEPENDENCIES } from '../shared/bundled-dependencies.js'; + +// We're using ESM here, but some packages still only export CJS, so we also need `require` +const require = createRequire(import.meta.url); + +const atLwcPackages = (await readdir('packages/@lwc')) + // skip dotfiles like .DS_Store + .filter((_) => !_.startsWith('.')) + .map((_) => `@lwc/${_}`); + +// Generate our LICENSE files for each package, including any bundled dependencies +// This is modeled after how Rollup does it: +// https://github.com/rollup/rollup/blob/0b665c3/build-plugins/generate-license-file.ts + +// Async fs.existsSync +async function exists(filename) { + try { + await stat(filename); + return true; + } catch (_err) { + return false; + } +} + +/** + * Tries `require.resolve` with additional paths (`packages/@lwc/___/node_modules`) + * and `import.meta.resolve` (unmodified) to find a package's entrypoint. + */ +function tryResolve(specifier) { + try { + // As far as I can tell, there's no way to modify the `import` lookup paths + return fileURLToPath(import.meta.resolve(specifier)); + } catch (err) { + // We expect to see missing packages, but throw other errors + if (err.code !== 'ERR_MODULE_NOT_FOUND') { + throw err; + } + } + // `require.resolve` accepts a second parameter of additional places to look + return require.resolve(specifier, { + paths: atLwcPackages.map((pkg) => path.join('packages', pkg, 'node_modules')), + }); +} + +/** + * Finds a dependency in our monorepo. + * @param {string} specifier - package name to find + */ +function findPackageDirectory(specifier) { + const resolved = tryResolve(specifier); + // An import can resolve to a nested directory, e.g. dist/index.js. We want the package + // root, which will always be the last node_modules/${specifier}. + const lookup = path.join('/node_modules', specifier); + return resolved.slice(0, resolved.lastIndexOf(lookup) + lookup.length); +} + +async function findLicenseText(depName) { + // Iterate through possible names for the license file + const names = ['LICENSE', 'LICENSE.md', 'LICENSE.txt']; + + const resolvedDepPath = findPackageDirectory(depName); + + for (const name of names) { + const fullFilePath = path.join(resolvedDepPath, name); + if (await exists(fullFilePath)) { + return (await readFile(fullFilePath, 'utf-8')).trim(); + } + } + + // Get the license from the package.json if we can't find it elsewhere + const { license, version } = JSON.parse( + await readFile(path.join(resolvedDepPath, 'package.json'), 'utf-8') + ); + + return `${license} license defined in package.json in v${version}.`; +} + +const coreLicense = await readFile('LICENSE-CORE.md', 'utf-8'); + +const bundledLicenses = await Promise.all( + BUNDLED_DEPENDENCIES.map(async (depName) => { + return `## ${depName}\n\n` + (await findLicenseText(depName)); + }) +); +const newLicense = + `# LWC core license\n\n${coreLicense}\n# Licenses of bundled dependencies\n\n${bundledLicenses.join( + '\n' + )}`.trim() + '\n'; + +const formattedLicense = await prettier.format(newLicense, { + parser: 'markdown', +}); + +// Check against current top-level license for changes +const shouldWarnChanges = + process.argv.includes('--test') && formattedLicense !== (await readFile('LICENSE.md', 'utf-8')); + +// Top level license +await writeFile('LICENSE.md', formattedLicense, 'utf-8'); + +// License file for each package as well, so that we publish it to npm + +const packages = ['lwc', ...atLwcPackages]; + +await Promise.all( + packages.map(async (pkg) => { + await writeFile(path.join('packages/', pkg, 'LICENSE.md'), formattedLicense, 'utf-8'); + }) +); + +if (shouldWarnChanges) { + const relativeFilename = path.relative(process.cwd(), import.meta.filename); + throw new Error( + 'Either the LWC core license or the license of a bundled dependency has been updated.\n' + + `Please run \`node ${relativeFilename}\` and commit the result.` + ); +}