diff --git a/packages/react-aria-components/package.json b/packages/react-aria-components/package.json index 6dde29cd818..0bfe9a35bf4 100644 --- a/packages/react-aria-components/package.json +++ b/packages/react-aria-components/package.json @@ -54,6 +54,10 @@ "publishConfig": { "access": "public" }, + "scripts": { + "prepack": "node ../../scripts/prepareForPublish.js prepare", + "postpack": "node ../../scripts/prepareForPublish.js restore" + }, "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/string": "^3.2.10", diff --git a/packages/react-aria-components/test/packing.test.js b/packages/react-aria-components/test/packing.test.js new file mode 100644 index 00000000000..58c6f76e1a4 --- /dev/null +++ b/packages/react-aria-components/test/packing.test.js @@ -0,0 +1,117 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const {execFileSync} = require('child_process'); +const tar = require('tar'); + +const root = path.resolve(__dirname, '../../..'); +const yarn = path.join(root, '.yarn/releases/yarn-4.18.0.cjs'); +const helper = path.join(root, 'scripts/prepareForPublish.js'); +const names = ['react-aria', 'react-stately', 'react-aria-components']; +let fixture; +const originals = new Map(); + +beforeAll(() => { + fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'spectrum-pack-')); + fs.writeFileSync( + path.join(fixture, 'package.json'), + JSON.stringify({private: true, workspaces: ['packages/*']}) + ); + for (let name of names) { + let manifest = JSON.parse(fs.readFileSync(path.join(root, 'packages', name, 'package.json'))); + delete manifest.dependencies; + delete manifest.devDependencies; + delete manifest.peerDependencies; + manifest.scripts = { + prepack: `node "${helper}" prepare`, + postpack: `node "${helper}" restore` + }; + let dir = path.join(fixture, 'packages', name); + fs.mkdirSync(path.join(dir, 'exports'), {recursive: true}); + fs.mkdirSync(path.join(dir, 'src')); + fs.mkdirSync(path.join(dir, 'dist/exports'), {recursive: true}); + fs.writeFileSync(path.join(dir, 'exports/index.ts'), 'export const source = true;'); + fs.writeFileSync(path.join(dir, 'src/index.ts'), 'export const source = true;'); + fs.writeFileSync(path.join(dir, 'dist/exports/index.mjs'), 'export const built = true;'); + let contents = JSON.stringify(manifest, null, 2) + '\n'; + originals.set(name, contents); + fs.writeFileSync(path.join(dir, 'package.json'), contents); + } + execFileSync(process.execPath, [yarn, 'install', '--mode=skip-build'], { + cwd: fixture, + env: {...process.env, YARN_ENABLE_IMMUTABLE_INSTALLS: 'false'}, + stdio: 'inherit' + }); +}, 120000); + +afterAll(() => { + fs.rmSync(fixture, {recursive: true, force: true}); +}); + +async function checkArchive(name, archive) { + let entries = []; + await tar.list({file: archive, onReadEntry: entry => entries.push(entry.path)}); + expect(entries.some(entry => /^package\/(src|exports)\//.test(entry))).toBe(false); + let destination = fs.mkdtempSync(path.join(fixture, 'unpacked-')); + await tar.extract({file: archive, cwd: destination}); + let manifest = JSON.parse(fs.readFileSync(path.join(destination, 'package/package.json'))); + let original = JSON.parse(originals.get(name)); + expect(manifest.source).toBeUndefined(); + for (let [key, conditions] of Object.entries(original.exports)) { + if (conditions && typeof conditions === 'object') { + let expected = {...conditions}; + delete expected.source; + expect(manifest.exports[key]).toEqual(expected); + } else { + expect(manifest.exports[key]).toEqual(conditions); + } + } + expect(fs.readFileSync(path.join(fixture, 'packages', name, 'package.json'), 'utf8')).toBe( + originals.get(name) + ); + expect( + fs.existsSync(path.join(fixture, 'packages', name, 'exports/.package-json-before-pack')) + ).toBe(false); +} + +test.each(names)( + 'npm packing %s omits source references and restores local resolution', + async name => { + let dir = path.join(fixture, 'packages', name); + execFileSync('npm', ['pack', '--pack-destination', dir], {cwd: dir}); + let archive = fs.readdirSync(dir).find(file => file.endsWith('.tgz')); + await checkArchive(name, path.join(dir, archive)); + fs.unlinkSync(path.join(dir, archive)); + }, + 120000 +); + +test.each(names)( + 'Yarn packing %s omits source references and restores local resolution', + async name => { + let dir = path.join(fixture, 'packages', name); + let archive = path.join(fixture, `${name}-yarn.tgz`); + execFileSync(process.execPath, [yarn, 'pack', '--out', archive], {cwd: dir}); + await checkArchive(name, archive); + }, + 120000 +); + +test('direct Verdaccio seeding strips source in tarballs and registry manifests', async () => { + let storage = path.join(fixture, 'storage'); + let input = names.map(name => JSON.stringify({name, location: `packages/${name}`})).join('\n'); + execFileSync(process.execPath, [path.join(root, 'scripts/verdaccio-seed.js'), storage], { + cwd: fixture, + input + }); + for (let name of names) { + let dir = path.join(storage, name); + let archive = fs.readdirSync(dir).find(file => file.endsWith('.tgz')); + await checkArchive(name, path.join(dir, archive)); + let registry = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'))); + let manifest = Object.values(registry.versions)[0]; + expect(manifest.source).toBeUndefined(); + expect(manifest.exports['.'].source).toBeUndefined(); + expect(manifest.exports['./*'].source).toBeUndefined(); + } +}, 120000); diff --git a/packages/react-aria/package.json b/packages/react-aria/package.json index 70d2b0de6a5..5fdf0106c44 100644 --- a/packages/react-aria/package.json +++ b/packages/react-aria/package.json @@ -47,6 +47,10 @@ "type": "git", "url": "https://github.com/adobe/react-spectrum" }, + "scripts": { + "prepack": "node ../../scripts/prepareForPublish.js prepare", + "postpack": "node ../../scripts/prepareForPublish.js restore" + }, "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/number": "^3.6.9", diff --git a/packages/react-stately/package.json b/packages/react-stately/package.json index 32bf4f983e1..d792f4a1276 100644 --- a/packages/react-stately/package.json +++ b/packages/react-stately/package.json @@ -37,6 +37,10 @@ "type": "git", "url": "https://github.com/adobe/react-spectrum" }, + "scripts": { + "prepack": "node ../../scripts/prepareForPublish.js prepare", + "postpack": "node ../../scripts/prepareForPublish.js restore" + }, "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/number": "^3.6.9", diff --git a/scripts/prepareForPublish.js b/scripts/prepareForPublish.js new file mode 100644 index 00000000000..7dd383bcf1b --- /dev/null +++ b/scripts/prepareForPublish.js @@ -0,0 +1,30 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +function prepare(dir) { + let manifestPath = path.join(dir, 'package.json'); + let contents = fs.readFileSync(manifestPath, 'utf8'); + let manifest = JSON.parse(contents); + fs.writeFileSync(path.join(dir, 'exports', '.package-json-before-pack'), contents, {flag: 'wx'}); + delete manifest.source; + for (let conditions of Object.values(manifest.exports)) { + if (conditions && typeof conditions === 'object') { + delete conditions.source; + } + } + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n'); +} + +function restore(dir) { + let backup = path.join(dir, 'exports', '.package-json-before-pack'); + fs.writeFileSync(path.join(dir, 'package.json'), fs.readFileSync(backup)); + fs.unlinkSync(backup); +} + +module.exports = {prepare, restore}; + +if (require.main === module) { + module.exports[process.argv[2]](process.cwd()); +} diff --git a/scripts/verdaccio-seed.js b/scripts/verdaccio-seed.js index b8a5c777a6e..45759a9cd41 100644 --- a/scripts/verdaccio-seed.js +++ b/scripts/verdaccio-seed.js @@ -19,6 +19,7 @@ const path = require('path'); const crypto = require('crypto'); const packlist = require('npm-packlist'); const tar = require('tar'); +const {prepare, restore} = require('./prepareForPublish'); const REGISTRY = process.env.SEED_REGISTRY || 'http://localhost:4000'; const CONCURRENCY = Number(process.env.SEED_CONCURRENCY) || 16; @@ -40,7 +41,7 @@ function tarballBuffer(dir, files) { async function seedOne(storagePath, location) { const dir = path.resolve(process.cwd(), location); - const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); + let manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); if (manifest.private) { return null; } @@ -49,8 +50,20 @@ async function seedOne(storagePath, location) { throw new Error(`Workspace at ${location} is missing name or version`); } - const files = await packlist({path: dir}); - const buf = await tarballBuffer(dir, files); + const stripSource = manifest.files?.includes('!exports/**') && manifest.exports?.['.']?.source; + let buf; + if (stripSource) { + prepare(dir); + } + try { + manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); + const files = await packlist({path: dir}); + buf = await tarballBuffer(dir, files); + } finally { + if (stripSource) { + restore(dir); + } + } const shasum = crypto.createHash('sha1').update(buf).digest('hex'); const integrity = 'sha512-' + crypto.createHash('sha512').update(buf).digest('base64'); @@ -126,7 +139,7 @@ async function main() { if (fs.existsSync(dbPath)) { try { db = JSON.parse(fs.readFileSync(dbPath, 'utf8')); - } catch (e) { + } catch { // corrupt/empty db, start fresh } }