fimo 0.6.0 → 0.7.0-experimental.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/content-templates/hooks-template.eta +73 -27
- package/assets/content-templates/types-template.eta +4 -1
- package/assets/skills/fimo/references/content.md +46 -42
- package/assets/skills/fimo-cli/references/content.md +14 -14
- package/assets/skills/fimo-cli/references/domains.md +1 -1
- package/assets/skills/fimo-studio/references/custom-domains.md +1 -1
- package/dist/cli/bundle.json +3 -2
- package/dist/cli/index.js +1203 -110450
- package/dist/runtime/content/client/createContentClient.d.ts +5 -0
- package/dist/runtime/content/client/createContentClient.js +300 -0
- package/dist/runtime/content/client/createSingletonClient.d.ts +5 -0
- package/dist/runtime/content/client/createSingletonClient.js +108 -0
- package/dist/runtime/content/client/internal.d.ts +44 -0
- package/dist/runtime/content/client/internal.js +236 -0
- package/dist/runtime/content/client/types.d.ts +136 -0
- package/dist/runtime/content/client/useQueryLocale.d.ts +2 -0
- package/dist/runtime/content/client/useQueryLocale.js +6 -0
- package/dist/runtime/content/utils/retrieve-data-sources.d.ts +1 -0
- package/dist/runtime/content/utils/retrieve-data-sources.js +27 -4
- package/dist/runtime/iframe/runtime/overlay-hmr.js +3 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/primitives/lib/primitives.d.ts +2 -2
- package/dist/runtime/primitives/lib/query.d.ts +5 -3
- package/package.json +31 -13
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/src/pages/Index.tsx +1 -1
- package/dist/build/vite/index.d.ts.map +0 -1
- package/dist/build/vite/plugins/content-sync.d.ts.map +0 -1
- package/dist/build/vite/plugins/content-sync.test.d.ts +0 -2
- package/dist/build/vite/plugins/content-sync.test.d.ts.map +0 -1
- package/dist/build/vite/plugins/content-sync.test.js +0 -64
- package/dist/build/vite/plugins/data-id.d.ts.map +0 -1
- package/dist/build/vite/plugins/fimo-config.d.ts.map +0 -1
- package/dist/build/vite/plugins/fimo-config.test.d.ts +0 -2
- package/dist/build/vite/plugins/fimo-config.test.d.ts.map +0 -1
- package/dist/build/vite/plugins/fimo-config.test.js +0 -46
- package/dist/build/vite/plugins/overlay.d.ts.map +0 -1
- package/dist/build/vite/plugins/robots.d.ts.map +0 -1
- package/dist/build/vite/plugins/seo.d.ts.map +0 -1
- package/dist/build/vite/plugins/server-config.d.ts.map +0 -1
- package/dist/build/vite/plugins/sitemap.d.ts.map +0 -1
- package/dist/build/vite/plugins/translations.d.ts.map +0 -1
- package/dist/build/vite/plugins/translations.test.d.ts +0 -2
- package/dist/build/vite/plugins/translations.test.d.ts.map +0 -1
- package/dist/build/vite/plugins/translations.test.js +0 -266
- package/dist/runtime/app/FimoProviders.d.ts.map +0 -1
- package/dist/runtime/app/FimoScripts.d.ts.map +0 -1
- package/dist/runtime/app/index.d.ts.map +0 -1
- package/dist/runtime/app/prefetch.d.ts.map +0 -1
- package/dist/runtime/content/DataSourcesProvider.d.ts.map +0 -1
- package/dist/runtime/content/RefreshContentProvider.d.ts.map +0 -1
- package/dist/runtime/content/RefreshContentProvider.test.d.ts +0 -2
- package/dist/runtime/content/RefreshContentProvider.test.d.ts.map +0 -1
- package/dist/runtime/content/RefreshContentProvider.test.js +0 -33
- package/dist/runtime/content/parseCollectionResponse.d.ts.map +0 -1
- package/dist/runtime/content/parseCollectionResponse.test.d.ts +0 -2
- package/dist/runtime/content/parseCollectionResponse.test.d.ts.map +0 -1
- package/dist/runtime/content/parseCollectionResponse.test.js +0 -20
- package/dist/runtime/content/utils/retrieve-data-sources.d.ts.map +0 -1
- package/dist/runtime/core/AppErrorBoundary.d.ts.map +0 -1
- package/dist/runtime/core/ViteLifecycles.d.ts.map +0 -1
- package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +0 -1
- package/dist/runtime/i18n/locale.d.ts.map +0 -1
- package/dist/runtime/i18n/locale.test.d.ts +0 -2
- package/dist/runtime/i18n/locale.test.d.ts.map +0 -1
- package/dist/runtime/i18n/locale.test.js +0 -58
- package/dist/runtime/iframe/ConsoleErrorProvider.d.ts.map +0 -1
- package/dist/runtime/iframe/ScreenshotProvider.d.ts.map +0 -1
- package/dist/runtime/iframe/runtime/overlay-hmr.d.ts.map +0 -1
- package/dist/runtime/iframe/runtime/overlay-runtime.d.ts.map +0 -1
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/paths/get-fimo-paths.d.ts.map +0 -1
- package/dist/runtime/paths/get-fimo-paths.test.d.ts +0 -2
- package/dist/runtime/paths/get-fimo-paths.test.d.ts.map +0 -1
- package/dist/runtime/paths/get-fimo-paths.test.js +0 -69
- package/dist/runtime/paths/index.d.ts.map +0 -1
- package/dist/runtime/paths/types.d.ts.map +0 -1
- package/dist/runtime/primitives/components/Boolean.d.ts.map +0 -1
- package/dist/runtime/primitives/components/Date.d.ts.map +0 -1
- package/dist/runtime/primitives/components/Image.d.ts.map +0 -1
- package/dist/runtime/primitives/components/RichText.d.ts.map +0 -1
- package/dist/runtime/primitives/components/StaticImage.d.ts.map +0 -1
- package/dist/runtime/primitives/components/Text.d.ts.map +0 -1
- package/dist/runtime/primitives/components/Video.d.ts.map +0 -1
- package/dist/runtime/primitives/components/index.d.ts.map +0 -1
- package/dist/runtime/primitives/index.d.ts.map +0 -1
- package/dist/runtime/primitives/lib/image-optimization.d.ts.map +0 -1
- package/dist/runtime/primitives/lib/index.d.ts.map +0 -1
- package/dist/runtime/primitives/lib/primitives.d.ts.map +0 -1
- package/dist/runtime/primitives/lib/query.d.ts.map +0 -1
- package/dist/runtime/primitives/lib/template.d.ts.map +0 -1
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +0 -1
- package/dist/runtime/primitives/public-surface.test.d.ts +0 -2
- package/dist/runtime/primitives/public-surface.test.d.ts.map +0 -1
- package/dist/runtime/primitives/public-surface.test.js +0 -45
- package/dist/runtime/primitives/translations.d.ts.map +0 -1
- package/dist/runtime/react-router/index.d.ts.map +0 -1
- package/dist/runtime/react-router/navigation-utils.d.ts.map +0 -1
- package/dist/runtime/react-router/navigation.d.ts.map +0 -1
- package/dist/runtime/react-router/navigation.test.d.ts +0 -2
- package/dist/runtime/react-router/navigation.test.d.ts.map +0 -1
- package/dist/runtime/react-router/navigation.test.js +0 -35
- package/dist/runtime/react-router/routes.d.ts.map +0 -1
- package/dist/runtime/react-router/routes.test.d.ts +0 -2
- package/dist/runtime/react-router/routes.test.d.ts.map +0 -1
- package/dist/runtime/react-router/routes.test.js +0 -67
- package/dist/runtime/routing/AppResetScrollOnNavigate.d.ts.map +0 -1
- package/dist/runtime/routing/AppRoutesProvider.d.ts.map +0 -1
- package/dist/runtime/seo/buildSeoForPath.d.ts.map +0 -1
- package/dist/runtime/seo/htmlProps.d.ts.map +0 -1
- package/dist/runtime/seo/index.d.ts.map +0 -1
- package/dist/runtime/seo/resolver.d.ts.map +0 -1
- package/dist/runtime/shared/fimo-config.server.d.ts.map +0 -1
- package/dist/runtime/shared/fimo-config.types.d.ts.map +0 -1
- package/dist/runtime/t/OptionalTracking.d.ts.map +0 -1
- package/dist/runtime/t/OptionalTracking.test.d.ts +0 -2
- package/dist/runtime/t/OptionalTracking.test.d.ts.map +0 -1
- package/dist/runtime/t/OptionalTracking.test.js +0 -11
- package/dist/runtime/t/Pageview.d.ts.map +0 -1
- package/dist/runtime/templates.d.ts.map +0 -1
- package/dist/scripts/generated-types.test.js +0 -212
- package/dist/scripts/lint-translation-keys.test.d.ts +0 -1
- package/dist/scripts/lint-translation-keys.test.js +0 -16
- package/dist/scripts/sync-schemas.test.d.ts +0 -1
- package/dist/scripts/sync-schemas.test.js +0 -124
- package/dist/scripts/validate-route-metadata.test.d.ts +0 -1
- package/dist/scripts/validate-route-metadata.test.js +0 -116
- package/dist/scripts/validate-schemas.test.d.ts +0 -1
- package/dist/scripts/validate-schemas.test.js +0 -263
- package/scripts/add-ext.mjs +0 -47
- package/scripts/bump-version.mjs +0 -58
- package/scripts/bundle-cli.mjs +0 -123
- package/scripts/install-cli.mjs +0 -36
- package/scripts/lib/channels.mjs +0 -70
- package/scripts/lib/dev-release-state.mjs +0 -44
- package/scripts/lib/dev-release-state.test.ts +0 -36
- package/scripts/lib/postinstall-onboarding.test.ts +0 -82
- package/scripts/lib/release-core.mjs +0 -225
- package/scripts/lib/release-core.test.ts +0 -147
- package/scripts/migration-qa-gate.test.ts +0 -155
- package/scripts/publish-npm.mjs +0 -180
- /package/dist/{scripts/generated-types.test.d.ts → runtime/content/client/types.js} +0 -0
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { isGlobalInstall, parseYesNo, shouldPromptForGlobalSkills } from './postinstall-onboarding.mjs';
|
|
4
|
-
|
|
5
|
-
describe('isGlobalInstall', () => {
|
|
6
|
-
it('detects npm global installs', () => {
|
|
7
|
-
expect(isGlobalInstall({ npm_config_global: 'true' })).toBe(true);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('detects package-manager location=global installs', () => {
|
|
11
|
-
expect(isGlobalInstall({ npm_config_location: 'global' })).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('ignores local installs', () => {
|
|
15
|
-
expect(isGlobalInstall({})).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('shouldPromptForGlobalSkills', () => {
|
|
20
|
-
it('prompts only for interactive global installs', () => {
|
|
21
|
-
expect(
|
|
22
|
-
shouldPromptForGlobalSkills({
|
|
23
|
-
env: { npm_config_global: 'true' },
|
|
24
|
-
stdinIsTTY: true,
|
|
25
|
-
stdoutIsTTY: true,
|
|
26
|
-
})
|
|
27
|
-
).toEqual({ ok: true });
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('skips CI and non-interactive installs', () => {
|
|
31
|
-
expect(
|
|
32
|
-
shouldPromptForGlobalSkills({
|
|
33
|
-
env: { npm_config_global: 'true', CI: '1' },
|
|
34
|
-
stdinIsTTY: true,
|
|
35
|
-
stdoutIsTTY: true,
|
|
36
|
-
})
|
|
37
|
-
).toMatchObject({ ok: false, reason: 'ci' });
|
|
38
|
-
expect(
|
|
39
|
-
shouldPromptForGlobalSkills({
|
|
40
|
-
env: { npm_config_global: 'true' },
|
|
41
|
-
stdinIsTTY: false,
|
|
42
|
-
stdoutIsTTY: true,
|
|
43
|
-
})
|
|
44
|
-
).toMatchObject({ ok: false, reason: 'non-interactive' });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('honors the explicit skip env var', () => {
|
|
48
|
-
expect(
|
|
49
|
-
shouldPromptForGlobalSkills({
|
|
50
|
-
env: { npm_config_global: 'true', FIMO_SKIP_POSTINSTALL_PROMPT: '1' },
|
|
51
|
-
stdinIsTTY: true,
|
|
52
|
-
stdoutIsTTY: true,
|
|
53
|
-
})
|
|
54
|
-
).toMatchObject({ ok: false, reason: 'disabled' });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('skips package-manager assume-yes installs', () => {
|
|
58
|
-
expect(
|
|
59
|
-
shouldPromptForGlobalSkills({
|
|
60
|
-
env: { npm_config_global: 'true', npm_config_yes: 'true' },
|
|
61
|
-
stdinIsTTY: true,
|
|
62
|
-
stdoutIsTTY: true,
|
|
63
|
-
})
|
|
64
|
-
).toMatchObject({ ok: false, reason: 'assume-yes-install' });
|
|
65
|
-
expect(
|
|
66
|
-
shouldPromptForGlobalSkills({
|
|
67
|
-
env: { npm_config_global: 'true', npm_config_force: 'true' },
|
|
68
|
-
stdinIsTTY: true,
|
|
69
|
-
stdoutIsTTY: true,
|
|
70
|
-
})
|
|
71
|
-
).toMatchObject({ ok: false, reason: 'assume-yes-install' });
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('parseYesNo', () => {
|
|
76
|
-
it('parses yes, no, and empty defaults', () => {
|
|
77
|
-
expect(parseYesNo('y')).toBe(true);
|
|
78
|
-
expect(parseYesNo('no')).toBe(false);
|
|
79
|
-
expect(parseYesNo('', true)).toBe(true);
|
|
80
|
-
expect(parseYesNo('', false)).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// Shared release-core: the channel-aware release.json bake.
|
|
2
|
-
//
|
|
3
|
-
// This is the single place that stamps which environment a published
|
|
4
|
-
// build talks to. Both the npm publish path and any future CI job call
|
|
5
|
-
// it, so the "which URL goes in which build" decision can't drift.
|
|
6
|
-
//
|
|
7
|
-
// `release.json` is a publish artifact: it is gitignored (apps/cli/.gitignore)
|
|
8
|
-
// and only exists transiently while packing. The CLI reads it at runtime as
|
|
9
|
-
// `FIMO_API_URL env || baked release.json || localhost` — so the bake only
|
|
10
|
-
// governs a bare `npm i -g fimo` with no env set. The sandbox always
|
|
11
|
-
// overrides it via the injected `.env`.
|
|
12
|
-
|
|
13
|
-
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
-
import { join, resolve } from 'node:path';
|
|
15
|
-
|
|
16
|
-
import { ALLOWED_API_URLS } from './channels.mjs';
|
|
17
|
-
|
|
18
|
-
const TEMPLATE_PKG_REL = 'templates/react-router/package.json';
|
|
19
|
-
const TEMPLATE_LOCK_REL = 'templates/react-router/pnpm-lock.yaml';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Write a `release.json` for a published (`source: 'npm'`) build using the
|
|
23
|
-
* channel's baked URLs. Refuses any apiUrl not on the allowlist so a
|
|
24
|
-
* tunnel/localhost can never reach the public registry (D5).
|
|
25
|
-
*
|
|
26
|
-
* @param {string} cliRoot - apps/cli root
|
|
27
|
-
* @param {import('./channels.mjs').Channel} channel
|
|
28
|
-
* @returns {string} the path written
|
|
29
|
-
*/
|
|
30
|
-
export function writeReleaseJson(cliRoot, channel) {
|
|
31
|
-
if (!ALLOWED_API_URLS.has(channel.apiUrl)) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`Refusing to bake non-allowlisted apiUrl "${channel.apiUrl}" into a published package. ` +
|
|
34
|
-
`Published channels may bake only staging or prod (D5).`
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
const payload = {
|
|
38
|
-
source: channel.source,
|
|
39
|
-
apiUrl: channel.apiUrl,
|
|
40
|
-
webUrl: channel.webUrl,
|
|
41
|
-
};
|
|
42
|
-
const file = resolve(cliRoot, 'release.json');
|
|
43
|
-
writeFileSync(file, JSON.stringify(payload, null, 2) + '\n');
|
|
44
|
-
return file;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Pin the bundled template's `fimo` dependency to an exact version at pack
|
|
49
|
-
* time, so a project scaffolded from `fimo@X` depends on exactly `fimo@X`
|
|
50
|
-
* (immutable, reproducible — no mutable URL, no caret drift). The committed
|
|
51
|
-
* template keeps a dev-friendly value; this overwrites it only for the
|
|
52
|
-
* tarball and the returned function restores it afterwards.
|
|
53
|
-
*
|
|
54
|
-
* @param {string} cliRoot
|
|
55
|
-
* @param {string} version - exact version to pin (e.g. "0.3.0" or "0.3.0-experimental.4")
|
|
56
|
-
* @returns {() => void} restore - rewrites the original file contents back
|
|
57
|
-
*/
|
|
58
|
-
export function pinTemplateFimo(cliRoot, version) {
|
|
59
|
-
const file = resolve(cliRoot, TEMPLATE_PKG_REL);
|
|
60
|
-
if (!existsSync(file)) {
|
|
61
|
-
return () => {};
|
|
62
|
-
}
|
|
63
|
-
const original = readFileSync(file, 'utf8');
|
|
64
|
-
const pkg = JSON.parse(original);
|
|
65
|
-
let changed = false;
|
|
66
|
-
for (const field of ['dependencies', 'devDependencies']) {
|
|
67
|
-
if (pkg[field] && typeof pkg[field].fimo === 'string' && pkg[field].fimo !== version) {
|
|
68
|
-
pkg[field].fimo = version;
|
|
69
|
-
changed = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (changed) {
|
|
73
|
-
writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');
|
|
74
|
-
}
|
|
75
|
-
return () => writeFileSync(file, original);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Remove the template's pnpm-lock.yaml from the tarball at pack time.
|
|
80
|
-
*
|
|
81
|
-
* We can't ship a correct lockfile: it would have to pin the exact version
|
|
82
|
-
* we're about to publish, which isn't on the registry yet (chicken-and-egg).
|
|
83
|
-
* A stale lockfile pinning a different spec than package.json breaks frozen
|
|
84
|
-
* installs (the 2026-06-11 outage class). With an exact, immutable version in
|
|
85
|
-
* package.json, `fimo create` + a fresh `pnpm install` resolves
|
|
86
|
-
* deterministically — so shipping no lockfile is the correct choice.
|
|
87
|
-
* `scaffold.ts` already deletes the lockfile whenever it rewrites the dep, so
|
|
88
|
-
* this only matters for the exact-npm-match path, but it's always safe.
|
|
89
|
-
*
|
|
90
|
-
* @param {string} cliRoot
|
|
91
|
-
* @returns {() => void} restore - rewrites the original lockfile back, if any
|
|
92
|
-
*/
|
|
93
|
-
export function stripTemplateLockfile(cliRoot) {
|
|
94
|
-
const file = resolve(cliRoot, TEMPLATE_LOCK_REL);
|
|
95
|
-
if (!existsSync(file)) {
|
|
96
|
-
return () => {};
|
|
97
|
-
}
|
|
98
|
-
const original = readFileSync(file, 'utf8');
|
|
99
|
-
rmSync(file, { force: true });
|
|
100
|
-
return () => writeFileSync(file, original);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Parse `npm view <name> versions --json` output into a flat list of version
|
|
105
|
-
* strings, tolerating every shape npm has shipped: an array of strings
|
|
106
|
-
* (npm <= 11), a bare string when exactly one version is published
|
|
107
|
-
* (npm <= 11), and the npm 12 wrapper — "npm view --json now always returns
|
|
108
|
-
* an array", one entry per matched version, i.e. `[[...versions]]` for a bare
|
|
109
|
-
* package name. Returns null when nothing version-shaped survives (error
|
|
110
|
-
* envelopes, empty output), so callers fall back to the run-number/timestamp
|
|
111
|
-
* counter instead of trusting garbage.
|
|
112
|
-
*
|
|
113
|
-
* @param {string} stdout - raw stdout of `npm view <name> versions --json`
|
|
114
|
-
* @returns {string[] | null}
|
|
115
|
-
*/
|
|
116
|
-
export function parsePublishedVersions(stdout) {
|
|
117
|
-
let parsed;
|
|
118
|
-
try {
|
|
119
|
-
parsed = JSON.parse(stdout);
|
|
120
|
-
} catch {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
const versions = (Array.isArray(parsed) ? parsed : [parsed]).flat().filter((version) => typeof version === 'string');
|
|
124
|
-
return versions.length > 0 ? versions : null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Next per-version prerelease counter: one past the highest already-published
|
|
129
|
-
* `<base>-<distTag>.<n>` on npm. Counting from what's actually published
|
|
130
|
-
* (instead of resetting blindly to 1) keeps the counter collision-proof even
|
|
131
|
-
* when the projected base flaps back to a value that already has prereleases —
|
|
132
|
-
* npm versions are immutable, so a blind reset would make the publish fail.
|
|
133
|
-
*
|
|
134
|
-
* @param {readonly string[]} existingVersions - all published versions of the package
|
|
135
|
-
* @param {string} base - X.Y.Z base (no prerelease suffix)
|
|
136
|
-
* @param {string} distTag
|
|
137
|
-
* @returns {number}
|
|
138
|
-
*/
|
|
139
|
-
export function nextPrereleaseCounter(existingVersions, base, distTag) {
|
|
140
|
-
const prefix = `${base}-${distTag}.`;
|
|
141
|
-
let max = 0;
|
|
142
|
-
for (const version of existingVersions) {
|
|
143
|
-
if (!version.startsWith(prefix)) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
const n = Number(version.slice(prefix.length));
|
|
147
|
-
if (Number.isInteger(n) && n > max) {
|
|
148
|
-
max = n;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return max + 1;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Compute the version to publish for a channel.
|
|
156
|
-
* - Stable (`latest`) publishes the exact package.json version.
|
|
157
|
-
* - Prerelease channels synthesize a disposable `X.Y.Z-<distTag>.<n>`.
|
|
158
|
-
* `n` counts per base version (`0.3.5-staging.1`, `.2`, …) when the
|
|
159
|
-
* published versions could be fetched from npm; otherwise it falls back
|
|
160
|
-
* to the CI run number, then a timestamp.
|
|
161
|
-
*
|
|
162
|
-
* @param {string} baseVersion - package.json version
|
|
163
|
-
* @param {import('./channels.mjs').Channel} channel
|
|
164
|
-
* @param {{ runId?: string, existingVersions?: readonly string[] }} [opts]
|
|
165
|
-
* @returns {string}
|
|
166
|
-
*/
|
|
167
|
-
export function computePublishVersion(baseVersion, channel, opts = {}) {
|
|
168
|
-
if (!channel.prerelease) {
|
|
169
|
-
return baseVersion;
|
|
170
|
-
}
|
|
171
|
-
// Strip any existing prerelease suffix from the base so we don't stack them.
|
|
172
|
-
const base = baseVersion.split('-')[0];
|
|
173
|
-
const n = opts.existingVersions
|
|
174
|
-
? String(nextPrereleaseCounter(opts.existingVersions, base, channel.distTag))
|
|
175
|
-
: (opts.runId ?? String(Date.now()));
|
|
176
|
-
return `${base}-${channel.distTag}.${n}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const BUMP_RANK = { patch: 1, minor: 2, major: 3 };
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Peek at pending changeset files and compute the version the next
|
|
183
|
-
* `changeset version` run will produce for `fimo`. Prerelease channels use
|
|
184
|
-
* this as their base so a staging build advertises the version prod will
|
|
185
|
-
* actually ship (`0.3.4-staging.26` while 0.3.4 is pending) instead of the
|
|
186
|
-
* already-released base. No pending changesets (or no .changeset dir) →
|
|
187
|
-
* base unchanged.
|
|
188
|
-
*
|
|
189
|
-
* @param {string} baseVersion - package.json version
|
|
190
|
-
* @param {string} changesetDir - absolute path to the repo's .changeset dir
|
|
191
|
-
* @returns {string}
|
|
192
|
-
*/
|
|
193
|
-
export function nextVersionFromChangesets(baseVersion, changesetDir) {
|
|
194
|
-
let entries;
|
|
195
|
-
try {
|
|
196
|
-
entries = readdirSync(changesetDir);
|
|
197
|
-
} catch {
|
|
198
|
-
return baseVersion;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
let maxBump = null;
|
|
202
|
-
for (const name of entries) {
|
|
203
|
-
if (!name.endsWith('.md') || name === 'README.md') {
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
const text = readFileSync(join(changesetDir, name), 'utf8');
|
|
207
|
-
const frontmatter = text.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? '';
|
|
208
|
-
const bump = frontmatter.match(/['"]?fimo['"]?\s*:\s*(patch|minor|major)/)?.[1];
|
|
209
|
-
if (bump && (!maxBump || BUMP_RANK[bump] > BUMP_RANK[maxBump])) {
|
|
210
|
-
maxBump = bump;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (!maxBump) {
|
|
215
|
-
return baseVersion;
|
|
216
|
-
}
|
|
217
|
-
const [major, minor, patch] = baseVersion.split('-')[0].split('.').map(Number);
|
|
218
|
-
if (maxBump === 'major') {
|
|
219
|
-
return `${major + 1}.0.0`;
|
|
220
|
-
}
|
|
221
|
-
if (maxBump === 'minor') {
|
|
222
|
-
return `${major}.${minor + 1}.0`;
|
|
223
|
-
}
|
|
224
|
-
return `${major}.${minor}.${patch + 1}`;
|
|
225
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
computePublishVersion,
|
|
9
|
-
nextPrereleaseCounter,
|
|
10
|
-
nextVersionFromChangesets,
|
|
11
|
-
parsePublishedVersions,
|
|
12
|
-
// @ts-expect-error plain mjs module without types
|
|
13
|
-
} from './release-core.mjs';
|
|
14
|
-
|
|
15
|
-
const stagingChannel = { prerelease: true, distTag: 'staging' };
|
|
16
|
-
const latestChannel = { prerelease: false, distTag: 'latest' };
|
|
17
|
-
|
|
18
|
-
describe('computePublishVersion', () => {
|
|
19
|
-
it('returns the base version verbatim for stable channels', () => {
|
|
20
|
-
expect(computePublishVersion('0.3.3', latestChannel, { runId: '7' })).toBe('0.3.3');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('synthesizes a prerelease version from the run id', () => {
|
|
24
|
-
expect(computePublishVersion('0.3.3', stagingChannel, { runId: '26' })).toBe('0.3.3-staging.26');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('strips an existing prerelease suffix before synthesizing', () => {
|
|
28
|
-
expect(computePublishVersion('0.3.3-staging.9', stagingChannel, { runId: '26' })).toBe('0.3.3-staging.26');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('counts per base version when published versions are provided', () => {
|
|
32
|
-
const existingVersions = ['0.3.4', '0.3.5-staging.1', '0.3.5-staging.2'];
|
|
33
|
-
expect(computePublishVersion('0.3.5', stagingChannel, { runId: '30', existingVersions })).toBe('0.3.5-staging.3');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('starts at 1 for a base with no published prereleases', () => {
|
|
37
|
-
const existingVersions = ['0.3.4', '0.3.4-staging.26', '0.3.4-staging.27'];
|
|
38
|
-
expect(computePublishVersion('0.3.5', stagingChannel, { runId: '30', existingVersions })).toBe('0.3.5-staging.1');
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('parsePublishedVersions', () => {
|
|
43
|
-
it('passes through the npm <= 11 array-of-strings shape', () => {
|
|
44
|
-
expect(parsePublishedVersions('["0.3.4", "0.3.5-staging.1"]')).toEqual(['0.3.4', '0.3.5-staging.1']);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('wraps the npm <= 11 bare string returned for a single published version', () => {
|
|
48
|
-
expect(parsePublishedVersions('"0.0.0"')).toEqual(['0.0.0']);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('unwraps the npm 12 shape (--json always returns an array)', () => {
|
|
52
|
-
expect(parsePublishedVersions('[["0.3.4", "0.3.5-staging.1", "0.3.5"]]')).toEqual([
|
|
53
|
-
'0.3.4',
|
|
54
|
-
'0.3.5-staging.1',
|
|
55
|
-
'0.3.5',
|
|
56
|
-
]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('returns null for unparseable output', () => {
|
|
60
|
-
expect(parsePublishedVersions('')).toBeNull();
|
|
61
|
-
expect(parsePublishedVersions('npm ERR! something')).toBeNull();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('returns null when nothing version-shaped survives (error envelopes, empty lists)', () => {
|
|
65
|
-
expect(parsePublishedVersions('{"error":{"code":"E401"}}')).toBeNull();
|
|
66
|
-
expect(parsePublishedVersions('[]')).toBeNull();
|
|
67
|
-
expect(parsePublishedVersions('[{"error":{"code":"E404"}}]')).toBeNull();
|
|
68
|
-
expect(parsePublishedVersions('null')).toBeNull();
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('nextPrereleaseCounter', () => {
|
|
73
|
-
it('returns 1 when nothing matches the base + tag', () => {
|
|
74
|
-
expect(nextPrereleaseCounter([], '0.3.5', 'staging')).toBe(1);
|
|
75
|
-
expect(nextPrereleaseCounter(['0.3.5', '0.3.5-experimental.4'], '0.3.5', 'staging')).toBe(1);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('continues past the highest published counter for the base', () => {
|
|
79
|
-
const versions = ['0.3.4-staging.26', '0.3.4-staging.27', '0.3.3-staging.25'];
|
|
80
|
-
expect(nextPrereleaseCounter(versions, '0.3.4', 'staging')).toBe(28);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('does not confuse prefix-sharing bases (0.3.4 vs 0.3.40)', () => {
|
|
84
|
-
expect(nextPrereleaseCounter(['0.3.40-staging.9'], '0.3.4', 'staging')).toBe(1);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('ignores non-integer counters (timestamps still count, garbage does not)', () => {
|
|
88
|
-
expect(nextPrereleaseCounter(['0.3.4-staging.abc', '0.3.4-staging.2.5'], '0.3.4', 'staging')).toBe(1);
|
|
89
|
-
expect(nextPrereleaseCounter(['0.3.3-experimental.1783410168478'], '0.3.3', 'experimental')).toBe(1783410168479);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('nextVersionFromChangesets', () => {
|
|
94
|
-
let dir: string;
|
|
95
|
-
|
|
96
|
-
afterEach(() => {
|
|
97
|
-
if (dir) {
|
|
98
|
-
rmSync(dir, { recursive: true, force: true });
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
function changesetDir(files: Record<string, string>): string {
|
|
103
|
-
dir = mkdtempSync(join(tmpdir(), 'changesets-'));
|
|
104
|
-
writeFileSync(join(dir, 'README.md'), '# Changesets\n');
|
|
105
|
-
for (const [name, frontmatter] of Object.entries(files)) {
|
|
106
|
-
writeFileSync(join(dir, name), `---\n${frontmatter}\n---\n\nsummary\n`);
|
|
107
|
-
}
|
|
108
|
-
return dir;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
it('returns the base when the directory does not exist', () => {
|
|
112
|
-
expect(nextVersionFromChangesets('0.3.3', '/nonexistent/.changeset')).toBe('0.3.3');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('returns the base when no changesets are pending', () => {
|
|
116
|
-
expect(nextVersionFromChangesets('0.3.3', changesetDir({}))).toBe('0.3.3');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('applies a patch bump for pending patch changesets', () => {
|
|
120
|
-
const d = changesetDir({ 'a.md': "'fimo': patch" });
|
|
121
|
-
expect(nextVersionFromChangesets('0.3.3', d)).toBe('0.3.4');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('takes the max bump across pending changesets', () => {
|
|
125
|
-
const d = changesetDir({
|
|
126
|
-
'a.md': "'fimo': patch",
|
|
127
|
-
'b.md': "'fimo': minor",
|
|
128
|
-
'c.md': "'fimo': patch",
|
|
129
|
-
});
|
|
130
|
-
expect(nextVersionFromChangesets('0.3.3', d)).toBe('0.4.0');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('handles major bumps and double-quoted package names', () => {
|
|
134
|
-
const d = changesetDir({ 'a.md': '"fimo": major' });
|
|
135
|
-
expect(nextVersionFromChangesets('0.3.3', d)).toBe('1.0.0');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('ignores changesets for other packages', () => {
|
|
139
|
-
const d = changesetDir({ 'a.md': "'@fimo/fimovm': minor" });
|
|
140
|
-
expect(nextVersionFromChangesets('0.3.3', d)).toBe('0.3.3');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('ignores an existing prerelease suffix on the base', () => {
|
|
144
|
-
const d = changesetDir({ 'a.md': "'fimo': patch" });
|
|
145
|
-
expect(nextVersionFromChangesets('0.3.3-staging.9', d)).toBe('0.3.4');
|
|
146
|
-
});
|
|
147
|
-
});
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
-
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const SCRIPT = path.resolve(__dirname, '../assets/skills/fimo-migration/scripts/write-migration-report.mjs');
|
|
11
|
-
|
|
12
|
-
interface GateResult {
|
|
13
|
-
status: number | null;
|
|
14
|
-
report: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let dir: string;
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
dir = mkdtempSync(path.join(tmpdir(), 'fimo-qa-gate-'));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
rmSync(dir, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
function runGate(manifest: unknown, opts: { withBuildArtifact?: boolean } = {}): GateResult {
|
|
28
|
-
mkdirSync(path.join(dir, '.fimo', 'migration'), { recursive: true });
|
|
29
|
-
writeFileSync(path.join(dir, '.fimo', 'migration', 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
30
|
-
if (opts.withBuildArtifact) {
|
|
31
|
-
mkdirSync(path.join(dir, 'dist', 'client'), { recursive: true });
|
|
32
|
-
writeFileSync(path.join(dir, 'dist', 'client', 'index.html'), '<html></html>');
|
|
33
|
-
}
|
|
34
|
-
const reportPath = path.join(dir, 'report.md');
|
|
35
|
-
const child = spawnSync(process.execPath, [SCRIPT, dir, '--no-update-manifest', '--output', reportPath], {
|
|
36
|
-
encoding: 'utf8',
|
|
37
|
-
});
|
|
38
|
-
return { status: child.status, report: readFileSync(reportPath, 'utf8') };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function statusLine(report: string): string {
|
|
42
|
-
const lines = report.split('\n');
|
|
43
|
-
const idx = lines.findIndex((line) => line.trim() === '## Status');
|
|
44
|
-
return lines[idx + 2]?.trim() ?? '';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function checkFailed(report: string, id: string): boolean {
|
|
48
|
-
return report.includes(`- [ ] `) && new RegExp(`- \\[ \\] .*\\(${id}\\)`).test(report);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('migration QA gate (write-migration-report.mjs)', () => {
|
|
52
|
-
it('fails generated-only entries even when seed JSON exists (no silent pass)', () => {
|
|
53
|
-
const { status, report } = runGate({
|
|
54
|
-
source: { mode: 'url-only', platform: 'wix' },
|
|
55
|
-
routes: [{ route: '/', status: 'verified' }],
|
|
56
|
-
contentTypes: [{ name: 'game', status: 'generated' }],
|
|
57
|
-
entries: [{ contentType: 'game', sourceCount: 11, generatedCount: 11, importedCount: 0 }],
|
|
58
|
-
media: [],
|
|
59
|
-
forms: [],
|
|
60
|
-
unresolved: [],
|
|
61
|
-
});
|
|
62
|
-
expect(status).toBe(1);
|
|
63
|
-
expect(statusLine(report)).toBe('draft-only');
|
|
64
|
-
expect(checkFailed(report, 'cms-counts')).toBe(true);
|
|
65
|
-
expect(checkFailed(report, 'schemas-pushed')).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('fails referenced media that is only bundled locally under public/', () => {
|
|
69
|
-
const { status, report } = runGate({
|
|
70
|
-
source: { mode: 'url-only', platform: 'wix' },
|
|
71
|
-
routes: [{ route: '/', status: 'verified' }],
|
|
72
|
-
contentTypes: [],
|
|
73
|
-
entries: [],
|
|
74
|
-
media: [{ id: 'm1', referencedBy: ['/'], status: 'imported', targetUrl: '/assets/thepark/hero.webp' }],
|
|
75
|
-
forms: [],
|
|
76
|
-
unresolved: [],
|
|
77
|
-
});
|
|
78
|
-
expect(status).toBe(1);
|
|
79
|
-
expect(statusLine(report)).toBe('draft-only');
|
|
80
|
-
expect(checkFailed(report, 'media-covered')).toBe(true);
|
|
81
|
-
expect(report).toContain('Uploaded to Fimo: 0');
|
|
82
|
-
expect(report).toContain('Bundled locally (not uploaded): 1');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('passes when entries are imported, schemas pushed, and media uploaded to Fimo', () => {
|
|
86
|
-
const { status, report } = runGate(
|
|
87
|
-
{
|
|
88
|
-
source: { mode: 'url-only', platform: 'wix' },
|
|
89
|
-
routes: [{ route: '/', status: 'verified' }],
|
|
90
|
-
contentTypes: [{ name: 'game', status: 'imported' }],
|
|
91
|
-
entries: [{ contentType: 'game', sourceCount: 3, generatedCount: 3, importedCount: 3, status: 'imported' }],
|
|
92
|
-
media: [
|
|
93
|
-
{ id: 'm1', referencedBy: ['/'], targetAssetId: '11111111-1111-1111-1111-111111111111' },
|
|
94
|
-
{
|
|
95
|
-
id: 'm2',
|
|
96
|
-
referencedBy: ['/'],
|
|
97
|
-
targetUrl: 'https://assets.fimo.team/abc/hero.webp',
|
|
98
|
-
sourceUrl: 'https://static.wixstatic.com/hero.webp',
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
forms: [],
|
|
102
|
-
unresolved: [],
|
|
103
|
-
},
|
|
104
|
-
{ withBuildArtifact: true }
|
|
105
|
-
);
|
|
106
|
-
expect(status).toBe(0);
|
|
107
|
-
expect(statusLine(report)).toBe('complete');
|
|
108
|
-
expect(report).toContain('Uploaded to Fimo: 2');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('rejects a target URL that merely echoes the source URL (nothing uploaded)', () => {
|
|
112
|
-
const { status, report } = runGate({
|
|
113
|
-
source: { mode: 'url-only', platform: 'wix' },
|
|
114
|
-
routes: [{ route: '/', status: 'verified' }],
|
|
115
|
-
contentTypes: [],
|
|
116
|
-
entries: [],
|
|
117
|
-
media: [
|
|
118
|
-
{
|
|
119
|
-
id: 'm1',
|
|
120
|
-
referencedBy: ['/'],
|
|
121
|
-
sourceUrl: 'https://static.wixstatic.com/hero.webp',
|
|
122
|
-
targetUrl: 'https://static.wixstatic.com/hero.webp',
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
forms: [],
|
|
126
|
-
unresolved: [],
|
|
127
|
-
});
|
|
128
|
-
expect(status).toBe(1);
|
|
129
|
-
expect(checkFailed(report, 'media-covered')).toBe(true);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('allows complete-with-unresolved when gaps are explicit, owner-visible unresolved items', () => {
|
|
133
|
-
const { status, report } = runGate(
|
|
134
|
-
{
|
|
135
|
-
source: { mode: 'url-only', platform: 'wix' },
|
|
136
|
-
routes: [{ route: '/', status: 'verified' }],
|
|
137
|
-
contentTypes: [{ name: 'game', status: 'unresolved' }],
|
|
138
|
-
entries: [{ contentType: 'game', sourceCount: 5, generatedCount: 5, importedCount: 0, status: 'unresolved' }],
|
|
139
|
-
media: [{ id: 'm1', referencedBy: ['/'], status: 'unresolved' }],
|
|
140
|
-
forms: [],
|
|
141
|
-
unresolved: [
|
|
142
|
-
{
|
|
143
|
-
severity: 'high',
|
|
144
|
-
area: 'cms',
|
|
145
|
-
message: 'Game entries need a source export before import.',
|
|
146
|
-
suggestedAction: 'Provide the Wix CMS export and re-run import.',
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
{ withBuildArtifact: true }
|
|
151
|
-
);
|
|
152
|
-
expect(status).toBe(0);
|
|
153
|
-
expect(statusLine(report)).toBe('complete-with-unresolved');
|
|
154
|
-
});
|
|
155
|
-
});
|