Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/console/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 ObjectQL

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
21 changes: 21 additions & 0 deletions packages/react-runtime/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 ObjectQL

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
21 changes: 21 additions & 0 deletions packages/sdui-parser/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 ObjectQL

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
242 changes: 242 additions & 0 deletions scripts/__tests__/package-files-exist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ interface WorkspacePackage {
private: boolean;
files: string[] | undefined;
hasBuildScript: boolean;
/** The `license` field verbatim, or undefined when the manifest omits it. */
license: string | undefined;
}

/**
Expand Down Expand Up @@ -147,6 +149,7 @@ function readWorkspacePackages(): WorkspacePackage[] {
private: Boolean(json.private),
files: Array.isArray(json.files) ? json.files : undefined,
hasBuildScript: Boolean(json.scripts?.build),
license: typeof json.license === 'string' ? json.license : undefined,
});
}
}
Expand Down Expand Up @@ -348,3 +351,242 @@ describe('package.json `files` entries exist on disk (objectui#3663)', () => {
).toBe(true);
});
});

/**
* objectui#3696: the guard above can only ask whether a path a package
* PROMISED in `files` is real. A package that never made the promise is
* invisible to it — and the license text is exactly the file nobody promises,
* because npm ships it whether you list it or not.
*
* ## Why `files` is the wrong place to look for this one
*
* npm keeps a small set of paths it packs REGARDLESS of `files`:
* `package.json`, `README*`, `LICENSE*`/`LICENCE*`, `COPYING*`, and the `main`
* entry. Measured on `packages/sdui-parser` (which declares `files: ["dist"]`)
* by renaming one file and re-running `npm pack --dry-run`:
*
* LICENSE / License / LICENCE / LICENSE.md / LICENSE.txt -> packed
* copying -> packed
* NOTICE / CHANGELOG.md / NOTALICENSE -> NOT packed
*
* So `files: ["dist"]` never excluded the license text, and adding `"LICENSE"`
* to `files` would not have included it. `@object-ui/react-runtime` and
* `@object-ui/sdui-parser` shipped every version of themselves declaring
* `"license": "MIT"` with no MIT text and no copyright notice in the tarball,
* for one reason only: the file was not on disk. That is what this guard reads.
*
* ## The predicate
*
* `!private && license` — the two conditions that together turn "MIT" from a
* label into an obligation. A private package makes no distribution and owes no
* text (`object-ui`, the vscode-extension, is exactly that case and is excluded
* on purpose). A package declaring no license at all is a different defect that
* this guard deliberately does not invent a verdict about.
*
* The population is DERIVED from the same workspace scan as the guard above, so
* a 40th package is covered the day it is added rather than the day someone
* repeats the by-hand census. objectui#3647 and objectui#3696 were both found
* by a human diffing every package by hand; twice in one week is the argument
* for a gate, not for more care.
*/

/**
* Spellings that count as license text. The `licen[cs]e` family from npm's
* always-packed set above, with an optional extension.
*
* `COPYING` is packed by npm too but is deliberately NOT accepted: every one of
* this repo's 38 license files is spelled `LICENSE`, and a guard that silently
* blesses a spelling the repo does not use would report success over a
* convention drift. Landing a `COPYING` should turn this red and be widened
* here on purpose.
*/
const LICENSE_TEXT_RE = /^licen[cs]e(\.[^.]+)?$/i;

/** Every license-text file in a package directory, by name. */
function licenseTextFiles(pkg: WorkspacePackage): string[] {
const dir = path.join(repoRoot, pkg.dir);
if (!fs.existsSync(dir)) return [];
return fs
.readdirSync(dir)
.filter((name) => LICENSE_TEXT_RE.test(name))
.sort();
}

/**
* Whether a package owes license text. Split out from the filter below so the
* truth table can be asserted directly: two of its four cases have no specimen
* in the tree today (every non-private package declares a license), and a
* predicate limb no test ever walks is one a later edit can invert with nothing
* turning red.
*/
function owesLicenseText(pkg: Pick<WorkspacePackage, 'private' | 'license'>): boolean {
return !pkg.private && pkg.license !== undefined && pkg.license.trim() !== '';
}

const owingPackages = packages.filter(owesLicenseText);
const withoutLicenseText = owingPackages.filter((p) => licenseTextFiles(p).length === 0);

/**
* Packages that owed license text and did not have it when this guard landed,
* measured on main@3e601773e.
*
* A RATCHET in the same shape as {@link KNOWN_MISSING}: a NEW offender fails
* without consulting this map, and an entry here that is no longer offending
* fails too, so it can only shrink.
*
* Currently empty, which is the intended resting state — the ratchet still
* fails on any NEW offender. It was written carrying one entry,
* `apps/console` (`@object-ui/console`): published for real
* (`publishConfig.access: "public"`, versioned 17.3.0 with the rest, in the
* `.changeset/config.json` `fixed` group) with the identical defect to the two
* packages objectui#3696 fixed, but outside that issue's `packages/*` scope, so
* it was booked to objectui#3702 instead of fixed silently. Triage folded
* objectui#3702 into the same PR, the root LICENSE was copied to
* `apps/console/LICENSE`, and the entry was banked — the ratchet reported it
* stale first, naming the cause (`it now ships license text (LICENSE)`).
*/
const KNOWN_LICENSE_TEXT_MISSING: Record<string, { issue: string }> = {};

describe('published packages that declare a license ship its text (objectui#3696)', () => {
it('discovers the packages that owe license text (guard cannot pass by finding nothing)', () => {
// Same anti-vacuity floor as the guard above, sitting just under the 39
// measured on main@3e601773e. A predicate that quietly matches nothing —
// a renamed `license` field, a lost workspace root, a `private` default
// flipped — would otherwise report success over an empty set.
expect(owingPackages.length).toBeGreaterThanOrEqual(38);

// The two packages objectui#3696 fixed must be in scope BY NAME. If either
// drops out of the population, this guard has stopped watching the exact
// spot that motivated it, and the pin below would pass on a package nobody
// is checking any more.
const names = owingPackages.map((p) => p.name);
expect(names, '@object-ui/react-runtime must still be required to ship license text').toContain(
'@object-ui/react-runtime',
);
expect(names, '@object-ui/sdui-parser must still be required to ship license text').toContain(
'@object-ui/sdui-parser',
);
});

it('every package that declares a license has the text on disk', () => {
const violations = withoutLicenseText
.filter((p) => !Object.hasOwn(KNOWN_LICENSE_TEXT_MISSING, p.dir))
.map((p) => `${p.name} (${p.dir}) declares "license": "${p.license}" but has no LICENSE file`);

expect(
violations,
[
'A published package claims a license in package.json and ships no license text.',
'The MIT terms require the licence and copyright notice to travel WITH the distribution,',
'so the published tarball does not actually grant what its own manifest advertises',
'(objectui#3647/#3696).',
'',
'npm packs LICENSE regardless of `files`, so the fix is the file itself, never a `files` entry:',
' cp LICENSE <package-dir>/LICENSE # all 38 are byte-identical, blob cf2ca285',
'',
'If the package should NOT be published, mark it `"private": true` instead — that is the',
'other true direction, and it is why `object-ui` (the vscode-extension) is not listed here.',
'',
...violations,
].join('\n'),
).toEqual([]);
});

it('the objectui#3696 license baseline only shrinks', () => {
// The other half of the ratchet, kept separate from the objectui#3663 one
// above so a stale entry names its own defect class.
const stillMissing = new Set(withoutLicenseText.map((p) => p.dir));
const stale = Object.keys(KNOWN_LICENSE_TEXT_MISSING).filter((dir) => !stillMissing.has(dir));

// WHY an entry stopped being a live defect, reported per entry rather than
// assumed — the convention objectui#3701 established for the baseline
// above, and for the same reason. An entry leaves this baseline by four
// routes and only the FIRST means someone added the licence: the other
// three mean the package stopped owing one, which is a different fact with
// a different reviewer response. Derived from the same data the predicate
// reads, so it cannot drift from the verdict it explains.
const causeOf = (dir: string): string => {
const pkg = packages.find((p) => p.dir === dir);
if (!pkg)
return 'no package sits at that path any more: it was moved or removed from the workspace, or this baseline key never matched one';
const texts = licenseTextFiles(pkg);
if (texts.length > 0) return `it now ships license text (${texts.join(', ')})`;
if (pkg.private) return 'it is now `private`, so it distributes nothing and owes no text';
return 'it no longer declares a `license` field, so there is no claim left to honour';
};

expect(
stale,
[
'A KNOWN_LICENSE_TEXT_MISSING entry no longer describes a live defect.',
'Delete its line from KNOWN_LICENSE_TEXT_MISSING to bank the progress — that is the',
'right move under every cause below.',
'',
'The cause is reported per entry rather than assumed, because only one of the four',
'routes out of this baseline means the licence was actually added; do not read a stale',
'line as proof that the package now ships its terms.',
'',
...stale.map((dir) => `${dir} (${KNOWN_LICENSE_TEXT_MISSING[dir].issue}) — ${causeOf(dir)}`),
].join('\n'),
).toEqual([]);
});

it('pins the three packages fixed in objectui#3696 / objectui#3702', () => {
// The general assertion covers these, but naming them means a revert points
// straight at the issue that explains why the files have to be there —
// and, unlike the general assertion, this cannot be satisfied by adding a
// baseline line.
const fixed: Record<string, string> = {
'packages/react-runtime': 'objectui#3696',
'packages/sdui-parser': 'objectui#3696',
// Found by re-running objectui#3696's census over ALL FOUR workspace
// roots instead of `packages/*` alone, which is how it was missed twice.
'apps/console': 'objectui#3702',
};

for (const [dir, issue] of Object.entries(fixed)) {
expect(
fs.existsSync(path.join(repoRoot, dir, 'LICENSE')),
`${dir}/LICENSE is required: the package declares "license": "MIT" and npm packs the file ` +
'regardless of `files`, so deleting it silently resumes publishing MIT-labelled tarballs ' +
`with no MIT text (${issue}).`,
).toBe(true);
}
});

it('requires license text only from packages that publish AND declare a license', () => {
// The predicate's truth table, asserted directly. Two of these four rows
// have no specimen in the tree today — every non-private package declares a
// license — so without this they would be untested logic that a later edit
// could invert with nothing turning red.
expect(owesLicenseText({ private: false, license: 'MIT' }), 'published + declared -> owes').toBe(true);
expect(owesLicenseText({ private: true, license: 'MIT' }), 'private -> makes no distribution').toBe(false);
expect(owesLicenseText({ private: false, license: undefined }), 'no claim -> nothing to honour').toBe(false);
expect(owesLicenseText({ private: false, license: ' ' }), 'blank claim is not a claim').toBe(false);
});

it('excludes private packages, and that exclusion is not vacuous', () => {
// The `private` limb only means something while some private package would
// otherwise be reported. Asserting the specimen set is NON-EMPTY first is
// what keeps this from becoming a test that passes because it examines
// nothing — today it is `object-ui` (packages/vscode-extension), which
// declares "MIT" and has no LICENSE file.
const privateOwingIfPublished = packages.filter(
(p) => p.private && p.license !== undefined && licenseTextFiles(p).length === 0,
);

expect(
privateOwingIfPublished.map((p) => p.name),
'No private package declares a license without shipping its text any more, so this boundary ' +
'case now proves nothing. Either pick a live specimen or delete this test — do not leave it ' +
'green over an empty set.',
).not.toEqual([]);

const reported = new Set(withoutLicenseText.map((p) => p.name));
for (const pkg of privateOwingIfPublished) {
expect(reported.has(pkg.name), `${pkg.name} is private and must not be required to ship license text`).toBe(
false,
);
}
});
});
Loading