fimo 0.9.0-experimental.5 → 0.9.0-experimental.7
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/assets/agent-templates/content-translator/scripts/write-locale-files.ts +66 -0
- package/assets/agents-starter/claude/settings.json +19 -0
- package/assets/content-templates/types-template.eta +1 -0
- package/assets/skills/fimo/references/translations.md +75 -0
- package/assets/skills/fimo-cli/references/create.md +90 -0
- package/assets/skills/fimo-cli/references/translations.md +43 -0
- package/assets/skills/fimo-migration/SKILL.md +2 -0
- package/assets/skills/fimo-migration/references/manifest.md +11 -0
- package/assets/skills/fimo-migration/references/pages.md +95 -0
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +217 -169
- package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
- package/dist/runtime/core/BuiltWithBadge.js +68 -0
- package/dist/runtime/seo/neutral.d.ts +22 -0
- package/dist/runtime/seo/neutral.js +17 -0
- package/dist/scripts/frameworks/discover.js +17 -0
- package/dist/scripts/frameworks/next/rewire.d.ts +13 -0
- package/dist/scripts/frameworks/next/rewire.js +26 -2
- package/dist/scripts/frameworks/next/routes.d.ts +6 -0
- package/dist/scripts/frameworks/next/routes.js +24 -1
- package/dist/scripts/inject-translations.d.ts +6 -0
- package/dist/scripts/inject-translations.js +168 -0
- package/dist/scripts/lib/discover-next-routes.d.ts +17 -0
- package/dist/scripts/lib/discover-next-routes.js +134 -0
- package/dist/scripts/routes/bindings.d.ts +93 -0
- package/dist/scripts/routes/bindings.js +117 -0
- package/dist/scripts/scan-wrappable.d.ts +1 -1
- package/dist/scripts/scan-wrappable.js +111 -1
- package/package.json +5 -1
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/translations/en.json +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { CmsCollectionSchema } from '../cms/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Page-binding evidence for the page registry (`fimo-config.json#routes`).
|
|
4
|
+
* A dynamic page template (`/blog/:slug`) is only navigable in the Studio
|
|
5
|
+
* preview once its content param carries a binding
|
|
6
|
+
* `{ schema, queryField, displayField }` — the schema whose entries
|
|
7
|
+
* materialize the page instances. No binding can be inferred with certainty,
|
|
8
|
+
* so this module grades evidence: `strong` bindings are written by the
|
|
9
|
+
* machine, `candidate` ones ship to the agent prompt as ready-to-apply
|
|
10
|
+
* suggestions. Neutral: evidence comes in as plain route/schema facts; the
|
|
11
|
+
* framework and CMS specifics stay in their adapters.
|
|
12
|
+
*/
|
|
13
|
+
export interface PageParamBinding {
|
|
14
|
+
schema: string;
|
|
15
|
+
queryField: string;
|
|
16
|
+
displayField: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PageBinding {
|
|
19
|
+
routeId: string;
|
|
20
|
+
path: string;
|
|
21
|
+
param: string;
|
|
22
|
+
binding: PageParamBinding;
|
|
23
|
+
confidence: 'strong' | 'candidate';
|
|
24
|
+
source: 'rewire' | 'cms-wiring';
|
|
25
|
+
}
|
|
26
|
+
export interface UnboundPage {
|
|
27
|
+
id: string;
|
|
28
|
+
path: string;
|
|
29
|
+
candidates: PageBinding[];
|
|
30
|
+
}
|
|
31
|
+
export interface PageBindingsResult {
|
|
32
|
+
/** Written into the registry by the caller. */
|
|
33
|
+
strong: PageBinding[];
|
|
34
|
+
/** Dynamic pages left without params — with any candidate suggestions. */
|
|
35
|
+
unbound: UnboundPage[];
|
|
36
|
+
/** Dynamic pages that already carry params (manual or a prior run). */
|
|
37
|
+
bound: Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
path: string;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/** A list page known to render a collection (the page-rewire join). */
|
|
43
|
+
export interface ListPageEvidence {
|
|
44
|
+
routePath: string | null;
|
|
45
|
+
schema: string;
|
|
46
|
+
}
|
|
47
|
+
export interface SchemaFieldsLike {
|
|
48
|
+
readonly [field: string]: {
|
|
49
|
+
readonly type?: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare function isLocaleParam(name: string): boolean;
|
|
53
|
+
interface ParsedDynamicPath {
|
|
54
|
+
/** The trailing content param name (without `:`/`*`). */
|
|
55
|
+
contentParam: string;
|
|
56
|
+
/** Route path minus the trailing content-param segment. */
|
|
57
|
+
parentPath: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A bindable dynamic path = optional locale-ish params + ONE trailing content
|
|
61
|
+
* param (catch-all or plain). Anything else (multiple content params) needs
|
|
62
|
+
* judgment beyond a single binding.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseDynamicPath(path: string): ParsedDynamicPath | null;
|
|
65
|
+
/** First title-ish string field of a schema — the picker label the Studio
|
|
66
|
+
* shows. Null when the schema has none (binding downgrades to candidate). */
|
|
67
|
+
export declare function displayFieldFor(fields: SchemaFieldsLike | undefined): string | null;
|
|
68
|
+
/** CMS collections that structurally look like page-builder page types: a
|
|
69
|
+
* top-level dynamic-zone (blocks oneOf) field. Structural, never name-based. */
|
|
70
|
+
export declare function cmsPageTypeEvidence(schemas: readonly CmsCollectionSchema[]): Array<{
|
|
71
|
+
schema: string;
|
|
72
|
+
fields: SchemaFieldsLike;
|
|
73
|
+
}>;
|
|
74
|
+
export declare function collectPageBindings(options: {
|
|
75
|
+
routes: Record<string, {
|
|
76
|
+
path?: string;
|
|
77
|
+
params?: unknown;
|
|
78
|
+
}>;
|
|
79
|
+
/** Rewired list pages (route-space, from the framework adapter). */
|
|
80
|
+
listPages?: readonly ListPageEvidence[];
|
|
81
|
+
/** Structural CMS page types (from `cmsPageTypeEvidence`), only when the
|
|
82
|
+
* CMS reads are actually wired to Fimo. */
|
|
83
|
+
cmsPageTypes?: ReadonlyArray<{
|
|
84
|
+
schema: string;
|
|
85
|
+
fields: SchemaFieldsLike;
|
|
86
|
+
}>;
|
|
87
|
+
/** Uids still provisional (machine drafts not yet adopted) — their bindings
|
|
88
|
+
* are candidates: `adopt-drafts` will rename them. */
|
|
89
|
+
provisionalUids?: ReadonlySet<string>;
|
|
90
|
+
/** Known fields per schema, for the displayField pick. */
|
|
91
|
+
schemaFields?: Record<string, SchemaFieldsLike>;
|
|
92
|
+
}): PageBindingsResult;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Params that identify a locale, not content — never schema-bound. The
|
|
2
|
+
* Studio preview substitutes content params only; locale params are a known
|
|
3
|
+
* limitation documented in the prompt. Mirrors the parity capture's list. */
|
|
4
|
+
const LOCALE_PARAMS = /^(locale|lang|language)$/i;
|
|
5
|
+
export function isLocaleParam(name) {
|
|
6
|
+
return LOCALE_PARAMS.test(name);
|
|
7
|
+
}
|
|
8
|
+
const TITLE_FIELDS = /^(title|name|label|heading)$/i;
|
|
9
|
+
/**
|
|
10
|
+
* A bindable dynamic path = optional locale-ish params + ONE trailing content
|
|
11
|
+
* param (catch-all or plain). Anything else (multiple content params) needs
|
|
12
|
+
* judgment beyond a single binding.
|
|
13
|
+
*/
|
|
14
|
+
export function parseDynamicPath(path) {
|
|
15
|
+
const params = [...path.matchAll(/:([^/*]+)\*?/g)].map((match) => match[1]);
|
|
16
|
+
if (params.length === 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const contentParams = params.filter((param) => !LOCALE_PARAMS.test(param));
|
|
20
|
+
if (contentParams.length !== 1) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const contentParam = contentParams[0];
|
|
24
|
+
if (!new RegExp(`/:${contentParam}\\*?$`).test(path)) {
|
|
25
|
+
return null; // the content param must be the trailing segment
|
|
26
|
+
}
|
|
27
|
+
const parentPath = path.replace(new RegExp(`/:${contentParam}\\*?$`), '') || '/';
|
|
28
|
+
return { contentParam, parentPath };
|
|
29
|
+
}
|
|
30
|
+
function isCatchAll(path, param) {
|
|
31
|
+
return path.endsWith(`:${param}*`);
|
|
32
|
+
}
|
|
33
|
+
/** First title-ish string field of a schema — the picker label the Studio
|
|
34
|
+
* shows. Null when the schema has none (binding downgrades to candidate). */
|
|
35
|
+
export function displayFieldFor(fields) {
|
|
36
|
+
for (const [name, def] of Object.entries(fields ?? {})) {
|
|
37
|
+
if (TITLE_FIELDS.test(name) && (def.type === undefined || def.type === 'string')) {
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** CMS collections that structurally look like page-builder page types: a
|
|
44
|
+
* top-level dynamic-zone (blocks oneOf) field. Structural, never name-based. */
|
|
45
|
+
export function cmsPageTypeEvidence(schemas) {
|
|
46
|
+
return schemas
|
|
47
|
+
.filter((schema) => !schema.isSingleton &&
|
|
48
|
+
Object.values(schema.fields).some((field) => field.type === 'blocks' && 'oneOf' in field.schema))
|
|
49
|
+
.map((schema) => ({ schema: schema.name, fields: schema.fields }));
|
|
50
|
+
}
|
|
51
|
+
export function collectPageBindings(options) {
|
|
52
|
+
const { routes, listPages = [], cmsPageTypes = [], provisionalUids, schemaFields = {} } = options;
|
|
53
|
+
const strong = [];
|
|
54
|
+
const unbound = [];
|
|
55
|
+
const bound = [];
|
|
56
|
+
for (const [id, route] of Object.entries(routes)) {
|
|
57
|
+
const path = route.path;
|
|
58
|
+
if (typeof path !== 'string' || !path.includes(':')) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (route.params && typeof route.params === 'object' && Object.keys(route.params).length > 0) {
|
|
62
|
+
bound.push({ id, path });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const params = [...path.matchAll(/:([^/*]+)\*?/g)].map((match) => match[1]);
|
|
66
|
+
if (params.every((param) => isLocaleParam(param))) {
|
|
67
|
+
continue; // locale-only page — nothing to bind, effectively static
|
|
68
|
+
}
|
|
69
|
+
const parsed = parseDynamicPath(path);
|
|
70
|
+
if (!parsed) {
|
|
71
|
+
unbound.push({ id, path, candidates: [] });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const candidates = [];
|
|
75
|
+
const make = (schema, source, baseConfidence) => {
|
|
76
|
+
const fields = schemaFields[schema] ?? cmsPageTypes.find((type) => type.schema === schema)?.fields;
|
|
77
|
+
const display = displayFieldFor(fields);
|
|
78
|
+
const confidence = provisionalUids?.has(schema) || display === null ? 'candidate' : baseConfidence;
|
|
79
|
+
return {
|
|
80
|
+
routeId: id,
|
|
81
|
+
path,
|
|
82
|
+
param: parsed.contentParam,
|
|
83
|
+
binding: { schema, queryField: 'slug', displayField: display ?? 'slug' },
|
|
84
|
+
confidence,
|
|
85
|
+
source,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
// Rewire join: a list page rendering collection X at the dynamic page's
|
|
89
|
+
// parent path is strong evidence the detail param resolves against X.
|
|
90
|
+
const parents = listPages.filter((page) => page.routePath === parsed.parentPath);
|
|
91
|
+
const parentSchemas = [...new Set(parents.map((page) => page.schema))];
|
|
92
|
+
if (parentSchemas.length === 1) {
|
|
93
|
+
candidates.push(make(parentSchemas[0], 'rewire', 'strong'));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
for (const schema of parentSchemas) {
|
|
97
|
+
candidates.push(make(schema, 'rewire', 'candidate'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// CMS-structural join: a catch-all page of a CMS-wired site renders the
|
|
101
|
+
// CMS's page-builder type. Unique page-builder type → strong.
|
|
102
|
+
if (isCatchAll(path, parsed.contentParam) && cmsPageTypes.length > 0) {
|
|
103
|
+
const unique = cmsPageTypes.length === 1;
|
|
104
|
+
for (const type of cmsPageTypes) {
|
|
105
|
+
candidates.push(make(type.schema, 'cms-wiring', unique ? 'strong' : 'candidate'));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const winner = candidates.find((candidate) => candidate.confidence === 'strong');
|
|
109
|
+
if (winner && candidates.filter((candidate) => candidate.confidence === 'strong').length === 1) {
|
|
110
|
+
strong.push(winner);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
unbound.push({ id, path, candidates });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { strong, unbound, bound };
|
|
117
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* user-visible attribute strings. Drives the wrap migration's coverage gate:
|
|
5
5
|
* every item must end wrapped, ignored-with-a-reason, or unresolved.
|
|
6
6
|
*/
|
|
7
|
-
export type WrappableKind = 'text' | 'image' | 'video' | 'file-link' | 'attribute';
|
|
7
|
+
export type WrappableKind = 'text' | 'formatted-text' | 'image' | 'video' | 'file-link' | 'attribute';
|
|
8
8
|
export interface WrappableItem {
|
|
9
9
|
kind: WrappableKind;
|
|
10
10
|
file: string;
|
|
@@ -118,6 +118,36 @@ const WRAPPABLE_ATTRIBUTES = new Set(['placeholder', 'title', 'aria-label']);
|
|
|
118
118
|
const FILE_LINK_PATTERN = /\.(pdf|zip|docx?|xlsx?|pptx?|csv|mp4|mov|webm)$/i;
|
|
119
119
|
const SKIP_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', '.fimo']);
|
|
120
120
|
const MIN_TEXT_LENGTH = 3;
|
|
121
|
+
/**
|
|
122
|
+
* Prose containers whose children mix raw text with inline elements or
|
|
123
|
+
* expressions. Wrapping such text node-by-node produces FRAGMENT labels
|
|
124
|
+
* (", and agencies building…") that nobody can find or edit as a sentence —
|
|
125
|
+
* the whole block merges into ONE `formatted-text` item instead (agent work:
|
|
126
|
+
* the fimo`` tagged template or RichText, never the auto-wrap codemod).
|
|
127
|
+
*/
|
|
128
|
+
const PROSE_TAGS = new Set([
|
|
129
|
+
'p',
|
|
130
|
+
'h1',
|
|
131
|
+
'h2',
|
|
132
|
+
'h3',
|
|
133
|
+
'h4',
|
|
134
|
+
'h5',
|
|
135
|
+
'h6',
|
|
136
|
+
'li',
|
|
137
|
+
'blockquote',
|
|
138
|
+
'figcaption',
|
|
139
|
+
'dt',
|
|
140
|
+
'dd',
|
|
141
|
+
'label',
|
|
142
|
+
'legend',
|
|
143
|
+
'td',
|
|
144
|
+
'th',
|
|
145
|
+
'caption',
|
|
146
|
+
'summary',
|
|
147
|
+
]);
|
|
148
|
+
function meaningfulLength(text) {
|
|
149
|
+
return text.replace(/[^\p{L}\p{N}]/gu, '').length;
|
|
150
|
+
}
|
|
121
151
|
const PREVIEW_LENGTH = 60;
|
|
122
152
|
function toPreview(value) {
|
|
123
153
|
const collapsed = value.replace(/\s+/g, ' ').trim();
|
|
@@ -465,6 +495,50 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
|
|
|
465
495
|
function detectCollection(declarator) {
|
|
466
496
|
detectCollectionFromDeclarator(declarator, code, 0, relPath, collections);
|
|
467
497
|
}
|
|
498
|
+
// Text nodes absorbed into a merged formatted-text item (keyed by offset) —
|
|
499
|
+
// the JSXText visitor skips them so fragments never become labels.
|
|
500
|
+
const mergedText = new Set();
|
|
501
|
+
function containsWrappedContent(node) {
|
|
502
|
+
if (!node || typeof node !== 'object') {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 't') {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
if (node.type === 'TaggedTemplateExpression' && node.tag?.type === 'Identifier' && node.tag.name === 'fimo') {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
if (node.type === 'JSXElement') {
|
|
512
|
+
const name = jsxElementName(node);
|
|
513
|
+
if (name && WRAPPING_COMPONENTS.has(name)) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const key of Object.keys(node)) {
|
|
518
|
+
const child = node[key];
|
|
519
|
+
if (Array.isArray(child)) {
|
|
520
|
+
if (child.some((item) => containsWrappedContent(item))) {
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else if (typeof child === 'object' && child !== null && containsWrappedContent(child)) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
/** Every JSXText inside a prose element, recursing through inline children
|
|
531
|
+
* (inside a `<p>`, nested elements are inline by HTML rules). */
|
|
532
|
+
function collectProseTextNodes(node, out) {
|
|
533
|
+
for (const child of node.children ?? []) {
|
|
534
|
+
if (child.type === 'JSXText') {
|
|
535
|
+
out.push(child);
|
|
536
|
+
}
|
|
537
|
+
else if (child.type === 'JSXElement') {
|
|
538
|
+
collectProseTextNodes(child, out);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
468
542
|
function visit(node, insideWrapped, parentTag, scopes) {
|
|
469
543
|
if (!node || typeof node !== 'object') {
|
|
470
544
|
return;
|
|
@@ -548,7 +622,43 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
|
|
|
548
622
|
}
|
|
549
623
|
}
|
|
550
624
|
}
|
|
551
|
-
|
|
625
|
+
// Mixed prose detection runs on the ELEMENT (before its children are
|
|
626
|
+
// visited): a prose tag whose direct children interleave raw text with
|
|
627
|
+
// inline elements/expressions becomes ONE formatted-text item, and its
|
|
628
|
+
// text nodes are recorded so the JSXText visitor below skips them.
|
|
629
|
+
if (node.type === 'JSXElement' && !insideWrapped) {
|
|
630
|
+
const proseName = jsxElementName(node);
|
|
631
|
+
if (proseName && PROSE_TAGS.has(proseName)) {
|
|
632
|
+
const children = node.children ?? [];
|
|
633
|
+
const directText = children.filter((child) => child.type === 'JSXText' && meaningfulLength(String(child.value ?? '')) > 0);
|
|
634
|
+
const inline = children.filter((child) => child.type === 'JSXElement' ||
|
|
635
|
+
(child.type === 'JSXExpressionContainer' && child.expression?.type !== 'JSXEmptyExpression'));
|
|
636
|
+
if (directText.length > 0 &&
|
|
637
|
+
inline.length > 0 &&
|
|
638
|
+
!directText.some((child) => mergedText.has(child.start)) &&
|
|
639
|
+
!children.some((child) => containsWrappedContent(child))) {
|
|
640
|
+
const textNodes = [];
|
|
641
|
+
collectProseTextNodes(node, textNodes);
|
|
642
|
+
const combined = textNodes.map((textNode) => String(textNode.value ?? '')).join(' ');
|
|
643
|
+
if (meaningfulLength(combined) >= MIN_TEXT_LENGTH) {
|
|
644
|
+
for (const textNode of textNodes) {
|
|
645
|
+
if (typeof textNode.start === 'number') {
|
|
646
|
+
mergedText.add(textNode.start);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const inner = code.slice(node.openingElement?.end ?? node.start ?? 0, node.closingElement?.start ?? node.end ?? 0);
|
|
650
|
+
items.push({
|
|
651
|
+
kind: 'formatted-text',
|
|
652
|
+
file: relPath,
|
|
653
|
+
line: lineOf(code, node.start ?? 0),
|
|
654
|
+
preview: toPreview(inner),
|
|
655
|
+
span: { start: node.start, end: node.end, parentTag: proseName },
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (node.type === 'JSXText' && !insideWrapped && !mergedText.has(node.start)) {
|
|
552
662
|
const text = String(node.value ?? '');
|
|
553
663
|
const meaningful = text.replace(/[^\p{L}\p{N}]/gu, '');
|
|
554
664
|
if (meaningful.length >= MIN_TEXT_LENGTH) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo",
|
|
3
|
-
"version": "0.9.0-experimental.
|
|
3
|
+
"version": "0.9.0-experimental.7",
|
|
4
4
|
"description": "Fimo CLI - create, deploy, and manage Fimo projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fimo": "dist/cli/index.js"
|
|
@@ -58,6 +58,10 @@
|
|
|
58
58
|
"types": "./dist/runtime/strapi/index.d.ts",
|
|
59
59
|
"import": "./dist/runtime/strapi/index.js"
|
|
60
60
|
},
|
|
61
|
+
"./seo": {
|
|
62
|
+
"types": "./dist/runtime/seo/neutral.d.ts",
|
|
63
|
+
"import": "./dist/runtime/seo/neutral.js"
|
|
64
|
+
},
|
|
61
65
|
"./remix": {
|
|
62
66
|
"types": "./dist/runtime/remix/index.d.ts",
|
|
63
67
|
"import": "./dist/runtime/remix/index.js"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|