-
-
Notifications
You must be signed in to change notification settings - Fork 89
Migrate kg-card-factory to TypeScript #1758
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kevinansfield
wants to merge
2
commits into
main
Choose a base branch
from
ts/kg-card-factory
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+318
−221
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| build/ | ||
| tsconfig.tsbuildinfo |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,33 @@ | ||
| import {fixupPluginRules} from '@eslint/compat'; | ||
| import {defineConfig} from 'eslint/config'; | ||
| import eslint from '@eslint/js'; | ||
| import ghostPlugin from 'eslint-plugin-ghost'; | ||
| import globals from 'globals'; | ||
| import tseslint from 'typescript-eslint'; | ||
|
|
||
| const ghost = fixupPluginRules(ghostPlugin); | ||
|
|
||
| export default [ | ||
| export default defineConfig([ | ||
| {ignores: ['build/**']}, | ||
| eslint.configs.recommended, | ||
| { | ||
| files: ['**/*.js'], | ||
| plugins: {ghost}, | ||
| files: ['**/*.ts'], | ||
| extends: [ | ||
| eslint.configs.recommended, | ||
| tseslint.configs.recommended | ||
| ], | ||
| languageOptions: { | ||
| globals: globals.node | ||
| parserOptions: {ecmaVersion: 2022, sourceType: 'module'} | ||
| }, | ||
| plugins: {ghost: ghostPlugin}, | ||
| rules: { | ||
| ...ghostPlugin.configs.node.rules, | ||
| // match ESLint 8 behavior for catch clause variables | ||
| 'no-unused-vars': ['error', {caughtErrors: 'none'}], | ||
| // disable rules incompatible with ESLint 9 flat config | ||
| 'ghost/filenames/match-exported-class': 'off', | ||
| 'ghost/filenames/match-exported': 'off', | ||
| 'ghost/filenames/match-regex': 'off' | ||
| ...ghostPlugin.configs.ts.rules, | ||
| '@typescript-eslint/no-explicit-any': 'error' | ||
| } | ||
| }, | ||
| { | ||
| files: ['test/**/*.js'], | ||
| plugins: {ghost}, | ||
| languageOptions: { | ||
| globals: { | ||
| ...globals.node, | ||
| ...globals.mocha, | ||
| should: true, | ||
| sinon: true | ||
| } | ||
| }, | ||
| files: ['test/**/*.ts'], | ||
| rules: { | ||
| ...ghostPlugin.configs.test.rules | ||
| ...ghostPlugin.configs['ts-test'].rules, | ||
| 'ghost/mocha/no-global-tests': 'off', | ||
| 'ghost/mocha/handle-done-callback': 'off', | ||
| 'ghost/mocha/no-mocha-arrows': 'off', | ||
| 'ghost/mocha/max-top-level-suites': 'off' | ||
| } | ||
| } | ||
| ]; | ||
| ]); |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,50 @@ | ||
| { | ||
| "name": "@tryghost/kg-card-factory", | ||
| "version": "5.1.12", | ||
| "repository": "https://github.com/TryGhost/Koenig/tree/master/packages/kg-card-factory", | ||
| "repository": "https://github.com/TryGhost/Koenig/tree/main/packages/kg-card-factory", | ||
| "author": "Ghost Foundation", | ||
| "license": "MIT", | ||
| "main": "index.js", | ||
| "main": "build/cjs/index.js", | ||
| "module": "build/esm/index.js", | ||
| "types": "build/esm/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./build/esm/index.d.ts", | ||
| "import": "./build/esm/index.js", | ||
| "require": "./build/cjs/index.js" | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "dev": "echo \"Implement me!\"", | ||
| "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", | ||
| "build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", | ||
| "prepare": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", | ||
| "pretest": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json && tsc -p tsconfig.test.json", | ||
| "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha --require tsx './test/**/*.test.ts'", | ||
| "lint": "eslint . --cache", | ||
| "posttest": "yarn lint" | ||
| }, | ||
| "engines": { | ||
| "node": "^22.13.1 || ^24.0.0" | ||
| }, | ||
| "files": [ | ||
| "index.js", | ||
| "lib" | ||
| "build" | ||
| ], | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "9.39.4", | ||
| "@types/mocha": "10.0.10", | ||
| "@types/node": "22.15.29", | ||
| "@types/should": "13.0.0", | ||
| "@types/sinon": "17.0.4", | ||
| "c8": "11.0.0", | ||
| "mocha": "11.7.5", | ||
| "should": "13.2.3", | ||
| "simple-dom": "1.4.0", | ||
| "sinon": "21.0.2" | ||
| "sinon": "21.0.2", | ||
| "tsx": "4.19.4", | ||
| "typescript": "5.9.3", | ||
| "typescript-eslint": "8.33.1" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| export interface FactoryOptions { | ||
| siteUrl?: string; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface CardPayload { | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface CardTransformOptions { | ||
| assetsOnly?: boolean; | ||
| siteUrl?: string; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface DomNode { | ||
| nodeType?: number; | ||
| nodeValue?: string; | ||
| appendChild?(child: unknown): void; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface DomProvider { | ||
| createComment(text: string): DomNode; | ||
| createDocumentFragment(): DomNode; | ||
| createElement?(tag: string): DomNode; | ||
| createTextNode?(text: string): DomNode; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface CardRenderEnv { | ||
| dom: DomProvider; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface CardRenderArgs { | ||
| env: CardRenderEnv; | ||
| payload: CardPayload; | ||
| options?: Record<string, unknown>; | ||
| } | ||
|
|
||
| export interface CardDefinition { | ||
| name: string; | ||
| type: string; | ||
| config?: { commentWrapper?: boolean }; | ||
| render(args: CardRenderArgs): DomNode; | ||
| absoluteToRelative?(payload: CardPayload, options: CardTransformOptions): CardPayload; | ||
| relativeToAbsolute?(payload: CardPayload, options: CardTransformOptions): CardPayload; | ||
| toTransformReady?(payload: CardPayload, options: CardTransformOptions): CardPayload; | ||
| } | ||
|
|
||
| export default class CardFactory { | ||
| factoryOptions: FactoryOptions; | ||
|
|
||
| constructor(options: FactoryOptions = {}) { | ||
| this.factoryOptions = options; | ||
| } | ||
|
|
||
| createCard(card: CardDefinition) { | ||
| const {factoryOptions} = this; | ||
| const {name, type, config = {}} = card; | ||
|
|
||
| return { | ||
| name, | ||
| type, | ||
| factoryOptions, | ||
|
|
||
| render({env, payload, options}: CardRenderArgs): DomNode { | ||
| const {dom} = env; | ||
| const cardOptions = Object.assign({}, factoryOptions, options); | ||
| const cardOutput = card.render({env, payload, options: cardOptions}); | ||
|
|
||
| if (cardOutput.nodeType === 3 && cardOutput.nodeValue === '') { | ||
| return cardOutput; | ||
| } | ||
|
|
||
| if (config.commentWrapper) { | ||
| const cleanName = name.replace(/^card-/, ''); | ||
| const beginComment = dom.createComment(`kg-card-begin: ${cleanName}`); | ||
| const endComment = dom.createComment(`kg-card-end: ${cleanName}`); | ||
| const fragment = dom.createDocumentFragment(); | ||
| fragment.appendChild!(beginComment); | ||
| fragment.appendChild!(cardOutput); | ||
| fragment.appendChild!(endComment); | ||
| return fragment; | ||
| } | ||
|
|
||
| return cardOutput; | ||
| }, | ||
|
|
||
| absoluteToRelative(payload: CardPayload, _options?: CardTransformOptions) { | ||
| if (card.absoluteToRelative) { | ||
| const defaultOptions = {assetsOnly: false, siteUrl: factoryOptions.siteUrl}; | ||
| const options = Object.assign({}, defaultOptions, _options); | ||
| return card.absoluteToRelative(payload, options); | ||
| } | ||
| return payload; | ||
| }, | ||
|
|
||
| relativeToAbsolute(payload: CardPayload, _options?: CardTransformOptions) { | ||
| if (card.relativeToAbsolute) { | ||
| const defaultOptions = {assetsOnly: false, siteUrl: factoryOptions.siteUrl}; | ||
| const options = Object.assign({}, defaultOptions, _options); | ||
| return card.relativeToAbsolute(payload, options); | ||
| } | ||
| return payload; | ||
| }, | ||
|
|
||
| toTransformReady(payload: CardPayload, _options?: CardTransformOptions) { | ||
| if (card.toTransformReady) { | ||
| const defaultOptions = {assetsOnly: false, siteUrl: factoryOptions.siteUrl}; | ||
| const options = Object.assign({}, defaultOptions, _options); | ||
| return card.toTransformReady(payload, options); | ||
| } | ||
| return payload; | ||
| } | ||
| }; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export {default} from './CardFactory.js'; | ||
| export type {FactoryOptions, CardPayload, CardTransformOptions, CardRenderEnv, CardRenderArgs, CardDefinition} from './CardFactory.js'; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In TypeScript, whencompilerOptions.moduleisCommonJS, how doesexport { default } from "./CardFactory.js"compile, and doesrequire("./build/index.js")return the default export directly or an object with a.defaultproperty?💡 Result:
With
compilerOptions.module: "CommonJS", TypeScript keeps the “default export” as a property onexports, and a re-export like:compiles to CommonJS roughly like:
So:
require("./build/index.js")returns the exports object, not the default value directly.require("./build/index.js").default.This is the standard interop shape TypeScript emits for
export defaultwhen targeting CommonJS. [1][2][3]Sources:
[1] https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-interop.html
[2] https://blog.andrewbran.ch/default-exports-in-commonjs-libraries/
[3] https://stackoverflow.com/questions/60577733/not-able-to-require-default-exported-function-from-typescript-file-in-nodejs
🏁 Script executed:
Repository: TryGhost/Koenig
Length of output: 1030
🏁 Script executed:
Repository: TryGhost/Koenig
Length of output: 107
This changes the CommonJS entry shape.
With
compilerOptions.moduleset toCommonJS, the re-exportexport {default} from './CardFactory.js'compiles toexports.default = ..., sorequire('@tryghost/kg-card-factory')returns an object with a.defaultproperty instead of returningCardFactorydirectly. This is a breaking change for existing CommonJS consumers unless the package is intentionally switching to true ESM.Possible compatibility-preserving fix
📝 Committable suggestion
🤖 Prompt for AI Agents
✅ Addressed in commits 0f13173 to 89707bf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
esModuleInterophandles this for TS consumers, and the existing JS code already used export default so isn't this the same behaviour as before?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kevinansfield, you raise a fair point —esModuleInteropdoes handle this correctly for TypeScript consumers, allowingimport CardFactory from '@tryghost/kg-card-factory'to work seamlessly even when the CJS output exposesexports.default.However, there's a subtle distinction worth noting: the old
index.jsactually usedmodule.exports = require('./lib/CardFactory')(notexport default), so the old CJS shape returnedCardFactorydirectly fromrequire(...), while the new CJS build ofexport {default}would produceexports.default = CardFactory. That said, if all consumers in the monorepo are TypeScript projects withesModuleInterop: true(which the search results suggest is the case), this shape difference has no practical impact — your existing consumers would be unaffected.So my original comment was overstated for your specific use case. You're right that for TypeScript consumers, this is effectively equivalent behavior.
✏️ Learnings added