Skip to content

Commit

Permalink
feat: convert to hybrid package (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone authored Aug 24, 2022
1 parent 200df03 commit 2c1e428
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 153 deletions.
12 changes: 12 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# These are supported funding model platforms

github: AlCalzone # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 changes: 13 additions & 0 deletions .github/config.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Define constants here:

PACKAGE_NAME=p-timeout
ESM2CJS_PACKAGE_NAME="@esm2cjs/${PACKAGE_NAME}"

UPSTREAM_REPO=sindresorhus/p-timeout
UPSTREAM_REPO_URL=https://github.com/${UPSTREAM_REPO}.git

TEST_SCRIPT_CJS=.github/test-scripts/test.cjs
TEST_SCRIPT_ESM=.github/test-scripts/test.mjs

GIT_USER=Botty
GIT_EMAIL=[email protected]
4 changes: 0 additions & 4 deletions .github/funding.yml

This file was deleted.

68 changes: 68 additions & 0 deletions .github/make_hybrid.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/bin/bash

mkdir -p esm cjs
mv index.js esm/index.js
mv index.d.ts esm/index.d.ts
sed -i 's#./index.js#./esm/index.js#' test.js
mv index.test-d.ts esm/index.test-d.ts
mv test.js test.mjs
sed -i 's#test.js#test.mjs#' test.mjs

# Placeholder to...
# Replace module imports in all ts files
# readarray -d '' files < <(find esm \( -name "*.js" -o -name "*.d.ts" \) -print0)
# function replace_imports () {
# from=$1
# to="${2:-@esm2cjs/$from}"
# for file in "${files[@]}" ; do
# sed -i "s#'$from'#'$to'#g" "$file"
# done
# }
# replace_imports "FROM" "@esm2cjs/TO"
# replace_imports "FROM" # to = "@esm2cjs/FROM"

PJSON=$(cat package.json | jq --tab '
del(.type)
| .description = .description + ". This is a fork of " + .repository + ", but with CommonJS support."
| .repository = "esm2cjs/" + .name
| .name |= "@esm2cjs/" + .
| .author = { "name": "Dominic Griesel", "email": "[email protected]" }
| .publishConfig = { "access": "public" }
| .funding = "https://github.com/sponsors/AlCalzone"
| .main = "cjs/index.js"
| .module = "esm/index.js"
| .files = ["cjs/", "esm/"]
| .exports = {}
| .exports["."].import = "./esm/index.js"
| .exports["."].require = "./cjs/index.js"
| .exports["./package.json"] = "./package.json"
| .types = "esm/index.d.ts"

This comment has been minimized.

Copy link
@jthemphill

jthemphill Jan 17, 2025

Unfortunately, you cannot specify the same type declaration file for both an ESM and CJS implementation file. TypeScript assumes each .d.ts file is a generated file created from exactly one file.

Attempting to import this project into a CommonJS project with the modern {compilerOptions: {module: "nodenext"}} setting will produce a TypeScript error of this form:

ts-cjs-repro/packages/app % pnpm build

> app@ build /Users/jhemphill/oss/ts-cjs-repro/packages/app
> tsc

index.ts:1:23 - error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("lib")' call instead.
  To convert this file to an ECMAScript module, change its file extension to '.mts', or add the field `"type": "module"` to '/Users/jhemphill/oss/ts-cjs-repro/packages/app/package.json'.

1 import * as dual from "lib";
                        ~~~~~


Found 1 error in index.ts:1

The "Are the Types Wrong?" tool gives the "❌ No types" error for your project when importing into CommonJS with "node16" module resolution.

You can see a minimal reproduction of the problem here: https://github.com/jthemphill/ts-cjs-repro

To fix, copy the esm/index.d.ts file to cjs/index.d.ts. Then use the "exports" field to bind each type file to its implementation file:

{
  "exports":
    ".": {
      "import": {
        "types": "./esm/index.d.ts",
        "default": "./esm/index.js"
      },
      "require": {
        "types": "./cjs/index.d.ts",
        "default": "./cjs/index.js"
      }
    }
  }
}
| .typesVersions = {}
| .typesVersions["*"] = {}
| .typesVersions["*"]["esm/index.d.ts"] = ["esm/index.d.ts"]
| .typesVersions["*"]["cjs/index.d.ts"] = ["esm/index.d.ts"]
| .typesVersions["*"]["*"] = ["esm/*"]
| .scripts["to-cjs"] = "esm2cjs --in esm --out cjs -t node12"
| .xo = {ignores: ["cjs", "**/*.test-d.ts", "**/*.d.ts"]}
')
# Placeholder for custom deps:
# | .dependencies["@esm2cjs/DEP"] = .dependencies["DEP"]
# | del(.dependencies["DEP"])

echo "$PJSON" > package.json

# Update package.json -> version if upstream forgot to update it
if [[ ! -z "${TAG}" ]] ; then
VERSION=$(echo "${TAG/v/}")
PJSON=$(cat package.json | jq --tab --arg VERSION "$VERSION" '.version = $VERSION')
echo "$PJSON" > package.json
fi

npm i -D @alcalzone/esm2cjs
npm run to-cjs
npm uninstall -D @alcalzone/esm2cjs

PJSON=$(cat package.json | jq --tab 'del(.scripts["to-cjs"])')
echo "$PJSON" > package.json

npm test
3 changes: 0 additions & 3 deletions .github/security.md

This file was deleted.

4 changes: 4 additions & 0 deletions .github/test-scripts/test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const pTimeout = require("@esm2cjs/p-timeout").default;
const assert = require("assert");

assert(typeof pTimeout === "function");
4 changes: 4 additions & 0 deletions .github/test-scripts/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import pTimeout from "@esm2cjs/p-timeout";
import assert from "assert";

assert(typeof pTimeout === "function");
66 changes: 66 additions & 0 deletions .github/workflows/check_releases.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Check upstream for new release
on:
schedule:
# check each night at 1 AM
- cron: '00 01 * * *'
workflow_dispatch: {}

jobs:
check-releases:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Load configuration
uses: cardinalby/export-env-action@v2
with:
envFile: '.github/config.env'
expand: 'true'

- name: Fetch existing tags
run: git fetch --tags origin

- id: check-version
name: Determine upstream version
uses: pozetroninc/[email protected]
with:
repository: ${{ env.UPSTREAM_REPO }}
excludes: prerelease,draft

- name: Check if tag exists locally
id: check-tag
run: |
RELEASE=${{ steps.check-version.outputs.release }}
echo "Latest release is: ${RELEASE}"
# our tags have a suffix to distinguish them from upstream
if git rev-parse -q --verify "refs/tags/${RELEASE}-cjs"; then
echo "Tag exists here"
else
echo "Tag does not exist here, triggering build..."
echo "::set-output name=tag::${RELEASE}"
fi
- name: Trigger build and wait
id: build
if: steps.check-tag.outputs.tag
uses: aurelien-baudet/workflow-dispatch@v2
with:
workflow: Sync with upstream and patch
token: ${{ secrets.REPO_ACCESS_TOKEN }}
inputs: '{"tag":"${{steps.check-tag.outputs.tag}}"}'
wait-for-completion: true
wait-for-completion-interval: 30s
display-workflow-run-url: false

- name: Publish new version
if: steps.check-tag.outputs.tag && steps.build.outputs.workflow-conclusion == 'success'
uses: aurelien-baudet/workflow-dispatch@v2
with:
workflow: Publish
token: ${{ secrets.REPO_ACCESS_TOKEN }}
inputs: '{"tag":"${{steps.check-tag.outputs.tag}}"}'
wait-for-completion: true
wait-for-completion-interval: 30s
display-workflow-run-url: false
21 changes: 0 additions & 21 deletions .github/workflows/main.yml

This file was deleted.

112 changes: 112 additions & 0 deletions .github/workflows/make_hybrid.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Sync with upstream and patch # Do not rename without updating check_releases.yml too
on:
workflow_dispatch:
inputs:
tag:
description: 'Which tag to build'
required: true

jobs:
patch:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x] # This should be LTS

permissions:
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Load configuration
uses: cardinalby/export-env-action@v2
with:
envFile: '.github/config.env'
expand: 'true'

- name: Prepare repository
env:
TAG: ${{ github.event.inputs.tag }}
run: |
# Restore files to the state in the upstream repo, except our CI scripts and the readme
git remote add upstream ${UPSTREAM_REPO_URL}
git fetch upstream
# The tag without suffix only exists on upstream, so we check it out directly
git restore --source=$TAG -- . ':!.github' ':!README.md' ':!readme.md'
ls -la
- name: Convert to CommonJS/ESM Hybrid
env:
TAG: ${{ github.event.inputs.tag }}
run: bash .github/make_hybrid.sh

- name: Test that both import and require work
run: |
mkdir __test
PACK_FILENAME=$(npm pack --silent --pack-destination __test)
cd __test
npm init -y
npm i $PACK_FILENAME
echo "Test import"
node ../${TEST_SCRIPT_ESM}
echo "Test require"
node ../${TEST_SCRIPT_CJS}
cd ..
rm -rf __test
- name: Remember changes for push
run: |
git add .
git diff --patch --staged > delta.patch
- name: Upload changes to cache
uses: actions/upload-artifact@v3
with:
name: delta
path: delta.patch
retention-days: 1 # We don't need this any longer than the workflow takes, but 1 day is the minimum

commit:
runs-on: ubuntu-latest
needs: [patch]

permissions:
contents: write

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Load configuration
uses: cardinalby/export-env-action@v2
with:
envFile: '.github/config.env'
expand: 'true'

- name: Download changes from cache
uses: actions/download-artifact@v3
with:
name: delta

- name: Push changes to repo
env:
TAG: ${{ github.event.inputs.tag }}
run: |
git apply delta.patch
rm delta.patch
git config --global user.name "${GIT_USER}"
git config --global user.email "${GIT_EMAIL}"
git add -A -- ':!.github/*' ':!README.md' ':!readme.md'
git commit -m "update repository with upstream changes"
# our tags have a suffix to distinguish them from upstream
git tag "$TAG-cjs"
git push
git push origin "$TAG-cjs"
36 changes: 36 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Publish

on:
workflow_dispatch:
inputs:
tag:
description: 'Which tag to build'
required: true

jobs:
publish:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [16.x] # This should be LTS

steps:
- name: Checkout code
uses: actions/checkout@v3
with:
# our tags have a suffix to distinguish them from upstream
ref: ${{ github.event.inputs.tag }}-cjs

# Do we need this step?
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

- run: npm pack --dry-run
# - name: Publish package to npm
# run: |
# npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
# npm whoami
# npm publish
Loading

0 comments on commit 2c1e428

Please sign in to comment.