-
Notifications
You must be signed in to change notification settings - Fork 0
feat(package): expose React-free text-position selector subpath #100
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
ee0a307
test(package): define text-position selector subpath contract
seonghobae 11f1c35
feat(package): add text-position selector entrypoint
seonghobae d073cba
build(package): add text-position selector bundle
seonghobae ae007b2
test(package): verify packed text-position selector subpath
seonghobae d1610f1
feat(package): declare text-position selector subpath
seonghobae 2e7e951
docs(package): document React-free selector subpath
seonghobae fd6973c
fix(docs): satisfy selector boundary contract
seonghobae 4f4afab
test(package): cover selector subpath exports
seonghobae 9e347e2
test(package): define selector authority-boundary review contract
seonghobae 9c9ad51
fix(package): enforce selector runtime authority boundary
seonghobae 48bbbe4
fix(docs): record active selector package authority
seonghobae 2c60e2f
Merge branch 'main' into feat/text-position-selector-subpath
opencode-agent[bot] 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
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
219 changes: 219 additions & 0 deletions
219
scripts/verify-text-position-selector-subpath-package.mjs
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,219 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { | ||
| existsSync, | ||
| mkdirSync, | ||
| mkdtempSync, | ||
| readFileSync, | ||
| renameSync, | ||
| rmSync, | ||
| symlinkSync, | ||
| writeFileSync, | ||
| } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { dirname, join, resolve } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | ||
| const packageJson = JSON.parse( | ||
| readFileSync(join(repositoryRoot, 'package.json'), 'utf8'), | ||
| ); | ||
| const verificationRoot = mkdtempSync( | ||
| join(tmpdir(), 'inkspan-text-position-selector-'), | ||
| ); | ||
| const extractionDirectory = join(verificationRoot, 'extracted'); | ||
| const consumerDirectory = join(verificationRoot, 'consumer'); | ||
| const packageDirectory = join( | ||
| consumerDirectory, | ||
| 'node_modules', | ||
| ...packageJson.name.split('/'), | ||
| ); | ||
|
|
||
| // The selector bundle is intentionally self-contained. Type-only ProseMirror | ||
| // declarations are allowed, but emitted JavaScript must not acquire runtime | ||
| // authority through any external static/dynamic import, re-export, or require. | ||
| const externalRuntimeImportPattern = | ||
| /(?:\bimport\s*(?:\(\s*['"][^'"]+['"]\s*\)|(?:[^'"\n;]*?\sfrom\s*)?['"][^'"]+['"])|\bexport\s+[^'"\n;]*?\sfrom\s*['"][^'"]+['"]|\brequire\s*\(\s*['"][^'"]+['"]\s*\))/u; | ||
|
|
||
| // A self-contained bundle must also remain free of ambient network and common | ||
| // environment-backed credential authority even when no module import is needed. | ||
| const ambientAuthorityPattern = | ||
| /(?:\bfetch\s*\(|\bXMLHttpRequest\b|\bWebSocket\b|\bEventSource\b|\bprocess\.env\b|\bimport\.meta\.env\b|\bDeno\.env\b|\bBun\.env\b)/u; | ||
|
|
||
| /** Execute one deterministic package-consumer command. */ | ||
| function run(command, argumentsList, cwd = repositoryRoot) { | ||
| return execFileSync(command, argumentsList, { | ||
| cwd, | ||
| encoding: 'utf8', | ||
| stdio: ['ignore', 'pipe', 'inherit'], | ||
| }); | ||
| } | ||
|
|
||
| /** Build one real npm tarball and install its files without executing scripts. */ | ||
| function preparePackage() { | ||
| mkdirSync(extractionDirectory, { recursive: true }); | ||
| mkdirSync(dirname(packageDirectory), { recursive: true }); | ||
| const packOutput = run('npm', [ | ||
| 'pack', | ||
| '--json', | ||
| '--ignore-scripts', | ||
| '--pack-destination', | ||
| verificationRoot, | ||
| ]); | ||
| const packResult = JSON.parse(packOutput)[0]; | ||
| assert.equal(packResult.name, packageJson.name); | ||
| assert.equal(packResult.version, packageJson.version); | ||
| const tarballPath = join(verificationRoot, packResult.filename); | ||
| assert.ok(existsSync(tarballPath)); | ||
| run('tar', ['-xzf', tarballPath, '-C', extractionDirectory]); | ||
| renameSync(join(extractionDirectory, 'package'), packageDirectory); | ||
| writeFileSync( | ||
| join(consumerDirectory, 'package.json'), | ||
| '{"name":"inkspan-text-position-selector-consumer","private":true,"type":"module"}\n', | ||
| 'utf8', | ||
| ); | ||
|
|
||
| // The package declares @tiptap/pm as a normal dependency. The packed fixture is | ||
| // extracted without a package-manager install, so expose the already-frozen | ||
| // repository dependency only for strict declaration resolution. | ||
| const repositoryTiptap = join(repositoryRoot, 'node_modules', '@tiptap'); | ||
| const consumerTiptap = join(consumerDirectory, 'node_modules', '@tiptap'); | ||
| assert.ok(existsSync(repositoryTiptap)); | ||
| symlinkSync(repositoryTiptap, consumerTiptap, 'dir'); | ||
| } | ||
|
|
||
| /** Prove emitted JavaScript carries no external or ambient runtime authority. */ | ||
| function verifyAuthorityFreeBundles() { | ||
| for (const filename of [ | ||
| 'cwl-text-position-selector.js', | ||
| 'cwl-text-position-selector.cjs', | ||
| ]) { | ||
| const bundlePath = join(packageDirectory, 'dist', filename); | ||
| const bundleSource = readFileSync(bundlePath, 'utf8'); | ||
| assert.doesNotMatch( | ||
| bundleSource, | ||
| externalRuntimeImportPattern, | ||
| `${filename} must not import external runtime authority`, | ||
| ); | ||
| assert.doesNotMatch( | ||
| bundleSource, | ||
| ambientAuthorityPattern, | ||
| `${filename} must not reference ambient network or credential authority`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** Exercise the exact public ESM and CommonJS subpath from the packed package. */ | ||
| function verifyRuntimeConsumers() { | ||
| const esmPath = join(consumerDirectory, 'consumer.mjs'); | ||
| writeFileSync( | ||
| esmPath, | ||
| `import assert from 'node:assert/strict'; | ||
| import { | ||
| TEXT_POSITION_PROJECTION_ID, | ||
| TEXT_POSITION_PROJECTION_VERSION, | ||
| TextPositionSelectorEvidenceError, | ||
| createTextPositionSelector, | ||
| } from '${packageJson.name}/text-position-selector'; | ||
| assert.equal(TEXT_POSITION_PROJECTION_ID, 'inkspan-prosemirror-text'); | ||
| assert.equal(TEXT_POSITION_PROJECTION_VERSION, 1); | ||
| assert.equal(typeof TextPositionSelectorEvidenceError, 'function'); | ||
| assert.equal(typeof createTextPositionSelector, 'function'); | ||
| `, | ||
| 'utf8', | ||
| ); | ||
|
|
||
| const cjsPath = join(consumerDirectory, 'consumer.cjs'); | ||
| writeFileSync( | ||
| cjsPath, | ||
| `const assert = require('node:assert/strict'); | ||
| const selector = require('${packageJson.name}/text-position-selector'); | ||
| assert.equal(selector.TEXT_POSITION_PROJECTION_ID, 'inkspan-prosemirror-text'); | ||
| assert.equal(selector.TEXT_POSITION_PROJECTION_VERSION, 1); | ||
| assert.equal(typeof selector.TextPositionSelectorEvidenceError, 'function'); | ||
| assert.equal(typeof selector.createTextPositionSelector, 'function'); | ||
| `, | ||
| 'utf8', | ||
| ); | ||
|
|
||
| run(process.execPath, [esmPath], consumerDirectory); | ||
| run(process.execPath, [cjsPath], consumerDirectory); | ||
| } | ||
|
|
||
| /** Compile one strict TypeScript consumer against only the public subpath. */ | ||
| function verifyDeclarationConsumer() { | ||
| const sourcePath = join(consumerDirectory, 'consumer.ts'); | ||
| const configurationPath = join(consumerDirectory, 'tsconfig.json'); | ||
| writeFileSync( | ||
| sourcePath, | ||
| `import { | ||
| TEXT_POSITION_PROJECTION_ID, | ||
| TEXT_POSITION_PROJECTION_VERSION, | ||
| TextPositionSelectorEvidenceError, | ||
| createTextPositionSelector, | ||
| type CwlEditorTextPositionSelector, | ||
| type CwlEditorTextProjectionIdentity, | ||
| type TextPositionSelectorEvidenceErrorCode, | ||
| } from '${packageJson.name}/text-position-selector'; | ||
| import type { Node as ProseMirrorNode } from '@tiptap/pm/model'; | ||
| import type { Selection } from '@tiptap/pm/state'; | ||
| declare const documentNode: ProseMirrorNode; | ||
| declare const selection: Selection; | ||
| const result = createTextPositionSelector(documentNode, selection); | ||
| const selector: CwlEditorTextPositionSelector = result.selector; | ||
| const projection: CwlEditorTextProjectionIdentity = result.textProjection; | ||
| const code: TextPositionSelectorEvidenceErrorCode = 'segmenter_unavailable'; | ||
| const failure = new TextPositionSelectorEvidenceError(code); | ||
| void [ | ||
| selector.start, | ||
| selector.end, | ||
| projection.id === TEXT_POSITION_PROJECTION_ID, | ||
| projection.version === TEXT_POSITION_PROJECTION_VERSION, | ||
| failure.code, | ||
| ]; | ||
| `, | ||
| 'utf8', | ||
| ); | ||
| writeFileSync( | ||
| configurationPath, | ||
| `${JSON.stringify( | ||
| { | ||
| compilerOptions: { | ||
| noEmit: true, | ||
| strict: true, | ||
| skipLibCheck: false, | ||
| module: 'NodeNext', | ||
| moduleResolution: 'NodeNext', | ||
| target: 'ES2022', | ||
| lib: ['ES2022', 'DOM', 'DOM.Iterable'], | ||
| types: [], | ||
| }, | ||
| files: ['./consumer.ts'], | ||
| }, | ||
| null, | ||
| 2, | ||
| )}\n`, | ||
| 'utf8', | ||
| ); | ||
| const compilerPath = join( | ||
| repositoryRoot, | ||
| 'node_modules', | ||
| 'typescript', | ||
| 'bin', | ||
| 'tsc', | ||
| ); | ||
| assert.ok(existsSync(compilerPath)); | ||
| run(process.execPath, [compilerPath, '--project', configurationPath], consumerDirectory); | ||
| } | ||
|
|
||
| try { | ||
| preparePackage(); | ||
| verifyAuthorityFreeBundles(); | ||
| verifyRuntimeConsumers(); | ||
| verifyDeclarationConsumer(); | ||
| console.log( | ||
| `Verified packed ${packageJson.name}/text-position-selector through authority-bounded ESM, CommonJS, and strict TypeScript consumers.`, | ||
| ); | ||
| } finally { | ||
| rmSync(verificationRoot, { recursive: true, force: true }); | ||
| } | ||
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,18 @@ | ||
| /** | ||
| * React-free W3C text-position selector projection surface. | ||
| * | ||
| * This subpath exposes only deterministic projection primitives. Interactive | ||
| * editor-handle capture and exact revision binding remain on the root Inkspan | ||
| * editor contract. | ||
| */ | ||
| export { | ||
| TEXT_POSITION_PROJECTION_ID, | ||
| TEXT_POSITION_PROJECTION_VERSION, | ||
| TextPositionSelectorEvidenceError, | ||
| createTextPositionSelector, | ||
| } from '../textPositionSelectorEvidence.js'; | ||
| export type { | ||
| CwlEditorTextPositionSelector, | ||
| CwlEditorTextProjectionIdentity, | ||
| TextPositionSelectorEvidenceErrorCode, | ||
| } from '../textPositionSelectorEvidence.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.
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.
🔒 Security & Privacy | 🟠 Major
문자열 리터럴이 아닌 동적 로더를 차단하십시오.
externalRuntimeImportPattern은 문자열 리터럴 인수를 사용하는import("...")와require("...")만 탐지합니다.import(specifier),import(\./${specifier}`),require(specifier)는 패턴을 통과합니다. 이러한 호출이 번들에 남아도verifyAuthorityFreeBundles()`와 Line 215의 성공 메시지는 권한 경계가 충족되었다고 판단합니다.모든
import()와require()호출을 거부하거나 JavaScript AST로 런타임 로더를 검사하십시오. 정적 import와 re-export의 여러 줄 형식도 처리하십시오.Also applies to: 215-215
🤖 Prompt for AI Agents